Lead Capture

Answering questions is table stakes. Capturing a visitor's contact details — and telling the bot owner the moment it happens — is what makes a business pay for this monthly instead of building a plain contact form.

Extraction

extractLeadInfo() runs a simple, deliberately conservative regex pass over each visitor message:

const EMAIL_REGEX = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
const PHONE_REGEX = /(\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}/;

This is v1 by design:

  • No name extraction — free-text name detection via regex is unreliable enough to be worse than not attempting it. If you want a visitor's name, have the bot's system_prompt ask for it directly as part of the conversation.
  • No LLM-based extraction pass — that would cost extra tokens per message for marginal gain over the regex approach at this stage.

Storage

Extraction runs once per assistant turn, after the response has streamed and before the request completes. If either an email or phone number is found:

  • An existing leads row for the same conversation_id is updated (new info fills in blanks, existing values aren't overwritten with nothing)
  • Otherwise a new row is inserted, scoped to bot_id and conversation_id
create table leads (
  id uuid primary key default gen_random_uuid(),
  bot_id uuid references bots(id) on delete cascade not null,
  conversation_id uuid references widget_conversations(id) on delete set null,
  name text,
  email text,
  phone text,
  metadata jsonb default '{}',  -- e.g. vehicle type, service requested
  created_at timestamptz default now()
);

The metadata column is a deliberate extension point — nothing writes to it yet, but it's there for structured intake fields per vertical (service type, preferred date, vehicle type, etc.) without a schema migration.

Owner notification

If the owner has RESEND_API_KEY configured and an email on file, sendLeadNotification() fires immediately after a lead is captured or updated — no batching or delay. For a high-traffic bot you may want to debounce this yourself before going live, since a fast back-and-forth conversation could otherwise trigger repeat emails as a visitor progressively shares more contact info.

Viewing leads

Bot owners see captured leads in /dashboard/bots/[botId] — name, email, phone, and captured date, scoped to that bot only via the RLS policy on leads (an owner can only select rows where the parent bots.user_id matches their own auth.uid()).

Related

  • Abuse Protection for how the request that triggers lead capture is itself protected from abuse
  • API Reference for the exact request/response shape of the chat endpoint this all runs inside