A complete, working implementation of this recipe — downloadable as a zip or browsable file by file. Generated by our build pipeline; tested with full coverage before publishing.
You’ll build a lead intake API for a small business — a Next.js endpoint that accepts web form submissions, classifies them using an AI agent mesh, sends SMS alerts for high-priority leads via Twilio, and hands off promising leads to your sales team. By the end you’ll have a working /api/leads route with auth, validation, AI classification, SMS notification with retries, handoff ticket creation, and memory-backed lead storage.
Prerequisites
Node.js >= 22 — check with node --version
pnpm 10.7.1 — install with npm install -g pnpm@10.7.1
An OpenRouter API key (via OPENAI_API_KEY env var) for agent memory embedding and extraction
A Twilio account with an active phone number, Account SID, and Auth Token for SMS notifications
Familiarity with TypeScript and Next.js App Router routing
Step 1: Scaffold the project
Create a new Next.js project with the App Router and TypeScript. We’ll also initialize a .gitignore so build artifacts and secrets don’t get committed.
Open package.json and replace the generated dependencies and scripts with the exact setup this project needs. Here’s the complete file — overwrite yours:
You should see pnpm install all dependencies and link binaries into node_modules/.bin.
Step 3: Configure TypeScript, ESLint, and Vitest
Replace the default tsconfig.json with strict settings targeting ES2022 and NodeNext module resolution — what Next.js 14 App Router expects in server code:
This logger is imported by every other module in the project. It uses environment-aware formatting and supports child loggers for per-module namespacing.
Step 6: Build the lead intake API route
This is the heart of the app — a POST /api/leads endpoint that orchestrates the entire pipeline: auth, validation, AI classification, routing, SMS notification, handoff, and memory storage.
Create src/app/api/leads/route.ts:
ts
import { type NextRequest, NextResponse } from "next/server";import { z } from "zod";import { proxyRequestSchema } from "@reaatech/agent-auth-proxy-core";import { handleInternalRequest } from "@reaatech/agent-mesh-gateway";import { sendSms } from "../../../lib/notify.js";import { createHandoffTicket } from "../../../lib/handoff.js";import { storeLead } from "../../../lib/memory.js";import logger from "../../../lib/logger.js";const LeadSubmissionSchema = z.object({ name: z.string().min(1).max
Here’s what it does step by step:
Reads the x-api-key header and validates it through @reaatech/agent-auth-proxy-core’s proxyRequestSchema
Parses the request body as JSON; returns 400 for malformed JSON
Validates the body against a Zod schema (name, email, phone (optional), message, source)
Passes the validated lead to handleInternalRequest from @reaatech/agent-mesh-gateway, which invokes the classifier and router
Reads the classification confidence and routing action from the gateway response
If confidence < 0.5 or the router asks to clarify: stores the lead in agent memory and returns priority: "low"
If confidence >= 0.5 and routing action is "route": sends an SMS via Twilio (if a phone is present), creates a handoff ticket, and returns priority: "high"
Any unhandled exception returns a generic 500
Step 7: Write the supporting library modules
The route depends on three library modules — create them now.
SMS notification with retries (src/lib/notify.ts)
This module wraps the Twilio client with phone validation and a retry loop (up to 3 attempts with exponential backoff):
ts
import { setTimeout } from "node:timers/promises";import twilio from "twilio";import { z } from "zod";import logger from "./logger.js";const PhoneSchema = z.string().regex(/^\+[1-9]\d{1,14}$/);export class NotifyError extends Error { constructor(message: string) { super(message); this.name = "NotifyError"; }}const MAX_RETRIES = 3;const INITIAL_DELAY_MS = 1000;const client = twilio( process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN,);export async function sendSms(to: string, body: string): Promise<void> { const phoneResult = PhoneSchema.safeParse(to); if (!phoneResult.success) { throw new NotifyError( `Invalid phone number format: ${phoneResult.error.message}`, ); } let lastError: Error | undefined; for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { try { const from = process.env.TWILIO_PHONE_NUMBER as string; const message = await client.messages.create({ body, to, from, }); logger.info("SMS sent successfully", { sid: message.sid, to, attempt, }); return; } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); logger.warn("SMS send attempt failed", { error: lastError.message, to, attempt, }); if (attempt < MAX_RETRIES) { const delay = INITIAL_DELAY_MS * 2 ** (attempt - 1); await setTimeout(delay); } } } throw new NotifyError( `Failed to send SMS after ${MAX_RETRIES} attempts: ${lastError?.message ?? "Unknown error"}`, );}
Handoff ticket creation with retry (src/lib/handoff.ts)
This module creates handoff tickets using @reaatech/agent-handoff’s withRetry utility, which provides exponential backoff:
ts
import { withRetry, HandoffError,} from "@reaatech/agent-handoff";import { z } from "zod";import logger from "./logger.js";const LeadDataSchema = z.object({ name: z.string().min(1, "Name is required"), email: z.string().email("Valid email is required"), phone: z.string().optional(), message: z.string().min(1, "Message is required"), source: z.string().min(1, "Source is required"),});let ticketCounter = 0;export async function createHandoffTicket( leadData: { name: string; email: string; phone?: string; message: string; source: string; }, agentId: string, priority: string,): Promise<string> { const parsed = LeadDataSchema.safeParse(leadData); if (!parsed.success) { throw new HandoffError( `Invalid lead data: ${parsed.error.message}`, "validation_error" as never, ); } const ticketId = await withRetry( async () => { await Promise.resolve(); ticketCounter++; const id = `ticket-${Date.now()}-${String(ticketCounter)}`; logger.info("Handoff ticket created", { ticketId: id, leadData: { name: parsed.data.name, email: parsed.data.email, phone: parsed.data.phone ?? undefined, message: parsed.data.message, source: parsed.data.source, }, agentId, priority, }); return id; }, { maxRetries: 2, backoff: "exponential" as const, baseDelayMs: 200, maxDelayMs: 5000, shouldRetry: (error: unknown) => error instanceof Error, }, ); return ticketId;}
Lead memory storage (src/lib/memory.ts)
This module stores and retrieves lead data using @reaatech/agent-memory with OpenAI embeddings:
The storeLead function converts lead data into a ConversationTurn, then calls extractAndStore which uses the configured LLM to extract facts and preferences. retrieveLeadContext retrieves up to 5 historical memories for a given lead email.
Step 8: Run type checking and linting
Before running the app, verify everything compiles and passes linting:
terminal
pnpm typecheck
Expected output: no errors — the command completes silently.
terminal
pnpm lint
Expected output: no lint errors. If any warnings or errors appear, address them before continuing.
Step 9: Run the tests
The project includes a comprehensive test suite covering every module. Run it with:
terminal
pnpm test
You should see Vitest run all test files and produce a coverage report. The key things to look for:
tests/route.test.ts — 13 tests covering the API route: auth failures (401), validation errors (400), high-priority leads (200 with SMS + handoff), low-priority leads (200 with memory storage), gateway failures (500), boundary conditions like confidence=0.5, missing classifications, and non-standard routing actions
tests/handoff.test.ts — 6 tests for ticket creation with valid data, validation failures, retry behavior, and option passing
tests/notify.test.ts — 5 tests for SMS sending: valid E.164 format, invalid phone rejection, Twilio failures, retry success on the third attempt, and exhaustion after all retries
tests/memory.test.ts — 3 tests for lead storage, context retrieval by email, and minimal data handling
tests/logger.test.ts — 2 tests confirming the default logger export and createChildLogger
The coverage report should show 90%+ across lines, branches, functions, and statements.
Step 10: Start the dev server and test the endpoint
Launch the Next.js dev server:
terminal
pnpm dev
Expected output:
code
▲ Next.js 14.2.21
- Local: http://localhost:3000
✓ Ready in 2s
In a second terminal, send a test lead submission:
terminal
curl -X POST http://localhost:3000/api/leads \ -H "Content-Type: application/json" \ -H "x-api-key: dev-key" \ -d '{ "name": "Jane Smith", "email": "jane@example.com", "phone": "+15551234567", "message": "I need a quote for your premium plan", "source": "web" }'
Depending on the classifier output, you’ll get either "priority":"low" with "nextSteps":"stored_for_followup" or "priority":"high" without an SMS (since no phone was provided, only the handoff ticket is created).
Try an invalid submission to see validation errors: