
# Testril MCP — worked query/response examples

Testril answers on-chain metric questions for agents over the Model Context
Protocol (MCP). You pick a catalog **Function** (a metric template), **bind** it
to a concrete scope (token, wallet, chain), **materialize** a block window
(paying a quoted price), then **read** the values. Priced work returns an x402
`payment_required` quote first; you approve, pay, and then re-run the same verb.

The examples below are **real, captured requests and responses** from our test
runs against the production endpoint `https://dev.testril.ai/mcp` (the live CDP
payment rail) and, where noted, the local **demo/mock** rail used for rehearsal.
They are lightly trimmed for readability — trimming is marked with `…`.

**Notes**
- All amounts are **Base Sepolia testnet USDC** (6 decimals, so 1 atom = $0.000001). No mainnet value moves.
- Wallet, contract and transaction addresses are shortened to the form `0x4c65…D999`.
- x402 payment payloads are signed client-side; signatures are never shown (`0x…sig…`).
- Prices come only from the server quote; callers never set a price.

Key addresses referenced below (Base Sepolia):
- Payer (buyer) `0x4c65…D999` · receiver Safe (payTo) `0x4559…5e52`
- USDC `0x036C…CF7e` · batch-settlement escrow `0x4020…0003` · receiver authorizer `0x91b0…086b`

---

## A. Discovery & health

### 1. Open the MCP session (initialize)
The standard MCP handshake. The server reports its protocol version and a block of usage `instructions`.
```json
// Request
{ "jsonrpc": "2.0", "id": 1, "method": "initialize",
  "params": { "protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": { "name": "testril-mcp-direct", "version": "0.1" } } }
```
```json
// Response
{ "jsonrpc": "2.0", "id": 1, "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": { "tools": {} },
    "serverInfo": { "name": "rmcp", "version": "3.1.4" },
    "instructions": "Testril answers on-chain metric questions for agents and builders: pick a catalog Function (template), bind it to concrete scope (token, wallet, chain, …), materialize a block window (pay if quoted), then read values. …" } }
```

### 2. List the tools (tools/list)
Testril exposes 11 tools. Schemas are omitted here for brevity.
```json
// Request
{ "jsonrpc": "2.0", "id": 2, "method": "tools/list" }
```
```json
// Response
{ "jsonrpc": "2.0", "id": 2, "result": { "tools": [
    { "name": "ask", "…": "…" },
    { "name": "bind", "…": "…" },
    { "name": "bind_prompt", "…": "…" },
    { "name": "claim_rewards", "…": "…" },
    { "name": "health_check", "…": "…" },
    { "name": "inspect", "…": "…" },
    { "name": "materialize", "…": "…" },
    { "name": "pay_quote", "…": "…" },
    { "name": "provenance", "…": "…" },
    { "name": "read", "…": "…" },
    { "name": "stop", "…": "…" } ] } }
```

### 3. Connectivity smoke test (health_check)
Confirms the process is up, which payment rail is active (`cdp` = live), and how many chains / functions / bound functions exist.
```json
// Request
{ "tool": "health_check" }
```
```json
// Response
{ "bound_functions": 21, "chains": [42161, 1, 84532], "functions": 3,
  "ok": true, "outcome": "success", "payment": "cdp",
  "transport": "http", "version": "0.1.3" }
```

### 4. Price list (inspect pricing)
Every priced unit and its per-unit price, in USD. Reads and materializes are billed from these rates.
```json
// Request
{ "tool": "inspect", "set_args": { "subject": "pricing" } }
```
```json
// Response
{ "subject": "pricing", "outcome": "success", "rates": [
    { "label": "rpc_ops",          "unit_price": 0.0 },
    { "label": "llm_tokens",       "unit_price": 0.0001 },
    { "label": "materialize_blocks","unit_price": 0.00012 },
    { "label": "query_read",       "unit_price": 0.00002 },
    { "label": "query_blocks",     "unit_price": 0.000001 },
    { "label": "extra_cache_days", "unit_price": 0.000001 } ] }
```

---

## B. Bind → quote → pay → deliver (the core loop, live CDP rail)

