Whop × The Uncommon Business

AI Employee Bootcamp: payments move to Whop

Whop charges every Fall 26 purchase. GoHighLevel keeps contacts, email and SMS — it stops handling money and stops deciding what a buyer bought.

15 Aug 2026 Doors close 29 Sep, 10:00 CT prod_ioq3LBLXi4g8F

The flow

One Worker sits between Whop and everything downstream. It knows what was bought at the moment of the charge, so nothing after it has to guess.

Buyer checks out on your pageWhop's form, embedded in the GHL funnel page. The page around it is unchanged.
Whop charges and fires payment.succeededTier, cart, funnel, affiliate cookie and all five UTMs were stamped on before the charge.
Worker upserts the GHL contact17 fields and the full tag set in one call, before any workflow runs.
Worker enters the workflows by IDPOST /contacts/{id}/workflow/{id}. A bumped cart enters two — base tier and upgrade.
FirstPromoter and Supabase, both directNeither waits on a GHL workflow firing correctly first.

GHL workflows no longer decide anything about the purchase.

They branched on order amount to work out the tier. There is no GHL order now, so those branches read nothing. Emails, SMS, waits and tag conditions all stay — they just stop guessing.

Affiliates

Sales reach FirstPromoter directly from the Worker. Eight GHL workflows are gone.

Capture

  • FirstPromoter's tracking script is in the funnel head code and sets _fprom_tid / _fprom_ref on go.theuncommonbusiness.co.
  • The checkout block reads those cookies at load and sends them with the payment, so they arrive as Whop payment metadata and are reported with the sale.
  • Cookies first, ?fpr= as fallback: the parameter only exists on the landing hop and is gone by the time most buyers reach checkout.
  • Same origin throughout, so the read works — provided FirstPromoter does not set those cookies HttpOnly. Nothing can read an HttpOnly cookie from a page script. The live payment test is what proves this end to end, and it is the single most important thing that test checks.

Plan strings the Worker sends

FirstPromoter's plan is free text, not an ID: whatever string we POST has to already exist in FirstPromoter with a reward rate on it, or the sale lands against the promoter at no commission. The Worker now sends your strings, from the First Promoter Product column of the Price Points database — not an invented set.

Sold asplan stringIn FirstPromoter
Early Bird $47bc_eb_47yes
General Admission $97bc_reg_97yes
VIP $197bc_vip_197yes
VIP $297  after Sep 20bc_vip_297yes
Platinum $397bc_platinum_397yes
Early Bird → VIP $150bc_ebvip_150yes
GA → VIP $100bc_vip_100yes
GA → VIP $200  after Sep 20bc_vip_200yes
Early Bird → VIP $250  after Sep 20bc_ebvip_250confirm
Early Bird → Platinum $350bc_ebplat_350confirm
GA → Platinum $300bc_gaplat_300confirm
VIP $197 → Platinum $200bc_vipplat_200confirm
VIP $297 → Platinum $100bc_vipplat_100confirm

These thirteen strings are exactly what the Worker sends — verified in the code, not copied from a doc. The eight marked yes were matched against your Price Points database on 15 Aug.

The five marked confirm have never had affiliate tracking under any setup — no Price Points row, no GHL workflow, no FirstPromoter plan — and nobody has confirmed since whether they were created. Only Lisa can check. If a string does not exist in FirstPromoter the sale still lands against the promoter, but at no commission and with no error anywhere. Every bootcamp row in Price Points sets commission equal to price, so unless someone says otherwise these five should be 100% too.

Do not reuse bc_vip_200 or bc_vip_100 for the Platinum upgrades

  • Those two are your GA→VIP upgrades. VIP→Platinum happens to cost $200 and $100 as well.
  • Reusing them pays the right amount and reports the wrong product — every Platinum upgrade would show up as a VIP one, and nobody would notice until the numbers were queried.

The price is in the string on purpose. FirstPromoter sets reward rates per plan, so a price change has to be a distinct plan or the commission silently follows the old rate — which is why the Sep 20 step-ups get their own entries rather than inheriting.

On a cart with an order bump the bump wins: one payment reports one plan, and it is the upgrade rung, because that is the rung the commission is owed on.

