Finish the Lead Machine

One SOW to take the concrete-leveling lead pipeline from "proven today" to "runs itself, feeds Dan's CRM, nothing slips." Built for a fresh session — all context included below, nothing assumed.

Written 2026-07-28 after the rail was proven live (test SMS + 14 lead packets delivered from the 626 number). Rule of the month: everything serves concrete-leveling leads for Dan.

Where things stand (proven, not assumed)

  • Lead delivery works end-to-end: lead qualifies → queued the same second → SMS from (626) 325-0175 to Akash — carrier-confirmed, alarmed if it ever fails.
  • All 14 real leads re-sent to Akash today in executive format. Nothing has ever been sent client-facing.
  • Dan's DripJobs credentials are already in the vault (both the direct lead-intake webhook URL and his API key, received Jul-27) — the CRM pipe needs wiring, not asking.
  • Every session captured & archived; deliverables indexed on the close-out page.
Executor note: this SOW is self-contained. Secrets are referenced by vault label only — load the brain helper (.codex/references/brain.py) and fetch by service/scope/label. Never paste secrets into pages or chat.

A Conversation audit — every error found, and its status

Source: full re-read of all 14 lead conversations (Jul 22–27) + delivery records + incident ledger.

Error foundLeads hitStatus
Bot promised "estimator will reach out" — nobody ever did (rail was dead)Ann Z., Scott E., + most of Jul 22–26rail fixed calls owed
Bot re-asked for info the lead already gave (Ann: "I sent this before")Ann Z.bot rule added Jul-23
Info given in chat never saved to the record (ZIP lost)Nathan G.backfill → WS-3
Duplicate lead recordsNathan G. ×2merge → WS-3
Project words written into city/ZIP fields ("Driveway" as a city)Twyler W., John M.cleaned + extractor guards Jul-27
Transfers queued for leads who declinedBradford C., Mark C.gate live — both correctly blocked; declines re-verified genuine
"Sent" recorded while the message went to a dead channel (old iMessage path)Sam E., Spencer J.channel killed; 626 rail live books mismatch → WS-6
Leads stuck silently with no contact info, no alertTwyler W., Mike Y.watchdog live rescue cadence → WS-4
Follow-up nudges never fired for one silent leadTwyler W.investigate → WS-3
Lead contact records deleted from the CRMCindy S., Richard W.re-create → WS-3
Fake "leads" minted from internal message threads3 internal contactsguard live Jul-27 root cause in CRM UI → WS-4
Job photos/videos not mirrored to durable pagesJoseph, Scott, Spencermirror → WS-3
Internal debug text leaking into packet notesSam E., Spencer J. packetsscrub template → WS-3

1 Instant delivery agent

Outcome: lead-to-phone in seconds, not "up to 5 minutes." Akash already said build it.

  • Add a webhook trigger to the sender workflow; database calls it the instant a transfer is queued.
  • Keep the 5-minute sweep as the safety net (event first, thin loop as backstop).
  • Prove with a synthetic lead: stopwatch from "qualified" to phone buzz. Target < 30 seconds.
Technical context Sender = n8n workflow 6ET0nCwUKY35WBwR "SP Lead Transfer Drain (GHL SMS)" on eyefly.app.n8n.cloud (n8n API cred: vault n8n/agency/rest-api-cowork-claude). Enqueue trigger = Postgres sp_enqueue_qualified_lead_transfer_trg on sp_client_leads (ledger 91882). Options for the push: pg_net HTTP call from the trigger, or a Supabase database webhook on INSERT into sp_client_lead_transfers → new n8n webhook trigger node that runs the same claim→send→verify chain. Test lead recipe: insert into sp_client_leads with source='meta_messenger', meta_page_id='1308868515634109', qualification_state='qualified', is_test=TRUE, metadata.allow_test_transfer='true'. Ledger every change; reversible.

2 DripJobs direct — leads onto Dan's board agent screenshot gate