### 5. Bind a Function to a concrete scope (bind)
Turns the `erc20_balance` template into a content-addressed **Bound Function** for one token + wallet on one chain. Binding is free.
```json
// Request
{ "tool": "bind", "set_args": {
    "function_slug": "erc20_balance",
    "chain_id": 1,
    "params": { "token_address": "0xA0b8…eB48", "wallet_address": "0x88e6…5640" } } }
```
```json
// Response
{ "outcome": "success", "created": true,
  "bound_function_id": "sha256:47ff…74c0",
  "chain_id": 1, "function_slug": "erc20_balance",
  "name": "erc20_balance@1:0xA0b8…eB48:0x88e6…5640",
  "params": { "token_address": "0xA0b8…eB48", "wallet_address": "0x88e6…5640" } }
```

### 6. Inspect a chain and a Bound Function (inspect)
Reads the finalized head and the Bound Function's already-materialized block ranges (what you don't need to pay for again). Free.
```json
// Request
{ "tool": "inspect", "set_args": { "subject": "bound_function",
    "bound_function_id": "sha256:18ec…a097" } }
```
```json
// Response
{ "subject": "bound_function", "outcome": "success",
  "bound_function_id": "sha256:18ec…a097",
  "chain_id": 42161, "function_slug": "erc20_transfer_volume",
  "name": "erc20_transfer_volume@42161:0xaf88…5831",
  "params": { "chain_id": 42161, "token_address": "0xaf88…5831" },
  "data_endpoints": {
    "data": "https://dev.testril.ai/v1/specs/sha256:18ec…a097/data",
    "provenance": "https://dev.testril.ai/v1/specs/sha256:18ec…a097/provenance" },
  "materialized": [
    { "from_block": 510282412, "to_block": 510282427, "cache_expiry_time": "1790844971" },
    { "from_block": 510316625, "to_block": 510316825, "cache_expiry_time": "1790855041" },
    "…" ],
  "materializing": [], "active_jobs": [] }
```

### 7. Ask to materialize a window → a priced quote (materialize → payment_required)
Requesting work you haven't paid for returns `outcome: "payment_required"` with the line items and an `accepts` menu of ways to pay. This is normal, not an error.
```json
// Request
{ "tool": "materialize", "set_args": {
    "bound_function_id": "sha256:18ec…a097",
    "from_block": 510271000, "to_block": 510271090, "blocks": 90 } }
```
```json
// Response
{ "outcome": "payment_required",
  "quote_id": "quote:01a0f618-de6d-7382-87d9-2545635a6570",
  "amount": 0.0108,
  "lines": [ { "label": "materialize_blocks", "units": 90, "unit_price": 0.00012, "amount": 0.0108 } ],
  "accepts": [
    { "scheme": "exact", "network": "eip155:84532",
      "asset": "0x036C…CF7e", "payTo": "0x4559…5e52", "amount": "10800",
      "extra": { "assetTransferMethod": "eip3009", "name": "USDC", "version": "2" } },
    { "scheme": "batch-settlement", "network": "eip155:84532",
      "asset": "0x036C…CF7e", "payTo": "0x4559…5e52", "amount": "10800",
      "extra": { "name": "USDC", "receiverAuthorizer": "0x91b0…086b", "version": "2", "withdrawDelay": 3600 } } ] }
```

### 8. The x402 challenge, explained
The `accepts` array above **is** the x402 payment challenge. Each row is one acceptable payment: a `scheme` (`exact` = a single EIP-3009 USDC transfer; `batch-settlement` = a signed voucher against an escrow channel), the `network` (CAIP-2), the `asset` and `amount` in atoms, and who to pay (`payTo`). For sub-cent quotes only `batch-settlement` is offered; at $0.01 and above you also get `exact`. The client picks one row, signs the matching payload, and submits it with `pay_quote`.

### 9. Pay the quote (pay_quote, exact scheme) → settlement receipt
The client signs an x402 `exact` payload (a USDC EIP-3009 authorization) and submits it. The response binds a `payment_id` to the quote and returns a receipt whose `reference` is the on-chain settlement tx hash.
```json
// Request  (signed payload omitted)
{ "tool": "pay_quote", "set_args": {
    "quote_id": "quote:01a0f618-de6d-7382-87d9-2545635a6570",
    "payload": "0x…sig…" } }
```
```json
// Response
{ "outcome": "success",
  "payment_id": "payment:01a0f618-de6d-7382-87d9-2545635a6570",
  "quote_id": "quote:01a0f618-de6d-7382-87d9-2545635a6570",
  "amount": 0.0108,
  "receipt": { "provider": "cdp", "state": "paid", "amount": 0.0108,
    "quote_id": "quote:01a0f618-de6d-7382-87d9-2545635a6570",
    "reference": "0x8e47…43a1" } }
```
On-chain, that settlement moved **10800 atoms ($0.0108) of testnet USDC** from the payer `0x4c65…D999` to the Safe `0x4559…5e52`; the facilitator paid the gas.

