# @t2000/serve

Merchant-side x402 router for Sui — wrap any API so agents can discover it and pay per call in USDC.

```ts
// app/api/search/route.ts (Next.js) — a complete paid endpoint
import { asNextRoute, createServeFromEnv } from '@t2000/serve';

const serve = createServeFromEnv(); // reads T2000_PAY_TO from env

export const { POST, OPTIONS } = asNextRoute(
  serve
    .route({ path: 'search' })
    .paid('0.01') // USDC per call
    .body(searchSchema) // zod v4 / valibot / arktype / anything Standard-Schema
    .handler(async ({ body }) => search(body)),
);
```

Next.js dispatches only the methods a `route.ts` exports — you must export
`OPTIONS` too (`asNextRoute` does it), or browser buyers fail CORS preflight;
serve's own CORS handling can't run on a request Next never delivers. Fetch
runtimes that mount one handler (Bun / Deno / workers) don't need this —
OPTIONS already reaches the same handler.

That route now answers x402 payment challenges, validates inputs, verifies and
settles payments on Sui, and is sellable as a Service on your Agent ID
and [t2000.ai](https://t2000.ai) — where every agent running
`t2 pay` or the t2000 MCP can find and pay it.

## Why Sui / why this package

- **No seller key, no seller gas.** Payment settles sign-then-settle: the buyer
  signs a gasless USDC transfer, this package verifies and submits it. Your
  server never holds a private key and never pays gas. (Every EVM x402 router
  needs an operator wallet key in the server env. This one doesn't.)
- **No charge on failure.** The handler runs *before* settlement. Invalid body →
  422, handler throws → 500 — in both cases the buyer's payment is never
  submitted. Getting this wrong by hand is the most common seller bug.
- **Correct by construction.** Challenge-once + digest-once replay protection,
  structural payment verification (right amount, right recipient, gasless-only,
  framework-calls-only), CORS for browser wallets — all defaults.

## Setup

```bash
npm install @t2000/serve
```

| Env var | Required | What it is |
|---|---|---|
| `T2000_PAY_TO` | yes | Your Sui address — payments settle here (`t2 address` prints it) |
| `T2000_NETWORK` | no | `mainnet` (default) or `testnet` |
| `T2000_ACTIVITY_REPORT_URL` | no | Fire-and-forget `x402.paid` reports after each successful settle (chain-verified server-side; never affects buyer responses). **Default-on for `createServeFromEnv`**: unset → `https://t2000.ai/api/activity/x402`, so your settled calls show on the [t2000.ai](https://t2000.ai) home stream and your seller profile's recent activity. Opt out with `false` \| `0` \| `off` \| `none`, or set a custom URL. Code-constructed `new Serve({...})` stays silent unless you pass `activityReportUrl`. |
| `T2000_BASE_URL` | no | Public URL of the deployed app (used in challenges + discovery) |
| `KV_REST_API_URL` / `KV_REST_API_TOKEN` | serverless: yes | Upstash-compatible KV for durable replay protection. Without it the store is in-memory (fine for one long-lived process, wrong for serverless). |

No wallet yet? `npm i -g @t2000/cli && t2 init` — wallet + free on-chain Agent ID.

## Routes

```ts
serve.route({ path: 'search' }).paid('0.01').body(schema).handler(fn);  // paid
serve.route({ path: 'health' }).unprotected().handler(() => ({ ok: true })); // free
```

- Prices are human-unit USDC strings (`'0.01'`), max 6 decimals.
- Prices above **5 USDC** work but are the wrong shape for per-call x402 —
  deliverable-priced work belongs in escrow (`t2 service create`).
- Handlers receive `{ body, req, payer }` — `payer` is the buyer's verified Sui
  address (wallet-based identity, no accounts).
- Return any JSON-serializable value (wrapped in a 200) or a `Response`.

## Discovery — agents find you

```ts
// Next.js
export const GET = serve.openapi(); // app/openapi.json/route.ts
export const GET = serve.llms();    // app/llms.txt/route.ts

// Fetch runtimes (Bun / Deno / Hono / Workers) — one handler for everything:
Bun.serve({ fetch: serve.fetch });
app.all('*', (c) => serve.fetch(c.req.raw)); // Hono
```

`/openapi.json` is OpenAPI 3.1 with the `x-payment-info` pricing extension on
every paid operation (the shape x402 tooling indexes); `/llms.txt` is plain-text
guidance for agents. Pass a JSON Schema as `.body(schema, jsonSchema)` (zod v4:
`z.toJSONSchema(schema)`) and buyers' agents build request bodies without
guessing — a wrong guess against a direct seller is a paid error.

Declare what a paid call returns with `.response(jsonSchema)` — published under
the 200 response and carried through the catalog so buyer surfaces render your
deliverable by type. Annotate with `contentMediaType` (`"image/svg+xml"`,
`"text/markdown"`) and `format: "color"` where they apply:

```ts
const output = z.object({ svg: z.string().meta({ contentMediaType: 'image/svg+xml' }) });
serve.route({ path: 'logo' }).paid('0.05').body(schema).response(z.toJSONSchema(output)).handler(fn);
```

## Get listed

Deploy, then:

```ts
console.log(serve.catalogSubmitCommand('https://api.example.com'));
```

prints the dry-run (`/api/catalog/preview`) and submit (`/api/catalog/submit`)
curls. The catalog verifies your live 402 challenge — no forms, no approval queue.

## Docs

Full guide: [docs.t2000.ai](https://docs.t2000.ai) → Sell to agents.
