02highAPI drift→ AGENTS.md
Document v23 breaking changes and the five agent mistakes
Agents wrote code from memory of older SDKs and failed or had to patch: run 86 removed `payment_method_types`, and run 85 fixed a renamed SDK field after the build failed. Runs 84 and 86 also needed a lazy client. Nothing in the package tells an agent what changed or what to do instead.
- run 86 grader: "worked around a removed payment_method_types param and a lazy-init need"
- run 85 grader: "it fixed a renamed SDK field after the build failed"
- run 84 grader: "added a lazily-initialized client"
- repo AGENTS.md: (absent)
The fix
Add AGENTS.md at the repo root and publish it in the tarball (add to `files`). Include a v22 to v23 breaking-change list with the exact old and new names. List the real renamed field from run 85 once confirmed against the changelog. Tell consumers to copy the snippet into their own AGENTS.md or CLAUDE.md.
Show proposed AGENTS.md · ready to paste
# AGENTS.md - using `stripe` (Node SDK v23) in a Next.js App Router app
## Install
```sh
npm install stripe server-only
```
## Env vars
| Var | Where to get it |
|---|---|
| `STRIPE_SECRET_KEY` | Dashboard > Developers > API keys (`sk_test_...` for dev) |
| `STRIPE_WEBHOOK_SECRET` | `stripe listen --forward-to localhost:3000/api/stripe/webhook` prints `whsec_...`; prod: Dashboard > Webhooks |
| `STRIPE_PRICE_ID` | Dashboard > Product catalog > price (`price_...`), recurring for subscriptions |
| `APP_URL` | Your origin, used for success_url and cancel_url |
Commit `.env.example` and add `!.env.example` to `.gitignore` (create-next-app ignores `.env*`).
## The one correct path
1. `src/lib/stripe.ts`: lazy `getStripe()` with `import 'server-only'` (see README).
2. `POST /api/checkout`: `checkout.sessions.create({ mode: 'subscription', line_items: [{ price, quantity: 1 }], client_reference_id, success_url: '...?session_id={CHECKOUT_SESSION_ID}', cancel_url })`, return `session.url`, then redirect on the client.
3. `POST /api/stripe/webhook`: `await req.text()`, then `webhooks.constructEvent(body, sig, secret)`. Return 400 on failure.
4. Handle `checkout.session.completed`, `customer.subscription.updated` and `customer.subscription.deleted`. Webhooks, not the success page, decide paid status.
5. Customer portal: `billingPortal.sessions.create({ customer, return_url })`. Needs a saved `customer` id.
## Mistakes agents make
1. `new Stripe(process.env.STRIPE_SECRET_KEY!)` at module top level. This breaks `next build` without keys. Use the lazy getter.
2. `await req.json()` in the webhook. Signature verification needs the raw text from `req.text()`.
3. Passing removed or renamed v23 params, for example `payment_method_types` on `checkout.sessions.create`. Omit it; Checkout picks methods from Dashboard settings. Check the v23 changelog before using older snippets.
4. Granting access on the success page only. Verify via webhook.
5. Persisting paid status to a local JSON file or in-memory Map. This is lost on serverless hosts. Store `stripe_customer_id` and subscription status on the user row in your database.
Do not set `apiVersion`; the SDK pins one.
## Verify (no keys needed)
```ts
const payload = JSON.stringify({ id: 'evt_test', object: 'event', type: 'checkout.session.completed', data: { object: {} } });
const header = stripe.webhooks.generateTestHeaderString({ payload, secret: 'whsec_test' });
// POST payload with header 'stripe-signature' to /api/stripe/webhook with STRIPE_WEBHOOK_SECRET=whsec_test -> expect 200
// POST with a bad signature -> expect 400
```
Then `npm run build && npm run lint`. For real flows use test keys and card 4242 4242 4242 4242.