.paid(), .upTo(), and .session() are mutually exclusive pricing modes; .siwx() and .apiKey() compose with pricing.
| Method | Purpose |
|---|---|
.paid(price) | Fixed, body-derived, or tiered payment up front (x402, MPP, or both). |
.upTo(maxPrice) | Handler-computed billing via charge(amount), settled once. x402 only. |
.session({ unitCost, maxPrice }) | Per-unit billing over an MPP payment channel. Required for streaming. MPP only. |
.siwx() | Wallet identity, no payment. |
.apiKey(resolver) | X-API-Key or Authorization: Bearer <key>. Composes with pricing modes. |
.unprotected() | No auth. |
.paid()
Fixed price per request:
Fixed
.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:
.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
charge() bills one unit:
Streaming
.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:
.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 asctx.body..query(schema): parses the query string and switches the route toGET, typed asctx.query..output(schema): declares the response shape for OpenAPI generation. The runtime does not validate handler return values; callschema.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:
Throwing errors
ThrowHttpError 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:
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.