# x402-fee/0.1 — a fee-disclosure convention for x402

**Status:** frozen. Additive changes only. Ships dark (`X402_ENABLED=0`).
**Profile id:** `x402-fee/0.1`
**Applies to:** x402 protocol v1 (primary) and v2 (additive; see §9).
**Reference server:** `compute.pangle.online` — `POST /api/x/rent`
**Reference client:** `sidecar/x402-client.py` in this repo.

---

## 0. What this is, and what it is not

This is **not a new payment protocol.** x402 already exists, it is an emerging
standard, and inventing a competitor to it would be starting an adoption curve
from zero for no reason. This document defines a **thin convention layered on
x402**: three properties, and nothing else.

1. **The fee is disclosed inside the challenge.** Not in a pricing page, not in
   a footer — in the same signed object the payer is deciding about, in dollars,
   before any key is touched.
2. **Every fill is published.** The receipt schema below lands on a public tape
   (`/api/receipts`) from fill number one. An adopter does not have to trust the
   fee statement; they can check it.
3. **Execution is non-custodial.** The payer authorises one exact transfer for
   one block. There is no balance, no deposit, no account.

Everything else here is x402's, unchanged. No protocol field is renamed,
removed, or reinterpreted. Our entire addition is one namespaced key.

**Sources this is built against** (read 2026-08-28):

- `https://github.com/coinbase/x402/blob/main/specs/x402-specification-v1.md`
- `https://github.com/coinbase/x402/blob/main/specs/transports-v1/http.md`
- `https://github.com/coinbase/x402/blob/main/specs/x402-specification-v2.md`
- `https://github.com/coinbase/x402/blob/main/specs/transports-v2/http.md`
- `https://github.com/coinbase/x402/blob/main/specs/transports-v1/mcp.md`
- `https://github.com/coinbase/x402/blob/main/specs/schemes/exact/scheme_exact_evm.md`

---

## 1. The flow

```
    client                          resource server                 facilitator
      │  POST /api/x/rent  (no payment)      │                            │
      │─────────────────────────────────────▶│                            │
      │  402 + challenge (fee in dollars)    │                            │
      │◀─────────────────────────────────────│                            │
      │  sign EIP-3009 transferWithAuthorization (off-chain, no gas)      │
      │  POST again, X-PAYMENT: base64(PaymentPayload)                    │
      │─────────────────────────────────────▶│                            │
      │                                      │  read Base: payer balance, │
      │                                      │  nonce state, window       │
      │                                      │  POST /verify              │
      │                                      │───────────────────────────▶│
      │                                      │  isValid  (NO money moves) │
      │                                      │◀───────────────────────────│
      │                                      │  reserve escrow (locked)   │
      │                                      │  place the Akash lease     │
      │                                      │  ── lease live ──          │
      │                                      │  POST /settle              │
      │                                      │───────────────────────────▶│
      │  200 + receipt, X-PAYMENT-RESPONSE   │  tx hash + confirmations   │
      │◀─────────────────────────────────────│◀───────────────────────────│
```

**The order is the product.** Verify the funds on-chain → reserve → place →
machine confirmed running → settle → confirm on-chain → record. A placement that
fails is never settled, which is what *charge only after the lease is live* means
in code rather than in prose. A settlement that fails after the lease is live
tears the lease down immediately: the station will not run a machine it could not
charge for, and will not charge for one it could not run.

**Why the funds are checked before the lease is placed.** A facilitator's
`/verify` is a signature check — it says the authorization is well-formed, never
that it will settle — and the lease is placed with the station's own tokens
before any money moves. An empty wallet signing a valid authorization would
therefore cost the station a placement and a teardown for the price of one
signature, repeatably. So before anything is reserved, the station reads Base
itself: the payer's USDC balance, the EIP-3009 nonce's `authorizationState`, and
the authorization's validity window. Money still moves last, for a block that was
delivered — non-delivery destroys the lease with nothing charged, which is why
there is no refund to owe and no customer balance to hold.

---

## 2. The 402 challenge

