> For the complete documentation index, see [llms.txt](https://docs.trezalabs.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.trezalabs.com/api/x402-payments.md).

# x402 Payments

Treza integrates the [x402 protocol](https://docs.cdp.coinbase.com/x402/welcome) to enable instant, autonomous stablecoin micropayments for API access. AI agents and developers can pay for Treza services programmatically using USDC on Base or Solana, with no API keys, subscriptions, or manual billing required.

Seven endpoints are payable. The first six sell one thing per payment and need no Treza account:

* [**Pay-per-video**](#pay-per-video): send a prompt (and optionally a first frame), get a generated clip back.
* [**Speech**](#speech-music-and-images): send text, get an ElevenLabs voiceover back.
* [**Music**](#speech-music-and-images): describe a track, get an original Lyria 3 piece back.
* [**Images**](#speech-music-and-images): send a prompt, get an image back on the model you pick.
* [**Clips**](#clips-and-narrated-shorts): send a YouTube link, get its most shareable moment back as a captioned clip.
* [**Narrated shorts**](#clips-and-narrated-shorts): send a topic, get a finished 30-second narrated short back.
* [**Credit top-up**](#funding-an-account-without-a-human): add to the prepaid balance that pipeline runs draw from.

### What is x402?

x402 is an open payment protocol built by [Coinbase](https://docs.cdp.coinbase.com/x402/welcome) that uses the HTTP `402 Payment Required` status code. When a client requests a paid resource:

1. The server responds with `402` and payment instructions (price, network, recipient)
2. The client signs a USDC payment
3. The client retries the request with a `Payment-Signature` header
4. The server verifies the payment and settles it on-chain through a facilitator, then returns the resource

Payments are instant, permissionless, and settled in USDC on Base (L2) or Solana.

### How Treza Uses x402

#### Paid Endpoints

| Endpoint                    | Method | Price      | Description                                                                                                                                       |
| --------------------------- | ------ | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `/api/x402/video`           | POST   | from $0.42 | Generate a video from a prompt or a first frame, returned in the same response when it renders within 45 seconds. Priced by model and clip length |
| `/api/x402/speech`          | POST   | from $0.02 | Turn text into an ElevenLabs voiceover (MP3), returned in the same response. Priced per character                                                 |
| `/api/x402/music`           | POST   | from $0.06 | Generate an original Google Lyria 3 track from a description                                                                                      |
| `/api/x402/image`           | POST   | from $0.02 | Generate an image on Nano Banana, GPT Image 2, Seedream, FLUX or Recraft. Priced by model and setting                                             |
| `/api/x402/clip`            | POST   | from $0.04 | Clip a YouTube link: its most shareable moment as a captioned vertical clip. Priced by the source's length                                        |
| `/api/x402/short`           | POST   | from $0.87 | A finished 30-second narrated short from a topic: script, scenes, narrator, music, captions                                                       |
| `/api/billing/credits/x402` | POST   | $5.00      | Top up a prepaid credit balance                                                                                                                   |

#### Networks

Every 402 challenge from every endpoint lists two ways to pay the same amount, Base first:

| Chain          | `network`                                 | USDC (`asset`)                                 | Gas in your wallet                                                     |
| -------------- | ----------------------------------------- | ---------------------------------------------- | ---------------------------------------------------------------------- |
| Base mainnet   | `eip155:8453`                             | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`   | None: the payment is an EIP-3009 authorization the facilitator submits |
| Solana mainnet | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` | `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` | None: the facilitator pays the network fee (`extra.feePayer`)          |

An x402 client pays the first entry it has a wallet for. A client with an EVM wallet pays on Base, and a client with a Solana wallet pays on Solana (see [Paying from a Solana wallet](#paying-from-a-solana-wallet)). Your wallet needs USDC on the chain it pays from, and nothing else.

Paid on Solana, the response's `network` is `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp`, `transaction` is a base58 transaction signature rather than a `0x` hash, and `payer` is your base58 address. A Base wallet and a Solana wallet are separate Treza wallet accounts: credit left on one is not visible to the other.

### Pay-Per-Video

`POST /api/x402/video` generates a video from a text prompt, or animates an image from its first frame. It returns the video in the same response when the render finishes within 45 seconds, otherwise a `statusUrl` to collect it from. Pay once per clip. No Treza account, API key, or signup is required. The payment is the only credential.

#### Price

Price is quoted per request and depends on the model and the clip length. Read it off the 402 challenge rather than hard-coding it. Each model sells its own lengths:

| `model`                | Prices (USDC)                     |
| ---------------------- | --------------------------------- |
| `minimax-h3` (default) | 5 s $0.42, 10 s $0.84, 15 s $1.26 |
| `veo-3.1-lite`         | 4 s $0.45, 6 s $0.68, 8 s $0.90   |
| `wan-2.7`              | 5 s $0.70, 10 s $1.40             |
| `veo-3.1-fast`         | 4 s $0.68, 6 s $1.01, 8 s $1.35   |
| `kling-3.0`            | 5 s $0.89, 10 s $1.77, 15 s $2.65 |
| `kling-3.0-pro`        | 5 s $1.18, 10 s $2.36, 15 s $3.53 |
| `seedance-2.5`         | 5 s $1.64, 10 s $3.27, 15 s $4.90 |
| `veo-3.1`              | 4 s $2.24, 6 s $3.36, 8 s $4.48   |

`16:9` and `9:16` cost the same, and so does a clip that starts from an `image_url`. The cheapest clip is $0.42 and the most expensive is $4.90.

#### Models

Send `model` to pick one. Omit it and the default renders, unless you send an `image_url`: then the cheapest model that takes an image at the length you ask for renders it, and the 402 quotes that model's price.

| `seconds`, with an `image_url` and no `model` | Renders on     |
| --------------------------------------------- | -------------- |
| `5` or `10`                                   | `wan-2.7`      |
| `15`                                          | `kling-3.0`    |
| `4`, `6`, `8`, or none                        | `veo-3.1-lite` |

So `{"prompt": "...", "seconds": 5, "image_url": "..."}` buys a 5-second clip on `wan-2.7` for $0.70. Naming `minimax-h3` with an `image_url` is refused, since it renders from text only.

| `model`                | Model                  | First frame | Worth knowing                                                                                                                                                                                                                                                                                                                                            |
| ---------------------- | ---------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `minimax-h3` (default) | MiniMax Hailuo 3       | No          | 768p with native audio, and the fewest prompt refusals of anything we run. The right choice unless you have a reason to pay more. Text only: it cannot take an `image_url`                                                                                                                                                                               |
| `veo-3.1-lite`         | Google Veo 3.1 Lite    | Yes         | Google Veo at its lowest price, with native audio, usually ready in about a minute. Google's safety filter returns nothing for some prompts; that is not charged and can be retried                                                                                                                                                                      |
| `wan-2.7`              | Alibaba Wan 2.7        | Yes         | Native audio at 5 or 10 seconds, and the most permissive with photos of real people as a first frame                                                                                                                                                                                                                                                     |
| `veo-3.1-fast`         | Google Veo 3.1 Fast    | Yes         | Sharper Veo with native audio, typically under two minutes                                                                                                                                                                                                                                                                                               |
| `kling-3.0`            | Kling 3.0              | Yes         | 720p with native audio                                                                                                                                                                                                                                                                                                                                   |
| `kling-3.0-pro`        | Kling 3.0 Pro          | Yes         | Kling's higher-fidelity tier, 720p with native audio                                                                                                                                                                                                                                                                                                     |
| `seedance-2.5`         | ByteDance Seedance 2.5 | Yes         | 720p with native audio and higher fidelity, at about four times the price of the default. Its content filter is strict: prompts that resemble a brand, a broadcast, or a famous scene are refused, and so are photos of real people. A refusal is not charged, and the response carries a `retryUrl` that renders a reworded prompt without paying again |
| `veo-3.1`              | Google Veo 3.1         | Yes         | Google's top-fidelity model with synchronized audio, for final shots                                                                                                                                                                                                                                                                                     |

A `model` value that is not on this list is refused with a `400` before any payment moves, naming the ones that are. Nothing is ever silently swapped for the default.

To read the current list and prices programmatically, `GET /api/x402/video` with no query parameters. It returns every model on sale with what it is for, whether it takes a first frame, and its prices, plus the request fields, the limits, and how delivery works. No payment or credential is needed.

```bash
curl https://www.trezalabs.com/api/x402/video
```

Abridged (notes and most of the 46 offers left out):

```json
{
  "service": "Treza pay-per-video",
  "description": "Generate a video from a text prompt, or animate an image from its first frame, and get the file back in the same response. The payment is the only credential: no account, API key, or signup.",
  "howToBuy": {
    "method": "POST",
    "body": { "prompt": "a manta ray gliding over a sunlit coral reef, slow cinematic drift", "seconds": 5, "aspectRatio": "16:9", "model": "minimax-h3" },
    "note": "..."
  },
  "fields": { "prompt": "...", "seconds": "...", "aspectRatio": "...", "model": "...", "image_url": "...", "async": "..." },
  "models": [
    { "id": "minimax-h3", "label": "MiniMax Hailuo 3", "default": true, "firstFrame": false, "note": "...", "prices": [{ "seconds": 5, "priceUsd": 0.42 }, { "seconds": 10, "priceUsd": 0.84 }, { "seconds": 15, "priceUsd": 1.26 }] },
    { "id": "veo-3.1-lite", "label": "Google Veo 3.1 Lite", "default": false, "firstFrame": true, "note": "...", "prices": [{ "seconds": 4, "priceUsd": 0.45 }, { "seconds": 6, "priceUsd": 0.68 }, { "seconds": 8, "priceUsd": 0.9 }] },
    { "id": "wan-2.7", "label": "Alibaba Wan 2.7", "default": false, "firstFrame": true, "note": "...", "prices": [{ "seconds": 5, "priceUsd": 0.7 }, { "seconds": 10, "priceUsd": 1.4 }] },
    { "id": "veo-3.1-fast", "label": "Google Veo 3.1 Fast", "default": false, "firstFrame": true, "note": "...", "prices": [{ "seconds": 4, "priceUsd": 0.68 }, { "seconds": 6, "priceUsd": 1.01 }, { "seconds": 8, "priceUsd": 1.35 }] },
    { "id": "kling-3.0", "label": "Kling 3.0", "default": false, "firstFrame": true, "note": "...", "prices": [{ "seconds": 5, "priceUsd": 0.89 }, { "seconds": 10, "priceUsd": 1.77 }, { "seconds": 15, "priceUsd": 2.65 }] },
    { "id": "kling-3.0-pro", "label": "Kling 3.0 Pro", "default": false, "firstFrame": true, "note": "...", "prices": [{ "seconds": 5, "priceUsd": 1.18 }, { "seconds": 10, "priceUsd": 2.36 }, { "seconds": 15, "priceUsd": 3.53 }] },
    { "id": "seedance-2.5", "label": "ByteDance Seedance 2.5", "default": false, "firstFrame": true, "note": "...", "prices": [{ "seconds": 5, "priceUsd": 1.64 }, { "seconds": 10, "priceUsd": 3.27 }, { "seconds": 15, "priceUsd": 4.9 }] },
    { "id": "veo-3.1", "label": "Google Veo 3.1", "default": false, "firstFrame": true, "note": "...", "prices": [{ "seconds": 4, "priceUsd": 2.24 }, { "seconds": 6, "priceUsd": 3.36 }, { "seconds": 8, "priceUsd": 4.48 }] }
  ],
  "offers": [
    { "model": "minimax-h3", "seconds": 5, "aspectRatio": "16:9", "priceUsd": 0.42 },
    { "model": "minimax-h3", "seconds": 5, "aspectRatio": "9:16", "priceUsd": 0.42 },
    { "model": "veo-3.1-lite", "seconds": 4, "aspectRatio": "16:9", "priceUsd": 0.45 }
  ],
  "limits": { "maxPromptChars": 2000, "seconds": [4, 5, 6, 8, 10, 15], "aspectRatio": ["16:9", "9:16"], "model": ["minimax-h3", "veo-3.1-lite", "wan-2.7", "veo-3.1-fast", "kling-3.0", "kling-3.0-pro", "seedance-2.5", "veo-3.1"] },
  "delivery": "The paid POST answers 200 at once and holds the connection while the render runs, sending a whitespace byte every two seconds (JSON parsers ignore it), then one JSON object: the finished result (status, video, statusUrl) or, after 45 seconds by default, status \"running\" with the statusUrl. The X-Status-Url response header carries the same URL from the first byte. \"Prefer: wait=<seconds>\" changes the wait (up to 720), and \"async\": true or \"Prefer: respond-async\" answers 202 with the statusUrl at once. GET the statusUrl exactly as given (the runId contains \"#\" and must stay encoded as %23); it takes \"Prefer: wait=<seconds>\" too."
}
```

#### Request

```json
{
  "prompt": "a manta ray gliding over a sunlit coral reef, slow cinematic drift",
  "seconds": 5,
  "aspectRatio": "16:9",
  "model": "minimax-h3"
}
```

| Field         | Required | Default                                                                             | Notes                                                                                                                                                                                                                                              |
| ------------- | -------- | ----------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `prompt`      | Yes      | none                                                                                | What the video should show. Max 2000 characters                                                                                                                                                                                                    |
| `seconds`     | No       | the model's shortest                                                                | A length the model sells (see [Price](#price)). Drives the price. Also accepted as `duration` or `durationSeconds`                                                                                                                                 |
| `aspectRatio` | No       | `16:9`                                                                              | `16:9` or `9:16`. Also accepted as `aspect`                                                                                                                                                                                                        |
| `model`       | No       | `minimax-h3`; with an `image_url`, the cheapest model that takes one at your length | One of the ids in [Models](#models). Drives the price                                                                                                                                                                                              |
| `image_url`   | No       | none                                                                                | First frame (image-to-video): a public https JPEG, PNG or WebP up to 10 MB. It is cropped to the `aspectRatio` you buy (1280x720 or 720x1280) and the video starts from it. Every model except `minimax-h3` takes one. Also accepted as `imageUrl` |
| `async`       | No       | `false`                                                                             | `true` answers `202` at once with a `statusUrl` to poll, instead of waiting for the render                                                                                                                                                         |

These fields are the whole request; they are also accepted nested under `"inputs"`. A request asking for a length or model that is not on sale, sending an `image_url` with `minimax-h3`, or carrying any field the endpoint does not read, is refused with a `400` **before the payment settles**, so a malformed call costs nothing, and nothing you send can be silently ignored and paid for.

An `image_url` is checked before the payment settles too, so an image that cannot be used is refused with nothing charged. The URL and the model are checked first; then the image is downloaded, and it must be fetchable within 4 seconds. Every `image_url` refusal carries a `code` alongside an `error` that says what to fix:

| `code`                   | Status | Meaning                                                                                                           |
| ------------------------ | ------ | ----------------------------------------------------------------------------------------------------------------- |
| `IMAGE_URL_INVALID`      | 400    | Not a plain https URL: `http`, a username or password, or a non-default port                                      |
| `IMAGE_URL_BLOCKED`      | 400    | Points at a private, loopback or internal host. Host the image somewhere public, such as a CDN or a public bucket |
| `IMAGE_MODEL_MISMATCH`   | 400    | Sent with `minimax-h3`, which renders from text only. Name another model, or leave `model` out                    |
| `IMAGE_FETCH_FAILED`     | 400    | The download failed: the host did not resolve, answered an error, or took longer than 4 seconds                   |
| `IMAGE_TOO_LARGE`        | 413    | Larger than 10 MB                                                                                                 |
| `IMAGE_UNSUPPORTED_TYPE` | 400    | Not a JPEG, PNG or WebP                                                                                           |
| `IMAGE_DIMENSIONS`       | 400    | Short edge under 256 px, an edge over 8000 px, over 40 megapixels, or a shape beyond 4:1                          |
| `IMAGE_STORE_FAILED`     | 503    | The image was fine but could not be stored on our side. Try again                                                 |

#### Flow

The payment settles before the render starts. The paid call then waits up to 45 seconds: a clip that renders in time comes back in the same response, and a longer render leaves you a `statusUrl` to collect it from. The 45-second default keeps the whole call inside fixed client timeouts, such as the 60 seconds an MCP tool call allows, so your client always receives its claim ticket.

```bash
# 1. Ask. With no payment you get the challenge back.
curl -i -X POST https://www.trezalabs.com/api/x402/video \
  -H 'content-type: application/json' \
  -d '{"prompt":"a manta ray gliding over a sunlit coral reef, slow cinematic drift","seconds":5,"aspectRatio":"16:9","model":"minimax-h3"}'

# HTTP/2 402
# Payment-Required: <base64 challenge: the price, with a Base and a Solana entry>
```

Any x402 client signs and retries for you. The paid response is streamed:

1. Status `200` and the headers arrive at once. `X-Status-Url` carries the `statusUrl`, so you hold the claim ticket before the render has finished.
2. While the render runs, the body is a whitespace byte every 2 seconds. JSON parsers ignore leading whitespace, so `res.json()` reads the body as usual.
3. The body ends with one complete JSON object: the result, or `"status": "running"` once the wait is over.

The HTTP status is `200` whatever happened to the render, so read `status` in the body. When the video is ready:

```json
{
  "runId": "2026-09-23T18:04:11.000Z#1a2b3c4d",
  "status": "success",
  "startedAt": "2026-09-23T18:04:11.000Z",
  "finishedAt": "2026-09-23T18:04:52.000Z",
  "durationMs": 41000,
  "video": "https://www.trezalabs.com/api/media/generated/x402_video/1a2b3c4d.mp4?s=...&c=...",
  "outputs": { "video": "https://www.trezalabs.com/api/media/generated/x402_video/1a2b3c4d.mp4?s=...&c=..." },
  "statusUrl": "https://www.trezalabs.com/api/x402/video?runId=...&token=...",
  "paidUsd": 0.42,
  "model": "minimax-h3",
  "seconds": 5,
  "aspectRatio": "16:9",
  "network": "eip155:8453",
  "transaction": "0x6d9154...",
  "payer": "0x0ac0ab..."
}
```

Use the `video` URL exactly as given, query parameters included. When you sent an `image_url`, it is echoed back as `imageUrl`. Paid on Solana, `network`, `transaction` and `payer` take their Solana forms (see [Networks](#networks)).

If the render takes longer than the wait (45 seconds by default), the body ends with `"status": "running"` instead:

```json
{
  "runId": "2026-09-23T18:04:11.000Z#1a2b3c4d",
  "status": "running",
  "statusUrl": "https://www.trezalabs.com/api/x402/video?runId=...&token=...",
  "pollAfterMs": 15000,
  "paidUsd": 0.42,
  "model": "minimax-h3",
  "seconds": 5,
  "aspectRatio": "16:9",
  "network": "eip155:8453",
  "transaction": "0x6d9154...",
  "payer": "0x0ac0ab...",
  "balanceUsd": 0.42
}
```

Then poll `statusUrl` until `status` is no longer `running`. The token in the URL is the only credential it needs, and polling is free. Use the URL exactly as given: the `runId` contains `#`, which must stay encoded as `%23`. To long-poll instead, send `Prefer: wait=N` and the status call holds its answer until the run finishes, for up to N seconds (at most 720), streamed the same way:

```bash
curl -N -H 'Prefer: wait=600' "$STATUS_URL"
```

**Waiting longer.** If your client can hold a connection open, send `Prefer: wait=N` on the paid call to wait up to N seconds instead of 45, at most 720.

**Getting the status URL at once.** To have the paid call answer immediately and poll yourself, send `"async": true` in the body or the header `Prefer: respond-async`. The paid call then answers `202` with the `runId`, `statusUrl` and `pollAfterMs`, as in the `running` body above.

Both the `200` and the `202` carry the `statusUrl` in an `X-Status-Url` header, and a `Preference-Applied` header echoes the preference that was honoured (`respond-async`, or `wait=N`).

**A render that outlasts its deadline.** Occasionally a provider finishes a clip after the render's own deadline. The run then reads `"status": "running"` with a `note` while the clip is collected from the provider, and turns `success` with the video when it arrives. Keep polling; a retry is refused (`409`) meanwhile, because the clip is still coming.

#### When a render fails

A render can fail after the payment has settled, most often because the model declined the prompt or the image. Nothing is charged for a failed render, so the payment stays on your wallet's account and the retry spends it instead of costing you again.

The response for a failed run, from the paid call or from `statusUrl`, carries what went wrong and what to do about it:

```json
{
  "runId": "2026-09-03T17:29:52.032Z#4509944a",
  "status": "partial",
  "error": "Video generation completed with no output (content may have been filtered)",
  "failedStep": "Video Generation",
  "reason": "prompt_refused",
  "retryable": true,
  "retryUrl": "https://www.trezalabs.com/api/x402/video/retry?runId=...&token=...",
  "message": "The video model refused this prompt or image, so nothing was rendered and nothing was charged for it. Your payment stays on your wallet balance. POST a reworded prompt to retryUrl to render again without paying twice.",
  "balanceUsd": 5.9
}
```

| Field        | Notes                                                                                                                                                         |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `status`     | `error` or `partial` (occasionally `cancelled`)                                                                                                               |
| `reason`     | `prompt_refused` means the model declined the prompt or image, so reword it. `render_failed` means the render did not finish, and the same prompt may succeed |
| `retryable`  | `true` until the run's one free retry has been used, then `false`                                                                                             |
| `retryUrl`   | Where to retry. It carries the same signed ticket, so it needs no other credential. Gone once the run has been retried                                        |
| `message`    | What happened and what to do, in one sentence                                                                                                                 |
| `balanceUsd` | Credit left on your wallet's account, which the retry spends                                                                                                  |

`POST` the `retryUrl` to render again without paying twice. Send `{"prompt": "..."}` to reword, which is the fix for `prompt_refused`, or send no body to run the original prompt again. The retry renders the same model, length, shape and first frame the failed run was sold, and each failed run can be retried once. Like the paid call, the retry waits up to 45 seconds by default (`Prefer: wait=N` for longer, at most 720) and ends with the result or with `"status": "running"` and its own `statusUrl`; send `{"async": true}` or `Prefer: respond-async` for an immediate `202` with a new `runId` and `statusUrl`.

```bash
curl -X POST "$RETRY_URL" \
  -H 'content-type: application/json' \
  -d '{"prompt":"a manta ray gliding over a coral reef at dawn"}'
```

Once a run has been retried, its own status (and a second retry attempt, which answers `409`) points at the retry with `retriedBy`, so a lost retry response never loses the clip. Poll `retriedBy.statusUrl` for the result:

```json
{
  "runId": "2026-09-03T17:29:52.032Z#4509944a",
  "status": "partial",
  "error": "Video generation completed with no output (content may have been filtered)",
  "failedStep": "Video Generation",
  "reason": "prompt_refused",
  "retryable": false,
  "retriedBy": {
    "runId": "2026-09-03T17:41:07.118Z#8c21d0e5",
    "statusUrl": "https://www.trezalabs.com/api/x402/video?runId=...&token=..."
  },
  "retriedByRunId": "2026-09-03T17:41:07.118Z#8c21d0e5",
  "message": "This run was retried. Poll retriedBy.statusUrl for the result."
}
```

#### Billing

Your payment credits an account keyed to your wallet address, and the render is charged against that balance. Anything left over stays as credit for your next call. The same wallet always maps to the same account, and a Base wallet and a Solana wallet are separate accounts.

A failed render is not charged, so the payment remains available for a retry.

Each payment buys one render. A payment presented again after it was redeemed is refused with a `409`, and nothing new is rendered or charged:

```json
{
  "error": "This payment has already been redeemed for a render.",
  "code": "PAYMENT_ALREADY_REDEEMED",
  "transaction": "0x6d9154..."
}
```

#### Full example

```typescript
import { x402Client } from '@x402/core/client';
import { registerExactEvmScheme } from '@x402/evm/exact/client';
import { safeBase64Encode, safeBase64Decode } from '@x402/core/utils';
import { privateKeyToAccount } from 'viem/accounts';

const ENDPOINT = 'https://www.trezalabs.com/api/x402/video';
const body = JSON.stringify({
  prompt: 'a manta ray gliding over a sunlit coral reef, slow cinematic drift',
  seconds: 5,
  aspectRatio: '16:9',
  model: 'minimax-h3',
});

const post = (paymentHeader?: string) =>
  fetch(ENDPOINT, {
    method: 'POST',
    headers: {
      'content-type': 'application/json',
      ...(paymentHeader ? { 'PAYMENT-SIGNATURE': paymentHeader } : {}),
    },
    body,
  });

// 1. Unpaid request returns the challenge, priced for this model and length.
const challenge = await post();
const paymentRequired = JSON.parse(safeBase64Decode(challenge.headers.get('payment-required')!));

// 2. Sign a payment for it. The client refuses any payment over $1 unless
//    you raise the cap, and clips run up to $4.90.
const client = new x402Client();
client.setSpendControls({ maxAmountPerPayment: '$5' });
registerExactEvmScheme(client, { signer: privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`) });
const payload = await client.createPaymentPayload(paymentRequired);

// 3. Pay. The response ends with the video if the render finishes within
//    45 seconds, or with status "running" and a statusUrl.
const paid = await post(safeBase64Encode(JSON.stringify(payload)));
if (!paid.ok) throw new Error(`${paid.status}: ${await paid.text()}`);
let run = await paid.json();

// 4. Only if the render outlasted the wait: poll the claim ticket.
const statusUrl: string = run.statusUrl;
while (run.status === 'running') {
  await new Promise((r) => setTimeout(r, run.pollAfterMs ?? 15000));
  run = await (await fetch(statusUrl)).json();
}

if (run.video) console.log(run.video);
else console.log(`${run.status}: ${run.message} Retry at ${run.retryUrl}`);
```

#### Paying from a Solana wallet

The same purchase from a Solana wallet, with [`@x402/fetch`](https://www.npmjs.com/package/@x402/fetch), [`@x402/svm`](https://www.npmjs.com/package/@x402/svm), `@solana/kit` and `bs58`. The secret key is the base58 64-byte export from Phantom and most wallets. The wallet needs USDC on Solana and no SOL.

```typescript
import { wrapFetchWithPaymentFromConfig } from '@x402/fetch';
import { ExactSvmScheme } from '@x402/svm/exact/client';
import { createKeyPairSignerFromBytes } from '@solana/kit';
import bs58 from 'bs58';

const signer = await createKeyPairSignerFromBytes(bs58.decode(process.env.SOLANA_PRIVATE_KEY!));

const paidFetch = wrapFetchWithPaymentFromConfig(fetch, {
  // Only Solana is registered, so the client pays the challenge's Solana entry.
  schemes: [{ network: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', client: new ExactSvmScheme(signer) }],
  // The client refuses any payment over $1 by default; clips run up to $4.90.
  spendControls: { maxAmountPerPayment: '$5' },
});

const response = await paidFetch('https://www.trezalabs.com/api/x402/video', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    prompt: 'a manta ray gliding over a sunlit coral reef, slow cinematic drift',
    seconds: 5,
    aspectRatio: '16:9',
    model: 'minimax-h3',
  }),
});
// Same body as on Base; network is solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp
// and transaction is a base58 signature. Poll statusUrl if status is "running".
const run = await response.json();
```

`ExactSvmScheme` reads the USDC mint from a Solana RPC; pass `{ rpcUrl }` as its second argument to use your own instead of the public endpoint.

A worked example is at [trezalabs.com/x402](https://www.trezalabs.com/x402), and a complete buyer script, which pays on Base or Solana, is at [treza-labs/x402-video-example](https://github.com/treza-labs/x402-video-example).

### Speech, Music and Images

Three more endpoints sell the same way as pay-per-video: POST the body, pay the 402, and the paid response ends with the file. Each is its own resource, with its own listing in the x402 Bazaar. A bare `GET` on any of them returns its menu (every option and price), and an unpaid `POST` returns the 402 quoting the exact price for your body. Only the documented fields are read; anything else is refused with a 400 before payment.

The response is the same streamed shape as pay-per-video: `200` at once with the `X-Status-Url` header, a whitespace byte every 2 seconds, then one JSON object whose `status` is `success`, `error` (with `retryUrl`) or `running` (poll `statusUrl`). The file URL is under `audio` for speech and music, and `image` for images. Use it exactly as given. These usually finish in 3 to 25 seconds, well inside the 45-second default wait. `"async": true` or `Prefer: respond-async` answers `202` with a `statusUrl` instead. A failed generation charges nothing, and `POST <endpoint>/retry?runId=...&token=...` (the `retryUrl`) tries again on the balance the payment left, once, optionally with a reworded `text` or `prompt`.

#### Speech: `POST /api/x402/speech`

Text in, an ElevenLabs voiceover (MP3) out.

| Field   | Required | Values                                                                                                                                                                                                           |
| ------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `text`  | yes      | Up to 3,000 characters. `input` is accepted as a synonym                                                                                                                                                         |
| `voice` | no       | `george` (default), `daniel`, `callum`, `alice`, `lily`, `brian`, `adam`, `asher`, `archie`, `mark`, `piper`, `rachel`, `charlie`                                                                                |
| `model` | no       | `eleven-v3` (default): the most expressive read; performs directions in square brackets, e.g. `[whispers]`, without speaking them. `eleven-multilingual-v2`: the steadiest read for long narration, 29 languages |
| `async` | no       | `true` for a `202` and a `statusUrl`                                                                                                                                                                             |

Price: $0.42 per 1,000 characters, rounded up to the cent, minimum $0.02 (1,000 characters is about a minute of speech). The response echoes `characters`, the count the price was computed from.

```bash
curl -X POST https://www.trezalabs.com/api/x402/speech \
  -H 'Content-Type: application/json' \
  -d '{"text": "Welcome back. Today we are looking at three small habits that make a big difference.", "voice": "george"}'
```

#### Music: `POST /api/x402/music`

A description of the sound you want in, an original track out.

| Field    | Required | Values                                                                                       |
| -------- | -------- | -------------------------------------------------------------------------------------------- |
| `prompt` | yes      | Genre, mood, instruments, tempo. Up to 2,000 characters                                      |
| `model`  | no       | `lyria-3-clip` (default): a 30-second piece, $0.06. `lyria-3-pro`: a full-length song, $0.12 |
| `async`  | no       | `true` for a `202` and a `statusUrl`                                                         |

```bash
curl -X POST https://www.trezalabs.com/api/x402/music \
  -H 'Content-Type: application/json' \
  -d '{"prompt": "warm lo-fi hip hop, soft piano chords, vinyl crackle, relaxed 80 bpm groove"}'
```

#### Images: `POST /api/x402/image`

A prompt in, one image out, on the model you pick.

| Field         | Required | Values                                             |
| ------------- | -------- | -------------------------------------------------- |
| `prompt`      | yes      | Up to 2,000 characters                             |
| `model`       | no       | See the table below. Defaults to `nano-banana`     |
| `aspectRatio` | no       | Defaults to `1:1`. Each model sells its own shapes |
| `resolution`  | no       | Only on the models priced by resolution            |
| `quality`     | no       | Only on `gpt-image-2`                              |
| `async`       | no       | `true` for a `202` and a `statusUrl`               |

| `model`                          | Shapes                                              | Price                                                          |
| -------------------------------- | --------------------------------------------------- | -------------------------------------------------------------- |
| `nano-banana` (Google, default)  | 1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, 4:5, 5:4, 21:9 | $0.06                                                          |
| `nano-banana-2` (Google)         | same as above                                       | `resolution` 512 $0.07, 1K $0.10 (default), 2K $0.15, 4K $0.22 |
| `nano-banana-pro` (Google)       | same as above                                       | `resolution` 1K $0.20 (default), 2K $0.20, 4K $0.35            |
| `gpt-image-2` (OpenAI)           | 1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, 21:9           | `quality` low $0.02, medium $0.10 (default), high $0.36        |
| `seedream-5-pro` (ByteDance)     | 1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, 4:5, 5:4, 21:9 | `resolution` 2K $0.13 (default), 1K $0.07                      |
| `flux-2-pro` (Black Forest Labs) | 1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, 21:9           | 1:1 $0.05, 21:9 $0.09, other shapes $0.07                      |
| `recraft-v4.1` (Recraft)         | 1:1, 16:9, 9:16, 4:3, 3:4                           | $0.05                                                          |

A setting the model does not offer (a `resolution` on `gpt-image-2`, a shape Recraft does not render) is refused with a 400 rather than silently dropped, so what you pay for is what renders.

```bash
curl -X POST https://www.trezalabs.com/api/x402/image \
  -H 'Content-Type: application/json' \
  -d '{"prompt": "a lighthouse on a sea cliff at golden hour, soft mist", "model": "nano-banana-pro", "resolution": "2K", "aspectRatio": "16:9"}'
```

The buyer scripts in [treza-labs/x402-video-example](https://github.com/treza-labs/x402-video-example) cover these too: `npm run buy:speech`, `npm run buy:music` and `npm run buy:image`.

### Clips and Narrated Shorts

Two endpoints sell finished edits rather than one generation. Both render on our worker in a few minutes, so the paid response usually ends after its 45-second wait with `"status": "running"` and a `statusUrl`: poll it (send `Prefer: wait=600` to hold one request open instead). Both return the file as `video`.

If one of these fails part way (a scene the video model refuses, a download YouTube blocks), the steps that finished are charged and the rest of the payment stays on your wallet balance. The free retry (`retryUrl`) with no body resumes the failed run: finished steps are reused, not re-billed, and only what failed runs again.

#### Clips: `POST /api/x402/clip`

A YouTube link in; its most shareable 30 to 60 seconds out, cut and reframed on the speaker, with burned-in captions. `outputs.moment` carries the pick as JSON: start, end, and a title and caption to post it with.

| Field         | Required | Values                                                                                                           |
| ------------- | -------- | ---------------------------------------------------------------------------------------------------------------- |
| `url`         | yes      | A YouTube video link: a watch page, a Short, or a youtu.be link. Other sites, playlists and channels are refused |
| `aspectRatio` | no       | `9:16` (default), `4:5`, `1:1` (all cropped on the speaker), or `16:9` (the original framing)                    |
| `async`       | no       | `true` for a `202` and a `statusUrl`                                                                             |

Price: by the source's length, $0.02 plus $0.005 a minute, times the 1.4 markup, rounded up to the cent. About $0.12 for a 12-minute video, $0.45 for an hour; sources up to 2 hours. The video is looked up before you pay, so the 402 quotes the exact price for your link, and a video that cannot be clipped (private, age-restricted, live, missing, too long) is refused with a 400 and a `code` before any payment moves. Clip only videos you have the rights to use.

```bash
curl -X POST https://www.trezalabs.com/api/x402/clip \
  -H 'Content-Type: application/json' \
  -d '{"url": "https://www.youtube.com/watch?v=KtTXyBJ2N30"}'
```

#### Narrated shorts: `POST /api/x402/short`

A topic in; a finished 30-second vertical short out: a written script, four scenes, an ElevenLabs narrator, a music bed, word-by-word captions and callout cards. `outputs.title` and `outputs.description` come with it.

| Field   | Required | Values                                                                                                                                           |
| ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `topic` | yes      | What the short is about, up to 500 characters                                                                                                    |
| `style` | no       | `motion` (default, $3.22): each scene animated with Google Veo 3.1 Lite. `stills` ($0.87): the same edit on generated stills with a slow push-in |
| `voice` | no       | The narrator, one of the speech voices (default `brian`)                                                                                         |
| `async` | no       | `true` for a `202` and a `statusUrl`                                                                                                             |

```bash
curl -X POST https://www.trezalabs.com/api/x402/short \
  -H 'Content-Type: application/json' \
  -d '{"topic": "how octopuses taste the world with their arms", "style": "stills"}'
```

### Funding an Account Without a Human

This is the one most agents want. Pipeline runs draw a prepaid credit balance, and an agent holding a wallet can refill that balance itself, with nobody signing in to a billing page.

Ask what the balance is and how to add to it, using [`get_credit_balance`](/api/mcp-server.md) over MCP or `GET /api/account/balance` with an API key:

```json
{
  "balanceUsd": 1.13,
  "typicalVideoChargeUsd": 1.06,
  "topUpUrl": "https://www.trezalabs.com/platform/settings",
  "x402": {
    "url": "https://www.trezalabs.com/api/billing/credits/x402",
    "topUpPerCallUsd": 5,
    "network": "eip155:8453",
    "networks": ["eip155:8453", "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"]
  }
}
```

An `x402` block means agent-native top-up is available on this deployment, and `networks` lists the chains it accepts, Base first. `topUpUrl` is the human path; the rest of this page is the other one.

**Paying the endpoint**

`PAYMENT-SIGNATURE` carries the payment and is the only required header.

`Authorization` is optional and selects which account is credited. Send a Treza credential (API key, OAuth token, or session) to credit that account; send none and the credits go to an account keyed to the paying wallet address (a Base wallet and a Solana wallet are separate accounts). The example pays on Base; a Solana wallet pays the same way with `ExactSvmScheme`, as in [Paying from a Solana wallet](#paying-from-a-solana-wallet).

```typescript
import { x402Client } from '@x402/core/client';
import { registerExactEvmScheme } from '@x402/evm/exact/client';
import { safeBase64Encode, safeBase64Decode } from '@x402/core/utils';
import { privateKeyToAccount } from 'viem/accounts';

const ENDPOINT = 'https://www.trezalabs.com/api/billing/credits/x402';
const headers = { authorization: `Bearer ${process.env.TREZA_API_KEY}` };

// 1. Ask, and get the terms back.
const challenge = await fetch(ENDPOINT, { method: 'POST', headers });
const paymentRequired = JSON.parse(
  safeBase64Decode(challenge.headers.get('payment-required')!)
);

// 2. Sign a payment for exactly those terms.
const client = new x402Client();
registerExactEvmScheme(client, { signer: privateKeyToAccount(process.env.WALLET_KEY) });
const payload = await client.createPaymentPayload(paymentRequired);

// 3. Ask again, paying.
const paid = await fetch(ENDPOINT, {
  method: 'POST',
  headers: { ...headers, 'PAYMENT-SIGNATURE': safeBase64Encode(JSON.stringify(payload)) },
});
console.log(await paid.json());
```

A successful call returns the grant and the transaction that paid for it:

```json
{
  "grantedUsd": 5,
  "duplicate": false,
  "balanceUsd": 16.13,
  "network": "eip155:8453",
  "transaction": "0x3e08158c2933cf4174d4f6b7429cdebd2df23b91f135c5a59eaef11a6cbd6450",
  "payer": "0x0Ac0AB6C04C3A92C929e35b39Be88E445360c199"
}
```

Call it again to add another $5. The credits are spendable immediately.

{% hint style="info" %}
The wallet needs USDC on Base mainnet (`0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`) or on Solana mainnet (`EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`). Neither needs gas money: on Base the facilitator broadcasts the transfer, and on Solana it pays the network fee.
{% endhint %}

**Paying twice for the same thing is not possible**

The ledger entry is keyed to the settled transaction (a hash on Base, a signature on Solana), so a repeated request credits once and answers `"duplicate": true`. On Base a replayed payment usually does not even reach that check: the authorization nonce is already spent on-chain, and the facilitator rejects it.

```json
{ "error": "invalid_payload: authorization nonce already submitted; transaction already on-chain" }
```

**When a payment is refused**

The response says why, rather than repeating the challenge:

| Message                                                  | Meaning                                                    |
| -------------------------------------------------------- | ---------------------------------------------------------- |
| `invalid_payload: ... execution reverted`                | The wallet cannot cover the payment                        |
| `invalid_payload: authorization nonce already submitted` | This payment was already spent                             |
| `Invalid or expired credentials`                         | The bearer is wrong, or the key lacks a `pipelines:` scope |
| `x402 payments are not configured on this deployment`    | Agent-native top-up is off here; use `topUpUrl`            |

Authorization is checked before the payment settles, so a request refused for a bad bearer costs nothing.

#### Payment Flow

The pay-per-video call end to end. The request, and any `image_url`, is checked after the payment verifies and before it settles, and the payment settles before the render starts. The response then stays open until the render finishes, for up to 45 seconds by default.

```
Client                          Treza API                     Facilitator         Chain
  │                                │                              │                  │
  │── POST /api/x402/video ───────▶│                              │                  │
  │                                │                              │                  │
  │◀── 402 Payment Required ───────│                              │                  │
  │    price: $0.42                │                              │                  │
  │    a Base and a Solana entry   │                              │                  │
  │    payTo per network           │                              │                  │
  │                                │                              │                  │
  │── Sign USDC payment ──┐        │                              │                  │
  │◀── Payment-Signature ─┘        │                              │                  │
  │                                │                              │                  │
  │── POST /api/x402/video ───────▶│                              │                  │
  │    + Payment-Signature         │── Verify payment ───────────▶│                  │
  │                                │◀── Valid ────────────────────│                  │
  │                                │  request and image_url       │                  │
  │                                │  checked: a refusal here     │                  │
  │                                │  costs nothing               │                  │
  │                                │── Settle payment ───────────▶│── USDC tx ──────▶│
  │                                │◀── Settled ──────────────────│                  │
  │                                │                              │                  │
  │◀── 200 + X-Status-Url ─────────│  render starts               │                  │
  │◀── " " every 2 seconds ────────│  up to 45 s by default       │                  │
  │◀── JSON: video, or running ────│                              │                  │
  │                                │                              │                  │
```

### Client-Side: Paying for Treza Services

#### Using the Treza SDK (Enclave as Wallet)

A Treza Enclave can act as the payment wallet itself, keeping the private key inside the hardware-isolated TEE. This is optional; any viem-compatible account works just as well.

**Install Dependencies**

```bash
npm install @treza/sdk @x402/core @x402/evm @x402/fetch
```

**Auto-Paying Fetch**

The simplest approach. `createEnclaveFetch` returns a `fetch` function that automatically handles 402 responses:

```typescript
import { TrezaClient, createEnclaveFetch } from '@treza/sdk';

const treza = new TrezaClient({
  baseUrl: 'https://trezalabs.com',
});

const paidFetch = await createEnclaveFetch(treza, {
  enclaveId: 'enc_abc123',
  verifyAttestation: true,  // verify enclave integrity before each payment
});

// Payments are handled automatically.
const response = await paidFetch('https://www.trezalabs.com/api/x402/video', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    prompt: 'a manta ray gliding over a sunlit coral reef, slow cinematic drift',
    seconds: 5,
    aspectRatio: '16:9',
    model: 'minimax-h3',
  }),
});
// The body completes when the render does, or after 45 seconds. status
// "success" carries video; status "running" means poll statusUrl.
const { status, video, statusUrl } = await response.json();
```

**x402 Client (Advanced)**

For more control, use the x402 client directly:

```typescript
import { TrezaClient, createEnclaveX402Client } from '@treza/sdk';

const treza = new TrezaClient({
  baseUrl: 'https://trezalabs.com',
});

const { x402, account } = await createEnclaveX402Client(treza, {
  enclaveId: 'enc_abc123',
  verifyAttestation: true,
});

// account.address → the enclave's signing address (fund this with USDC)
console.log('Payment wallet:', account.address);
```

**Enclave Account (Low-Level)**

Create a viem-compatible account backed by the enclave for use with any x402 library:

```typescript
import { TrezaClient, createEnclaveAccount } from '@treza/sdk';

const treza = new TrezaClient({
  baseUrl: 'https://trezalabs.com',
});

const account = await createEnclaveAccount(treza, {
  enclaveId: 'enc_abc123',
  verifyAttestation: true,
});

// Use with x402 directly
import { x402Client } from '@x402/core/client';
import { registerExactEvmScheme } from '@x402/evm/exact/client';

const client = new x402Client();
registerExactEvmScheme(client, { signer: account });
```

#### Using Any x402 Client (External Wallet)

You don't need the Treza SDK to pay. Any x402-compatible wallet works:

```typescript
import { wrapFetchWithPayment } from '@x402/fetch';
import { x402Client } from '@x402/core/client';
import { registerExactEvmScheme } from '@x402/evm/exact/client';
import { privateKeyToAccount } from 'viem/accounts';

const account = privateKeyToAccount('0xYourPrivateKey...');

const client = new x402Client();
registerExactEvmScheme(client, { signer: account });
// The client refuses any payment over $1 by default; clips run up to $4.90.
client.setSpendControls({ maxAmountPerPayment: '$5' });

const paidFetch = wrapFetchWithPayment(fetch, client);

const response = await paidFetch('https://www.trezalabs.com/api/x402/video', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    prompt: 'a manta ray gliding over a sunlit coral reef, slow cinematic drift',
    seconds: 5,
    aspectRatio: '16:9',
    model: 'minimax-h3',
  }),
});
// The body completes when the render does, or after 45 seconds with status
// "running" and a statusUrl to poll.
const run = await response.json();
```

#### Using curl (Manual)

To inspect the 402 response without paying:

```bash
curl -i -X POST https://www.trezalabs.com/api/x402/video \
  -H 'content-type: application/json' \
  -d '{"prompt":"a manta ray gliding over a sunlit coral reef, slow cinematic drift","seconds":5,"aspectRatio":"16:9","model":"minimax-h3"}'

# HTTP/2 402
# Payment-Required: <base64 challenge>
#
```

Decoded, the challenge carries the price for the model and clip length you asked for, once per chain (abridged):

```json
{
  "x402Version": 2,
  "accepts": [
    {
      "scheme": "exact",
      "network": "eip155:8453",
      "amount": "420000",
      "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      "payTo": "0xe8dC41F9e675Ba8340140c99180CFaEE65D4d160",
      "maxTimeoutSeconds": 120,
      "extra": { "...": "..." }
    },
    {
      "scheme": "exact",
      "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
      "amount": "420000",
      "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
      "payTo": "EX4MyYWHc7NKBGuytmMpxixM7ovSWYLNy5acN2mFfoGd",
      "maxTimeoutSeconds": 120,
      "extra": { "feePayer": "<a Coinbase fee payer address>" }
    }
  ]
}
```

Amounts are in token atomic units (USDC has 6 decimals on both chains, so the price times 1,000,000): `420000` is 0.42 USDC and a $4.90 clip is `4900000`. Pay to the `payTo` the challenge names rather than a copied address.

### Server-Side: Adding x402 to Your Own Endpoints

If you're building on the Treza platform and want to monetize your own API endpoints, use the `withX402Payment` wrapper.

#### Basic Usage

```typescript
import { NextRequest, NextResponse } from 'next/server';
import { withX402Payment } from '@/lib/x402';

async function handleGET(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
): Promise<NextResponse> {
  // Your endpoint logic here
  return NextResponse.json({ data: 'your response' });
}

export const GET = withX402Payment(handleGET, {
  price: '0.001',       // USDC price per request
  description: 'Get resource data',
});
```

#### With Bazaar Discovery

Make your endpoint discoverable by AI agents on the [x402 Bazaar](https://docs.cdp.coinbase.com/x402/bazaar):

```typescript
export const POST = withX402Payment(handlePOST, {
  // A fixed price, or a function of the request body for per-request pricing.
  price: (body) => `$${priceFor(body).toFixed(2)}`,
  // Name the route. Without this it registers as `*`, which the Bazaar
  // publishes under a generated template like `:var1`.
  routePattern: 'POST /api/x402/video',
  serviceName: 'Treza',
  tags: ['video', 'text-to-video', 'image-to-video', 'ai'],
  description:
    'Generate a video from a text prompt, or animate an image_url from its first frame, and get the video URL back in the same response.',
  discovery: {
    method: 'POST',
    bodyType: 'json',
    input: {
      prompt: 'a manta ray gliding over a sunlit coral reef, slow cinematic drift',
      seconds: 5,
      aspectRatio: '16:9',
      model: 'minimax-h3',
    },
    inputSchema: {
      type: 'object',
      properties: {
        prompt: { type: 'string', maxLength: 2000 },
        seconds: { type: 'number', enum: [4, 5, 6, 8, 10, 15] },
        aspectRatio: { type: 'string', enum: ['16:9', '9:16'] },
        model: {
          type: 'string',
          enum: ['minimax-h3', 'veo-3.1-lite', 'wan-2.7', 'veo-3.1-fast', 'kling-3.0', 'kling-3.0-pro', 'seedance-2.5', 'veo-3.1'],
        },
        image_url: { type: 'string', format: 'uri' },
        async: { type: 'boolean' },
      },
      required: ['prompt'],
      additionalProperties: false,
    },
    output: {
      example: {
        runId: '2026-09-23T18:04:11.000Z#1a2b3c4d',
        status: 'success',
        video: 'https://www.trezalabs.com/api/media/generated/x402_video/1a2b3c4d.mp4?s=...&c=...',
        statusUrl: 'https://www.trezalabs.com/api/x402/video?runId=...&token=...',
        paidUsd: 0.42,
        model: 'minimax-h3',
      },
    },
  },
});
```

Declare `method` and `bodyType` for any route that takes a request body. Without them the listing describes the route as query-parameterized, and an agent will send its arguments in the URL of a request that only reads JSON.

#### Environment Variables

| Variable                   | Required          | Default                                         | Description                                                                                                                                                                                                             |
| -------------------------- | ----------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `X402_ENABLED`             | No                | `false`                                         | Set to `true` to enable x402 payment gating                                                                                                                                                                             |
| `TREZA_X402_PAY_TO`        | Yes (if enabled)  | none                                            | Base wallet address that receives USDC payments                                                                                                                                                                         |
| `TREZA_X402_PAY_TO_SOLANA` | No                | none                                            | Solana address that receives USDC payments. When set, and the facilitator settles Solana mainnet, every 402 lists a Solana entry after the Base one                                                                     |
| `X402_NETWORK`             | No                | `eip155:8453`                                   | Network identifier (Base mainnet)                                                                                                                                                                                       |
| `X402_FACILITATOR_URL`     | No                | `https://api.cdp.coinbase.com/platform/v2/x402` | Payment verification facilitator                                                                                                                                                                                        |
| `X402_CDP_API_KEY`         | Yes (CDP mainnet) | none                                            | CDP API key id; the CDP facilitator authenticates every call                                                                                                                                                            |
| `X402_CDP_API_SECRET`      | Yes (CDP mainnet) | none                                            | CDP API key secret                                                                                                                                                                                                      |
| `X402_TOPUP_USD`           | No                | `5`                                             | USD credited per successful top-up call                                                                                                                                                                                 |
| `X402_VIDEO_PIPELINE`      | No                | `pipeline_x402_video`                           | The one published pipeline every pay-per-video purchase renders on                                                                                                                                                      |
| `X402_VIDEO_WAIT_MS`       | No                | `45000`                                         | How long a paid call waits for the render by default before answering with status `running`, in ms. Capped at 720000; `0` answers `202` by default. Leave it unset rather than empty, since an empty value reads as `0` |

{% hint style="warning" %}
Mainnet settlement runs through Coinbase's facilitator, which authenticates every verify and settle call. Without `X402_CDP_API_KEY` and `X402_CDP_API_SECRET` the endpoint fails closed with a 503 and stops advertising itself, rather than answering payments it cannot settle.
{% endhint %}

**Testnet Configuration**

```env
X402_ENABLED=true
TREZA_X402_PAY_TO=0xYourTestnetAddress
X402_NETWORK=eip155:84532
X402_FACILITATOR_URL=https://www.x402.org/facilitator
```

**Mainnet Configuration**

```env
X402_ENABLED=true
TREZA_X402_PAY_TO=0xYourWalletAddress
# Optional: also accept USDC on Solana mainnet, paid to this address.
TREZA_X402_PAY_TO_SOLANA=YourSolanaAddress
X402_NETWORK=eip155:8453
X402_FACILITATOR_URL=https://api.cdp.coinbase.com/platform/v2/x402

# Pay-per-video. Every model, length, shape and first frame on sale renders
# on one published pipeline; what was bought travels with each run. Optional.
X402_VIDEO_PIPELINE=pipeline_x402_video
# How long a paid call waits for the render by default, in ms (default
# 45000, cap 720000). Callers can ask for longer with Prefer: wait=<seconds>.
X402_VIDEO_WAIT_MS=45000
# Legacy: the per-combination pipelines that sold clips before the single
# pipeline. Runs bought on them, and their free retries, are recognised and
# priced from this map, so keep it set until at least 24 hours after the
# single-pipeline deploy (claim tickets last 24 hours).
X402_VIDEO_PIPELINES={"minimax-h3-5s-16x9":"..."}
# Provider cost per generated second on the default model. Price is this times
# the credits markup.
X402_VIDEO_PER_SECOND_USD=0.06
# Per-model overrides, keyed by public model id, for re-pricing one model
# without a deploy when its provider rate moves.
X402_VIDEO_MODEL_RATES_USD={"seedance-2.5":0.233}
```

### Discovering Payable Services

AI agents can discover Treza's x402-payable endpoints through the Bazaar:

```typescript
import { discoverPayableServices } from '@treza/sdk';

const services = await discoverPayableServices({
  maxPrice: '0.01',          // filter by max price per call
  network: 'eip155:8453',   // filter by network
});

services.forEach(service => {
  console.log(service.url, service.description, service.accepts);
});
```

### Security Model

| Layer                        | Protection                                                                                                                  |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| **TEE Signing**              | Payment signatures are created inside the hardware-isolated enclave, so private keys never leave the Nitro Enclave boundary |
| **Attestation Verification** | Optional pre-signing attestation check ensures the enclave is untampered before authorizing payments                        |
| **Facilitator Verification** | Payment signatures are verified by the Coinbase facilitator before the server returns data                                  |
| **On-Chain Settlement**      | Payments settle as real USDC transfers on Base or Solana, providing an immutable audit trail                                |
| **Replay Protection**        | Each payment signature is bound to a specific request and cannot be reused                                                  |

### Architecture

```
┌─────────────────────────────────────────────────────────┐
│  AI Agent / Client                                      │
│  ┌───────────────┐  ┌──────────────┐                    │
│  │  @treza/sdk   │  │  @x402/fetch │                    │
│  │  createEnclave│──│  auto-pay    │                    │
│  │  Fetch()      │  │  on 402      │                    │
│  └───────┬───────┘  └──────────────┘                    │
│          │                                              │
│  ┌───────▼───────┐                                      │
│  │ EnclaveAccount│  Signs payments inside TEE           │
│  └───────┬───────┘                                      │
└──────────│──────────────────────────────────────────────┘
           │
           ▼
┌──────────────────┐     ┌──────────────────┐     ┌──────┐
│  Treza Platform  │────▶│  Facilitator     │────▶│ Base │
│  withX402Payment │     │  (Coinbase/x402) │     │  or  │
│  402 → verify →  │     │  verify + settle │     │Solana│
│  settle → respond│     │                  │     │ USDC │
└──────────────────┘     └──────────────────┘     └──────┘
```

### FAQ

**Do I need USDC to use Treza?** Only if x402 is enabled on the endpoint you're calling. During development and on testnet, x402 is disabled by default.

**Which chains?** USDC on Base mainnet or Solana mainnet. Every 402 challenge lists both at the same price, Base first, and your client pays with whichever wallet it has; neither chain needs gas money in your wallet. A Base wallet and a Solana wallet are separate accounts, so credit left on one is not visible to the other. Testnet deployments use Base Sepolia.

**What if I send the same payment twice?** It is redeemed once. A second video request with the same payment answers `409` with `code: "PAYMENT_ALREADY_REDEEMED"` and renders nothing, and a second top-up credits nothing and answers `"duplicate": true`. Send a new payment to buy again.

**Can I use a regular wallet instead of an enclave?** Yes. Any viem-compatible account or x402 client works. The enclave-as-wallet pattern is optional; it just adds the security of TEE-based key management.

**How much do API calls cost?** Video generation is priced by model and clip length, from $0.42 for a five-second clip on the default model to $4.90 for fifteen seconds on Seedance 2.5; the 402 challenge quotes the exact figure for what you asked for, and `GET /api/x402/video` lists every model's prices. Credit top-up is $5.00 per call.

**Do I have to poll for the video?** Not for quick renders. The paid call returns the video URL in the same response when the render finishes within 45 seconds, and `Prefer: wait=N` waits longer (at most 720 seconds). Poll `statusUrl` when the body ends with `"status": "running"`, when you sent `"async": true`, or when your connection dropped: the `X-Status-Url` header gave you the URL before the render finished.

**Can I animate my own image?** Yes. Send `image_url`, a public https JPEG, PNG or WebP up to 10 MB, and the video starts from it, cropped to the aspect ratio you buy. Every model except `minimax-h3` takes one. Without a `model`, the cheapest model that takes an image at your length renders it: `wan-2.7` for 5 or 10 seconds, `kling-3.0` for 15, `veo-3.1-lite` for 4, 6, 8 or no length. The image must be fetchable within 4 seconds, and it is checked before the payment settles, so one that cannot be used costs nothing.

**Do I need a Treza account?** No, for either x402 endpoint. Video generation needs only the payment, and credit top-up without a bearer credits an account keyed to your wallet address. An account is required for the pipeline API.

**What if the render fails?** A failed render is not charged. The payment stays as credit on your wallet's account, and the response carries a `retryUrl` you can POST to render again without paying twice, optionally with a reworded prompt. See [When a render fails](#when-a-render-fails).

**Is there a minimum balance?** No minimum. You just need enough USDC in your wallet to cover the call price.

### Related

* [x402 Protocol Documentation](https://docs.cdp.coinbase.com/x402/welcome)
* [x402 Bazaar](https://docs.cdp.coinbase.com/x402/bazaar)
* [Coinbase Payments MCP](https://docs.cdp.coinbase.com/payments-mcp/welcome)
* [Pay-per-video walkthrough](https://www.trezalabs.com/x402)
* [Treza SDK README](https://github.com/treza-labs/treza-sdk)
* [Agent Commerce Guide](https://github.com/treza-labs/treza-sdk/blob/main/docs/AGENT_COMMERCE_GUIDE.md)
