Skip to content

brutusdev0/client-intake-agent

Repository files navigation

client-intake-agent

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.


Why this exists

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 }
  ]
}

How it works

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.

Quick start

# 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:3000

With 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.


Configure your intake

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

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.


Lead destinations

Configure any combination — or none (leads log to the console in dev).

Webhook

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 }.

Supabase

Set SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY, then apply the schema:

# Supabase SQL editor, or:
supabase db execute --file supabase/schema.sql

Leads 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.

Add your own

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.


Deploy

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.


Project structure

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

Testing

pnpm test         # run the suite
pnpm typecheck    # tsc --noEmit
pnpm lint         # ESLint

The core logic — scoring rules, tool-schema generation, prompt assembly, and webhook signing — is covered by unit tests that run in CI on every push.

License

MIT © Rick (brutusdev0). See LICENSE.

About

AI chat intake agent that runs the first pass of your discovery calls. Qualifies leads, extracts structured answers with Claude, scores them hot/warm/cold, and delivers to Supabase + webhook. Deployable Next.js template, config-driven.

Topics

Resources

License

Stars

1 star

Watchers

1 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors