Next.js
Charge per call for Next.js App Router route handlers with paid() from @tollstile/next.
npm install tollstile @tollstile/nextimport { createTollstile, memoryLedger, testRail } from "tollstile";
export const toll = createTollstile({ rails: [testRail()], ledger: memoryLedger() });import { paid } from "@tollstile/next";
import { toll } from "@/lib/toll";
export const GET = paid(
toll.price("$0.01", { resource: "GET /reports/[id]" }),
async (request, { params, payment }) => {
const { id } = await params;
return Response.json({ id, paidWith: payment.via });
},
);curl -i localhost:3000/reports/42 # 402 Payment Required
curl -i -H "Payment: test" localhost:3000/reports/42 # 200 OK, payment-receipt: test_settlement_…paid(gate, handler) returns a route handler. Unpaid requests get a 402 with every rail's challenge; paid requests run your handler with { params, payment }, and the payment is completed — settled, or released — before the response is returned. The package has no dependency on next.
Route handlers only: pages, server components, and server actions are not guarded. Tutorial: Add a paid route to Next.js.
Behavior
| Handler result | Payment |
|---|---|
A response with status below 400 | Completed as succeeded: settled on the authorization flow, receipt headers added |
A response with status 400 or above | Completed as failed: released, or refunded on the upfront flow |
| Throws or rejects | Completed as failed, then the error is rethrown |
| Settlement rejected | The response body is cancelled and a fresh 402 with error code settlement_rejected is returned instead |
| Settlement unknown | The response is returned without a receipt; reconciliation resolves the charge |
redirect()andnotFound()fromnext/navigationwork by throwing, so they count as failures. ReturnNextResponse.redirect()when a redirect is the paid result.- Call
payment.fulfill()inside the handler to mark the service as delivered earlier; a later failure then does not undo the charge. - The resource is
"<METHOD> <pathname>", without the query string. Dynamic segments make that set unbounded, so name the route withtoll.price(amount, { resource }). - Responses with immutable headers (from
fetch()orResponse.redirect()) are copied so the receipt can be added; the copy keeps the status, headers, and unread body stream. - The handler receives the request typed as
Request. Next.js passes aNextRequest; usenew URL(request.url)for the URL. memoryLedger()lives in one process. On serverless deployments use a database ledger, and give every instance the samesecret.
Options
paid(gate, handler, options?)
| Option | Type | Description |
|---|---|---|
principal | (request: Request) => Principal | null | Promise<Principal | null> | Resolves the authenticated caller for subscriber() and credits(). Defaults to no principal. |
Verification status
Tested by calling the exported handler the way Next.js does — (request, { params: Promise }) — against the test rail, memory ledger, and memory balance: the 402 → pay with the quote → 200 round trip, params passthrough, receipts on immutable responses, releases on thrown errors and 4xx, a single completion per request, principals reaching credits(), and type assignability for static, dynamic, and catch-all routes.
Not run inside a Next.js application. To verify, add the example to an app, run next build (which type-checks route exports) and next dev, then the two curl commands.
Idempotent retries
The adapter forwards Idempotency-Key to core automatically. Send the same key on the first paid request and its retries; keep the request unchanged. A completed payment returns 409 already_paid with chargeId, settlement, and result; the handler does not run again. In-flight requests return 409 request_in_progress, unknown outcomes return 503 payment_outcome_unknown, and a key used for a different request returns 422 idempotency_key_reused.
Use payment.fulfill({ resultRef }) to record where your application stored its result. Tollstile does not cache the response. Released or refunded attempts may run again; subscriber grants are not deduplicated. See Idempotency for matching, rail identity scopes, and retry limits.