HTTP `402 Payment Required`, `Content-Type: application/json`, body is x402's
`PaymentRequirementsResponse` verbatim:

```json
{
  "x402Version": 1,
  "error": "X-PAYMENT header is required",
  "accepts": [
    {
      "scheme": "exact",
      "network": "base",
      "maxAmountRequired": "410000",
      "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      "payTo": "0x085cacDDd02fe32212DEEfFC858D197b31b80719",
      "resource": "https://compute.pangle.online/api/x/rent",
      "description": "1 prepaid block(s) of rtx4090 GPU time (3600s) on Akash. Price $0.400000 + platform fee $0.010000 = $0.410000. No refunds; you are charged only after the lease is live.",
      "mimeType": "application/json",
      "outputSchema": null,
      "maxTimeoutSeconds": 120,
      "extra": {
        "name": "USD Coin",
        "version": "2",
        "x402_fee": {
          "x402_fee_version": "0.1",
          "profile": "x402-fee/0.1",
          "lease_id": "x402-72a00cdffaf4e5b9",
          "block": { "gpu": "rtx4090", "seconds": 3600 },
          "blocks": 1,
          "price_usd": 0.4,
          "fee_bps": 250,
          "fee_min_usd": 0.01,
          "fee_cap_usd_per_lease_day": 5.0,
          "fee_usd": 0.01,
          "total_usd": 0.41,
          "no_refunds": true,
          "charge_only_after_lease_live": true,
          "notice_days_for_fee_change": 30,
          "receipts_url": "https://compute.pangle.online/api/receipts",
          "spec_url": "https://github.com/aitools420/compute-wick/blob/master/docs/X402-FEE-0.1.md",
          "originator": null
        }
      }
    }
  ]
}
```

### 2.1 Why the disclosure is nested under one key

`extra` on the `exact`/EVM scheme is **not free space**: the scheme requires
`name` and `version` there, and they are the EIP-712 domain fields the payer
signs over. Splashing our fields across `extra` would put a convention's naming
in permanent collision risk with a scheme's. Everything of ours therefore lives
under the single key `extra.x402_fee`, which a facilitator ignores and a future
scheme cannot collide with.

**`extra.name` is `"USD Coin"`, not `"USDC"`.** The published x402 examples show
`"USDC"` because they use Base *Sepolia* USDC. Base **mainnet** USDC's `name()`
is `"USD Coin"`. Verified on-chain 2026-08-28: the EIP-712 domain separator for
`{"USD Coin","2",8453,0x8335…2913}` reproduces the contract's own
`DOMAIN_SEPARATOR()` (`0x02fa7265…7834f`); `"USDC"` does not. An implementer who
takes the example literally on mainnet produces signatures that fail
verification, every time. Read these two fields from the challenge; never
hardcode them.

### 2.2 The disclosure fields

| Field | Type | Meaning |
| --- | --- | --- |
| `x402_fee_version` | string | `"0.1"`. The version of THIS convention. |
| `profile` | string | `"x402-fee/0.1"`. Stable id, usable as an extension key. |
| `lease_id` | string | The station's id for this rental. **Send it back** (§3.3). |
| `block` | object | `{gpu, seconds}` — what one block is. |
| `blocks` | integer | How many blocks this challenge prices. |
| `price_usd` | number | The compute, before any fee. |
| `fee_bps` | integer | Platform fee rate in basis points (250 = 2.5%). |
| `fee_min_usd` | number | Floor. A percentage of a small block rounds to nothing without it. |
| `fee_cap_usd_per_lease_day` | number | Cumulative ceiling per `lease_id` per UTC day. |
| `fee_usd` | number | **The fee for this purchase, in dollars, cap already applied.** |
| `total_usd` | number | `price_usd + fee_usd`. Must equal `maxAmountRequired`. |
| `no_refunds` | boolean | `true` at 0.1. There is no refund path. |
| `charge_only_after_lease_live` | boolean | `true`. Failed placement ⇒ no charge. |
| `notice_days_for_fee_change` | integer | 30. See §7. |
| `receipts_url` | string | Where every fill is published. |
| `spec_url` | string | This document, in the repo it is maintained in — never a served copy, which drifts. |
| `originator` | string \| null | Optional attribution label. See §6. |

