Skip to content
Back to field notes
Launch checksJune 6, 20266 min read

Stripe payment security checks for AI-built apps before launch

Adding Stripe adds new launch blockers an AI-generated checkout often gets wrong. This note helps a founder decide what to verify before the payment URL goes public.

StripePaymentsLaunch checks

The launch risk payments add

The moment an app accepts money, a few specific mistakes stop being cosmetic. An AI-built app Stripe payment security checklist exists because generated checkout flows tend to fail in predictable, money-relevant ways, and two of those failures are visible from outside the app before anyone enters a card.

Here is the list of what commonly goes wrong, separated by whether an outside observer can actually see it:

The distinction matters for how you check. A leaked sk_... and a sk_test_ prefix are signals on the public surface. Signature verification and server-side price enforcement live in code a public scan cannot read.

  • A Stripe SECRET key (sk_...) ends up in the client bundle or a public env value, when only the publishable key (pk_...) belongs on the client. This is publicly observable.
  • Test-mode keys (sk_test_ / pk_test_) are shipped to production, so real customers hit a sandbox or live charges silently fail. The key prefix on the public surface is observable.
  • The webhook route does not verify the Stripe-Signature, so a forged POST can fake a payment succeeded event. This is server logic and is not observable from outside.
  • The price or amount is set or trusted from the client, so a user can change it and pay less. This is also server logic and is not observable from outside.

Why AI-built checkout hits this

Stripe's own quickstarts are correct, but an AI coding tool stitches a flow together from many fragments and optimizes for "the payment went through in the demo," not for the trust boundary between client and server.

None of this means generated payment code is automatically broken. It means these four spots are where the review loop usually gets skipped.

  • The publishable and secret key look similar, so a generated .env or component can place the secret where the publishable key should go, or expose it through a VITE_, PUBLIC_, or NEXT_PUBLIC_ variable that ships to the browser.
  • Test keys are what you build with. If nobody swaps them for live keys, or swaps only one of the pair, the production deploy keeps the sandbox behavior.
  • Verifying the webhook signature requires the raw request body and the endpoint's signing secret. A generated handler that just reads req.body as JSON often skips the signature check entirely, and it still appears to work.
  • Computing the charge from a cart total sent by the client is the shortest path to a working demo. Recomputing the price server-side from your own catalog is an extra step that is easy to skip.

What to check before launch

Run these against your own deployed app, not local development. Production env values, proxies, and the live build are what decide the real answer.

The first two checks you can perform from a browser on the public URL. The last two require reading the server code, because the public surface does not reveal them.

  • Open the production site, open DevTools, and search the loaded scripts (Sources, or the bundle in the Network tab) for sk_live and sk_test. Safe result: no sk_ string anywhere on the client; only pk_ appears. Suspicious result: any sk_... is reachable in the bundle, network responses, or a window/global config object. Treat a reachable secret key as a launch blocker and rotate it in the Stripe dashboard immediately.
  • Confirm live versus test on the public surface. The publishable key the client uses should start with pk_live_ for a real launch, not pk_test_. A pk_test_ key in production usually means the secret side is still on test too. Verify in the Stripe dashboard that you are looking at live-mode keys before you announce.
  • For the webhook, this is a code check, not a URL check. Open your webhook handler and confirm it calls stripe.webhooks.constructEvent (or your SDK's equivalent) with the raw request body and the endpoint signing secret, and rejects requests whose signature does not verify. If the handler parses JSON and trusts the event without verifying, a forged POST to the webhook route can fake checkout.session.completed.
  • For pricing, open the code path that creates the Checkout Session or PaymentIntent. Confirm the amount comes from your server-side catalog or a price ID you control, and never from an amount passed in the client request. The safe pattern is the client sends an item or price ID, and the server looks up the price.

How to fix or reduce it

Each fix is specific, and each has a boundary worth stating.

A note on the trust boundary: signature verification confirms the request came from Stripe (authentication of the sender), not that the user is entitled to whatever the event grants (authorization). Keep those two checks distinct.

  • Keep the secret key server-only. It belongs in a server runtime variable that is never prefixed for client exposure (not VITE_, PUBLIC_, or NEXT_PUBLIC_). After moving it, rebuild and re-check the bundle. This fixes exposure of that one key. It does not retroactively protect a key that was already public, so rotate any secret that ever shipped to the client.
  • Use live keys in production and confirm the pair matches. Both the publishable and secret key must be live-mode. This fixes the test-vs-live mismatch. It does not validate that the rest of the flow is correct.
  • Verify every webhook with the raw body and signing secret, and return a non-2xx for any request that fails verification. This blocks forged events to that route. It does not cover replay handling or idempotency, so also make event processing idempotent so the same valid event cannot be double-counted.
  • Enforce the amount server-side from a price you control. This stops a client from paying less. It does not cover authorization, meaning whether this user is allowed to buy this item is a separate check you still owe.

Where VibeCodeGuard fits, and what it does not prove

VibeCodeGuard's Launch Check looks at the public surface of your deployed app. For Stripe specifically, it can detect a Stripe secret key exposed in the client bundle, and it can flag test-versus-live key signals visible on that public surface. Those map directly to the two observable items in the list above.

The other two items are out of reach for any public scan. Whether your webhook verifies the Stripe-Signature, and whether your server enforces the price instead of trusting the client, are server-logic concerns that need manual code review. A scan reads what the browser receives; it does not read your handler.

Practical next step: do the two browser checks now (search the bundle for sk_, confirm pk_live_), then read your webhook handler and Checkout Session code for the two server-logic checks. Run a focused Launch Check on the public surface before you share the payment URL publicly, and pair it with that short manual review of the two code paths a scan cannot see.

A clean Launch Check on the payment surface means no secret key leaked into the client and no obvious test-key mismatch. It does not prove your webhook is verified or your prices are enforced. Those require reading the server code.

> launch check

Scan the public surface before launch.

Get severity, evidence, and practical fix guidance for the checks VibeCodeGuard can run from the outside.