Freebo’s new checkout — v2 — went live at the end of September. The old one worked but was crusty. The new one has a cleaner payment step, promo codes that no longer fight with the primary CTA, add-ons, and a saner quote lifecycle. It also went into production with a brand-new price card, a brand-new mobile layout, and brand-new iframe sizing logic.
For five days after it shipped, not a single real customer paid.
Not “paid and bounced.” Not “declined by Stripe.” Nobody got as far as the payment form. Shoppers were still showing up, still picking times, still generating quotes. Then they would hit a wall I couldn’t see, back out, and leave.
0
shoppers reached a flow
0
picked a time
0
payments
I was supposed to be celebrating the launch. Instead I had a Hermes alert in my phone that read: PROBLEM — CHECKOUT MAY BE BROKEN FOR EVERYONE.
Spin up a swarm
What I actually typed into Claude was, verbatim:
No new checkouts from the web since the new checkout v2 went live. I am very concerned. Spin up a swarm of agents to determine what is wrong and propose fixes.
That is not a prompt I would have written a year ago. A year ago I would have grabbed one terminal, started grepping, and tried to be smart. The problem with “smart” is that it is sequential. Smart reads the git log, picks the most likely culprit, chases it for ninety minutes, finds nothing, then picks the second most likely. Smart compounds its own mistakes.
A swarm is dumber per agent and radically faster per hour. Five Claude sessions, each with a narrow lane, each with its own scratchpad, each reporting back to a coordinator session that I was reading over coffee.
I did not pre-decide where the bug was. I pre-decided which five surfaces it could possibly live on, and I pointed one agent at each.
What each lane came back with
The database lane came back first. Nothing was blocking checkout at the data layer. 26 real sessions since go-live. Every one had a valid quote. Every one failed to collect a name or email. No orphaned payment intents. No “paid but not booked.” Stripe was fine. Formance was fine. The ledger was fine.
The API log lane came back next. Shoppers are reaching checkout — they stop between “time picked” and the payment step. Legacy had 9 picks → 5 contact entries → 5 intents → 2 bookings. v2 had 13 picks → 2 payment-step opens → 0 contact entries → 0 intents → 0 bookings. The two shoppers who did open the payment step left it within 24 seconds, went back, and switched products.
The PostHog lane confirmed it from the client side. 173 real v2 sessions. The deepest step reached was checkout_page by exactly one person. Mobile was 152 of those 173. Zero rage clicks on the payment form, because nobody was reaching the payment form to rage at.
The static-code lane came back with “no hard blocker in the source.” A payment-blocking regression had been live for the first nineteen hours (promos and add-ons were killing the Pay button), but a second deploy had patched it. After that patch, the code looked fine. Button was enabled on the right conditions. API calls were correct. The whole code path rendered a working checkout.
Four lanes, four flavors of “nothing is wrong,” and shoppers were still vanishing.
The lane that actually found it
The fifth agent was the only one not reading data. It was driving a real browser.
I had told it: walk every entry path we have. The widget popup. The direct /book/... URL. Links from the partner site. Links from the operator’s own site. Phone size, desktop size, every path.
Its first report came back triumphant. “Every path reaches a live Stripe card form. Pay button enables when I type a test card. Nothing is broken.” I almost closed the thread.
What saved the day was that I made it go back and audit the operator-side embeds — the ones living on third-party marketing sites built in page builders. Specifically: an inline embed, on a phone, on a partner’s “Book Now” page. Not a popup widget. Not a direct link. The inline iframe variant that very few test flows ever exercise, because real engineers would never embed something that way.
The agent came back with a screenshot of a blank navy rectangle.
After “Continue to checkout”, an iPhone-size screen is entirely blank. The contact form sits 770 to 1,050 pixels above the viewport. Scrolling up, the rest works.
— live-walkthrough agent, 18:36Z
The bug was geometry.
Why the iframe disappeared
Our checkout, when embedded inline, lives inside the host site’s own layout. We send postMessage events out to our widget script, which resizes our iframe as the content grows and shrinks. On the product page the iframe is roughly 3,900 pixels tall. On the payment step, after a shopper picks a time and clicks continue, our content shrinks — the calendar and time grid go away and the contact form is a short block. We tell the host iframe to shrink too, from ~3,900 px to ~1,260 px.
The host, however, is a page builder. The host dropped our widget inside its own fixed-height wrapper div. Phone wrapper: 301 × 2,643. Desktop wrapper: 972 × 1,804. Those numbers are hard-coded into the host’s layout engine the moment the page is published.
So here is what the shopper actually sees on a phone:
- The page is scrolled 2,000 pixels down because that is where our widget sits.
- They tap a time. They tap “Continue to checkout.”
- Our iframe resizes from 3,900 px to 1,260 px, obedient.
- The host wrapper does not resize. It stays 2,643 px tall.
- Our contact form is now sitting at the top of our 1,260 px iframe — which is now the top of a 2,643 px void.
- The shopper, scrolled where they were, is looking at the middle of a navy rectangle. No form. No message. No scroll-into-view.
They tap around. They see “More trips →” from the host site creeping into frame below the empty box. They assume the booking is dead and go back.

What I’m taking out of this
Three things.
One: five dumb agents beat one smart one when the question is “which surface is the bug on.” Nobody on my team is good enough to look at an API log, a Stripe webhook stream, a PostHog funnel, a git diff, and a real mobile browser all at the same time. A swarm with scratchpads can. The coordinator session reading their reports is the smart part. The lanes are mandatory for coverage, not for cleverness.
Two: the test matrix a real engineer builds is not the test matrix real customers hit. I had covered the widget popup, the direct URL, and the operator’s own iframe embed. I had not covered a third-party partner’s page builder dropping our inline embed inside a fixed-height wrapper. That configuration existed. Real traffic went through it. My tests did not. If you ship a widget, the test matrix is “every host site anyone has ever embedded you on,” and you cannot afford to assume any of them are sane.
Three: when the code looks fine, the logs look fine, and the data looks fine — the bug is in the layer between layers. It is almost never in the layer you own. It is in the handshake between the layer you own and the layer the host owns. In our case: between our postMessage resize contract and a wrapper div written by somebody who had never heard of us.
The fix shipped as a wrapper detection pass and a scroll-into-view on step change. The first real mobile booking landed less than an hour after the deploy.
I’m keeping the swarm.