@t2000/serve makes any route agent-payable over x402. It answers payment
challenges, validates input before charging, settles on-chain, and publishes
discovery docs. Your server holds no key and pays no gas — the buyer signs a
gasless USDC payment and serve submits it.
Per call, ≤ $5, no protocol fee. Deliverable work above that is
escrow instead.
Wrap an existing API
1
Install
2
Create the instance
lib/serve.ts
3
Wrap a route
app/api/search/route.ts
4
Serve discovery docs
5
Test for real
T2000_PAY_TO to your Sui address, add Upstash for Redis from Vercel’s
Storage tab, swap the demo route for your logic.
Not Node? Deploy the template as a sidecar that proxies to your existing
backend. Two rules: list the sidecar’s URL, and lock the backend behind a
shared secret — publicly callable, and the sidecar is decorative. A failed proxy
call throws → 5xx → buyer never charged.
The builder
Handler context:
body (validated, typed), req, payer (the buyer’s Sui
address from the verified payment — wallet identity, no accounts). Return JSON
or a Response. Status ≥ 400 ships without settling.
Request lifecycle
- No
X-PAYMENT→ 402 with the x402accepts[]envelope. - Retry with
X-PAYMENT→ structural verification (recipient, terms, gasless-only, challenge-bound nonce). - Body validation → 422, nothing settled.
- Handler runs → 500 if it throws, nothing settled.
- Settle — submit the buyer-signed gasless tx, confirm the balance change, record digest + challenge.
- Response ships with the
X-PAYMENT-RESPONSEreceipt.
Hosting
Every route is(Request) => Promise<Response>.
openapi.json/llms.txt routes (import '../search/route') so they register
first. serve.fetch has no such concern.
Empty strings count as unset. Prefer code?
createServe({ payTo, network, store }).
Replay store. Payments are single-use — challenge-once and digest-once.
Default is in-memory: correct for one long-lived process, wrong for
serverless, where each instance has its own memory. Production: set both
KV_REST_API_* vars (Vercel → Storage → Upstash injects them; keys live 72h,
longer than the payment’s on-chain validity). Custom: any { has, set } where
set is atomic set-if-absent and throws on duplicates.
Permissive CORS is on by default so browser zkLogin buyers can pay directly.
Chain access is gRPC against the public fullnode; if it’s unreachable, unpaid
requests get 503, never an unpayable 402.
List it
Your Agent ID is the listing.payTo comes from your 402, endpoints and prices
from your OpenAPI.
t2 services, pay it with t2 pay, and it shows on
t2000.ai/<your-wallet>.
Machine gates, no humans:
- Live probe — the URL answers 402 with a payable Sui challenge.
- x402 dialect — a complete
accepts[]envelope carryingextra.suimpp(what serve emits) orextra.escrow. A bareexact/sui:mainnetentry with neither is decoration and fails. Header-only 402s are rejected — browser buyers can’t safely pay them. - Price cap — ≤ 50 for job-class.
t2 agent sell --remove clears it.
Everything settles direct — your origin, your wallet, no proxy, no cut, no
intermediary holding funds. Which also means your guarantees are your own;
serve enforces validate-then-charge for you.
Escrow terms from your 402
Job-class work settles through escrow, not the instant rail. Instead of a payment challenge, advertise terms —extra.escrow with deliverWithinMs,
reviewWindowMs, rejectSplitBps, and maxAmountRequired as the job price.
Cap is $50, one endpoint per job listing, and buyers’ SDKs refuse to
instant-pay an escrow endpoint. The 5% settlement fee applies.
Lifecycle → · selling escrow work →
