Skip to main content
Every route picks exactly one auth mode. .paid(), .upTo(), and .session() are mutually exclusive pricing modes; .siwx() and .apiKey() compose with pricing.
Fixed price per request:
Fixed
Compute the price from the parsed body: the challenge quotes the exact amount before payment. Requires .body(schema):
Body-derived
maxPrice caps the computed amount. If the pricing function throws an HttpError, the request is rejected with that status before any payment; any other throw falls back to quoting maxPrice. Or pick a tier from a body field:
Tiered

.upTo()

Handler-computed billing, x402 only. The handler calls charge(amount) as it works; the request settles once for the running total, capped at maxPrice:
A charge() that would push the total past maxPrice throws a 400 CHARGE_OVER_CAP error and nothing settles: it is not silently clamped. Guard your loop if partial work should still be billed.

.session()

Per-unit billing over an MPP payment channel, MPP only. Requires MPP_OPERATOR_KEY (see Configuration). In request mode, .handler() bills exactly unitCost per request:
Request mode
Streaming serves the session over SSE; each charge() bills one unit:
Streaming
Streaming is MPP-only: .stream() on any other pricing mode throws at registration.

.siwx()

Free routes gated by wallet identity. Unpaid requests get a 402 with a SIWX challenge; the verified wallet address is available to the handler:

Pay once, replay free

.paid() and .upTo() compose with .siwx(): the first request pays normally, the wallet is recorded, and later requests with a valid SIWX signature skip payment:
Serverless deployments need a real KV store for this: without one, entitlements live in a per-process Map and a wallet that paid on one instance gets charged again on another. See Configuration.

.apiKey()

The resolver receives the key from X-API-Key or Authorization: Bearer; return null to reject with 401:

Schemas and examples

Chain .body(), .query(), and .output() with Zod schemas. Types propagate through the builder, so body, query, and the expected return type are fully typed in the handler:
  • .body(schema): parses and validates the JSON body, typed as ctx.body.
  • .query(schema): parses the query string and switches the route to GET, typed as ctx.query.
  • .output(schema): declares the response shape for OpenAPI generation. The runtime does not validate handler return values; call schema.parse() inside the handler if you want strict output validation.

Discovery examples

.inputExample() and .outputExample() attach examples to the OpenAPI document. Both are validated against the registered schemas at registration, so a drifted example fails at build time instead of misleading agents:

.description()

A one-line, human-readable summary of the route. It surfaces in the OpenAPI spec and llms.txt, so it’s often the first thing an agent reads when deciding whether to call you:

Path params

Paths declare {param} segments, extracted in every hosting mode:

Pre-payment validation

.validate() runs before the 402 challenge, so callers are never charged for a request that was going to fail:
Pipeline order: body parse → validate → 402 challenge → payment → handler. On paid routes, the paying retry always parses and validates the body before settlement, so a 400 never costs the caller money.

Throwing errors

Throw HttpError from handlers, pricing functions, or validators to reject with a specific status code. A plain Error with a numeric status property works the same way; anything else becomes a 500:
In a body-derived pricing function, only HttpError instances are rethrown before the 402 challenge (so callers see the rejection without paying); any other throw (including a plain error with a status property) falls back to quoting maxPrice instead. Use HttpError when a pricing function needs to reject. Handlers can also return a Response directly for full control over status, headers, and body.