playbooks / payments
Stripe Without Leaks
Prices live on the server, webhooks prove themselves, and test keys stay test keys.

duck@duckaudit
The client sent the amount to charge. The client. Sent the amount. Breathe with me.
how it burns you
Payment bugs are the ones that cost actual money in both directions: attackers buying your product for one cent because the client set the price, or fake webhook calls marking orders as paid. Stripe gives you the tools to prevent all of it; AI generated checkout code regularly skips them.
Never trust a price from the browser
Anything the client sends can be edited before it arrives. Look prices up on the server from your own catalog or Stripe price ids, and let the client send nothing but which product it wants.
// app/api/checkout/route.ts
const PRICES: Record<string, string> = { pro: "price_1ABCpro", team: "price_1ABCteam" };
export async function POST(req: Request) {
const { plan } = await req.json();
const price = PRICES[plan];
if (!price) return new Response("Unknown plan", { status: 400 });
const session = await stripe.checkout.sessions.create({
mode: "subscription",
line_items: [{ price, quantity: 1 }],
success_url: process.env.APP_URL + "/done",
cancel_url: process.env.APP_URL + "/pricing",
});
return Response.json({ url: session.url });
}Make webhooks prove they are Stripe
Your webhook endpoint is a public URL, and anyone can POST a convincing looking event to it. The signature check is the only thing separating a real payment from a fictional one.
export async function POST(req: Request) {
const sig = req.headers.get("stripe-signature")!;
const body = await req.text(); // raw body, not parsed JSON
let event;
try {
event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
} catch {
return new Response("Bad signature", { status: 400 });
}
if (event.type === "checkout.session.completed") {
// fulfill here, and only here
}
return new Response("ok");
}Fulfill from the webhook, not the redirect
The success page is a place users land, including users who bookmarked it or never paid. Grant access when the verified event arrives, and make fulfillment idempotent because Stripe retries deliveries.
Keep live keys out of test land
Test mode locally and in previews, live mode only in production env vars. A live secret key in a .env.local that later leaks is a real money incident, and a test key in production is a store that cannot charge.
Restrict what your key can even do
Stripe lets you mint restricted keys with only the permissions your app uses. If the worst happens, a leaked restricted key embarrasses you; a leaked full key liquidates you.
Make your AI do it
Paste this into Cursor or Claude Code from your project root.
Review the payment flow in this repo. 1) Trace where the charge amount originates and flag any path where the client influences price, currency, or quantity. 2) Show the webhook handler and verify it checks the Stripe signature against the raw body, returns 400 on failure, and is idempotent on retries. 3) Confirm fulfillment happens only from verified webhook events, never from the success redirect. 4) Check which env holds which key mode and flag any live key reachable outside production. Output findings with file paths and a fix diff for the worst one.
The 10 minute check
0/5receipts · every claim has a source
next playbook
You Got Pwned. Now What. →