Tollstile

Quickstart

Add pay-per-call pricing to an API in five minutes with the test rail. No wallet or account.

Public Beta · early access

Or try it without installing anything

demo.tollstile.com is a paid API running this code on Cloudflare Workers: meet a 402, pay it with the test rail, and watch the charge land in the ledger. The same demo is a paid MCP tool at https://demo.tollstile.com/mcp, where one tool is charged only after a person approves the amount — see Consent.

This guide prices a Next.js route handler with the test rail and memory ledger, so there is nothing to sign up for. The same two steps work on Express, Hono, any fetch handler, and MCP tools.

1. Install

npm install tollstile @tollstile/next

Or start from a template: npx create-tollstile my-paid-api.

2. Price a route

lib/toll.ts
import { createTollstile, memoryLedger, testRail } from "tollstile";

export const toll = createTollstile({
  rails: [testRail()],
  ledger: memoryLedger(),
});
app/weather/route.ts
import { paid } from "@tollstile/next";
import { toll } from "@/lib/toll";

export const GET = paid(toll.price("$0.01"), () => {
  return Response.json({ forecast: "clear" });
});

That is the whole change: the handler you already had, with a price in front of it.

3. Call it without paying

curl -i localhost:3000/weather
HTTP/1.1 402 Payment Required

{
  "error": { "code": "payment_required", "retryable": true, "action": "pay", "message": "Payment required.", "detail": null },
  "price": "$0.01",
  "quote": "eyJ2IjoxLCJpZCI6…",
  "accepts": [{ "rail": "test", "amount": "10000", "flow": "authorization", … }]
}

The quote is a signed record of what the server offered. It is never stored.

4. Pay with the quote

curl -i -H "Payment: test quote=eyJ2IjoxLCJpZCI6…" localhost:3000/weather
HTTP/1.1 200 OK
payment-receipt: test_settlement_chg_…

{ "forecast": "clear" }

The ledger now holds one authorization and one charge in settled/completed. For fixed prices, Payment: test without a quote also works.

5. Switch to a real rail

Replace testRail() with live rails and memoryLedger() with a database ledger — on serverless, a database ledger and one shared secret across instances. The route does not change. The tutorials walk through it:

Next

Retry the same operation

curl -i -H 'Payment: test' -H 'Idempotency-Key: weather-1' localhost:3000/weather
curl -i -H 'Payment: test' -H 'Idempotency-Key: weather-1' localhost:3000/weather

The first succeeds; the second returns 409 already_paid. The default test payer is stable even though Payment: test creates a fresh proof. See Idempotency.

On this page