An autonomous SMS lead-qualification agent for real-estate acquisitions, built on Twilio + Claude with a Next.js dashboard.
Upload leads (with proof of consent), and the agent opens the conversation,
qualifies over multiple turns, follows up with silent leads on its own, and
hands hot leads to a human with a filled-in qualification card. See PRD.md
for the full product spec.
- Conversations produce structured CRM data automatically. As it texts,
the agent extracts timeline, condition, occupancy, price expectation, and
motivation into lead fields via its
update_lead_profiletool — visible live on the thread's qualification card. - AI-written follow-ups. Silent leads get contextual bumps referencing the
actual conversation (campaign-configurable delays, default 2/5/9 days). The
agent can also decide to give up; exhausted follow-ups mark the lead
no_response. - Human-in-the-loop. Pause any thread to take over; "Suggest" drafts an AI reply you can edit before sending. The agent escalates hostile/confused leads itself and pings Slack on hot leads and handoffs.
- Compliance in the send path. Consent timestamp + proof URL required at import; STOP opts out instantly and blocks re-import; HELP is left to Twilio's auto-responder; quiet hours (per-lead timezone) and per-campaign daily caps are enforced by the worker; delivery receipts are persisted.
- One considered reply per burst. Inbound texts are debounced ~20s so a lead firing four messages gets one reply that addresses all of them.
apps/
web/ Next.js 15 dashboard + Twilio webhook routes
worker/ BullMQ consumers: outreach (openers + follow-ups), agent-reply
packages/
db/ Prisma schema + client + demo seed
agent/ Claude agent loop + tool definitions
Postgres stores campaigns, leads, messages, agent-run audit log, and opt-outs.
Redis backs BullMQ. Inbound SMS → webhook persists the message → debounced
agent-reply job → worker runs Claude with tools → reply goes out through
Twilio. Openers and follow-ups flow through the outreach queue, which
re-delays itself for quiet hours, paused campaigns, and the daily cap.
Agent tools: send_reply, update_lead_profile, mark_hot_lead,
schedule_callback, mark_not_interested, flag_for_human.
cp .env.example .env # set ANTHROPIC_API_KEY and DRY_RUN="1"
pnpm install
docker compose up -d # postgres :5433, redis :6380
pnpm db:migrate
pnpm db:seed # demo campaign + 8 leads across the funnel
pnpm dev:web # http://localhost:3000
pnpm dev:worker # second terminalWith DRY_RUN=1 every send is logged by the worker instead of hitting
Twilio, so the full pipeline (upload → opener → follow-ups) runs with zero
credentials. The seed fills the dashboard, threads, and qualification cards.
- Fill
TWILIO_*,BUSINESS_NAME,PUBLIC_BASE_URLin.env; unsetDRY_RUN. - Expose the webhook (
ngrok http 3000) and point your Twilio Messaging Service inbound webhook at<url>/api/twilio/incoming. Status callbacks are requested automatically per message. - Create a campaign (it starts as
draft), upload a CSV, review, then flip the campaign toactive— openers only send while a campaign is active.
CSV columns — required: phone, consent_timestamp, consent_proof_url;
optional: name, property_address, property_city, property_state,
property_zip, lead_source, timezone.
- Register a brand + A2P 10DLC campaign in the Twilio console
- Enable Advanced Opt-Out on the Messaging Service (STOP/HELP auto-replies)
- Keep
BUSINESS_NAME+ opt-out language in the opener template - Verify STOP handling end-to-end with a real handset
- Confirm quiet hours match your jurisdictions (default 8am–9pm local)
pnpm typecheck # all workspaces
pnpm test # unit tests (quiet hours, follow-up parsing, message mapping)
pnpm db:studio # browse the database