### 10. Run the work with the receipt (materialize with payment_id only)
After paying, re-call the **same verb** with only `payment_id`. The job is queued and runs asynchronously.
```json
// Request
{ "tool": "materialize", "set_args": { "payment_id": "payment:01a0f618-de6d-7382-87d9-2545635a6570" } }
```
```json
// Response
{ "outcome": "success", "result": "materialized", "regime": "async",
  "job": { "job_id": "job:37", "quote_id": "quote:01a0f618-…-5a6570",
           "blocks_total": 90, "state": "queued" } }
```

### 11. Poll the job (inspect subject=job)
```json
// Request
{ "tool": "inspect", "set_args": { "subject": "job", "job_id": "job:37" } }
```
```json
// Response
{ "subject": "job", "outcome": "success", "state": "done",
  "job_id": "job:37", "blocks_done": 90, "blocks_total": 90,
  "bound_function_id": "sha256:18ec…a097",
  "from_block": 510271000, "to_block": 510271090, "spent": 0.0108 }
```

### 12. Ask to read values → a priced quote (read → payment_required)
Reading a window prices two lines: a fixed `query_read` plus a per-block `query_blocks`. Here 1 read over 10 blocks = $0.00003 (30 atoms).
```json
// Request
{ "tool": "read", "set_args": {
    "bound_function_id": "sha256:18ec…a097",
    "from_block": 510603582, "to_block": 510603592 } }
```
```json
// Response
{ "outcome": "payment_required",
  "quote_id": "quote:01a0f6ca-d6bf-7d72-add2-d45ac59f8dfa",
  "amount": 0.00003,
  "from_block": 510603582, "to_block": 510603592,
  "bound_function_id": "sha256:18ec…a097",
  "lines": [
    { "label": "query_read",   "units": 1,  "unit_price": 0.00002,  "amount": 0.00002 },
    { "label": "query_blocks", "units": 10, "unit_price": 0.000001, "amount": 0.00001 } ],
  "accepts": [ { "scheme": "batch-settlement", "network": "eip155:84532",
      "asset": "0x036C…CF7e", "payTo": "0x4559…5e52", "amount": "30",
      "extra": { "name": "USDC", "receiverAuthorizer": "0x91b0…086b", "version": "2", "withdrawDelay": 3600 } } ] }
```

### 13. Read the data (read with payment_id only) — the data is the answer
After paying that read quote, re-call `read` with the `payment_id`. The paid result is the metric itself; there is no second bill.
```json
// Request
{ "tool": "read", "set_args": { "payment_id": "payment:01a0f6ca-d6bf-7d72-add2-d45ac59f8dfa" } }
```
```json
// Response
{ "outcome": "success",
  "bound_function_id": "sha256:18ec…a097",
  "from_block": 510603582, "to_block": 510603592,
  "fields": [ { "name": "count", "type": "u256" }, { "name": "volume", "type": "u256" } ],
  "values": [
    { "block": 510603582, "entity": "0xaf88…5831", "count": "3", "volume": "1181594340" },
    { "block": 510603584, "entity": "0xaf88…5831", "count": "2", "volume": "26179016" },
    { "block": 510603585, "entity": "0xaf88…5831", "count": "5", "volume": "146701109" },
    { "block": 510603589, "entity": "0xaf88…5831", "count": "4", "volume": "14786922" },
    "…" ] }
```

### 14. Attest the inputs (provenance)
`provenance` returns the exact block hashes behind a materialized attribute, so a result can be independently verified.
```json
// Request
{ "tool": "provenance", "set_args": {
    "bound_function_id": "sha256:47ff…74c0", "attribute": "balance",
    "from_block": 26096269, "to_block": 26096369 } }
```
```json
// Response
{ "attribute": "balance", "block_count": 100, "blocks": [
    { "number": 26096269, "block_hash": "0x3d5d…2084" },
    { "number": 26096270, "block_hash": "0x37ce…53f4" },
    "…" ] }
```

