<!-- Generated raw Markdown for /reference/protocol; product pin 638fa1e9ca7face19a442cef43754f0d627c7705. -->

This page restates the normative wire surface of SubEtha's x402 scheme as
fixed by
[`docs/PROTOCOL.md`](https://github.com/peaceandwhisky/SubEtha/blob/638fa1e9ca7face19a442cef43754f0d627c7705/docs/PROTOCOL.md)
at the pinned product commit. The TypeScript types exported by
`@subetha/x402-scheme`
([`packages/x402-scheme/src/types.ts`](https://github.com/peaceandwhisky/SubEtha/blob/638fa1e9ca7face19a442cef43754f0d627c7705/packages/x402-scheme/src/types.ts))
— `SubethaExtra`, `SubethaPermitPayload`, `SubethaSelfTransferPayload` — are
the normative definitions; that document and this page mirror them. On any
divergence, the pinned product sources win.

## Identifiers and envelope

| item | value |
|---|---|
| Scheme identifier | `subetha-zerc20` (exported as `SUBETHA_SCHEME`) |
| Network identifier | CAIP-2, `eip155:<chainId>` (e.g. `eip155:31337` for the local stack) |
| Envelope and headers | **unmodified x402 v2** — `PAYMENT-REQUIRED` (base64 JSON `PaymentRequired`) on the 402 response, `PAYMENT-SIGNATURE` (base64 JSON `PaymentPayload`) on the paying request, `PAYMENT-RESPONSE` (base64 JSON `SettleResponse`) on the served response |
| x402 v1 fallback | none — SubEtha does not provide an `X-PAYMENT` fallback on this surface |

## Two-phase settlement: what "settle" means here

A zERC20 payment has two phases:

- **accepted** — the burn transfer to the one-time `payTo` address is
  confirmed on-chain (receipt success + IndexedTransfer + balance). The
  resource MAY be served: the funds are already burned, the payer cannot
  reclaim them.
- **finalized** — the proof-gated mint to the real recipient lands
  (asynchronously, minutes later; never on the request path).

Mapping onto x402: **`/settle` success == accepted.** The mint stays
out-of-band. A `SettleResponse` therefore carries
`extra: { phase: "accepted" }`; `transaction` is the **burn** transaction hash
(the on-chain event a payer can audit), not the mint.

## PaymentRequirements (the offer)

| field | value |
|---|---|
| `scheme` | `"subetha-zerc20"` |
| `network` | `eip155:<chainId>` |
| `asset` | the zERC20 token contract address |
| `amount` | the **quoted** amount in base units (decimal string). Under payer-charged fees this is list price + fee |
| `payTo` | a **one-time burn address**, freshly derived per 402. It is an opaque hash — the real recipient never appears anywhere the payer sees (F1) |
| `maxTimeoutSeconds` | seconds until the challenge expires |
| `extra` | `SubethaExtra` (below) |

```ts
extra: {
  challengeNonce: Hex;      // 32-byte server nonce binding the offer to one payment
  expiresAt: number;        // unix seconds (mirrors maxTimeoutSeconds as an absolute time)
  helper?: Address;         // PermitBurner address — present iff the gasless path is offered
  deadline?: number;        // unix seconds bound for the permit + burn-bind signatures
  fee?: { amount: string;   // facilitator fee already included in `amount`
          bps: number };    // present ONLY under payer-charged fees (never provider-charged)
}
```

Offers expire: a client must obtain a fresh 402 rather than reuse an old
offer. Every 402 carries a **new** `payTo` — burn addresses are never reused
across payments.

## PaymentPayload (the payment)

The paying request carries `PAYMENT-SIGNATURE: base64(JSON(PaymentPayload))`
with the **full x402 v2 envelope**:

```ts
{ x402Version: 2,
  accepted: PaymentRequirements,   // the offer received in the 402, echoed VERBATIM
  payload:  { ... } }              // one of the two shapes below
```

`accepted` MUST be the exact requirements object received in the 402 —
unmodified keys and values, unknown fields preserved. The seller middleware
deep-equal matches the core fields and requires its own `extra` to be a subset
of the echoed one; any normalization (key reordering is fine — JSON objects
are unordered — but dropping or rewriting fields is not) makes the match fail
and the payment is refused. The optional `resource` field of the envelope may
be omitted.

`PaymentPayload.payload` takes exactly one of two shapes.

**Gasless permit (preferred; requires `extra.helper`):**

```ts
{ challengeNonce: Hex,
  payer: Address,                       // EOA only (EIP-2612 recovery)
  permit: { value: string, nonce: string, deadline: string,   // decimal strings
            v: number, r: Hex, s: Hex },                      // EIP-2612 permit signature
  burnAuthSig: Hex }                    // EIP-712 burn-bind signature (PermitBurner ADR-001)
```

The payer only signs; the facilitator broadcasts the burn and pays its gas.
`permit.value` must equal the quoted `amount` exactly.

**Self-transfer (fallback; payer pays gas):**

```ts
{ challengeNonce: Hex, burnTxHash: Hex }
```

The client MUST broadcast the ERC20 transfer of `amount` to `payTo` and wait
for a successful receipt before sending the payload (`@subetha/x402-scheme`
enforces this and throws on a reverted/unconfirmed burn).

**Discrimination rule (server side).** If any of `permit` / `payer` /
`burnAuthSig` is present, all three MUST be — a partial triple is rejected as
malformed and never falls through to the self-transfer interpretation.
Otherwise `burnTxHash` selects the self-transfer shape.

## EIP-712 definitions (normative — the gasless permit path)

Both signatures are standard EIP-712 typed-data signatures by the payer EOA,
bound to the offer's `deadline` (`extra.deadline`, unix seconds).

**Permit** — the EIP-2612 approval that lets the PermitBurner helper move the
funds:

| | |
|---|---|
| domain | `{ name: <token.name()>, version: "1", chainId: <from network>, verifyingContract: <asset> }` |
| types | `Permit(address owner, address spender, uint256 value, uint256 nonce, uint256 deadline)` |
| message | `owner` = payer, `spender` = `extra.helper`, `value` = `amount` (exact), `nonce` = `token.nonces(payer)` read live, `deadline` = `extra.deadline` |

The wire `permit` object carries `value`/`nonce`/`deadline` as **decimal
strings**, `v` as a number, and `r`/`s` as 32-byte hex.

**BurnAuthorization** — SubEtha's burn-bind (permit alone does not bind the
destination; ADR-001 in
[`contracts/README.md`](https://github.com/peaceandwhisky/SubEtha/blob/638fa1e9ca7face19a442cef43754f0d627c7705/contracts/README.md)):

| | |
|---|---|
| domain | `{ name: "SubEthaPermitBurner", version: "1", chainId: <from network>, verifyingContract: <extra.helper> }` |
| types | `BurnAuthorization(address token, address payer, address burnAddress, uint256 value, bytes32 challengeNonce, uint256 deadline)` |
| message | `token` = `asset`, `payer`, `burnAddress` = `payTo`, `value` = `amount`, `challengeNonce` = `extra.challengeNonce`, `deadline` = `extra.deadline` |

The wire `burnAuthSig` is the 65-byte `r ‖ s ‖ v` signature as 0x-hex. The
facilitator recovers both signatures and rejects the payment unless both
recover to `payer`.

## Facilitator HTTP API

Standard x402 v2 facilitator surface plus one SubEtha extension:

| endpoint | role |
|---|---|
| `GET /supported` | advertises `{ kinds: [{ x402Version: 2, scheme: "subetha-zerc20", network: "eip155:<chainId>" }], extensions: [], signers: {} }`. Resource servers using the official middleware require this at initialization |
| `POST /verify` | body `{ x402Version, paymentPayload, paymentRequirements }` → `VerifyResponse`. **Read-only substantive check** (below) |
| `POST /settle` | same body → `SettleResponse`. Executes the payment: routes the payload to the burn-confirmation path |
| `POST /challenge` | **SubEtha extension.** Body `{ amount: "<base units>" }` (the LIST price) → a fresh challenge offer `{ payTo, amount (quoted), token, chainId, challengeNonce, expiresAt, helper?, deadline?, fee? }`. Exists because a `payTo` burn address can only be derived by the facilitator (seed holder) — a resource server cannot mint its own PaymentRequirements the way vanilla x402 sellers do. The scheme server plugin calls it per 402 |

Requests' `paymentRequirements` are **untrusted**: both `/verify` and
`/settle` compare `network`, `asset`, `payTo`, `amount` and
`extra.challengeNonce` against the facilitator's own challenge record and
reject any mismatch.

### Verify semantics

`/verify` checks, without mutating anything: the challenge exists and is
unexpired; for self-transfer — the burn tx receipt exists, succeeded, carries
an IndexedTransfer of ≥ `amount` to `payTo` on the right token, and the burn
address holds the funds; for permit — both signatures recover to `payer`, the
token nonce is live, the deadline holds, the value matches, and the payer
balance covers the amount.

**Verify is point-in-time, not a settlement guarantee (TOCTOU).** The official
middleware runs verify → handler → settle; a payer can consume its permit
nonce or move its balance between verify and settle, in which case settle
fails after the handler already ran. Providers of computationally expensive
resources should apply ordinary rate limiting / abuse controls; the funds
themselves are never at risk (nothing is served on a failed settle — the
middleware replaces the buffered response with a 402).

### Status and error semantics

- **`/settle` success == accepted.** `SettleResponse.extra.phase` is
  `"accepted"`; `transaction` is the burn transaction hash.
- **Pending.** Self-transfer clients wait for the burn receipt before paying,
  so `/settle` normally answers definitively. On the permit path the
  facilitator broadcasts inside `/settle` and polls the receipt to a bound; if
  the burn is still unmined it answers
  `{ success: false, errorReason: "pending_burn" }`. Re-sending the same
  payload is **idempotent and safe** — a broadcast permit is reconciled via
  its on-chain replay ledger (the burn is never double-executed) — but
  automatic client retry is not part of this version.
- **Profile gating.** The packaged facilitator daemon accepts payments only
  under its local profile in this release. A non-local profile answers
  `403 { "error": "payment acceptance is disabled for profile …" }` on
  `POST /challenge`, `{ "isValid": false, "invalidReason": "profile_unsupported" }`
  on `POST /verify`, and
  `{ "success": false, "errorReason": "profile_unsupported", … }` on the
  official-shape `POST /settle` (see
  [`docs/FACILITATOR.md`](https://github.com/peaceandwhisky/SubEtha/blob/638fa1e9ca7face19a442cef43754f0d627c7705/docs/FACILITATOR.md)).

### Legacy surface

The pre-P0 JSON shapes (`POST /challenge` unchanged; `POST /settle` with a
direct `{ challengeNonce, burnTxHash }` /
`{ challengeNonce, payer, permit, burnAuthSig }` body) remain supported for
existing integrations (the web demo). New integrations should use the
official-shape endpoints.

## Compatibility policy

- **Additive-only.** New optional fields may appear in `extra`, payloads and
  responses; unknown keys MUST be ignored.
- The scheme name, payload discrimination rule and the meaning of settle
  (= accepted) are frozen for `x402Version: 2`.
- Anything SubEtha-specific lives in `extra` / `payload` /
  `SettleResponse.extra` — the x402 envelope is never altered.

See [Versions & compatibility](/reference/compatibility/) for the protocol
scope and package/registry facts.

## Implementation sources (at the pinned commit)

| surface | source |
|---|---|
| Normative wire spec | [`docs/PROTOCOL.md`](https://github.com/peaceandwhisky/SubEtha/blob/638fa1e9ca7face19a442cef43754f0d627c7705/docs/PROTOCOL.md) |
| Wire types + requirements validation | [`packages/x402-scheme/src/types.ts`](https://github.com/peaceandwhisky/SubEtha/blob/638fa1e9ca7face19a442cef43754f0d627c7705/packages/x402-scheme/src/types.ts) |
| Payer-side payload builder | [`packages/x402-scheme/src/client.ts`](https://github.com/peaceandwhisky/SubEtha/blob/638fa1e9ca7face19a442cef43754f0d627c7705/packages/x402-scheme/src/client.ts) |
| Fee parsing / offer ranking | [`packages/x402-scheme/src/fee.ts`](https://github.com/peaceandwhisky/SubEtha/blob/638fa1e9ca7face19a442cef43754f0d627c7705/packages/x402-scheme/src/fee.ts) |
| Provider-side scheme server + validation | [`packages/provider/src/scheme-server.ts`](https://github.com/peaceandwhisky/SubEtha/blob/638fa1e9ca7face19a442cef43754f0d627c7705/packages/provider/src/scheme-server.ts), [`packages/provider/src/validate.ts`](https://github.com/peaceandwhisky/SubEtha/blob/638fa1e9ca7face19a442cef43754f0d627c7705/packages/provider/src/validate.ts) |
| Facilitator HTTP daemon | [`apps/facilitator/src/http.ts`](https://github.com/peaceandwhisky/SubEtha/blob/638fa1e9ca7face19a442cef43754f0d627c7705/apps/facilitator/src/http.ts) (source-only; not an npm package) |
| Python reference payer | [`python/src/subetha/wire.py`](https://github.com/peaceandwhisky/SubEtha/blob/638fa1e9ca7face19a442cef43754f0d627c7705/python/src/subetha/wire.py) |
