DEV Community

Cover image for It Passed Every Investor Demo. It Failed the First Real Credit Card.
Sneh
Sneh

Posted on

It Passed Every Investor Demo. It Failed the First Real Credit Card.

A founder came to us right after closing a small pre-seed round, three weeks out from a public launch. He wanted one thing before he flipped real payments on: a technical pass over the app he'd built with an AI app builder, just to make sure nothing was going to embarrass him in front of paying customers.

He wasn't worried about payments specifically. He'd demoed the upgrade flow a dozen times, to investors, to friends, to us on the first call. Click "Upgrade to Pro," a Stripe checkout window opens, you pay, you land back on the dashboard with a Pro badge next to your name. Clean, fast, no errors. It had worked every single time.

It had also never once been tested with a real card.

What a test card actually proves

Stripe ships a small set of numbers built for developers, and

4242 4242 4242 4242
Enter fullscreen mode Exit fullscreen mode

is the famous one. Type it into any checkout form in test mode, and Stripe approves it instantly, every time, no exceptions. That's the whole point. It lets you build and demo a payment flow without needing a real bank account on the other end.

But "instant and guaranteed" is also exactly what a real card is not. A real charge can sit in a holding state while the bank checks it. It can get flagged for review and declined a few seconds after the checkout page already said "success." It can time out, get disputed later, or clear five seconds after the customer has already been redirected back to your app. Test mode can't show you any of that, because it isn't designed to. It's designed to prove your buttons are wired to the right place, not to prove your app can handle the ways a real payment can wobble on its way through.

We asked the founder one simple question: when someone pays, what actually flips their account to Pro? He didn't know off the top of his head, which told us where to look first.

The one line doing all the trust

We found it in the code the AI builder had generated for the checkout success page. The moment Stripe redirected the browser back to the app, this ran:

js
// on the /checkout/success page
useEffect(() => {
  updateUserPlan(userId, 'pro');
}, []);
Enter fullscreen mode Exit fullscreen mode

That's it. No check with Stripe asking "did this payment actually clear." Just a redirect happening, treated as proof enough. In test mode, that's harmless, because test payments never fail after the redirect. Every single test run, the money and the redirect arrive together, so nobody ever saw the two things come apart.

Real money doesn't always move on the same schedule as the page load. Stripe's own recommended pattern is to wait for a webhook, a message Stripe sends to your server once it has actually confirmed the charge, and only unlock the paid feature when that message arrives. The redirect and the webhook usually land close together. Usually isn't every time, and the gap between them is exactly where this app had nothing watching.

Why nobody caught it before us

This wasn't sloppiness. It's close to the default shape most AI builders produce, because a webhook needs a real, publicly reachable web address to send its message to, and most of a project's life happens before that address exists. You can build, demo, and even raise money entirely inside a preview environment that no outside service can reach. The redirect-based shortcut works everywhere a webhook can't, so it becomes the whole payment flow instead of a temporary stand-in for it.

That's a common shape for what these tools skip, not because the tools are bad at what they do, but because the one thing that would have exposed this, a real webhook hit by a real, unpredictable card, never has a reason to happen until the app is actually live in front of strangers.

What we found when we checked the real numbers

We asked for read access to the Stripe dashboard and compared it against the app's own database. It took about ten minutes to find six accounts marked "Pro" with no matching successful charge in Stripe. Every one of them followed the same pattern: a card that got held for review or declined a few seconds after checkout, after the redirect had already fired and already flipped the flag. Nobody had done anything wrong. They'd just been the first real cards to hit a flow that had only ever been tested with one that couldn't fail.

Six accounts on a small early user base doesn't sound dramatic, and it isn't, yet. It's what happens at ten times that scale, after a launch post gets some traction, that made it worth fixing before it went out instead of after.

The fix, and why it's smaller than it sounds

The actual repair was one afternoon of work. We added a webhook endpoint that listens for Stripe's payment_intent.succeeded event and only then marks the account Pro. Between checkout and confirmation, the user now sees a short "finishing up" state instead of an instant badge, which in practice clears in a second or two. If a charge fails after the redirect, the account simply never upgrades, and the user gets a clear message instead of a feature they were never actually charged for.

This is the same rule we hold at EnactOn before anyone flips a Stripe key from test to live: prove the account unlocks because Stripe confirmed it, not because the browser assumed it. It's a small rule, but it's the difference between a bug you catch in a code review and one your first real customer finds for you.

Testing this properly means testing past the test card

A test card only proves the happy path exists. Proving the flow is safe means testing the parts a test card can't reach: a charge that gets declined after redirect, a webhook that arrives late, a user who closes the tab before it fires at all. Stripe has tools for exactly this, like its CLI's trigger command, which fires a real webhook event at your server without needing an actual card at all. A real pre-launch QA pass covers this kind of edge case on purpose, the ones that never show up if the only thing anyone ever clicked was the happy path, over and over, in a demo built to look finished.

Why this matters more than it looks like it should

Most early products that stall don't fail because nobody wanted them. They fail because something ordinary broke at the exact moment it mattered most, usually the first time a real stranger tried to give the founder money. An investor demo and a paying customer are testing two different things, even when they click the exact same button. One is testing whether the idea is convincing. The other is testing whether the app can be trusted with something that doesn't reverse itself if it goes wrong.

Building that trust into the process from the start costs less than finding out it was missing after launch, and it's a different kind of build than what most one-shot AI tools are optimized to produce first, because a demo has never needed a webhook to look finished. A paying customer does.

Has your app ever passed every test you threw at it, and then met the one thing you never tried? What was it?

Top comments (0)