Outcome: every qualified lead lands as a deal card on Dan's DripJobs pipeline in seconds — Akash's 626 text keeps arriving in parallel. Zero client-facing sends until the gate passes.

  • Wire the vaulted direct lead-intake webhook (Angi-format — Dan already provided it Jul-27) as a second delivery destination, disabled.
  • Extend the sender to speak that format: name, phone, email, city/ZIP, job description (exec summary), lead source "St. Pierre – Facebook", media page link, street address when known.
  • Fire ONE test card "SP-Test Booking DeleteMe" → screenshot to Akash → his approval flips it on.
  • Parallel ask to Dan (bundle with service-area text): copy his public Booking Form link (Settings → Booking Form) so leads can self-schedule — then the link rides in every packet.
Technical context Vault: dripjobs/levelking-webhook = direct lead-intake POST endpoint (Angi-format; anyone with the URL can inject leads — treat as secret, never print). dripjobs/levelking = API key (untested at capture). Field map lives in the Dan guide page + 01-Projects/St_Pierre_Digital/lead-transfer-automation/dan-dripjobs-zapier-setup.md (Zapier path now OBSOLETE for lead intake — the direct webhook replaces it; keep the doc's field map). Yesterday's working context: session "Lead transfer weekend issue" (transcript 60f9290a…jsonl, ~16:14–17:19 block) — card auto-creation via the webhook was proven there. Destination table: sp_client_delivery_destinations (add channel='webhook', provider='dripjobs', is_enabled=FALSE). Drain currently handles channel='sms' only.

3 Fix everything the audit caught agent

Outcome: every open row in the audit table above flips to resolved, with proof.

  • Backfill Nathan G.'s ZIP (75022, sitting in his chat) + merge his duplicate record.
  • Re-create Cindy S. + Richard W. as CRM contacts from their saved data; find out what deleted them (Akash's own contact was also deleted+recreated Jul-26 — likely same event).
  • Mirror Joseph A. / Scott E. / Spencer J.'s job media to durable per-lead pages; write each page URL to the lead + CRM field. Approved format = one media page link per lead in the packet — never raw file attachments.
  • Scrub internal debug text from the packet notes template (no "enriched from thread…" lines).
  • Investigate why one silent lead never got follow-up nudges when the other did (follow-up config predates her thread?).
Technical context Media mirror: POST https://sp-lead-media.stpierre.workers.dev/mirror body {slug, firstName, city, service, capturedAt, attachments:[{url,type}]}, header x-sp-media-secret (vault cloudflare/stpierre/sp-lead-media-mirror-secret), browser User-Agent required or Cloudflare 403s. Source attachment URLs (GHL durable) are already extracted in scratchpad main-resend-convo-live.json of session b5d855a5 (also re-fetchable via conversations API; GHL PIT via brain.get_pit(location_id='6c4MSgjdfngrPhwWD2cb'), Version 2021-07-28 + Chrome UA). Media page URL → GHL custom field sp_media_link (h6TB4j6dRtIBJda0K61b) + sp_client_leads.metadata.media_url. Lead ids + contact ids in the same scratchpad file. Notes scrub: the drain's formatPacket() already strips phone/zip — extend cleanNotes to drop "Qualified via Messenger bot…/enriched from…" sentences at the SOURCE (project_details writer in the bridge enrich node).

4 Your clicks — CRM & bot Akash, ~20 min total

Outcome: the bot triggers match what we designed, no stray automations, silent leads get rescued.

  • Save the Handoff trigger condition (the 496-character version — phone AND location AND final message, decline guard, default-no).
  • Paste the 6-step follow-up cadence (each step chases the missing phone/ZIP; steps 5–6 best-effort past Messenger's 24h window).
  • Add the hard contact-info gate + verify the "qualified" tag step — numbered click-paths in the audit pack (Items 1–3).
  • Flip 6 workflows to draft (list in collapsed notes) — kills the stray-automation class incl. the internal-contact re-fire path.
  • Clear the 4 stale needs-review tags.
The 6 draft toggles + the trigger text + cadence copy

Draft these 6 (workflow list page toggles): SP — Pre-Call Nurture (12) · Trigger 2: Qualification Form → Remove from Confirmation Dialer · Trigger 2b: Unqualified → Remove from all workflows · SP - Launch Call Booked Tagger · SP - Launch Prep Packet Task · duplicate "z. Standard: Unsubscribe" (keep ONE live). Do NOT touch: the five *-numbered workflows, "Qualified Tag → Lead Submitted Event", the SP Shim forwarders, the DND pair, the live Unsubscribe, the contractor demo.

Handoff trigger (496 chars, paste exactly):

Trigger ONLY when ALL are true from what the lead actually typed in THIS conversation: (1) they named a service we offer (foundation repair, waterproofing, concrete leveling, crawl space); (2) they confirmed a location - city or ZIP; (3) they provided a valid phone number; (4) the bot has sent the final handoff message to schedule the estimator. Do NOT trigger if the lead declined or disqualified (not interested, project changed, already handled it, no thanks). When in doubt, do NOT trigger.

Cadence (6 steps): 5m "still there?" · 30m "no rush — whenever you're back, just drop your zip and best phone number and i'll get you on the estimator's schedule" · 4h "want me to just have the estimator give you a call? drop your number here and i'll set it up" · 1d "should i close this out or did you still want help?" · 2d "we're scheduling estimates in your area this week — still want yours? just need your zip + number" · 3d "last check-in — if the timing's off, no worries. reply whenever and i'll pick it right back up"

Canonical trigger texts live in the atlas: Build-Plans/ghl-atlas-text-extraction/wf-live-panels-jordan-bot-goals-2026-07-26.md (corrected verbatim Jul-28). GHL UI limit: trigger conditions 10–500 chars, revalidated on any edit.

5 Dress rehearsal — the one test that proves everything agent after WS-4 trigger save

Outcome: a fake homeowner DMs the Facebook page → Jordan qualifies → packet on Akash's phone (and DripJobs card if WS-2 is live) with zero manual pushes. Timed, logged as the official proof.

  • Run the synthetic Messenger conversation end-to-end; record per-hop timestamps.
  • Then let the 3-day Dallas bot watch (already running as a board card) accumulate real-lead proof.
Technical context Level King page id 1308868515634109 (m.me link on file). Prior "proofs" failed by testing single links — the standard (set Jul-28): no transfer claim without a full synthetic end-to-end run + carrier receipt. Watch for: qualified tag exact string, bridge webhook fire, enqueue same-second, send hop, GHL message status reaching "delivered", media page link presence. Compare total time pre/post WS-1.

6 Housekeeping agent

Outcome: the books always match reality, and known small hazards are closed.

  • Bookkeeping fix: record every actual send in the transfer table (today's 626 re-sends of two Jul-28 re-sends exist in GHL but not in our records — that mismatch caused the "3 days of failure" confusion).
  • Fix the delivery-verifier bug (route-check ordering) so "verified" stamps are trustworthy.
  • Lock the client proof-report link before client #2 (token-keyed access).
  • Home-folder brain copy fix — separate task chip already running.
Technical context Verifier bug: sales-crm/lead-delivery.js verifyGhlLeadDeliveries() — route-check runs before settled-status check (ready task 09315d32 in brain.tasks). The n8n drain's Verify node already has the corrected order — port it back. Bookkeeping: the 03:34 ET Jul-28 manual re-drive sent 4 packets via GHL (message ids in conversation eapXgwOJjmdrSJiTGwWm) with no transfer rows; write catch-up rows with drained_by='manual_redrive_0728'. Client report: stpierre-client-report.pages.dev token enumerable — brain task b995e671.

Standing gates — the new session must honor these

  • Nothing client-facing without Akash's explicit go. All sends → Akash's phone (…5477) only. Dan sees nothing until the DripJobs screenshot gate passes.
  • Auto-send to leads stays OFF. Reply drafts are staged, never sent.
  • No transfer fix counts without a synthetic end-to-end test + carrier receipt.
  • CRM workflow-builder edits are Akash-only (hand him click-paths); Conversation-AI API writes are allowed.
  • Never edit ad sets/campaigns with spend; approved copy is frozen. Packet format = executive summary + media page link (no raw attachments).
  • Ledger every external write with a rollback note.