Hercules Stripe Billing Code Review: We Asked an AI to Build Our Billing System, Then Read Every Line
Billing code is where vibe coding gets genuinely scary. A broken layout is embarrassing. A broken webhook handler means people pay you and don’t get access, or don’t pay you and do.
So after testing whether Hercules could get farm accounting right, we ran a second test in a completely different domain for this Hercules Stripe billing code review: we asked it to build a real Stripe subscription integration — checkout, webhooks, plan entitlement, customer portal — and then read all four generated backend files line by line against the checklist a security engineer would actually use.
The result is a clean split, and it’s the same split we found in the accounting test.
What We Checked, and Why
Payment webhook code has four classic failure modes, and each one has burned real companies:
- Unverified webhook signatures — anyone who knows your webhook URL can POST a fake “payment succeeded” event and grant themselves a paid plan.
- Client-controlled prices — the checkout call trusts an amount or price ID sent from the browser, so a user edits it and pays ₹1 for a ₹5,000 plan.
- Client-reachable entitlement writes — the function that sets
plan: "paid"is callable directly from the frontend, so anyone can call it on themselves. - Non-idempotent handlers — a webhook delivered twice applies twice.
These are not hypothetical. Variants of the first three account for a large share of the AI-app-builder incidents in the public record.
What Hercules Got Right
All four of those. Cleanly.
Signature verification is correct, and correctly architected. The webhook receiver reads the request body as raw text and passes it, unparsed, to a separate Node.js action that calls Stripe’s constructEvent() with the signing secret before anything else touches it. This matters more than it sounds: Stripe signs the raw bytes, so the common failure is parsing the body to JSON first — which breaks verification and tempts a developer into skipping it entirely and trusting the parsed payload.
There’s a real architectural subtlety here that the AI navigated correctly. The platform’s backend runs webhook endpoints in a JavaScript runtime that can’t execute the Stripe SDK’s crypto operations, so verification has to happen in a separate Node runtime action. The generated code splits it exactly that way. That’s not a lucky guess; that’s the documented correct pattern for this stack.
The price is locked server-side. The checkout session uses a price ID read from a server environment variable. The only things the client sends are the success and cancel redirect URLs. There is no argument a user could tamper with to change what they’re charged.
The user identity is spoof-proof end to end. The checkout session stamps the authenticated user’s identifier into Stripe metadata, taken from the server-side session — never from a client argument. The webhook later reads that identifier back out of Stripe’s own event object to decide whose plan to upgrade. So the chain from “who paid” to “whose account gets upgraded” is anchored in real authentication at both ends.
Every entitlement-writing function is internal-only. The three mutations that set plan, subscriptionStatus, and subscription IDs are declared as internal mutations — not reachable from any client SDK call, only from trusted server code. There is no path for a browser to call “make me paid.”
The customer portal is correctly scoped. It looks up the caller’s own Stripe customer ID from their authenticated record and never accepts one as a parameter, so you can’t open somebody else’s billing portal.
And duplicate deliveries are harmless. All three mutations are plain field patches — writing the same values twice changes nothing. Not formally idempotent (see below), but safe against the ordinary retry case.
If you had asked us to guess in advance how an AI builder would do on payment security, we would have guessed worse than this. This is the part of the report we didn’t expect to be writing.
What It Got Wrong
Four things. Once again — and this is the whole point of this Hercules Stripe billing code review — none of them is a security vulnerability. They’re correctness bugs, and one of them is the cleanest example of the pattern we’ve found yet.
1. Every Subscription’s Renewal Date Is Stored as 1 January 1970
The generated code pins a recent Stripe API version, then tries to read current_period_end off the Subscription object.
That field doesn’t exist anymore.
Stripe removed current_period_start and current_period_end from the Subscription resource as of the 2025-03-31 “basil” API version, moving them onto individual subscription items — because subscriptions can now have items on different billing intervals. Stripe documented this as a breaking change and told developers to read items.data[].current_period_end instead. (Stripe changelog)
The generated code pins a later version than basil, so the field is definitively gone. And here’s the part worth dwelling on — the AI noticed something was wrong. It left a comment in the code saying the SDK types “may vary” for this API version, and wrote a defensive cast to “access safely,” with a fallback of 0 if the field is missing.
The fallback fires every single time. 0 becomes a Unix timestamp of zero. Every subscriber in the database gets a renewal date of 1970-01-01T00:00:00.000Z.
The AI detected the symptom, correctly diagnosed that there was a type mismatch, and then wrote code that converts a hard failure into a silent wrong answer. A crash here would have been better — someone would have fixed it in five minutes.
Right now this bug is dormant, because access control runs off subscription status, not the date. It detonates the moment anyone adds the most obvious next feature: a renewal reminder email, a grace period, a “your plan renews on…” line in the UI, or an admin dashboard sorted by expiry.
2. No Protection Against Out-of-Order Webhooks
Stripe doesn’t guarantee events arrive in the order they were created. Nothing in the generated handler compares timestamps or checks whether the incoming event is newer than what’s already stored — it just overwrites.
So a stale past_due event, redelivered after a newer active one, would silently downgrade a current paying customer to the free plan. No error, no log, no notification to you or to them. Just a support ticket, eventually, from someone who’s still being charged.
3. A First-Payment Race Condition That Silently Swallows an Event
The subscription-update handler looks up the user by Stripe customer ID. If it doesn’t find one, it returns quietly — the code comments even acknowledge this is a race condition on first payment.
The problem is that returning quietly means the webhook responds with success. Stripe sees a 200, considers the event delivered, and never retries. If a subscription-update event genuinely arrives before the checkout-completion event has finished linking the customer ID to the user, that update is gone permanently — not delayed, gone — with no log line recording that anything was dropped.
4. Unvalidated Redirect URLs (the One Actual Security Nit)
The checkout function accepts successUrl and cancelUrl as raw client arguments and hands them straight to Stripe with no allowlist or same-origin check. Stripe will redirect the browser wherever it’s told after payment completes.
This is a genuine open redirect, and it’s the only finding in this entire test that a security researcher would file as a vulnerability — but it’s a low-severity one. It needs a real completed payment to trigger, nothing sensitive leaks through the redirect, and the money still goes to the right place. The realistic abuse is a phishing page that borrows the credibility of your checkout flow. It’s a three-line fix: check the URLs start with your own domain.
The Pattern, Now Confirmed Twice
Two tests. Two completely unrelated domains — farm accounting and payment infrastructure. Same result both times:
| Accounting test | Stripe test | |
|---|---|---|
| Authorization / access control | ✅ Correct throughout | ✅ Correct throughout |
| Client-trusted privileged input | ✅ None found | ✅ None found |
| Ownership / tenant scoping | ✅ Correct | ✅ Correct |
| Cross-period correctness | ❌ Compounding error | ❌ Epoch dates on every record |
| Two views agreeing | ❌ Screen ≠ PDF | ❌ N/A |
| Definitional correctness | ❌ Wrong ratio formula | ❌ Wrong field, wrong API version |
| Silent failure handling | ❌ No enforcement on server | ❌ Events dropped without trace |
The generated code is defensively good and semantically shaky. It knows how to protect data. It doesn’t reliably know whether the data is right.
There’s a reasonable explanation for that asymmetry. Security patterns are heavily represented in training data as patterns — verify the signature, check ownership, don’t trust the client — and they look the same in every codebase. Domain correctness is specific to your business, your fiscal year, your API version, this month. There’s no pattern to match. There’s only the spec you wrote, and whether anyone checked the output against it.
Usually nobody does.
What This Means if You’re Shipping on AI-Built Billing
Concrete checks, in the order we’d run them:
- Test a renewal, not just a signup. Almost every billing bug we’ve seen lives past the first payment. Use Stripe’s test clocks to fast-forward a subscription and watch what your database actually stores.
- Read every date field your app writes. If you see 1970, 1 Jan, or a suspiciously round number, something upstream returned undefined and something downstream swallowed it.
- Check your pinned API version against the changelog for the SDK your app generated. Breaking field moves like the one above are announced, dated, and easy to miss if you weren’t the one who wrote the integration.
- Grep for silent returns in webhook handlers. Every
returnthat isn’t an error is a place where an event can vanish. - Validate your redirect URLs.
SuperApps does this as a service, and we do it because we run this code ourselves. Our own products take real payments from real customers; we’ve debugged the renewal edge cases, the out-of-order webhooks, and the “why is this user still being charged” tickets in production, not in theory. When we review an AI-built app, what you get is this document — the actual findings, ranked by what will break first, with the fix for each — not a scanner report.
If your app takes money and nobody has read the billing code, that’s the review to start with.
Caveat, Same as Last Time
One app, one prompt, four files. This establishes that these failure modes occur; it doesn’t measure how often. We’d rather publish a narrow finding we can fully evidence than a broad claim we can’t.
Every technical assertion in this Hercules Stripe billing code review was read directly from generated source code, and the Stripe API change is linked to Stripe’s own changelog above.
Series note: no affiliate links in this post. We keep testing and code-review posts free of referral relationships.
Sources
- Stripe changelog — Adds subscription item-level billing periods and removes subscription-level periods (2025-03-31 basil)
- Stripe Basil quietly moved current_period_end off Subscription — and a lot of code broke
More in this series
Get new posts by email
Pick the topics you care about — we only email you when something new is published in them.