Rules that are live now

  • A comped seat pays no commission. A 100%-off code collects $0, and $0 sends nothing to FirstPromoter. TUB's call, 16 Aug. A partial discount reports what was actually collected, not list price.
  • A retry cannot pay twice. Every sale carries the Whop payment ID as event_id, which is how FirstPromoter dedupes.
  • On a bumped cart the bump wins. One payment reports one plan string, and it is the upgrade rung — the rung the commission is owed on.

Still open — refunds

  • A refund in Whop is not fed back to FirstPromoter. The commission stays on the promoter's report until Lisa removes it by hand.
  • Raised 18 Aug and unresolved. Either a refund webhook posts the reversal, or Lisa needs a Slack notification on every refund of a referred sale.

The affiliate tag

The Worker applies aieb-fall26_affiliate to the contact whenever the buyer arrived through a promoter link. This is the tag the old FirstPromoter BID is not empty branch used to apply, and it is applied on exactly the same condition the Worker uses to decide the sale is a referral — so the tag and the FirstPromoter POST can never disagree about whether one happened.

One difference from the old branch, deliberately: it fires on the tracking cookie or the ?fpr= parameter, not the cookie alone. A buyer who lands on a promoter link before the cookie is set is still an affiliate sale, and that is the case that used to disappear without trace.

Your side  Tracking is on every funnel already and carries to checkout. Outstanding: the plan strings above, and add the bootcamp link to the promoter signup so promoters have one to share.

Attribution

Organic, ads and tripwire are one attribute on the page. No logic to build.

  • Each block declares data-funnel="organic" | "ads" | "tripwire". It rides the payment; the Worker turns it into a tag.
  • New funnel page = copy the block, change data-tier and data-funnel. Nothing else.
  • Declared, not inferred, because two bootcamp funnels share one page URL.
  • The exact page slug also lands in aieb_page, so GHL conditions can target one ad page without a code change.
  • Tripwire is a real third value. GHL only ever wired ads and organic.
  • Confirmed 16 Aug: /bootcamp-2026 is the organic funnel, /ads-bootcamp-2026 is paid. Only organic has to be live for Monday.

An upgrade never overwrites where the original sale came from.

What was there before. Every workflow ran an Ads / Organic / Tripwire block that read GHL's Funnel field and applied _organic or _ads. It ran on upgrade workflows too. So a buyer who came in through an ad and later upgraded on a page sitting in the organic funnel was re-tagged organic, and the ad lost the credit for a sale it had made.

What happens now. The Worker applies the funnel tag on a first purchase only, and skips it entirely when the offer is an upgrade. The original source survives. The upgrade is still fully recorded — aieb_page holds the exact page it was bought on, and the tier and path tags mark it as an upgrade — it just does not touch the attribution set on the first sale.

GHL changes

Tier workflows stay separate, one per tier. Each loses its payment trigger and its price branching, and keeps everything that speaks to the buyer.

The live set is the folder The AI Employee Bootcamp — WHOP: 1 — General Admission (Early Bird + GA), 2 - VIP, 2a — VIP upgrade, 3 - Platinum, 3a - Platinum upgrade, plus 4 - Bounce Email. All five are published and the Worker enters them by ID. The originals are untouched, kept only so the old build can be read side by side — sending a Whop buyer into one would re-run branches that read a GHL order amount that no longer exists.

Remove

  • Every Payment received trigger — keys on a GHL order that no longer exists
  • The price branch block and its tag nodes
  • Ads / Organic / Tripwire, its three branches and two tag nodes
  • The duplicate n8n webhook — keep one, on the main line
  • Any Go to that only existed to rejoin a deleted branch

Keep

  • Aevent, disable DND, the waits, Wisernotify
  • Every SMS and email, and add-to-WF4
  • The email-opened branch — engagement, not purchase
  • Any branch reading a tag, prior purchase or contact state
  • WF3's 180 / 195 Platinum seat notifications

Rule for anything ambiguous

Branch reads Amount, Workflow trigger or Funnel → delete. The Worker knows, and the branch now reads a value that never arrives.

Branch reads tags, prior purchases or email opens → keep. That is contact state and still works.

