A deployable AI intake agent that runs the first pass of your discovery calls for you.
Instead of a dead contact form that dumps name / email / message into your
inbox, this is a short, friendly chat that qualifies the lead — asks the right
follow-ups, extracts structured answers, scores the opportunity, and drops a
hot / warm / cold summary into your database and/or a webhook.
It's a Next.js template you clone, edit one config file, and deploy. The shipped example is an AI-consulting discovery intake (the author's own), but the whole flow is driven by a single typed config — retool it for any business.
Discovery calls are where consultants and agencies burn the most time on the worst-fit leads. The first 10 minutes of nearly every call are the same questions: what do you do, what's the problem, how soon, what's the budget, are you the decision-maker. A plain web form can't ask a good follow-up, so you end up on a call to find out the budget was $500.
This template does that first pass automatically:
| Plain contact form | client-intake-agent | |
|---|---|---|
| Interaction | Static fields | A real conversation that adapts to answers |
| Missing / vague answers | You find out on the call | The agent asks a follow-up on the spot |
| Output | Free-text blob | Structured JSON keyed by field |
| Prioritization | You read every submission | Auto-scored hot / warm / cold with a reason breakdown |
| Where it goes | Your inbox | Supabase table + webhook (Zapier, n8n, CRM, …) |
Example. A visitor chats through the intake; the agent extracts:
{
"name": "Dana Reyes",
"company": "Northwind Logistics",
"email": "dana@northwind.co",
"problem": "Manually re-keying supplier invoices into our ERP",
"team_size": 40,
"timeline": "immediately",
"budget": "$20k-$50k",
"decision_maker": true
}…and scores it before it ever hits your inbox:
{
"score": 100,
"maxScore": 100,
"tier": "Hot",
"breakdown": [
{ "label": "Ready to start now", "points": 30 },
{ "label": "Strong budget", "points": 30 },
{ "label": "Is a decision maker", "points": 20 },
{ "label": "Team of 10+", "points": 10 },
{ "label": "Described a concrete problem", "points": 10 }
]
}Visitor ──chat──▶ Next.js UI ──POST /api/chat──▶ Claude (tool use)
│
extracts fields via the `submit_intake` tool
│
┌────────────────┼────────────────┐
score lead store in Supabase POST webhook
(pure, typed) (optional) (optional, signed)
Each turn is one turn-based (non-streaming) call to Claude with a
submit_intake tool whose JSON schema is generated from your configured fields.
The agent chats normally until it has everything it needs, then calls the tool —
that's the signal to score the lead and deliver it. Scoring is a pure,
fully-tested function, so the qualification logic is deterministic and doesn't
depend on the model.
- Model: Claude Sonnet 5 by default (override with
ANTHROPIC_MODEL). - Extraction: Anthropic tool use, schema derived from
intake.config.ts. - Persistence: Supabase (optional).
- Delivery: signed webhook (optional). Add your own destinations easily.
# 1. Clone (or use this repo as a GitHub template) and install
git clone https://github.com/brutusdev0/client-intake-agent.git
cd client-intake-agent
pnpm install
# 2. Configure environment
cp .env.example .env
# → set ANTHROPIC_API_KEY (required)
# → optionally set LEAD_WEBHOOK_URL / Supabase vars
# 3. Run
pnpm dev
# open http://localhost:3000With no webhook or database configured, completed leads are logged to the server console — so you can try the full flow with just an Anthropic key.
Everything lives in intake.config.ts. You edit this
file, not the code under lib/. It has four parts:
export const intakeConfig: IntakeConfig = {
business: { name: 'Rivet AI Consulting', agentName: 'Ava' },
greeting: "Hi! I'm Ava… what should I call you, and what company are you with?",
persona: 'Warm, concise, curious. Speaks like a consultant on a discovery call.',
fields: [
{ key: 'name', label: 'Name', type: 'text', description: '…', required: true },
{
key: 'budget',
label: 'Budget range',
type: 'select',
description: 'Rough budget they have in mind.',
options: ['under $5k', '$5k-$20k', '$20k-$50k', '$50k+', 'not sure yet'],
required: true,
},
// …
],
scoring: { rules: [/* … */], tiers: [/* … */] },
};Fields support text, longtext, email, number, select, and
boolean. The description is guidance for the model — be specific; it's what
the agent uses to decide what to ask and how to interpret answers. required
fields must be collected before the agent can submit.
Scoring turns extracted answers into a tier. Each rule awards (or subtracts)
points when a field matches a condition; the highest tier whose minScore
is met wins.
scoring: {
rules: [
{ field: 'timeline', equals: 'immediately', points: 30, label: 'Ready to start now' },
{ field: 'budget', oneOf: ['$50k+', '$20k-$50k'], points: 30, label: 'Strong budget' },
{ field: 'decision_maker', equals: true, points: 20, label: 'Is a decision maker' },
{ field: 'team_size', gte: 10, points: 10, label: 'Team of 10+' },
{ field: 'problem', present: true, points: 10, label: 'Described a concrete problem' },
{ field: 'timeline', equals: 'just exploring', points: -10, label: 'Just exploring' },
],
tiers: [
{ name: 'Hot', minScore: 70 },
{ name: 'Warm', minScore: 40 },
{ name: 'Cold', minScore: 0 },
],
}Supported conditions: equals, oneOf, includes (case-insensitive
substring), gte, lte, and present. String comparisons are
case-insensitive. The breakdown in the output lists exactly which rules fired,
so every score is explainable.
Configure any combination — or none (leads log to the console in dev).
Set LEAD_WEBHOOK_URL and every completed lead is POSTed there as JSON. Point
it at Zapier, n8n, Make, a CRM, or your own endpoint.
If you also set LEAD_WEBHOOK_SECRET, each request is signed with an
X-Signature: sha256=<hmac> header over the raw body. Verify it on your end:
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(rawBody: string, signature: string, secret: string): boolean {
const expected = `sha256=${createHmac('sha256', secret).update(rawBody).digest('hex')}`;
const a = Buffer.from(signature);
const b = Buffer.from(expected);
return a.length === b.length && timingSafeEqual(a, b);
}The webhook payload is the full lead: { createdAt, fields, score, transcript }.
Set SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY, then apply the schema:
# Supabase SQL editor, or:
supabase db execute --file supabase/schema.sqlLeads are inserted into a leads table (fields, score, tier,
total_score, transcript). The service-role key is used server-side only
in the API route — it never reaches the browser, and Row Level Security stays on.
Destinations live in lib/destinations/. Write a module
beside webhook.ts (email via Resend, a Google Sheet append, a CRM call) and
wire it into deliverLead() in lib/destinations/index.ts. Delivery is
best-effort per destination — one failing won't block the others or the visitor.
This is a standard Next.js app — deploy anywhere that runs Next 16. On
Vercel: import the repo, set the environment variables
from .env.example, and deploy. The /api/chat route runs on the Node.js
runtime.
intake.config.ts ← the file you edit (fields, persona, scoring)
app/
page.tsx server component → renders the chat
api/chat/route.ts one turn: run agent → score → deliver
components/Chat.tsx the chat UI (client component)
lib/
intake.ts builds the system prompt + submit_intake tool from config
llm.ts the Anthropic call for one turn
scoring.ts pure, tested lead scoring
db.ts Supabase persistence (optional)
destinations/ webhook + delivery orchestration
types.ts shared types
supabase/schema.sql the leads table
test/ Vitest unit tests for scoring, intake, webhook
pnpm test # run the suite
pnpm typecheck # tsc --noEmit
pnpm lint # ESLintThe core logic — scoring rules, tool-schema generation, prompt assembly, and webhook signing — is covered by unit tests that run in CI on every push.
MIT © Rick (brutusdev0). See LICENSE.