Runtimes
@t2000/serve is fetch-native — every route is (Request) => Promise<Response>.
With per-route Next.js exports, import every route file from your
openapi.json/llms.txt routes (import '../search/route') so they
register before the discovery docs render. serve.fetch doesn’t have this
concern — everything lives in one module graph.Environment contract
createServeFromEnv() reads (empty strings count as unset — the classic
misconfig is caught, not masked):
Prefer code over env?
createServe({ payTo, network, store, ... }) takes the
same options directly.
The replay store
Payments must be single-use. serve enforces challenge-once (a signed payment payload can’t be replayed) and digest-once (a settled transaction can’t unlock a second response) through aDigestStore:
- Default: in-memory. Correct for a single long-lived process. Wrong for serverless — every lambda instance gets its own memory, so a replayed payment could land on a fresh instance.
- Production: Upstash KV. Set both
KV_REST_API_*vars (on Vercel: Storage → Upstash for Redis injects them). Keys live 72 h — longer than the payment’s on-chain validity window, so expiry can never re-open a replay. - Custom: pass any
{ has(key), set(key) }implementation ascreateServe({ store })—setmust be atomic set-if-absent and throw on duplicates.
CORS
Permissive CORS (*) is on by default, exposing the payment headers — so
browser wallets (Audric Passport / zkLogin buyers) can pay your API directly.
Return your own Response with CORS headers from a handler to override.
Sui connectivity
All chain access is gRPC against the public fullnode (fullnode.mainnet.sui.io)
— chain identifier cached forever, epoch cached 10 minutes. Override with
createServe({ rpcUrl }) if you run your own node. If chain info is
unreachable, unpaid requests get a 503 (never an unpayable 402).