Stripe x GHL x Mission Control - Full Chain Fix

Triggered by "Ty Carson ordered $500 today and I can't see his form or details" - 2026-05-26
5 bugs surfaced 4 commits shipped 2 repos touched 1 SaaS dep removed Ty visible at $500 won All deploys aliased

Trigger

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.

The chain (state before)

form.rankingreels.com (LazyForm) | +--> POST /api/submit ----> [DEAD] Supabase processed_payments <-- BUG #2 (silent fail) | (project deleted; NXDOMAIN) | +--> Stripe Payment Link -> [LIVE] charge succeeds, NO metadata <-- BUG #4 (orphan payment) | (no customer, no description) | v GHL Ranking Reels -> contact created from form, tags=[] <-- BUG #5 (mismatched email) (ty@calinetworks.com) nothing tags it as paid | v sync-ghl-leads.py -> HTTP 403 on every poll <-- BUG #1 (Cloudflare 1010) (Cloudflare blocks python urllib default UA) | v even when fixed, sync only inserts new Convex grand-greyhound-113 <-- BUG #3 (no update path) leads: Ty status=new, value=null, notes=generic | v merlinoai.com /leads - shows Ty as untouched lead - Mike's frustration

5 bugs surfaced

BUG #1 - GHL leads sync returns 403 on every poll

Symptom: scripts/sync-ghl-leads.py returned HTTP Error 403: Forbidden for days. Daily refresh logged the 403 and pulled zero new leads.
Root cause: Cloudflare in front of services.leadconnectorhq.com returns Error 1010 ("Access denied based on browser signature") against Python urllib's default User-Agent. Misdiagnosed earlier as a PIT scope issue.
Fix: Added User-Agent: curl/8.0.0 header in ghl_get(). Commit b4ad858. Verified live: pulled 42 contacts, inserted 2 new leads.

BUG #2 - LazyForm's Supabase project doesn't exist

Symptom: 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.
Root cause: Supabase project ptkemxcseqxlzilpjmch was deleted weeks ago. DNS returns NXDOMAIN. LazyForm's env still pointed at it.
Fix: Moved idempotency to GHL contact custom fields (the data belongs next to the contact it's about). Stripped dead cache from get-services. Removed @supabase/supabase-js dep entirely (-8 npm packages). Commits 63e1968 (Gino) + bf296cc (cache strip). PR #1 merged staging->master.

BUG #3 - GHL sync never updates existing leads

Symptom: Docstring claimed it updated status/notes/value on tag changes. Reality: code only inserted new leads, silently skipped existing ones. Tag changes (e.g., paid-customer added) never reached Convex.
Root cause: existing-match branch in main() only skipped += 1 with no patch call.
Fix: Added status-only update path in the existing-match branch. Preserves manual value and notes (richer sources). Commit a891ca6. Dry-run verified zero false updates on 42 contacts.

BUG #4 - Stripe Payment Links have zero metadata

Symptom: Ty's $500 charge had metadata={}, customer=null, description=null. Zero trail from "Ty paid" back to "Ty filled the form."
Root cause: 3 active Payment Links (Single $100 / 5-pack $300 / 25-pack $1000) created with no metadata or payment_intent_data.metadata. Future charges had no product context.
Fix: Patched all 3 PLs with metadata at both PL level + payment_intent_data.metadata level. Future charges now carry tier, product_sku, product_name, source, ghl_location_id, ghl_target_tag.

BUG #5 - Email mismatch hides paying customer

Symptom: Ty filled the form 2026-05-13 with ty@calinetworks.com, paid 2026-05-26 with ty@goclearit.com. Two systems, two emails, never reconciled.
Root cause: No process tied the Stripe charge back to the GHL contact. Even if Supabase had been alive, it would have just stored the PI ID, not bridged the email gap.
Fix (immediate, manual): tagged Ty's GHL contact with 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.
Fix (systemic): the metadata patch (Bug #4) + GHL idempotency move (Bug #2) means future charges via the Payment Links carry the GHL location and target tag. A Stripe webhook handler can now do the linking automatically using billing_details.email + metadata.

Commits shipped

CommitRepoSubjectStatus
b4ad858mission-controlfix(ghl-sync): add User-Agent header to bypass Cloudflare Error 1010on main
a891ca6mission-controlfeat(ghl-sync): wire status updates for existing leads on GHL tag changeon main
63e1968LazyFormfeat: move payment idempotency to GHL custom fields (Gino's commit)on master via PR #1
bf296ccLazyFormfix(get-services): strip dead Supabase cache, drop @supabase/supabase-js entirelyon master via PR #1

Architectural moves

Idempotency relocation: Supabase to GHL

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:

The 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.

Stripe Payment Link metadata patch

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.

Ty Carson - resolved state

FieldBeforeAfter
Convex statusnewwon
Convex valuenull$500.00
Convex companynullCali Networks / GoClearIT
Convex notesRanking Reels prospectPAID $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]

Net delta (this session)

MetricBeforeAfter
Active SaaS in payment pipelineSupabase + GHL + Stripe + ConvexGHL + Stripe + Convex
Dead Supabase pointers in code2 (LazyForm /api/submit + /api/get-services)0
npm packages in LazyFormnn - 8 (@supabase/supabase-js + deps removed)
LazyForm src LOC removed0-173
Stripe PLs with metadata0 / 33 / 3
GHL contacts with idempotency custom fields0 wiredall (live on next form submit)
Mission Control leads with stale tag state~unknown, never updatedflows on next sync (6:15 AM daily)

Verification

GHL leads sync

$ 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

Stripe charge for Ty

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

LazyForm build after Supabase strip

$ 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

Live URLs

Outstanding (next session)