One addition per workflow: the n8n webhook's custom data gains whop_payment_id. It replaces ghl_order_id as the dedupe key and is the same value Supabase stores as processor_ref.

One exception: the Is Free? branch

  • It reads Amount, so the rule says delete — but its output, aieb-fall26_free, is how a free seat is identified downstream. The Worker now writes that tag itself at charge time, so the branch can go; what is still open is the $0 GHL comp form, which never reaches Whop and so never reaches the Worker.
  • Unresolved until we know which route free seats take — a 100%-off Whop code is handled and tags the buyer; the $0 GHL comp form never reaches Whop, so it reaches nothing.

Fields & tags

Written to the contact on every purchase, before any workflow runs.

FieldHolds
whop_payment_idThe Whop payment. The join key across GHL, n8n, Supabase and FirstPromoter.
aieb_tierga · vip · platinum
aieb_offerThe exact rung, e.g. ga-to-platinum
aieb_funnelorganic · ads · tripwire
aieb_pageThe page slug they checked out on
aieb_cohortfall26
promo_codeThe code the buyer entered, if any. Stamped on the payment so a free seat is explicable by which code, not just by the amount being zero.
fp_planFirstPromoter plan string, for plan-level reward rates
fp_amount_centsSale amount in cents, stamped at charge time
firstpromoter_bidReferral tracking ID, when there is one
utm_source … utm_termAll five, carried from the landing page

The tier map

Every rung that sells, what it costs, and the tags it leaves behind. This is the whole ladder — there is nothing else to sell.

General Admission

aieb-fall26_purchased_eb
aieb-fall26_purchased_ga
  • Early Bird — $47  to 13 Sep_eb
  • General Admission — $97  13 Sep → close_ga

Nothing upgrades into GA. It is where almost everyone starts.

VIP

aieb-fall26_purchased_vip
aieb-fall26_purchased_vip-upgrade  upgrades only
  • VIP — $197  to 20 Sep
  • VIP — $297  20 Sep → close
  • EB → VIP — $150, then $250 after 20 Sep
  • GA → VIP — $100, then $200 after 20 Sep

An upgrade adds the VIP tags and keeps the tag from the tier they started on.

Platinum

aieb-fall26_purchased_platinum
aieb-fall26_purchased_platinum-upgrade  upgrades only
  • Platinum — $397  all launch
  • EB → Platinum — $350
  • GA → Platinum — $300
  • VIP → Platinum — $200

No seat cap — dropped 16 Aug rather than enforced. The VIP $297 → Platinum $100 rung is not built; nothing sells it before 20 Sep.

Tags that go on top

aieb-fall26_purchased  every buyer, always first
aieb-fall26_organic | _ads | _tripwire  first purchase only — never re-applied on an upgrade, so an ad keeps the credit
aieb-fall26_affiliate  arrived through a promoter link
aieb-fall26_free  charged $0 — a 100%-off code

Hub gating reads the entitlement; the dashboard reads the trail. Prices, windows and plan IDs live in worker/src/plans.js — that file is the source, this map is the summary.

Checkout map

data-tier and data-funnel are the only two things that differ between one checkout and the next.

AI Employee Bootcamp — main funnel

PageSellsdata-tierdata-funnel
/general-admission-2026Early Bird $47 + 2 bumpseb-ga,gaorganic
/vip-admission-2026VIP $197 + Platinum bumpviporganic
/platinum-admission-2026Platinum $397platinumorganic
/vip-admission-upgradeEB → VIP $150eb-ga-to-viporganic
/platinum-admission-upgradeEB → Platinum $350eb-ga-to-platinumorganic
/vip-platinum-admission-upgradeVIP → Platinum $200vip197-to-platinumorganic
/bootcamp-2026-confirmationReceipt block

AI Employee Bootcamp Ads — rebuilt 15 Aug, new slugs

PageSellsdata-tierdata-funnel
/general-admission-2026-5280Early Bird $47 + 2 bumpseb-ga,gaads
/vip-admission-2026-9281VIP $197 + Platinum bumpvipads
/platinum-admission-2026-9922Platinum $397platinumads
/vip-upgrade-eb-5042EB → VIP $150eb-ga-to-vipads
/vip-upgrade-g-2170GA → VIP $100ga-to-vipads
/bootcamp-2026-confirmation-2847Receipt block

