Skip to content
← back

2026

Davis

Evidence-grounded AI sales development agent

The problem

Outbound sales is hours of manual research per prospect: finding the right people, reading what they have posted, working out what they actually care about, then writing an email that does not sound automated. Most tooling automates the sending and skips the thinking. The tools that do score prospects hand you a number with nothing behind it, so nobody trusts it, and an agent that can send email on your behalf is one bad generation away from embarrassing you in front of a customer.

What I built

A multi-tenant AI sales development agent that finds prospects, researches them, and drafts personalised outreach, where every prospect score cites a source URL, a snippet and an observed-at timestamp, so a human can check the evidence behind any number. Sending is approval-first: nothing leaves the system until a person approves it, and a policy guard makes duplicate sends impossible.

Architecture

Next.js · dashboard, prospects, email editor, approvals queue
        ↓
FastAPI · 68 endpoints across 17 routers
  ├── LangGraph   multi-stage research → signal extraction → draft
  ├── OpenAI      LLM inference
  ├── Pinecone    per-workspace namespaces, vector memory
  ├── Python      deterministic scorer, 6 dimensions summing to 100
  └── Braintrust  tracing over the whole pipeline
        ↓
Supabase · Postgres + auth · 40 RLS policies over 45 tables
Redis · cache & queues
        ↓
Vercel (frontend) · Railway (backend)

Highlights

Engineering decisions

Split scoring into LLM signal extraction plus a deterministic Python scorer

instead of Asking the model for the score directly

A number straight out of an LLM is not reproducible, not unit-testable, and cannot be explained to the salesperson relying on it. Letting the model do only what it is good at, pulling signals out of messy text, and handing the arithmetic to 6 weighted Python dimensions summing to 100, means identical inputs always give byte-identical scores, the weights are visible and tunable, and the scorer has real tests.

Approval-first sending with a unique idempotency key

instead of Letting the agent send autonomously

An agent with unsupervised send access is one bad generation away from damaging a real customer relationship, and a retry after a timeout is one duplicate away from doing it twice. An explicit pending → approved → sent state machine keeps a human in the loop, and a unique key on the send makes duplicates impossible at the database level rather than 'unlikely' at the application level.

Row-level security in Postgres for tenant isolation

instead of Filtering by tenant ID in application code

With 45 tables, application-level filtering means every future query is a chance to leak another organisation's pipeline, the worst possible bug in a sales tool. Pushing isolation into RLS policies makes the database refuse cross-tenant reads regardless of what the application asks for, so correctness does not depend on remembering a WHERE clause.

Evidence attached to every score, not just a number

instead of Surfacing a bare confidence score

Salespeople ignore scores they cannot interrogate. Storing the URL, the snippet and the observed-at timestamp behind each signal makes the score checkable, makes stale research visible, and turns a debugging session from 'why did it say 82?' into reading the three sources it actually used.

Unsubscribe recall gated in CI at 1.0

instead of Tracking classifier accuracy on a dashboard

Missing an opt-out is not a quality regression, it is a compliance failure and a person who asked to be left alone being contacted again. Of the 12 intents the reply classifier handles, that is the one where a single miss is unacceptable, so the release fails rather than the metric dipping quietly on a chart nobody opens.

Draft only from a human-approved claims table

instead of Telling the model in the prompt not to invent things

A prompt instruction is a request, and the one time it is ignored, a false claim about a prospect's company goes out over a real person's name. Restricting the drafting context to claims a human has approved makes unverified personalisation unreachable by construction: the model cannot state something it was never given.

Stack