npm packages are not installable yet: [email protected] can be published after 2026-09-16 10:30 UTC. Details
Tollstile

Fetch (Workers, Deno, Bun)

Charge per call for Web-standard Request/Response handlers on Cloudflare Workers, Deno, Bun, and Node with paid() from @tollstile/fetch.

Public Beta · early access
npm install tollstile @tollstile/fetch
src/index.ts
import { createTollstile, memoryLedger, testRail } from "tollstile";
import { paid } from "@tollstile/fetch";

const toll = createTollstile({ rails: [testRail()], ledger: memoryLedger() });

const weather = paid(toll.price("$0.01"), (request, { payment }) =>
  Response.json({ forecast: "clear", paidWith: payment.via }),
);

export default { fetch: weather }; // Workers
// Bun: Bun.serve({ fetch: weather }) · Deno: Deno.serve(weather)
curl -i localhost:8787/weather                      # 402 Payment Required
curl -i -H "Payment: test" localhost:8787/weather   # 200 OK, payment-receipt: test_settlement_…

paid(gate, handler) turns a priced route into a (request) => Promise<Response> handler. Unpaid requests get a 402 with every rail's challenge; paid requests run your handler, and the payment is completed — settled, or released — before the response is returned, with the rail's receipt headers on it.

It guards one handler and does not route. Match paths yourself, or use Hono on the same runtimes. Tutorial: Charge per call on Cloudflare Workers.

Behavior

Handler resultPayment
A response with status below 400Completed as succeeded: settled on the authorization flow, receipt headers added
A response with status 400 or aboveCompleted as failed: released, or refunded on the upfront flow
Throws or rejectsCompleted as failed, then the error is rethrown
Settlement rejectedThe response body is cancelled and a fresh 402 with error code settlement_rejected is returned instead
Settlement unknownThe response is returned without a receipt; reconciliation resolves the charge
  • Call payment.fulfill() inside the handler to mark the service as delivered earlier; a later failure then does not undo the charge.
  • Responses with immutable headers (from fetch() or Response.redirect()) are copied so the receipt can be added. The copy keeps the status, status text, headers, and the unread body stream.
  • The resource is "<METHOD> <pathname>", without the query string. Name routes with parameters: toll.price("$0.01", { resource: "GET /users/:id" }).
  • On Workers, create the instance once per isolate and pass a shared secret, so quotes issued by one isolate verify in another. Use a database ledger such as SQLite on D1.

Options

paid(gate, handler, options?)

OptionTypeDescription
principal(request: Request) => Principal | null | Promise<Principal | null>Resolves the authenticated caller for subscriber() and credits(). Defaults to no principal.

Verification status

Tested with Node 22's Request and Response against the test rail, memory ledger, and memory balance: the 402 → pay with the quote → 200 round trip, receipts on mutable and immutable responses, streamed bodies, releases on thrown errors and 4xx, a single completion per request, and principals reaching credits().

Not run on Cloudflare Workers, Deno, or Bun. To verify on a runtime, run the example (wrangler dev, deno run --allow-net, or bun run) and the two curl commands: the first must return 402 with a quote in the body, the second 200 with a payment-receipt header.

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.

On this page