Source: live GHL funnel pull, 15 Aug 2026, 15:10 ET.

Two things to watch

Two ads funnels exist

  • AI Employee Bootcamp Ads is the live one, rebuilt 15 Aug. 2026 - Bootcamp - ads has not been touched since May and still carries /general-admission-2026-ads-checkout. Anything pointing at the old slugs is pointing at the wrong funnel.

Newsletter, advertorial, extended enrollment

  • Still on the old checkout, so they sell nothing until a block is pasted. That is the tripwire route.

Pasted once, never again.

A page holds two lines — a <div> with its tier and funnel, and a script tag. Fields, bumps, summary, payment element, terms and button are all served by the Worker.

Wording and styling change in worker/src/copy.js, one wrangler deploy, all twelve checkouts at once. Prices, the terms link and the confirmation URL were already dynamic — all three changed today without touching a page.

The GHL page still owns the headline, body copy and images. Those stay editable in the page editor.

Editing the checkout

A checkout page holds two lines. Everything inside the block is served from one place, so a wording or styling change is one edit and one deploy — never twelve pages re-pasted.

What a page contains

On the pageWhat it is
<div id="aieb-checkout" data-tier="…" data-funnel="…">Empty. Says which offer this page sells and which funnel it belongs to. The only thing that differs page to page.
<script src="…/embed.js" async>Renders the whole block into that div: fields, bumps, order summary, payment element, terms, button, styling.

Who edits what

Your team, in GHL — no deploy

  • Headline, body copy, images, layout — everything around the block, edited in the page editor exactly as before
  • Per-page wording overrides on the div: data-cta, data-title, data-legend-order, data-legend-pay, data-terms-label, data-secure-note
  • Hide promo entry on one page with data-promo="off". Hidden means not rendered — there is no field to type into and no code is sent.

Whop side — one deploy, all pages

  • Wording inside the block, and the block's styling — worker/src/copy.js
  • Hide promo entry on every page — showPromo: false in copy.js
  • Product names and bump labels — worker/src/catalog.js
  • Prices, sale windows, plan IDs — worker/src/plans.js

Three things were never on the page to begin with.

Prices, the terms link and the confirmation URL are all fetched from the Worker when the block loads. That is why the 13 Sep and 20 Sep price steps need no page edits at all, and why the terms link and confirmation URL both changed on 15 Aug without anyone opening a funnel.

Links

WhatWhere
GHL workflows — the Whop setThe AI Employee Bootcamp — WHOP — all five rebuilt workflows plus WF4
Main funnelgo.theuncommonbusiness.co/bootcamp-2026 — AI Employee Bootcamp, the one live on Monday
Ads funnelgo.theuncommonbusiness.co/ads-bootcamp-2026 — checkouts not swapped yet, due end of next week
Organic funnel — second— to be confirmed
Tripwire funnel— none named yet. The Worker supports the value the day a page declares it.
Cloudflaredash.cloudflare.com — account Hello@theuncommonbusiness.co
Codegithub.com/The-Uncommon-Business/aieb-checkout — private. Send GitHub usernames to be added as collaborators.

Where it runs

Everything is on your own Cloudflare accountHello@theuncommonbusiness.co, account 81ac8dfc3cfa5c403247c595aef3db16. Nothing is hosted by Whop, so it stays yours at handover.

ThingNameWhat it does
Workeraieb-fall26-checkoutServes embed.js and receipt.js, prices every cart, creates the payment, receives the Whop webhook, writes to GHL and FirstPromoter
Pagesaieb-fall26-handoffThis document

Getting to it: dash.cloudflare.comCompute (Workers)aieb-fall26-checkout. Login credentials live in your password manager, never on this page.

What the dashboard gives you

  • Logs — live request stream. Every payment, webhook and error as it happens.
  • Deployments — every version, timestamped. One click rolls back to any earlier one, which is the fastest fix if something ships broken.
  • Metrics — requests, errors, CPU time.
  • Settings → Variables — the names of the stored secrets. Values are write-only and cannot be read back, by anyone.

