AI-native lead management · technical assessment

Every lead read in seconds.
Every action approved by a person.

An AI agent reviews each used and damaged car lead against the company's rules, written in plain English and stored as data. It recommends the next step with its reasoning, and an employee approves, edits or rejects every action.

1,000+leads a day, 2,000 on a peak day
~12kagent reviews on a peak day (~6 events per lead)
$50koffer limit above which only a manager approves
0actions that reach a seller without a person's approval
Agenda 1 · The problem 2 · AI-native design 3 · AI and the employee 4 · Architecture 5 · Live demo 6 · Safety 7 · Path to production
Jaime García · November 2026
The business need

Three problems, and what the design does about each

Today every lead is read and processed by hand in a legacy CRM, against rules only engineering can change.

Manual review

1,000+ leads a day, each read by a person before anything happens.

→
The agent reads every eventNew lead, seller reply, document, price change: reviewed in seconds in the background.
It drafts, people decideA pending recommendation with reasoning, rules cited and what it checked.
The price is never inventedThe first offer comes from the pricing formula, explained line by line.

Rigid rules

Business rules sit in code or in people's heads; a change means a ticket and a release.

→
Rules are dataPlain English with parameters, versioned like code, scoped to pipeline stages.
The business owns themDepartment heads edit their own department's rules: draft, test, publish.
Live with no deploymentA published version applies on the next agent run, with its own audit trail.

Scale

2,000 leads on a peak day. At that volume the people approving become the bottleneck.

→
Built for throughputPrioritised inbox, escalations first, keyboard shortcuts, bulk approval of low-risk items.
Cost is tracked from day oneEvery LLM call is stored with tokens, cost and latency; budget per lead.
Autonomy is earnedApproval rate per action type shows which work could stop needing a click.
AI in the core workflow

The LLM reasons. Deterministic code enforces.

The agent sits in the main path of every lead, not in a side panel. Four decisions make it safe to put it there.

1

Rules as data

Immutable versions with a live pointer. Every recommendation records the exact rule versions it used.

R-06 · Negotiation counter · v2
Seller counter within 5% 3% of our offer: accept. Otherwise counter and split the gap.
2

Stage-aware context

Only the live rules for the lead's stage, in priority order, plus facts from deterministic tools:

  • Pricing quote and its inputs
  • VIN decode vs. the seller's year, make and model
  • Possible duplicates in the window
  • Latest messages, redacted, with no contact details
3

Structured, explainable output

A JSON schema, validated with zod. Rule IDs must exist.

{
  "action": "counter_offer",
  "params": { "amount": 4075 },
  "reasoning": "Outside 3%…",
  "rule_version_ids": ["R-06 v2"],
  "confidence": 0.86,
  "checked": [ … ]
}
4

Human approval

The agent can only write a pending recommendation. It cannot act.

  • Approve, edit or reject, with a reason code
  • The diff between recommended and approved is stored
  • The action runs under the approver's identity
  • Employees can propose their own action, through the same guardrails
A deliberate line

Where AI enables, empowers, decides, or acts

Each action type has a level, stored as configuration, and shown in the app with the evidence behind it.

The employee leadsThe AI leads
AI informs · person decides

Enables

The agent surfaces what it found and steps back. Anything unsure goes to a person.

EscalationAsk the agent (read-only)
Approval: a person decides what happens
AI drafts · person owns the number

Empowers

Money moves only with a person. Employees approve up to $50k; above that, a manager.

Send first offerCounterAccept counterClose lead
Approval: employee up to $50k, manager above
AI decides · person confirms

Decides

Low-risk process steps. One click, or bulk approval above 0.9 confidence.

Request infoRequest documentsProof of ownershipFlag duplicateMove stage
Approval: employee, one click or bulk
AI acts on the employee's behalf

Acts

Not assignable today. The settings schema rejects it. An action type becomes a candidate only with evidence:

≥ 98% approved as is≥ 30 real decisions
Approval: none, once earned and switched on
Why Trust is earned per action type, from real decisions, not given to "the AI" all at once.
Architecture

One API, one worker, one database

