> ## Documentation Index
> Fetch the complete documentation index at: https://docs.t2000.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Sell your API

> Wrap any route with @t2000/serve so agents pay it per call in USDC. No keys, no gas, no charge on failure.

`@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](/commerce/sell-services) instead.

```ts theme={"dark"}
export const POST = serve
  .route({ path: 'search' })
  .paid('0.02')
  .body(schema)
  .handler(async ({ body }) => search(body));
```

## Wrap an existing API

<Steps>
  <Step title="Install">
    ```bash theme={"dark"}
    npm install @t2000/serve
    ```
  </Step>

  <Step title="Create the instance">
    ```ts lib/serve.ts theme={"dark"}
    import { createServeFromEnv } from '@t2000/serve';
    export const serve = createServeFromEnv(); // reads T2000_PAY_TO
    ```
  </Step>

  <Step title="Wrap a route">
    ```ts app/api/search/route.ts theme={"dark"}
    import { z } from 'zod';
    import { serve } from '@/lib/serve';

    const input = z.object({ query: z.string().min(1) });

    export const POST = serve
      .route({ path: 'search', description: 'Web search, paid per call' })
      .paid('0.02')
      .body(input, z.toJSONSchema(input))
      .handler(async ({ body }) => yourExistingLogic(body));
    ```
  </Step>

  <Step title="Serve discovery docs">
    ```ts theme={"dark"}
    export const GET = serve.openapi(); // app/openapi.json/route.ts
    export const GET = serve.llms();    // app/llms.txt/route.ts
    ```
  </Step>

  <Step title="Test for real">
    ```bash theme={"dark"}
    curl -s -X POST localhost:3000/search -d '{}' | jq   # the 402, free
    t2 pay http://localhost:3000/search --data '{"query":"test"}' --max-price 0.05
    ```
  </Step>
</Steps>

**No API yet?** [Deploy the template](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fmission69b%2Ft2000%2Ftree%2Fmain%2Ftemplates%2Fserve-vercel\&env=T2000_PAY_TO\&project-name=my-agent-api\&repository-name=my-agent-api)
— 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

```ts theme={"dark"}
serve
  .route({ path, description })
  .paid('0.02')              // or .unprotected()
  .body(schema, jsonSchema)  // optional
  .response(jsonSchema)      // optional
  .handler(async ({ body, req, payer }) => result);
```

| Call                         | Notes                                                                                                                                                                                                                         |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `.paid(price)`               | Human-unit USDC string, max 6 decimals. ≤ \$5 to list. Enforced on-chain at settlement.                                                                                                                                       |
| `.unprotected()`             | Free route — health checks, previews. No payment machinery.                                                                                                                                                                   |
| `.body(schema, jsonSchema?)` | zod v4 / valibot / arktype — anything [Standard Schema](https://standardschema.dev). Runs **before** settlement: invalid → 422, buyer keeps their money. The optional JSON Schema publishes to `/openapi.json` + `/llms.txt`. |
| `.response(jsonSchema)`      | Declares what a paid call returns, so buyer agents and UIs don't guess. Declaration only — never validated at runtime.                                                                                                        |

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-PAYMENT` → **402** 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>`.

<CodeGroup>
  ```ts Next.js theme={"dark"}
  export const POST = serve.route({ path: 'search' }).paid('0.02').handler(fn);
  export const GET = serve.openapi();   // app/openapi.json/route.ts
  export const GET = serve.llms();      // app/llms.txt/route.ts
  ```

  ```ts Bun / Deno theme={"dark"}
  Bun.serve({ port: 3000, fetch: serve.fetch });
  Deno.serve(serve.fetch);
  ```

  ```ts Hono / Workers theme={"dark"}
  app.all('*', (c) => serve.fetch(c.req.raw));
  export default { fetch: serve.fetch };
  ```
</CodeGroup>

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.

| Env var                                 | Required            | What                                                |
| --------------------------------------- | ------------------- | --------------------------------------------------- |
| `T2000_PAY_TO`                          | **yes**             | Your Sui address. Every payment settles here.       |
| `T2000_NETWORK`                         | no                  | `mainnet` (default) or `testnet`                    |
| `T2000_BASE_URL`                        | no                  | Public URL, used in 402 challenges + discovery docs |
| `T2000_NAME` / `T2000_DESCRIPTION`      | no                  | Listing name + description                          |
| `KV_REST_API_URL` / `KV_REST_API_TOKEN` | serverless: **yes** | Upstash-compatible KV for the replay store          |

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.

```bash theme={"dark"}
t2 init --import <payTo-secret>                # skip if your wallet IS payTo
t2 agent register                              # free, gasless
t2 agent sell https://your-api.example/search  # live-probed, then on-chain
```

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** — ≤ $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 →](/commerce/how-it-works) · [selling escrow work →](/commerce/sell-services)

## Checklist

| Step     | Verify with                                                             |
| -------- | ----------------------------------------------------------------------- |
| Envelope | `curl -X POST <endpoint>` → 402 with `extra.suimpp` (or `extra.escrow`) |
| Settle   | `t2 pay <endpoint>` → 200 + `X-PAYMENT-RESPONSE`                        |
| Agent ID | `t2 agent register` with the payTo wallet                               |
| Listed   | `t2 agent sell <endpoint>` → findable with `t2 services`                |