What it does not give you

  • Editing the checkout. Wording and styling live in the repo, not the dashboard — changing them means an edit plus wrangler deploy, which is a different credential from the Cloudflare login.
  • Cloudflare access is for watching and rolling back. Changing is a code change.

Source

github.com/The-Uncommon-Business/aieb-checkout — private. Send the GitHub usernames who need it and they will be added as collaborators.

Everything is in there: the Worker, the generated checkout blocks, the GHL inventory scripts and this document. No secrets — env/.env is gitignored and the Worker's credentials are stored in Cloudflare, write-only.

Deploying a change

StepCommand
1. Checknode scripts/check-embed.mjs — compiles the exact script the browser will receive
2. Deploycd worker && npx wrangler deploy
3. Confirmnode scripts/check-embed.mjs --live — checks what production is now serving

Step 1 is not optional. embed.js is generated, so the source file can be valid while the string it produces is not — that took every checkout down silently on 15 Aug, with a blank space where the form should be and nothing in the console.

Running it yourselves

Added 24 Aug at Lex's ask. Who owns what, which jobs need a deploy and which do not, and how to drive the repo with Claude without taking the cart down.

Who owns what

PersonOwnsWorks in
LexOps lead. Offers, prices and launch calls — anything that changes what a buyer pays goes through her.GHL, Whop dashboard
ScottThe repo, the deploy, and the Supabase feed (a separate Whop webhook straight to Railway). The only person who ships to production.GitHub, Cloudflare, Supabase
AbsPromo codes, new products, domain and email verification, page content, customer look-ups.Whop dashboard, GHL
NikhilGHL workflows, funnel pages, pixels and tracking, confirmation-page routing, split tests.GHL
LisaAffiliates end to end: FirstPromoter campaigns, promoter links, plan strings, commission rates.FirstPromoter
ChrisThe money: payouts, sales tax categories, billing descriptor, processor connections, the Stripe → Whop migration audit.Whop dashboard
NickCheckout and order-bump copy.Slack → whoever holds the repo
Parisa, Ari, MadelineWhop side: worker changes, platform bugs, account settings, migration support.Whop, this repo

Anyone not on this list has no system ownership recorded — say so and it gets added. Philip was named on the 14 Aug kickoff as the most dev-forward of the team and has not been in a thread since; if he is the one taking the repo, he needs the GitHub and Claude setup below.

Does this job need a deploy?

Most of the bottleneck is this question being unanswered. Almost nothing your team wants to do needs code.

JobWhoWhereDeploy?
Create a promo codeAbsWhop dashboardNo
Limit that code to one tierAbs asks → repoworker/src/routes.jsYes
Refund, cancel or look up a paymentAbs, ChrisWhop dashboardNo
Headline, images, body copy around the checkoutNikhil, AbsGHL page editorNo
A new funnel page selling a tier that already existsNikhilGHL — copy the two lines, change data-tier / data-funnelNo
Emails, SMS, waits and tag conditions inside a workflowNikhilGHLNo
Roll back a checkout that just brokeAnyone with the Cloudflare loginCloudflare → DeploymentsNo — one click
Wording inside the checkout block, button text, secure noterepoworker/src/copy.jsYes
A price, a sale window, a new planrepoworker/src/plans.jsYes
Have a purchase enter a new GHL workflowrepoworker/src/index.jsYes
A new product with its own checkoutAbs, or repoDepends — see the runbook belowSometimes

Set up once

Everyone

  • Whop dashboard. Chris or Lex invites you. Ask for the Operations role; ask for the Developer tab as well if you need to see webhooks.
  • Cloudflare. Shared login hello@theuncommonbusiness.co, in your password manager. Read the logs, roll a deploy back. Nothing here edits the checkout.
  • The ops channel. Where a new promo code or a new workflow ID gets posted. Skipping it is how a $397 seat went to $0 three times.

Only if you touch the repo

  • A GitHub account, added to the The-Uncommon-Business org by Scott.
  • Claude Code, signed in.
  • The GitHub CLI, so Claude can open pull requests as you.
  • Nobody needs this to create a coupon, refund a customer or edit a funnel page.

