# Add Stripe Checkout to an Express App

Yes. An agent adds a Checkout Session route and a signature-checked webhook to your Express app, tests both with the Stripe CLI and opens a pull request.

[Run this on Rush](https://cloud.getrush.ai/run/add-stripe-checkout/express/), in a Project that has your repository.

## What the agent does

The agent starts by reading how the app is put together. In an Express API like gothinkster/node-express-realworld-example-app it opens `package.json` and finds Express `~4.18.1`, `@prisma/client` and `prisma` at `^4.16.1`, TypeScript `~5.2.2`, Jest 29 and an Nx 17.2.6 workspace whose `start`, `build` and `test` scripts call `nx serve`, `nx build` and `nx test`. The lockfile is `package-lock.json`, so it installs with `npm ci`. It reads `src/main.ts`, where `cors()`, `bodyParser.json()` and `bodyParser.urlencoded()` are mounted on the whole app before `routes`, and `src/app/routes/routes.ts`, which mounts the tag, article, profile and auth controllers under `/api`.

That order matters for Stripe. `stripe.webhooks.constructEvent` needs the exact bytes Stripe sent, and `bodyParser.json()` has already parsed them by the time any router runs. So the agent registers the webhook route in `src/main.ts` above the JSON parser, with `express.raw({ type: 'application/json' })` on that one path, and leaves every other route as it was. The Checkout endpoint goes in a new `src/app/routes/checkout/checkout.controller.ts` beside the existing controllers, mounted through `routes.ts` so it lives at `/api/checkout`. It is protected with `auth.required` from `src/app/routes/auth/auth.ts`, the same `express-jwt` middleware the article routes use, so the Session is created for a signed-in user and carries that user's id as `client_reference_id`.

Recording the purchase needs somewhere to put it. `src/prisma/schema.prisma` has `User`, `Article`, `Comment` and `Tag` on a Postgres `datasource` and nothing about payments, so the agent adds a `Purchase` model keyed by the Checkout Session id (unique, so a webhook Stripe retries cannot record the same payment twice) with the user relation, amount, currency and status, and generates a migration next to the four under `src/prisma/migrations/`. It adds `stripe` to `dependencies`, reads `STRIPE_SECRET_KEY`, `STRIPE_PRICE_ID` and `STRIPE_WEBHOOK_SECRET` from the environment the way `auth.ts` reads `JWT_SECRET`, and adds a plain success route that confirms the order by reading the Session back from Stripe. The existing service tests in `src/tests/services/` mock Prisma through `jest-mock-extended` in `src/tests/prisma-mock.ts`, so the agent writes the new service test the same way.

Then it runs the real thing. It starts Postgres in the sandbox, applies the migrations, runs `npm run start`, and in another shell runs `stripe listen --forward-to localhost:3000/api/checkout/webhook` with the Stripe test key, which prints the signing secret the server needs. It calls the Checkout endpoint with a JWT for a seeded user, runs `stripe trigger checkout.session.completed`, and checks that the handler answered 200 and a `Purchase` row exists. It also sends one request with a tampered signature and confirms the handler answers 400 and writes nothing. Finally it runs `npm test` and `nx build` so the pull request shows the existing suite still passes.

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "claude",
  "input": "Add Stripe Checkout to this Express app for one fixed-price product. Add POST /api/checkout, behind the existing auth.required middleware, that creates a Checkout Session for STRIPE_PRICE_ID with the user's id as client_reference_id. Add a success route that reads the Session back from Stripe. Add POST /api/checkout/webhook, registered in src/main.ts before bodyParser.json() with express.raw so the signature check sees the raw body, that verifies the Stripe-Signature header with STRIPE_WEBHOOK_SECRET and records checkout.session.completed in a new Prisma Purchase model keyed by the Session id, with a migration. Test it in the sandbox: run Postgres, start the server, forward events with stripe listen, run stripe trigger checkout.session.completed, and also send one request with a bad signature. Run npm test and nx build. Open a pull request whose description includes the stripe listen output, the trigger output and the test output.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
```

1. **Get an API key.** In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
2. **Send the request.** POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
3. **Read what came back.** Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

## What comes back

A pull request on a branch of its own. The diff touches `src/main.ts` (the raw-body webhook route above the JSON parser), `src/app/routes/routes.ts` (one more `.use`), a new `src/app/routes/checkout/` folder with the controller and a service, `src/prisma/schema.prisma` with the `Purchase` model, one new migration folder, a test under `src/tests/services/`, and `package.json` with `package-lock.json` for the `stripe` dependency. The description lists the three environment variables the code reads and pastes the terminal output from the sandbox: `stripe listen` forwarding the event, the handler's 200, the 400 for the bad signature, and the Jest summary.

Before merging, a reviewer checks three things. That the webhook path is the only one using `express.raw`, so every other route still gets parsed JSON. That the unique Session id on `Purchase` makes a replayed `checkout.session.completed` a no-op rather than a second row. And that the price comes from `STRIPE_PRICE_ID` on the server, never from the request body. The session's transcript keeps every command the agent ran, so a reviewer who doubts a line of the description can find the step that printed it.

## Run it on every customer project that needs payments

A services firm that adds payments to client apps can start one session per client repository from its own backend, with the client's repository in `resources` and the client's product details in the prompt. Send an `Idempotency-Key` built from the client and the job so a retried request returns the run already started instead of opening a second pull request.

Set a webhook URL on the API key and Rush POSTs `run.completed` when the turn ends, with `session_url`. Your server reads the session, finds the pull request on it, and posts the link to the client's channel. A run that ends `run.failed` posts that instead, and the transcript shows which step stopped it.

## Why run it on Rush?

Testing a Stripe webhook on a laptop means a running database, the Stripe CLI logged in, a second terminal for `stripe listen`, and a signing secret pasted into a `.env` that nobody remembers to delete. The agent does all of that in its own sandbox: Postgres, the server, the forwarder and the trigger run side by side on a machine that exists only for this job, and the Stripe test key never touches a developer's shell history.

The pull request carries its own proof. The forwarded event, the 200 from the handler, the refused bad signature and the Jest run are the output of commands that ran, and the transcript keeps each one with its output. That is what a reviewer needs to merge payment code, and it is the same whether one app gets Checkout or a dozen run at once up to the key's concurrent run limit.

## Frequently asked questions

### Where does the Stripe test key come from?

Save it as a Project secret named `STRIPE_SECRET_KEY` with `POST /o/{org}/secrets` and start the session in that Project with `POST /o/{org}/p/{project}/sessions`, which takes the same body. The agent reads it as an environment variable in the sandbox. Use a test-mode key; the prompt never needs a live one.

### Why does the webhook route have to come before bodyParser.json()?

Stripe signs the raw request body, and `constructEvent` recomputes that signature from the bytes it is given. Once `bodyParser.json()` has parsed the body, the original bytes are gone and every event fails verification. Registering the webhook path first with `express.raw` keeps its body as a Buffer while the rest of the API is unchanged.

### Does it change my existing routes or auth?

No. It adds one route to `routes.ts`, one route in `main.ts`, and reuses `auth.required` as it is. The existing controllers, `auth.ts` and the JWT handling are left alone, and the pull request diff shows exactly which lines moved.

### Can it handle subscriptions instead of a one-time price?

Yes, if the prompt says so. Change the prompt to a recurring price and ask it to record `customer.subscription.created` and `customer.subscription.deleted` as well, and the agent adds those events to the handler and triggers each one with the Stripe CLI before opening the pull request.