---

## C. NFT reward card (purchases, rewards, refunds, claims)

Every paid purchase is recorded to a claim-right "NFT" P&L card keyed by wallet. The card tracks lifetime `paid` / `refunded` / `cost` / `earned` / `claimed` / `unclaimed` / `pnl` (all in atoms) and lists each purchase.

### 15. Inspect the P&L card (inspect subject=nft)
```json
// Request
{ "tool": "inspect", "set_args": { "subject": "nft", "wallet": "0x0000…0000" } }
```
```json
// Response  (purchases trimmed)
{ "subject": "nft", "outcome": "success", "nft": "1", "wallet": "0x0000…0000",
  "paid": "124830", "refunded": "2520", "cost": "122310",
  "earned": "208", "claimed": "0", "unclaimed": "208", "pnl": "-122102",
  "purchases": [
    { "bound_function_id": "sha256:18ec…a097", "quote_id": "quote:9b40…2cf6",
      "paid": "1800", "refunded": "0", "cost": "1800",
      "requested_window": { "from_block": 510309176, "to_block": 510309191 } },
    { "bound_function_id": "sha256:18ec…a097", "quote_id": "quote:4485…83d5",
      "paid": "1830", "refunded": "1800", "cost": "30",
      "requested_window": { "from_block": 510343589, "to_block": 510343604 } },
    "…" ] }
```

### 16. Buy a window (sub-cent) via a batch-settlement voucher
A 15-block materialize costs $0.0018 (1800 atoms) — below the $0.01 floor, so only `batch-settlement` is offered. Paying it signs a voucher against the escrow channel; the receipt `reference` is the channel id.
```json
// Request (quote)
{ "tool": "materialize", "set_args": {
    "bound_function_id": "sha256:18ec…a097", "from_block": 510603580, "to_block": 510603595 } }
```
```json
// Response (quote)
{ "outcome": "payment_required", "quote_id": "quote:01a0f6ca-93dc-7b42-9c9c-2a9b0ec91961",
  "amount": 0.0018,
  "lines": [ { "label": "materialize_blocks", "units": 15, "unit_price": 0.00012, "amount": 0.0018 } ],
  "accepts": [ { "scheme": "batch-settlement", "network": "eip155:84532",
    "asset": "0x036C…CF7e", "payTo": "0x4559…5e52", "amount": "1800",
    "extra": { "name": "USDC", "receiverAuthorizer": "0x91b0…086b", "version": "2", "withdrawDelay": 3600 } } ] }
```
```json
// Request (pay — signed voucher omitted)
{ "tool": "pay_quote", "set_args": { "quote_id": "quote:01a0f6ca-93dc-7b42-9c9c-2a9b0ec91961", "payload": "0x…sig…" } }
```
```json
// Response (pay)
{ "outcome": "success", "amount": 0.0018,
  "payment_id": "payment:01a0f6ca-93dc-7b42-9c9c-2a9b0ec91961",
  "quote_id": "quote:01a0f6ca-93dc-7b42-9c9c-2a9b0ec91961",
  "receipt": { "provider": "cdp", "state": "paid", "amount": 0.0018, "reference": "0xdf9f…783f" } }
```

### 17. The purchase is credited to the card immediately (demo/mock rail)
Right after paying, the new purchase appears on the P&L card with its `quote_id` and window — no claim step needed to record it.
```json
// Request
{ "tool": "inspect", "set_args": { "subject": "nft", "wallet": "0x0000…0000" } }
```
```json
// Response  (lifetime totals + the just-added line; others trimmed)
{ "subject": "nft", "outcome": "success", "nft": "1", "wallet": "0x0000…0000",
  "paid": "2020350", "refunded": "30960", "cost": "1989390",
  "earned": "912", "claimed": "0", "unclaimed": "912", "pnl": "-1988478",
  "purchases": [ "…",
    { "bound_function_id": "sha256:18ec…a097", "quote_id": "quote:01a0f729-b68c-7fc0-b812-e116ae414102",
      "paid": "1800", "refunded": "0", "cost": "1800",
      "requested_window": { "from_block": 509988008, "to_block": 509988023 } } ] }
```

