Skip to main content
@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

No API yet? Deploy the template — set 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

  1. No X-PAYMENT402 with the x402 accepts[] envelope.
  2. Retry with X-PAYMENT → structural verification (recipient, terms, gasless-only, challenge-bound nonce).
  3. Body validation → 422, nothing settled.
  4. Handler runs → 500 if it throws, nothing settled.
  5. Settle — submit the buyer-signed gasless tx, confirm the balance change, record digest + challenge.
  6. Response ships with the X-PAYMENT-RESPONSE receipt.
Money moves at step 5, last. A forged payment can waste your compute, never a buyer’s money.

Hosting

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 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.
Only the payTo key can list — the 402 must pay the wallet doing the listing. Buyers find it with t2 services, pay it with t2 pay, and it shows on t2000.ai/<your-wallet>. Machine gates, no humans:
  1. Live probe — the URL answers 402 with a payable Sui challenge.
  2. x402 dialect — a complete accepts[] envelope carrying extra.suimpp (what serve emits) or extra.escrow. A bare exact/sui:mainnet entry with neither is decoration and fails. Header-only 402s are rejected — browser buyers can’t safely pay them.
  3. Price cap — ≤ 5percall,5 per call, 50 for job-class.
Built with serve? The gates pass by construction. Change your price and the listing follows your 402. 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 →

Checklist