**A client MUST check `total_usd * 10^decimals == maxAmountRequired`** and refuse
to pay if they disagree. A disclosure that does not match the wire amount is the
one failure mode this convention exists to make visible; the reference client
exits non-zero on it.

### 2.3 The fee, stated plainly

The platform fee is charged on **what this station settles** — the prepaid block
— and never on a provider's own price. It is 250 bps with a $0.01 floor and a
cumulative $5.00 ceiling per `lease_id` per UTC day. The cap is the reason an
agent renting at scale never has an economic reason to route around the station:
capped at single-digit dollars a day, the fee cannot exceed the cost of building
and operating a direct integration.

The cap is applied **at quote time** against fees already charged to the same
`lease_id` today, so the number in the challenge is the number charged. It can
only ever reduce the fee, never raise it.

---

## 3. Paying

### 3.1 The header

`X-PAYMENT: <base64 of the JSON PaymentPayload>` — x402 v1 HTTP transport,
unchanged:

```json
{
  "x402Version": 1,
  "scheme": "exact",
  "network": "base",
  "payload": {
    "signature": "0x…",
    "authorization": {
      "from":        "0x…payer",
      "to":          "0x…payTo from the challenge",
      "value":       "410000",
      "validAfter":  "1787914880",
      "validBefore": "1787915120",
      "nonce":       "0x…32 random bytes"
    }
  }
}
```

The signature is EIP-712 over EIP-3009 `TransferWithAuthorization`, domain
`{name, version, chainId, verifyingContract}` = `{extra.name, extra.version,
8453, asset}`. The client signs; the facilitator broadcasts. **The payer never
sends a transaction and never pays gas.**

### 3.2 What the server checks before it calls anyone

In this order, each an immediate refusal:

| Check | `reason` |
| --- | --- |
| header is base64, is JSON, is an object | `malformed_payment` |
| `x402Version` ∈ {1, 2} | `unsupported_version` |
| `scheme == "exact"` | `unsupported_scheme` |
| `network` matches the station's | `invalid_network` |
| `asset` matches, when the payload declares one | `invalid_asset` |
| `authorization.to == payTo` | `invalid_pay_to` |
| `authorization.from` is an address | `invalid_payer` |
| `authorization.value == maxAmountRequired` **exactly** | `invalid_amount` |
| `validAfter <= now + skew` and `validBefore > now + skew` | `invalid_timing` |
| `nonce` is 32 bytes of hex | `invalid_nonce` |
| `nonce` has not been used | `replay` |

Then, on-chain, before anything is reserved or placed (`X402_PRECHECK`, on):

| Check | `reason` | Status |
| --- | --- | --- |
| the authorization is inside its own window right now | `authorization_expired` | 402 |
| USDC `balanceOf(payer)` ≥ the exact amount | `insufficient_funds` | 402 |
| `authorizationState(payer, nonce)` is false | `nonce_used` | 402 |
| the payer's balance and nonce could be read at all | `precheck_unavailable` | 502 |

All three refusals carry a fresh, payable challenge: fund the wallet, sign a
fresh nonce, sign a fresh window.

Only then does the server call the facilitator's `/verify` — the signature half,
which is its own job — and only then does it reserve. The reservation is the
rate limit, the escrow arithmetic and the nonce claim under ONE lock, so two
concurrent requests cannot both place against one lease's worth of escrow:

| Check | `reason` | Status |
| --- | --- | --- |
| this payer is under `X402_MAX_PLACEMENTS_PER_PAYER_10MIN` | `rate_limited` | 429 |
| the station can still fund another lease | `insufficient_escrow` | 503 |

The lease is placed after that, and `/settle` only once the provider reports the
machine running.

**Exact, not at-least.** The x402 spec permits `value >= maxAmountRequired`.
This profile requires **equality**. With `no_refunds: true`, accepting an
overpayment would mean keeping money nobody was quoted, so an overpayment is
refused for the same reason an underpayment is. A client that pads its
authorization will be refused; do not pad.