### 18. Paid reads earn rewards (demo/mock rail)
Each paid read earns rewards at **0.5 atoms per block**. After a paid 10-block read the card's `earned`/`unclaimed` rise by 5 (912 → 917); a 100-block read earns +50, a 2-block cross-boundary read +1.
```json
// Request
{ "tool": "inspect", "set_args": { "subject": "nft", "wallet": "0x0000…0000" } }
```
```json
// Response  (totals; purchases trimmed)
{ "subject": "nft", "outcome": "success", "nft": "1", "wallet": "0x0000…0000",
  "paid": "2020350", "refunded": "30960", "cost": "1989390",
  "earned": "917", "claimed": "0", "unclaimed": "917", "pnl": "-1988473",
  "purchases": [ "…" ] }
```

### 19. Buy a future window, then stop it for a refund (stop) — demo/mock rail
A window past the finalized head is billed but not yet deliverable; `stop` cancels the undelivered part and refunds it. Here a $0.00183 future buy (1830 atoms = 1800 materialize + 30 cache) is stopped, refunding the 1800 undelivered atoms (cost settles to 30).
```json
// Request (buy a future window)
{ "tool": "materialize", "set_args": {
    "bound_function_id": "sha256:18ec…a097", "from_block": 510664710, "to_block": 510664725, "extra_cache_days": 2 } }
```
```json
// Response (quote)
{ "outcome": "payment_required", "quote_id": "quote:01a0f72a-3573-7f21-84c3-9e46491bc006",
  "amount": 0.00183,
  "lines": [
    { "label": "materialize_blocks", "units": 15, "unit_price": 0.00012,  "amount": 0.0018 },
    { "label": "extra_cache_days",   "units": 30, "unit_price": 0.000001, "amount": 0.00003 } ],
  "accepts": [ { "scheme": "batch-settlement", "…": "…" } ] }
```
```json
// Request (stop the paid-but-undelivered quote)
{ "tool": "stop", "set_args": {
    "bound_function_id": "sha256:18ec…a097",
    "wallet": "0x0000…0000",
    "quote_id": "quote:01a0f72a-3573-7f21-84c3-9e46491bc006" } }
```
```json
// Response
{ "outcome": "success", "unspent_blocks": 15,
  "bound_function_id": "sha256:18ec…a097",
  "jobs": [ { "job_id": "job:163", "quote_id": "quote:01a0f72a-3573-…-1bc006",
              "refunded": 0.0018, "unspent_blocks": 15 } ] }
```

### 20. Claim rewards (claim_rewards)
Claims convert `unclaimed` reward atoms on-chain once they reach the threshold (10000 atoms). Below threshold the call still succeeds and claims nothing (dust), leaving the card unchanged.
```json
// Request
{ "tool": "claim_rewards", "set_args": { "wallet": "0x0000…0000" } }
```
```json
// Response
{ "outcome": "success", "claimed": "0", "unclaimed": "208" }
```

---

## D. Batch-settlement rail — escrow channel, vouchers, claim, sweep (live CDP)

Sub-cent charges are paid off-chain with signed **vouchers** against an on-chain **escrow channel**. The buyer makes one **deposit**, then each payment is a cumulative voucher; Testril's receiver authorizer later **claims** on-chain and **sweeps** to the Safe.

### 21. Look up open channels (inspect subject=channels) — none yet
Before a batch payment, inspect channels for the buyer address. Empty means the first payment must open a channel (deposit + voucher).
```json
// Request
{ "tool": "inspect", "set_args": { "subject": "channels", "payer": "0x4c65…D999", "network": "eip155:84532" } }
```
```json
// Response
{ "subject": "channels", "outcome": "success",
  "payer": "0x4c65…D999", "network": "eip155:84532", "channels": [] }
```

### 22. First batch payment = deposit + voucher
A $0.0036 charge (3600 atoms) with no channel yet: the client signs a **$0.10 escrow deposit** plus a voucher for `maxClaimable = 3600`. The paid receipt returns the new channel id.
```json
// Request (signed deposit + voucher omitted)
{ "tool": "pay_quote", "set_args": { "quote_id": "quote:01a0f6ab-2385-7cd1-85f0-0f71af78f715", "payload": "0x…sig…" } }
```
```json
// Response
{ "outcome": "success", "amount": 0.0036,
  "payment_id": "payment:01a0f6ab-2385-7cd1-85f0-0f71af78f715",
  "quote_id": "quote:01a0f6ab-2385-7cd1-85f0-0f71af78f715",
  "receipt": { "provider": "cdp", "state": "paid", "amount": 0.0036, "reference": "0x96f2…7bd5" } }
```

