Mike: "Ty Carson ordered today and I cant see his form or details." Mission Control showed status=new on his lead, value blank. Started as a sync question; turned into a full integration chain audit.
scripts/sync-ghl-leads.py returned HTTP Error 403: Forbidden for days. Daily refresh logged the 403 and pulled zero new leads.User-Agent: curl/8.0.0 header in ghl_get(). Commit b4ad858. Verified live: pulled 42 contacts, inserted 2 new leads.processed_payments idempotency check + dfy_service_cache 7-day cache both failing silently. Every read returned null, every write threw and was swallowed by try/catch.ptkemxcseqxlzilpjmch was deleted weeks ago. DNS returns NXDOMAIN. LazyForm's env still pointed at it.@supabase/supabase-js dep entirely (-8 npm packages). Commits 63e1968 (Gino) + bf296cc (cache strip). PR #1 merged staging->master.main() only skipped += 1 with no patch call.a891ca6. Dry-run verified zero false updates on 42 contacts.metadata={}, customer=null, description=null. Zero trail from "Ty paid" back to "Ty filled the form."metadata or payment_intent_data.metadata. Future charges had no product context.payment_intent_data.metadata level. Future charges now carry tier, product_sku, product_name, source, ghl_location_id, ghl_target_tag.ty@calinetworks.com, paid 2026-05-26 with ty@goclearit.com. Two systems, two emails, never reconciled.paid-customer, video-order, stripe-ch_3TbUvtGobiFOOXQA1ful70j7, alt-email-ty@goclearit.com. Convex lead patched: status=won, value=$500, notes referencing Stripe charge ID + email mismatch.billing_details.email + metadata.| Commit | Repo | Subject | Status |
|---|---|---|---|
b4ad858 | mission-control | fix(ghl-sync): add User-Agent header to bypass Cloudflare Error 1010 | on main |
a891ca6 | mission-control | feat(ghl-sync): wire status updates for existing leads on GHL tag change | on main |
63e1968 | LazyForm | feat: move payment idempotency to GHL custom fields (Gino's commit) | on master via PR #1 |
bf296cc | LazyForm | fix(get-services): strip dead Supabase cache, drop @supabase/supabase-js entirely | on master via PR #1 |
Old: split brain. Supabase processed_payments held "have I processed this Stripe PI?" while GHL held the contact. Two databases, two source-of-truths, hope they stay consistent.
New: single source of truth. GHL contact carries two custom fields:
stripe_payment_intent (id t8f8q8zUT3rpjSvAlbk7) - the dedup keylazyform_order_number (id hl2zshpYTNAEoPmzQCOg) - what to return on idempotent hitThe thing we don't want to duplicate IS a GHL contact. Dedup data now lives next to the thing it's about. Mike's framing: "isn't GHL the obvious answer?" - it was.
All 3 active PLs now carry metadata at both levels:
{
"tier": "single | 5-pack | 25-pack",
"product_sku": "prod_USmp...",
"product_name": "Ranking Reels <tier>",
"source": "stripe-payment-link",
"ghl_location_id": "kc2L3xyrcGkqyd9Pd4j6",
"ghl_target_tag": "paid-customer,video-order,tier-<tier>"
}
This is on the PL itself AND on payment_intent_data.metadata so it flows through to charge.metadata - the broken piece that left Ty's charge orphan.
| Field | Before | After |
|---|---|---|
| Convex status | new | won |
| Convex value | null | $500.00 |
| Convex company | null | Cali Networks / GoClearIT |
| Convex notes | Ranking Reels prospect | PAID $500 - Stripe ch_3TbUvtG... GHL tagged paid-customer + video-order. Form filled 2026-05-13 from ty@calinetworks.com; paid from ty@goclearit.com (alt-email mapped in GHL). |
| GHL tags | [] | [paid-customer, video-order, stripe-ch_3tbuvtg..., alt-email-ty@goclearit.com] |
| Metric | Before | After |
|---|---|---|
| Active SaaS in payment pipeline | Supabase + GHL + Stripe + Convex | GHL + Stripe + Convex |
| Dead Supabase pointers in code | 2 (LazyForm /api/submit + /api/get-services) | 0 |
| npm packages in LazyForm | n | n - 8 (@supabase/supabase-js + deps removed) |
| LazyForm src LOC removed | 0 | -173 |
| Stripe PLs with metadata | 0 / 3 | 3 / 3 |
| GHL contacts with idempotency custom fields | 0 wired | all (live on next form submit) |
| Mission Control leads with stale tag state | ~unknown, never updated | flows on next sync (6:15 AM daily) |
$ python scripts/sync-ghl-leads.py
=== sync-ghl-leads - 2026-05-27T01:36:21Z ===
Pulling contacts from GHL location kc2L3xyrcGkqyd9Pd4j6...
GHL returned 42 contacts
Convex has 41 existing leads (41 have email)
Snapshot: leads-pre-sync-2026-05-27.json
=== DONE ===
Inserted: 2, Skipped (dedupe): 40, Failed: 0
id: ch_3TbUvtGobiFOOXQA1ful70j7
amount: $500.00 USD succeeded
billing.name: Tyrel K Carson
billing.email: ty@goclearit.com
metadata: {} <-- the orphan signature, now patched on future charges
customer: null
$ npm run build
✓ Generating static pages using 31 workers (12/12) in 414.1ms
Route (app)
┌ /
├ /api/check-rankings
├ /api/checkout
├ /api/get-paas
├ /api/get-services
├ /api/pipeline
├ /api/search-places
├ /api/submit
├ /get-started
└ /get-started/success
grand-greyhound-113 (leads table now 43 rows)form.rankingreels.com/api/stripe-webhook - consumes the new metadata + fires GHL tag automatically. Without this, the metadata sits in Stripe doing nothing.qzawzwxmsvhxownpfzmk, referenced in CLAUDE.md) is also dead. CLAUDE.md needs updating or the project needs reviving.