**Asset binding on v1.** A v1 `PaymentPayload` carries no `asset` field — the
asset is bound by the EIP-712 domain the payer signed, which only the facilitator
can check. The server checks the asset itself when the payload declares one
(x402 v2's `accepted.asset`, or a v1 client that echoes `asset`), and otherwise
relies on the facilitator's `isValid`. This is stated rather than papered over.

### 3.3 Renewing, and why `lease_id` matters

The challenge mints a `lease_id`. **Send it back in the request body** on the
paying call, and on every later block of the same rental:

```json
{"blocks": 1, "lease_id": "x402-72a00cdffaf4e5b9"}
```

It is the accrual key for the daily fee cap. A rental that keeps its `lease_id`
across renewals accrues toward one $5/day ceiling; a rental that mints a fresh
one each time starts a fresh ceiling — which costs the payer more, not less.
Losing it also means the quote is re-derived for a different rental and the
amount check refuses the payment.

### 3.4 Idempotency

The EIP-3009 `nonce` is the idempotency key. It is single-use by construction on
chain, and the server treats it the same way: a repeat of the same `X-PAYMENT`
returns the **first receipt** with `"idempotent_replay": true`, and never buys a
second machine. A nonce whose first attempt failed is refused (`reason:
"replay"`) — sign a fresh authorization.

---

## 4. The receipt

`200 OK`, `X-PAYMENT-RESPONSE: <base64 SettlementResponse>`, body:

```json
{
  "x402_fee_version": "0.1",
  "profile": "x402-fee/0.1",
  "lease_id": "x402-c8ac8c25917535e3",
  "dry_run": false,
  "payer": "0x…",
  "nonce": "0x…",
  "network": "base",
  "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  "settlement_tx": "0x…",
  "settlement_confirmation": { "checked": true, "block_number": 1715516,
                               "log_index": 9, "amount_atomic": "410000",
                               "confirmations": 2, "confirmations_required": 2 },
  "block": { "gpu": "rtx4090", "seconds": 3600 },
  "blocks": 1,
  "seconds": 3600,
  "price_usd": 0.4,
  "fee_usd": 0.01,
  "fee_bps": 250,
  "fee_min_usd": 0.01,
  "fee_cap_usd_per_lease_day": 5.0,
  "total_usd": 0.41,
  "provider": "akash",
  "placement": { "…": "the SDL, the derived manifest, the provider, the dseq" },
  "expires_at": 1787918547,
  "auto_destroy": { "armed": true, "at": 1787918547, "note": "…" },
  "no_refunds": true,
  "charge_only_after_lease_live": true,
  "receipts_url": "https://compute.pangle.online/api/receipts",
  "usage_event_id": 2,
  "settled_at": 1787914947,
  "originator": null
}
```

`fee_usd` and `fee_bps` are read back from the ledger row, not copied from the
quote: the receipt publishes **what was charged**, not what was estimated.

`settlement_confirmation` is the station's OWN reading of the chain, not the
facilitator's word for it. Before access is given, the server fetches the
transaction receipt from a Base RPC and requires a USDC `Transfer` of the exact
atomic amount, from the payer, to `payTo` — and **emitted by the USDC contract
itself**, because anyone can emit a `Transfer` with matching topics from a
worthless token. The block number and log index it matched are published here so
a payer can re-check the identical claim. `confirmations` is how far the receipt
had been buried when access was granted (`eth_blockNumber` minus the receipt's
block, so a receipt in the head block has none): status 1 on the head is not
depth, and a Base reorg after access is a block given away. The requirement is
`X402_SETTLE_CONFIRMATIONS`, default 2, waited for inside the same confirmation
window — a settlement that never gets there is refused like one that never
appeared. A
settlement that cannot be confirmed is treated exactly like one that failed —
the lease is destroyed and nothing is charged (`reason: "settlement_unconfirmed"`,
HTTP 502). `checked: false` means the confirmation did not run (dry run, or the
operator turned `X402_SETTLE_RECHECK` off) and says which in `note` — it never
claims a check that did not happen.

`dry_run: true` means the station is in dry-run mode: nothing was verified
on-chain, nothing was settled, `settlement_tx` is empty, and no lease exists. A
dry run never invents a transaction hash.

`auto_destroy` is the end of the prepaid block. The station's guard closes the
lease when the bought seconds run out. **The guard lives in process memory**: if
the station restarts before the block ends, the lease is not closed on time.
That is disclosed here rather than discovered later.

### 4.1 The public tape

`GET /api/receipts` publishes, for every fill, from the first one:

```
ts, kind, provider, instance_id (= lease_id), gpu_model, gpu_count,
price_hr, hours, amount_usd, fee_bps, fee_usd, originator
```

`kind` is `x402_block_settled` when a block is bought and `x402_block_ended` when
its time runs out. No addresses, no tokens, no keys. This is what makes the fee
statement checkable instead of trusted: the format is free to copy, the audited
history is not.

---

## 5. Errors

The HTTP transport's mapping, unchanged:

| Situation | Status | Body |
| --- | --- | --- |
| no payment attached | 402 | the challenge |
| payment refused (any `reason` in §3.2, or the facilitator's) | 402 | the challenge, `error` set to the refusal |
| malformed request (bad `blocks`, bad `originator`, unsupported version) | 400 | `{"detail": "…"}` |
| placement failed — **nothing charged** | 502 | `{"detail": "placement failed, so nothing was charged: …"}` |
| too many placements from one payer (`rate_limited`) | 429 | `{"detail": "…"}` |
| the station cannot fund another lease (`insufficient_escrow`) | 503 | `{"detail": "…"}` |
| facilitator unreachable, or the payer's funds unreadable | 502 | `{"detail": "…"}` |
| rail not enabled / no house address | 404 / 503 | `{"detail": "…"}` |

A 402 always carries a fresh, payable challenge. Read the `error` string, fix,
re-sign.

---

## 6. `originator` — an optional label, and only a label

`originator` is an **optional** opaque string, at most 64 characters from
`[A-Za-z0-9._:@/+-]`, naming the client implementation or spec author that
produced the fill — a handle, a reverse-DNS name. Absent or `null` means
unattributed, which is not an error.

- The client may set it in the **request body** (`{"originator": "…"}`), which
  works identically on v1 and v2, or inside the payment payload's
  `extensions["x402-fee/0.1"].originator`, which is x402 v2's purpose-built slot.
  A label in the payload wins over one in the body.
- The server echoes it into the challenge's disclosure, carries it **unmodified**
  onto the receipt as `originator`, and writes it onto the public ledger row, so
  a future contributor reward can be computed from the published tape alone.
- **It MUST NOT affect price, fee, verification or settlement.** The same block
  costs the same to the atomic unit whether it is labelled or not. This is
  enforced by test, not by intention.
- It is not carried inside a v1 `PaymentPayload`. That object is forwarded
  byte-for-byte to a third-party facilitator, and adding non-spec keys to
  something another party parses is how an extension breaks a protocol.
- A malformed label is a **400 on the request shape**, raised before any payment
  is examined, so it can never be mistaken for a payment being rejected.

---

## 7. Versioning policy

**The interface is frozen.** An adopter who writes a client against this document
must not have it broken.

1. **Additive only.** New keys may be added to `extra.x402_fee`, to the receipt,
   and to the tape. Existing keys are never renamed, retyped, or removed within
   a version. A client MUST ignore keys it does not recognise.
2. **A breaking change is a new profile id** — `x402-fee/0.2` — served alongside,
   never in place of.
3. **Old versions are honoured for 90 days** from the day a successor ships. The
   deprecation date is published at `receipts_url` and in this document's header
   before the clock starts, not after.
4. **The fee's EXISTENCE is permanent from fill one.** It is not introduced later
   and it is not waived for early volume. Fills at zero teach nothing about
   willingness to pay.
5. **The fee's LEVEL changes only with 30 days' public notice** (`notice_days_for_fee_change`).
   The notice is published before the change, on the same page as the receipts. A
   change never reaches backwards: `fee_bps` is stamped on each ledger row at
   event time, so already-priced usage keeps its price forever.

---

## 8. What a second implementer needs

A server implementing `x402-fee/0.1`:

1. Speak x402 v1 over HTTP: 402 + `PaymentRequirementsResponse`, read
   `X-PAYMENT`, answer with `X-PAYMENT-RESPONSE`. Nothing custom.
2. Put a `x402_fee` object in `accepts[i].extra` with at least
   `x402_fee_version`, `price_usd`, `fee_usd`, `total_usd`, `fee_bps`,
   `fee_min_usd`, `fee_cap_usd_per_lease_day`, `no_refunds`,
   `charge_only_after_lease_live`, `receipts_url`.
3. Guarantee `total_usd × 10^decimals == maxAmountRequired`.
4. Verify before you place; place before you settle; settle before you record.
   If you place with your own money before you settle, **verify has to check the
   funds on-chain**, not just the signature — otherwise an empty wallet grieves
   your float for the price of a signature.
5. Treat the EIP-3009 `nonce` as the idempotency key.
6. Publish every fill at `receipts_url`, from the first one, with at least
   `ts, amount_usd, fee_bps, fee_usd`.
7. Change the fee's level only with the notice period you published.

A client implementing `x402-fee/0.1`:

1. POST unpaid, read the 402.
2. Read `accepts[0].extra.x402_fee` and **check `total_usd` against
   `maxAmountRequired`**. Show the human the fee in dollars.
3. Sign EIP-3009 with the domain fields from `extra.name` / `extra.version` —
   read them, never hardcode them.
4. `value` exactly equals `maxAmountRequired`.
5. Fresh 32-byte random `nonce` per attempt; retrying the same `X-PAYMENT` is
   safe and idempotent, but a new purchase needs a new nonce.
6. Send `lease_id` back on renewals.
7. Ignore keys you do not recognise.

`sidecar/x402-client.py` does all seven in about 250 lines of stdlib plus
`eth_account`. Start there.

---

## 9. x402 v2

Upstream shipped protocol v2 on 2025-12-09: CAIP-2 network ids (`eip155:8453`),
`amount` in place of `maxAmountRequired`, a top-level `ResourceInfo`, an
`extensions` map, and new header names (`PAYMENT-REQUIRED`, `PAYMENT-SIGNATURE`,
`PAYMENT-RESPONSE`).

**v1 is the frozen surface of this profile.** v2 is served **additively** when
the caller asks for it (`X402-Version: 2`, or by sending `PAYMENT-SIGNATURE`),
because an agent built this year may speak only v2 and a rail that ignores it is
a rail nobody can reach. Under v2 the same disclosure appears twice: in
`accepts[i].extra.x402_fee`, exactly as in v1, and in
`extensions["x402-fee/0.1"]` as `{info, schema}` — the mechanism v2 built for
precisely this, with a JSON Schema so a client can validate what it was shown.

If v2 ever replaces v1 upstream, that is a **profile bump** (`x402-fee/0.2`)
under §7, not an edit to this document.

---

## 10. MCP

The `rent_x402` MCP tool mirrors the HTTP rail: call it with no arguments to get
the challenge object, then again with `x_payment` set to the same base64 payload
the HTTP rail sends. It returns the same receipt.

**Deviation, stated:** the x402 MCP transport puts the payment in
`_meta["x402/payment"]`. This tool takes it as a tool **argument** instead. The
payload is byte-identical; only the channel differs, because a tool argument is
the one place every MCP client can portably put it. The HTTP rail is fully
transport-conformant, and is the surface to build against.

---

## 11. Scope at 0.1

**In:** one GPU class (RTX 4090) on Akash, prepaid blocks, USDC on Base, one
facilitator, the fee and its cap, the public tape, the auto-destroy at block end.

**Out, deliberately:** refunds (there are none), postpaid or credit of any kind,
custody of a balance, multi-class inventory, fiat, RunPod (resale is gated on
written consent) and Vast (resale is forbidden by its ToS — never). Akash is the
venue because brokering there is protocol-native and needs nobody's permission.