### 23. Channel now open (inspect subject=channels)
The channel shows escrow `balance` (100000 atoms = $0.10 deposited), `chargedCumulativeAmount` (3600 so far) and `totalClaimed` (0, nothing claimed yet). Reuse its `channelConfig` for top-ups.
```json
// Response  (one row)
{ "subject": "channels", "outcome": "success", "payer": "0x4c65…D999", "network": "eip155:84532",
  "channels": [ {
    "channelId": "0xdf9f…783f",
    "network": "eip155:84532",
    "channelConfig": {
      "payer": "0x4c65…D999", "payerAuthorizer": "0x4c65…D999",
      "receiver": "0x4559…5e52", "receiverAuthorizer": "0x91b0…086b",
      "token": "0x036C…CF7e", "withdrawDelay": 3600, "salt": "0x4080…a889" },
    "balance": "100000", "chargedCumulativeAmount": "3600",
    "signedMaxClaimable": "3600", "totalClaimed": "0", "closed": false } ] }
```

### 24. Top-up with a voucher only (no new deposit)
The next sub-cent charge reuses the channel: sign a voucher raising `maxClaimable` from 3600 to 7200. No on-chain tx and no new deposit — only the signed cumulative amount changes.
```json
// Response (pay)
{ "outcome": "success", "amount": 0.0036,
  "payment_id": "payment:01a0f6ab-3ae7-7661-85dc-1802ee5df705",
  "quote_id": "quote:01a0f6ab-3ae7-7661-85dc-1802ee5df705",
  "receipt": { "provider": "cdp", "state": "paid", "amount": 0.0036, "reference": "0xdf9f…783f" } }
```
After three such charges the channel reads `chargedCumulativeAmount: "10800"` while `balance` stays `100000` and `totalClaimed` stays `0`.

### 25. Testril claims the vouchers on-chain (claim)
Once cumulative charges cross the $0.01 claim threshold, Testril's receiver authorizer submits a single on-chain claim for all 10800 charged atoms. Observed by polling the escrow:
```json
// Observed on-chain (Base Sepolia escrow)
{ "totalClaimed_api": "10800",
  "claim_txs": [ { "tx": "0xce25…46a1", "claimAmount": 10800, "newTotalClaimed": 10800, "block": 47538278 } ] }
```

### 26. …then sweeps the claimed funds to the Safe (sweep/settle)
Shortly after the claim, Testril sweeps the claimed amount from escrow into the receiver Safe, which rises by 10800 atoms ($0.0108).
```json
// Observed on-chain (Base Sepolia)
{ "safe_usdc_delta_atoms": 10800,
  "sweep_txs": [ { "tx": "0xbdb2…df27", "amount_atoms": 10800, "block": 47538293 } ] }
```
The remaining escrow (balance 100000 − claimed 10800 = **89200 atoms, $0.0892**) stays refundable in the channel.

---

## E. Mock rail vs. live CDP rail

The demo/mock rail mirrors the exact same tool loop so integrators can rehearse without spending. Two tells distinguish it.

### 27. A mock quote offers a bare batch-settlement row
On the mock rail the `accepts` menu has no asset/payTo/escrow fields — there is nothing to sign on-chain.
```json
// Response (mock materialize quote)
{ "outcome": "payment_required", "quote_id": "quote:01a0f729-b68c-7fc0-b812-e116ae414102",
  "amount": 0.0018,
  "lines": [ { "label": "materialize_blocks", "units": 15, "unit_price": 0.00012, "amount": 0.0018 } ],
  "accepts": [ { "scheme": "batch-settlement" } ] }
```

### 28. A mock payment settles instantly with a `provider: "mock"` receipt
No signature is needed and the receipt `reference` is a synthetic `mock:…` id, not a tx hash.
```json
// Request
{ "tool": "pay_quote", "set_args": { "quote_id": "quote:01a0f729-b68c-7fc0-b812-e116ae414102" } }
```
```json
// Response
{ "outcome": "success", "amount": 0.0018,
  "payment_id": "payment:01a0f729-b68c-7fc0-b812-e116ae414102",
  "quote_id": "quote:01a0f729-b68c-7fc0-b812-e116ae414102",
  "receipt": { "provider": "mock", "state": "paid", "amount": 0.0018,
    "reference": "mock:quote:01a0f729-b68c-7fc0-b812-e116ae414102" } }
```

