The problem
HVAC operators miss roughly 35 calls a week. Each one is a $250 ticket walking out the door, call it $11K/month in recovered revenue at a 30% close rate. They know it. They just don't buy "AI receptionist" pitches because they've heard them, and a deck doesn't prove anything.
I needed a demo that answered the only real question: "does this thing actually sound like a receptionist on the phone?" A 90-second live call with the AI on the prospect's own phone, followed by a recording they can replay and a custom ROI estimate based on their numbers, that's a sales conversation that starts with conviction instead of skepticism.
Architecture
Seven stages spanning a Next.js form, HMAC magic links, Vapi outbound dial, two parallel n8n workflows (post-call + follow-up), and a daily cleanup cron. Numbers in code, judgment in the model. HTTP routes are the state machine.
Form Intake + Trust Scoring
A 5-field form on /voice-demo runs Turnstile, Zod validation, MX/disposable-domain checks, and per-IP/email/domain rate limits. Borderline trust scores get triaged by a Haiku call before any phone number is dialed.
Magic-Link Delivery
An HMAC-signed magic link (independent secret from the audit lane) lands in the prospect's inbox. The token is single-use; clicking is what consumes it, with an atomic UPDATE on consumed_at that rolls back automatically if the Vapi dial fails.
Outbound Dial → Live AI Call
Click the link → Vapi dials the prospect's phone. Casey, an HVAC-tuned AI receptionist, opens with a two-party-consent disclosure (legal in CA/FL/MA/PA/IL/WA), then handles a real booking conversation. 5-min hard cap and a $50/mo account spend cap keep cost predictable.
End-of-Call Webhook
Vapi's end-of-call webhook hits /api/voice/webhook with constant-time signature verification. The handler does an idempotent insert (unique vapi_call_id) and only fans out to n8n if this request was the inserter; over-emission is harmless because the routes own state transitions.
Post-Call Pipeline
n8n's Post-Call workflow extracts the transcript, runs two Claude calls with forced tool_choice (no fence-stripping), then computes ROI deterministically in a Code node. Claude can't be trusted with structured math even when the prose is right. The synthesized HTML POSTs to a Vercel function that returns a hosted PDF URL.
Dual Email Delivery
~5 minutes after hangup the prospect gets a recording + transcript + custom ROI PDF + an NPS feedback row. I get a tier-prefixed internal email (hot/qualified/marginal/unqualified) with conversation quotes and signal extraction so I know who's worth following up with.
Day-3 + Day-10 Follow-Up
An hourly Schedule Trigger fans to two parallel branches (not sequential, since sequencing would let an empty Day-3 block Day-10). Each branch nominates candidates; the HTTP route re-checks unsubscribed_at + replied_at + ai_tier before sending and advances state via guarded UPDATE. Daily 03:00 UTC cron auto-purges Vapi recordings older than 30 days.
The actual workflows
Both n8n graphs are running in production. The first ingests Vapi's end-of-call webhook and emits the conversion email pair; the second handles Day-3 + Day-10 nudges.
Voice Demo · Post-Call
17 nodes. Verify Vapi secret → idempotent insert → Claude Analysis (forced tool_choice) → deterministic ROI math → PDF render → 2-email send.
Voice Demo · Follow-Up
9 nodes. Hourly trigger fans into 2 parallel branches (Day-3 + Day-10) so an empty Day-3 never blocks Day-10. The HTTP route owns all state transitions.
Key decisions
The choices that separate this from a generic Vapi demo.
Why a self-serve voice demo instead of a sales call
Service operators don't need to be sold AI; they need to feel it. A 90-second live call where the AI asks for their address, books a slot, and confirms the appointment beats any pitch deck. The prospect arrives at the sales conversation already convinced, which compresses the cycle from weeks to days.
Why HTTP routes own state, not n8n
n8n is a scheduler that nominates candidates; every route re-checks every precondition (unsubscribed, replied, status, age, tier) before acting. Over-emission from n8n is harmless: duplicate nominations get rejected by the route. This makes the system crash-safe and easy to reason about: state lives in Postgres with guarded UPDATEs, not in workflow execution memory.
Why ROI math runs in code, narrative runs in Claude
Claude can write a beautiful paragraph and emit a number that doesn't add up, even when forced into structured output. So Claude does the qualitative read of the transcript (signals, quotes, tier) and a deterministic Code node does the arithmetic from extracted inputs. Numbers in code, judgment in the model.
Why hard caps are non-negotiable
Vapi $50/mo account ceiling + 5-min per-call cap + 1 demo per email/7d + 1 per IP/24h + 1 per domain/7d. The unit economics survive abuse: at 50 demos/month the bill is ~$25, and a malicious actor can't burn more than $50 in a month. This is what makes the demo public-facing without supervision.
Precautions baked in
This dials real phones, records real conversations, and emails real prospects. The guardrails are the product.
- HMAC-signed magic links with secret independent from the audit lane: token leak in one product can't compromise the other
- Two-party-consent disclosure spoken at call start so the recording is admissible in CA/FL/MA/PA/IL/WA
- 30-day recording retention with automated daily cleanup cron: privacy-by-default
- Constant-time verification on Vapi webhook signatures: timing attacks on the shared secret fail silently
- Atomic single-use UPDATE on magic-link consumption: race conditions can't double-dial a prospect
- Trust scoring + Haiku triage on borderline submissions before any paid call is placed
Outcome
- Form fill → live AI phone call in under 60 seconds, fully self-serve
- Recording + transcript + custom ROI PDF in the prospect's inbox ~5 min after hangup
- Hard cap of $50/mo on Vapi spend; expected operating cost ~$25/mo at 50 demos
- Both n8n workflows active in production with synthetic + live-call validation
- Day-3 nudge and Day-10 breakup automated based on tier, with unsubscribe + reply guards
- Daily 03:00 UTC cron auto-deletes recordings older than 30 days: zero manual hygiene
Hear what an AI receptionist sounds like on your phone
Drop your number. The AI calls you in seconds. Recording + custom ROI estimate hits your inbox after.