Claude, connected to the repo

Five commands, once. After this Claude can read every file in the worker, explain what a change would do, make it, and open a pull request — all from the folder on your machine.

StepWhat you do
1. GitHubCreate an account, send Scott the username, get added to The-Uncommon-Business.
2. Install Claudenpm install -g @anthropic-ai/claude-code, then run claude and /login.
3. Install the GitHub CLIbrew install gh on a Mac, then gh auth login → GitHub.com → HTTPS → log in with a browser. This is the "connect Claude to GitHub" step — Claude uses that same login to push and to open PRs.
4. Get the codegh repo clone The-Uncommon-Business/aieb-checkout, then cd aieb-checkout.
5. Startclaude, then ask: "Read README.md and CHECKLIST.md and tell me what this repo does." Everything it needs to know about the checkout is in the repo.

For reporting rather than code, the Whop MCP connected to Claude will pull custom reports out of the account — payments, refunds, payouts — without anyone writing a query. It is being rebuilt on the Whop side, so check one number against the dashboard before a figure goes into a deck.

Asking Claude for the right thing

Works

  • "Add esga to PROMO_TIERS as GA-only, run node scripts/check-worker-invariants.mjs, and open a PR."
  • "The VIP price steps to $297 on 20 Sep. Show me exactly which lines in plans.js change, then open a PR. Do not deploy."
  • "Read worker/src/index.js and tell me which GHL workflows a VIP cart with the Platinum bump enters."
  • "Why did this checkout not tag the contact? Here is the payment ID."

Do not

  • "Deploy this." Deploys are a GitHub Actions run Scott triggers. A laptop deploy is what reverted the launch fixes on 17 Aug.
  • "Push straight to main." Every change goes through a pull request so the two checks run.
  • Paste a secret into a prompt. The worker's credentials live in Cloudflare, write-only, and are not in the repo.
  • Change GHL and the worker in one go. One system per change, so a rollback is one action.

Runbook — a coupon code

Abs. Steps 1–3 are yours alone and need nobody. Step 4 is the one that gets missed.

Creating one

  1. Whop dashboard → create the codePercentage or flat amount, and an expiry. Tick one per customer if it should be.
  2. Scope it to the product, never to plansWe charge an inline plan minted per payment, so a plan-scoped code matches nothing a buyer ever buys and Whop refuses it at the charge: "This promo code cannot be used on this pricing option." Product scope works. Unscoped works.
  3. Leave "new users only" and "existing memberships only" untickedThe checkout validates the code before charging and cannot see those conditions, so the code shows as applied, the summary shows the discount, and it dies at the till.
  4. Post the code and the tier it is for in the ops channelA code the worker has not been told about works on every tier — including a $397 Platinum seat at $0. This has happened three times: AFFVIP, then ESGA and ESVIP six hours after it was fixed. Nothing in the dashboard prompts for a tier, and Whop cannot express one for us, so the restriction can only live in the worker.
  5. One line, then a deployThe code and its tiers go into PROMO_TIERS in worker/src/routes.js; Scott merges and deploys. Until it deploys, delete the code — a deleted code is refused instantly and needs no deploy. Deleting archives rather than erases, so you can recreate it exactly.

Three gotchas that have each cost an afternoon

  • Percentage is a plain number. 100 means 100% off; 1 means one percent off, not 100%.
  • A bare expiry date is read in European time. 2026-10-07 becomes 6 Oct 22:00 UTC — seven hours early. Give a full date and time.
  • There is no edit. Changing a code means deleting and recreating it, which is why codes accumulate archived duplicates. Harmless; the live one is always the one used.

Runbook — a new product and its checkout

Abs, with Lex. One question decides the route: does the purchase have to reach GHL?

Money only — no code, ten minutes

  • Whop dashboard → product → plan → checkout link → set the redirect to your thank-you page. Paste the link on the button.
  • Right for a standalone ticket, a deposit, an ad-hoc invoice.
  • What you give up: nothing lands in GHL — no fields, no tags, no workflow, no FirstPromoter commission — and the buyer's email comes from their Whop account rather than your form, which quietly creates a second GHL contact.