---

## F. Error cases (all real, captured)

### 29. Payment payload must be raw JSON, not base64 (pay_quote)
Submitting a base64-encoded x402 payload is rejected; the rail wants raw JSON.
```json
// Response
{ "outcome": "error", "kind": "invalid_params",
  "message": "invalid request: invalid payload json: expected value at line 1 column 1" }
```

### 30. The live rail requires a signed payment (pay_quote, no payload)
```json
// Response
{ "outcome": "error", "kind": "invalid_params",
  "message": "invalid request: missing payload: cdp rail requires a signed payment" }
```

### 31. `exact` is not offered below the sub-cent floor
Trying to pay a sub-cent quote with the `exact` scheme is refused — only `batch-settlement` is in that quote's menu.
```json
// Response
{ "outcome": "error", "kind": "exact_below_floor_payload_refused",
  "message": "invalid request: exact_below_floor_payload_refused: exact is not in this quote's accepts menu" }
```

### 32. Don't resubmit a paid quote (replay)
```json
// Response
{ "outcome": "error", "kind": "exact_replay_after_paid",
  "message": "this quote is already paid; do not resubmit a payment payload" }
```

### 33. Malformed batch voucher (missing channelId)
A voucher payload that isn't the expected `payload.voucher.{channelId,…}` shape is rejected.
```json
// Request (inner payload)
{ "channelId": "0x0000…0000", "maxClaimableAmount": "360" }
```
```json
// Response
{ "outcome": "error", "kind": "invalid_params",
  "message": "invalid request: invalid_batch_settlement_evm_voucher_payload: missing channelId" }
```

### 34. Deposit with an empty channelConfig (missing payer)
```json
// Request (inner payload)
{ "type": "deposit", "channelConfig": {},
  "voucher": { "channelId": "0x0000…0000", "maxClaimableAmount": "360", "signature": "0x…sig…" } }
```
```json
// Response
{ "outcome": "error", "kind": "invalid_params", "message": "invalid request: missing payer" }
```

### 35. Only the owner can stop/refund (wrong wallet)
`stop` takes an explicit wallet and refunds only the quote's owner; passing a non-owner wallet is refused.
```json
// Request
{ "tool": "stop", "set_args": {
    "bound_function_id": "sha256:18ec…a097", "wallet": "0x0000…0000",
    "quote_id": "quote:01a0f6ca-e6cd-7291-b43a-54565b265cb7" } }
```
```json
// Response
{ "outcome": "error", "kind": "invalid_params", "message": "invalid request: caller is not the agreed owner" }
```

### 36. Overlapping quotes: re-quote when coverage changed
If a competing quote changes a window's "holes" before you pay, the stale quote is refused with a remedy to re-quote the same window.
```json
// Response
{ "outcome": "error", "kind": "state",
  "message": "[state] pay for a backfill of `sha256:18ec…a097`: the quoted holes changed — coverage or paid jobs changed since this quote was issued — request a new materialize quote for the same window, then pay the returned quote_id (materialize)",
  "remedy": [ { "action": "request a new materialize quote for the same window, then pay the returned quote_id", "call": "materialize" } ] }
```

### 37. REST parity — the card is also a plain GET (and validates input)
The same NFT card is available over REST. An unused wallet returns zero totals and no purchases:
```json
// GET https://dev.testril.ai/v1/nft?wallet=0x57a9…7c69   → 200
{ "wallet": "0x57a9…7c69", "paid": "0", "refunded": "0", "cost": "0",
  "earned": "0", "claimed": "0", "unclaimed": "0", "pnl": "0", "purchases": [] }
```
A missing wallet is a 400:
```json
// GET https://dev.testril.ai/v1/nft   → 400
{ "outcome": "error", "kind": "invalid_params", "message": "wallet is required" }
```
A malformed wallet is a 400:
```json
// GET https://dev.testril.ai/v1/nft?wallet=0x1234   → 400
{ "outcome": "error", "kind": "invalid_params",
  "message": "invalid request: payer must be a 20-byte address (0x + 40 hex), not length 6" }
```

---

*All examples are drawn from real Testril test runs on Base Sepolia (testnet). Prices and amounts are testnet USDC. Identifiers and addresses have been shortened; signatures and payment payloads are never shown.*
