AI scaffolding routinely generates /api/checkout handlers that accept amount, price, or priceId from the client request body and pass the value straight into stripe.checkout.sessions.create(...), paddle.transactions.create(...), lemonSqueezy.checkouts.create(...), polar.checkouts.create(...), or braintree.transaction.sale(...). Any buyer can open browser dev tools, rewrite the outbound request, and pay $0.01 for a $999 product — the handler dutifully forwards the tampered number to the processor, which has no idea the merchant's intended price was different. This pattern is one of the most common indie-SaaS exploits reported on Twitter/X and in Shopify-app vulnerability disclosures since 2019; Stripe's own Checkout.sessions.create documentation opens with an explicit warning against it. The fix is trivial (look up the price server-side from a catalog or validated allowlist), but the LLM default — "accept what the client sent, pass it through" — produces the broken shape every time unless the prompt specifies server-side derivation. Once the site has real traffic, automated scrapers find mispriced checkouts within days.
Critical because every successful exploit is a direct revenue loss that compounds per sale, and attackers can script unlimited $0.01 purchases across the entire catalog in minutes.
Look up the authoritative price server-side from a catalog — a hardcoded constant map, environment variable, or database row keyed by the product slug — and pass that value to the payment processor's .create() call. If the client sends a priceId, validate it against a server-held allowlist (const ALLOWED_PRICE_IDS = new Set(['price_abc', 'price_def'])) before use. Never let req.body.amount, req.body.price, or an unvalidated req.body.priceId reach the processor.
Deeper remediation guidance and cross-reference coverage for this check lives in the payment-security and api-security Pro audits — run those after applying this fix for a more exhaustive pass on the same topic.
project-snapshot.abuse.payment-amount-server-side-onlycriticalapp/api/**, pages/api/**, server/**, or equivalent that calls a payment-processor creation method: stripe.checkout.sessions.create(...), stripe.paymentIntents.create(...), stripe.subscriptions.create(...), paddle.transactions.create(...), lemonSqueezy.checkouts.create(...) (or createCheckout), polar.checkouts.create(...), braintree.transaction.sale(...). For each call, trace the amount, unit_amount, price, priceId, or line_items[].price argument back to its source. PASS when the value comes from a server-side constant, environment variable, database lookup keyed by product slug, or a priceId validated against a server-held allowlist. FAIL when the value comes from req.body.amount, req.body.price, req.body.priceId (without allowlist check), params.amount, searchParams.get('amount'), or any other client-controllable input.priceId against a server-held allowlist before use.package.json contains none of stripe, @paddle/*, @lemonsqueezy/*, @polar-sh/*, or braintree, and no other payment-processor library is detected. Report the package-json lookup evidence.amount from req.body and passes it to the processor, even if "validated" with a non-negative check — a non-negative number is still attacker-controlled."0 payment-processor create() calls found across N handler files"."Inspected X create() call sites; all sourced from server-side catalog / allowlisted priceId"."Client-controlled amount in app/api/checkout/route.ts — req.body.amount passed directly to stripe.checkout.sessions.create()" — name the file, the processor method, and the client-source field..create() call. If the client sends a priceId, validate it against a server-held allowlist before use. Never let req.body.amount, req.body.price, or an unvalidated req.body.priceId reach the processor.