Part of the machine — needs a deploy

  • The checkout block, exactly like the bootcamp: two lines on the page, everything else served by the worker.
  • Needs plans, a catalog entry and a deploy. Roughly half a day once the page exists and whoever builds it can see the page's code.
  • The middle path: a checkout link created through the API can carry metadata and a redirect — the dashboard simply does not show that field. That is how a hosted checkout can still land in GHL.

Runbook — purchase → tag → workflow

Nikhil. The worker writes 17 fields and the whole tag set onto the contact before any workflow runs, so nothing downstream has to work out what was bought.

You wantDo this
A workflow to fire on a purchaseTrigger on the tag — aieb-fall26_purchased_vip and the rest, listed under Fields & tags. Never trigger on Payment received: there is no GHL order any more, so it fires for nobody.
A branch on tier, funnel or pageRead the field — aieb_tier, aieb_funnel, aieb_page. Never branch on amount.
A purchase to enter a new workflowSend the workflow ID. The worker enters workflows by ID — one line each.
A new field written at purchaseCreate the custom field in GHL first. Unrecognised keys are dropped silently on upsert with no error — that is why five fields flagged on 14 Aug still are not landing.

Deploying — the only sanctioned path

This supersedes the local wrangler deploy in Editing the checkout above. A laptop deploy from stale source is exactly what reverted the 3-D Secure and Apple Pay fixes on 17 Aug, twenty-five minutes after they went in.

Open a pull request into mainBoth checks run automatically on every PR: the embed compiles as the browser will receive it, and the launch-QC fixes are still present.
Merge itScott's call. Nothing is live yet.
GitHub → Actions → deploy → Run workflowType deploy to confirm. Manual on purpose — merging never ships anything by itself.
The run re-checks before and afterBoth guards run again before wrangler, then it verifies what the live worker actually serves. Source passing is not the same as the browser getting a working script.

If something ships broken: Cloudflare → Compute (Workers)aieb-fall26-checkoutDeployments → roll back to the previous version. One click, no code, faster than any fix.

Three rules that are not negotiable

  • Never deploy from a laptop. The Actions run is the only path that checks anything.
  • A dashboard promo code is live on every tier until the worker says otherwise. Post it in the channel the moment you make it.
  • The Whop payments bundle we load is unpinned, so an upstream change can alter the checkout with no deploy from anyone. That is what took the cart down on 23 Aug — found and fixed the same afternoon. If checkouts break and nobody shipped, check that first.

Supabase

Scott's own Whop webhook writes the purchase into Postgres from Railway. It subscribes to Whop directly, so nothing of ours sits between — and it keeps recording sales even if our Worker does not.

  • That keeps sales private — reaching it over the API would have meant exposing all 17 tables for one insert.
  • Rows land as processor = 'whop', processor_ref = the Whop payment ID — the same shape as the 25,000 GHL rows, so reporting spans both.

One payment can hold two products

A plan is one product at one price — that never changes. But an order bump is one payment carrying two of them: Early Bird $47 plus the VIP upgrade $150, charged once as $197.

Whop's payments API takes a single plan with a single price and has no line-item array, so the parts cannot be expressed as plan IDs on the charge itself. They ride in the payment metadata instead, as three parallel comma-separated lists in line order, base first:

Metadata keyExample — Early Bird with the VIP bump
plan_idsplan_NWlCLuu4NotNr,plan_GzR4MwQNIv42l
plan_keyseb-ga,eb-ga-to-vip
line_amounts4700,15000 — cents
offereb-ga+eb-ga-to-vip — the whole cart as one string
tiervip — what they ended up entitled to
promo_codeThe code used, if any. Identifies a comp seat by which code rather than by the amount being zero.

One row per entry, resolved by plan ID. That is the same two-row shape a GHL multi-line order already produces against one processor_ref — the 539 pairs already in purchases. A single-item cart is a list of one, so it stays one code path. The idempotency key needs to include the plan ID to keep both rows.

Other webhooks

Only payment.succeeded is subscribed today. Refunds, failed payments, cancellations and disputes each have their own event at dev.whop.com. Refunds are the one that matters before launch — without it a refunded seat still counts as sold.