SOURCES Revv page sell-my-car · React on Cloudflare Workers Employee screens React + Vite · inbox, leads, rules, experiments, metrics Simulator seller personas, autopilot, 36 golden scenarios Seller SMS / email Cloudflare Access Lead tool API Fastify · zod · sessions + RBAC Intake Leads & inbox Decisions Rules Experiments Metrics Ask the agent Admin Signed, idempotent intake · checkApproval on every decision (check #1) Action executor Runs as the approver · re-checks permissions (check #2) · audit Worker · agent orchestrator claims outbox rows · stateless · idempotent 1 Experiment variant (sticky) 2 Context: stage rules + tool facts 3 Pre-checks (duplicates, waiting) 4 LLM call, JSON schema output 5 Validate output (zod, rule IDs) 6 Guardrails: override / escalate 7 Store pending recommendation Never changes the lead and never messages the seller. Guardrail engine Pure functions in code: $50k manager route, ≤ asking price, confidence, amounts. Used by orchestrator, API and executor. Providers (interfaces) real implementation | deterministic fake LlmProvider · OpenRouter | Mock PricingProvider · Revv quote() v1 VinDecoder · Mock MessagingProvider · Real | Sim DocumentReader (OCR) · Mock Postgres append-only events, one schema everywhere (PGlite in tests) leads lead_events rule_versions · live agent_runs llm_calls recommendations decisions audit_log outbox messages · docs experiments · assignments metric views (SQL) API message event + outbox, one transaction claim row pending recommendation + telemetry
The flow, step by step

One seller reply, through every module

Live demo

One damaged 2016 Civic, from intake to a rule change

M
Maria · employee, Acquisitions
P
Priya · department head, owns the rules
S
Seller · simulated phone
Step 1

A damaged car arrives

No VIN. The agent cites R‑04 and asks for it. Maria approves; the text reaches the seller.

DecidesR-04
Step 2

First offer from the formula

VIN decoded and checked. Quote $3,750 with its breakdown. Maria edits to $4,000 with a reason.

EmpowersEdit + reason
Step 3

The seller pushes back

$4,150 is within 5%: the agent says accept. Maria rejects and counters $4,075 herself.

Override = dataR-06
Step 4

Deal, documents, audit

Accepted. The title shows another name: proof of ownership. Full history. $50k route on a Porsche.

R-08R-05G-01
Step 5

A rule changed live

Priya sets R‑06 from 5% to 3%, tests on sample leads, publishes. Same counter: the agent now does what Maria did.

Test → publishNo deploy
If time allows: experiments (5% vs 3%), autopilot at volume, dashboards, the AI map. Switch to the app →
Structure, data model, tests

A TypeScript monorepo with hard edges between modules

Structure

apps/
  api · Fastify routes, auth
  worker · outbox loop
  web · React screens
  simulator · personas, scenarios
  e2e · Playwright
packages/
  orchestrator · review pipeline
  agent · LLM, prompts, schema
  guardrails · pure checks
  rules · versions, publish
  executor · apply as approver
  pricing · messaging · experiments
  stats · db · shared · intake-schema

Data model

Everything is an event. The audit trail, metrics and experiment results are queries over the same tables.

leadslead_eventsrulesrule_versionsrule_liveagent_runsllm_callsrecommendationsdecisionsmessagesaudit_logexperimentsassignmentsoutbox
  • Append-only tables enforced by database triggers
  • Lead status is derived from pending items, never stored
  • Each run stores prompt version, model, context hash, rule versions

Tests that mean something

121Vitest files, on in-memory Postgres (PGlite)
36golden scenarios in YAML, one per rule and edge case
11Playwright journeys, including the full demo storyline
  • "Never runs without an approval"
  • "Blocks when the approver lost the authority"
  • "Turns invalid output into a could-not-assess escalation"
Safety and reliability

No prompt, rule edit or experiment can weaken the hard limits

Guardrails in code

Locked rules: admin-only, every stage, never overridden by a variant.

G-01Offers over $50k route to a manager
G-02Never offer above asking: blocked
G-03Confidence below 0.7, invalid output or no rule: escalate
G-04Amounts in the message must match the action
G-05Negotiation amounts computed in code, not by the model

Bad or uncertain output

1Strict JSON schema, validated with zod
2One retry, then a stored "could not assess" escalation
3Cited rule IDs must exist; values must be sane
4Provider down or timeout: escalate, never guess
5Invalid settings fail closed
6Permissions checked twice: at the click and at execution

Audit, privacy, cost

  • Audit trail: every event, agent run, LLM call and decision, by whom and when; never updated or deleted
  • Privacy: contact details never reach the model; messages are redacted first
  • Cost: tokens, cost and latency per call; $0.50 budget per lead with a warning; rate limit with backoff
  • Free CI: a deterministic mock LLM makes tests and the demo repeatable
Path to production

Off the legacy CRM in stages, with evidence at each step

1 · Shadow

The agent reviews, nobody acts

Leads copied from the legacy CRM. Measure agreement with what employees actually did.

2 · Assist

One team, one stage

For example New lead. The legacy CRM stays the system of record, synced both ways.

3 · Strangle

Move stage by stage

Until the legacy CRM is read-only, then retire it. No big-bang switch.

Scaling path

Demo · Docker Compose on one Droplet, behind Cloudflare. Pulumi.

Early production · managed Postgres, Valkey queue, workers scaled separately, SSO.

Larger · Kubernetes, workers autoscaled on queue depth, a warehouse.

The limit is LLM calls and the human queue, not compute. Stateless services and env config keep each move to infrastructure code.

Trade-offs I chose

ChoiceCostNext
Outbox, not a queueMore database load at volumeValkey behind the same interface
Mock LLM by defaultProves the flow, not reasoning qualityMeasure real models; model A/B first
Human approves allInbox is the bottleneckBulk approve, then earned autonomy
Dashboards in the appA new chart needs an engineerBI tool on the same SQL views