Skip to main content

Local Business Cold-Email Engine — Operator Runbook

Verified: 2026-06-11

What this is

The church cold-email demo mechanism (personalized demo website + AI chat/voice baked in per prospect, founder-pressed email with the demo link) extended to LOCAL SERVICE BUSINESSES — plumbing, roofing, vet, hvac, salon, etc. — under the WiseAI Agency brand. It is a generic engine built ALONGSIDE the church one: the church UnifiedTemplate and church voice paths are never touched. Offer (founder-locked 2026-06-07): AI Front Desk, $0 setup ($1,695 setup waived), then $395/mo (CAD), no free months. Pricing source is src/lib/local-business/package-config.ts + currency.ts — NOT PRICING.md (known gap). All paths below are in C:\dev\churchwiseai-web unless noted.

Phase status

PhaseWhatStatus
0–3Demo engine: /s/{slug} business demos, per-trade theming, demo chat with full safety harnessDONE — PR #799 merged 2026-06-07, live demos verified (plumbing/roofing/vet)
Visuals/polishPer-trade hero/separator imagery, 15 trades in C:\dev\lb-assets\DONE (founder shooting custom hero MP4s later)
4Lead sourcing + master campaign_members send path + CASL unsubDONE — PR #808 merged; send route live on prod
5$0-setup cold emailDONE — campaign ee27023a (Home Services, London/Oxford): 70/70 SENT 2026-06-09, founder-pressed (5 bounces parked skipped)
6Voice on prospect demosDemo voice capture works — capture_lead fix shipped (PR #832 + voice deploy fjPBWSPgPuQX, verified end-to-end 2026-06-09). Demo-leads surface + founder alert shipped PR #868 (2026-06-11)

Architecture

  • Demo render: src/app/s/[slug]/page.tsxgetLocalBusinessDemoData(slug) (fallback chain church → funeral → local-business) reads local_businesses + local_business_setup_profiles, shims into the PremiumChurch shape, renders via src/components/templates/HomeServicesDemo.tsx (on the ServiceBusinessTemplate seam funeral uses).
  • Per-trade config: src/lib/local-business/demo-config.ts (palette/labels/persona) + business-video-library.ts (hero/slideshow/separator media keyed by metadata.category).
  • Demo chat: src/app/api/chatbot/demo/route.ts handleBusinessDemo() — additive branch reusing the FULL church safety harness (pre-LLM crisis patterns → 988, injection refusal, leak canary). Church path byte-identical.
  • Demo voice: voice-agent-livekit/verticals/local_business/ + main.py:_build_local_business_prospect_pathLocalBusinessProspectCoordinatorAgent (demo capture_lead tags metadata.is_demo=true, NO owner-notify; endpoint POST /api/voice/local-business-lead skips notify when is_demo). Paid LocalBusinessCoordinatorAgent untouched.
  • Provisioning: src/lib/outreach/provision-local-business.ts provisionLocalBusinessDemo() — generates services/faqs/hours/staff via claude -p CLI (Rule #5 hygiene: deletes ANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKEN/CLAUDE_CODE_ENTRYPOINT/CLAUDECODE from spawn env, prompt via stdin not shell arg, greeting-marker guard, quality gate + category-default fallback + rollback). Runners: scripts/business-prospects/provision-one.mjs "Name" trade "City" "ON" and provision-batch.mjs.
  • Data model: leads / lead_organizations / lead_events / campaign_members (NOT the church outreach_contacts model). Suppression spine: email_unsubscribersunsub_sync trigger → leads.is_suppressed. Resend webhook matches campaign_members and bounces flip leads.is_suppressed (PR #866).
  • Asset intake: founder drops images into C:\dev\lb-assets\<trade>\ (hero ~1.78 AR, separator ~2.5 AR) → optimize via ffmpeg → public/templates/<trade>/ → register in business-video-library.ts.

How to run a batch

  1. Source leads to a JSONL in C:\dev\lb-leads\ (see lb-leads\SCHEMA.md), then stage: scripts/business-prospects/stage-campaign.mjs — upserts leads/lead_organizations (origin_property='wiseaiagency', origin_source='cold_scrape'), creates the campaign + campaign_members; suppressed leads are skipped.
  2. Provision demos: scripts/business-prospects/provision-one.mjs (or provision-batch.mjs). Demos render at churchwiseai.com/s/{slug}.
  3. Draft emails via the single-source renderer src/lib/email.ts:renderBusinessProspectEmailHtml (re-render with scripts/business-prospects/rerender-drafts.mjs). NEVER SQL-patch draft_body. Golden guard: src/lib/outreach/business-golden.ts (forbids church prices $49.95/$54.95; TRADE_NOUNS fixes "plumbings" → "plumbers").
  4. NeverBounce: scripts/verify-campaign-members-emails.mjs --apply (writes leads.nb_result/nb_checked_at). Deliverable = valid|catchall|unknown; move bounces + seeds to status='skipped' + skip_reason — the preflight is a whole-batch blocker, so un-sendable rows must LEAVE the drafted set first.
  5. Founder preview: scripts/preview-campaign-drafts.mjs (HTML previews, e.g. C:\dev\lb-leads\real-draft-previews\) and/or scripts/business-prospects/send-founder-preview.mjs.
  6. Dry-run on prod: POST https://churchwiseai.com/api/founder/campaigns/<id>/send?dry=1&token=<FOUNDER_TOKEN> (SSE; expect preflight event pass:true; writes/sends nothing). Pull the token via vercel env pull; never print it.
  7. FOUNDER presses send: same POST without ?dry=1. Throttle 30–60s, 520s wall budget → ~12–15 sends per call, returns partial/willContinue — re-fire until complete and partial:false. Idempotent: each member is stamped sent_at + resend_message_id, re-fires never double-send. From address: WISEAIAGENCY_OUTREACH_FROM (john@wiseaiagency.com, Vercel prod). NOTE: the outreach-engine UI reads outreach_contacts and shows these campaigns EMPTY — send via the route.

Send gates & guardrails (absolute house policy)

  • Outbound sends are founder-pressed ONLY. NO cron or auto-send, ever. DB writes / NeverBounce / re-renders are fine without blessing; sends always need founder preview + blessing.
  • Send route: src/app/api/founder/campaigns/[id]/send/route.ts (+ src/lib/outreach/{send-members,campaign-members-preflight,business-golden}.ts). Founder-token POST only (GET → 405); mandatory NeverBounce-required preflight; sends pre-rendered draft_subject/draft_body VERBATIM.
  • Every LLM step in batch scripts uses claude -p CLI — never the Anthropic/OpenAI API and never /api/chatbot/* endpoints — with ANTHROPIC_API_KEY (and auth-token/entrypoint vars) deleted from the spawn env, prompt via stdin. Batch >100 prospects with LLM → ask founder with a cost estimate first.
  • Business signature is "John Moelker / WiseAI Agency" (NOT the church pastor signature); CASL footer + ChurchWiseAI LTD address + unsubscribe required; pricing must be WiseAI Agency pricing (golden check enforces).
  • Strictly additive: never touch church UnifiedTemplate or church/funeral/vet voice paths. moderation.py is LIFE-SAFETY — untouched.

Verification

  • Demo live: open churchwiseai.com/s/{slug} (Playwright, not curl) — populated services/staff/imagery, chat answers in-trade.
  • Drafts: SQL over campaign_members — every draft contains $1,695 + $395/mo + $0 setup, zero church prices, golden-required present / forbidden absent.
  • Preflight: prod dry-run (?dry=1) shows pass:true, sendable:N, all check counts 0.
  • After send: members stamped sent_at+resend_message_id; bounces/complaints flow via the Resend webhook into leads.is_suppressed.
  • Demo engagement: local_business_leads WHERE metadata->>'is_demo'='true' / GCC /founder/[token]/growth/demo-leads.

Open work

  • Phase 6 (full voice productization) for paid LB customers beyond the demo path — pending.
  • Outreach-engine UI can't display/send campaign_members-model campaigns (reads outreach_contacts only) — wire to the new model or UNION.
  • Manual-unsubscribe route/UI covers only the old outreach_contacts/churches model; LB unsubs are done via SQL insert into email_unsubscribers (trigger does the rest).
  • Code-hygiene patch C:\dev\lb-leads\email-pricing-fix.patch (renderer offerLine + golden guard) — DB drafts corrected in place 2026-06-07; confirm the patch landed on main before drafting a new batch (re-render + golden check will catch it either way).
  • WiseAI Agency pricing not in PRICING.md (source remains package-config.ts).
  • Prospect browser voice calls land with empty classification metadata (non-blocking).