x402 v2 on SubEtha: request, payment, settlement
SubEtha is a local, non-production proof of concept. It carries a zERC20
payment inside the official x402 v2 protocol — the envelope and headers are
unmodified x402 v2, and everything SubEtha-specific rides inside the standard
extension points (PaymentRequirements.extra and PaymentPayload.payload).
This page explains the request/payment/settlement lifecycle conceptually; the
normative field-by-field wire format is the
wire protocol reference.
The three parties
Section titled “The three parties”| party | role |
|---|---|
| Payer (an AI agent or script) | requests the resource and pays; sees only a one-time burn address, never the provider’s real payout destination |
| Provider (resource server) | serves the HTTP resource behind a paywall; uses the official x402 middleware with the SubEtha scheme plugin |
| Facilitator | derives the one-time burn addresses, verifies and settles payments, and later drives finalization; the only component that touches the chain-side settlement machinery |
The envelope: unmodified x402 v2
Section titled “The envelope: unmodified x402 v2”SubEtha registers the scheme subetha-zerc20 and uses x402 v2’s three
headers exactly as the official protocol defines them:
| header | direction | contents |
|---|---|---|
PAYMENT-REQUIRED |
402 response → client | base64 JSON PaymentRequired (the offer) |
PAYMENT-SIGNATURE |
paying request → server | base64 JSON PaymentPayload (the payment) |
PAYMENT-RESPONSE |
served response → client | base64 JSON SettleResponse (the settlement result) |
SubEtha does not provide an x402 v1 (X-PAYMENT) fallback on this surface.
Clients without a subetha-zerc20 handler skip SubEtha offers gracefully via
standard x402 scheme filtering — advertising the scheme cannot break foreign
clients.
The lifecycle
Section titled “The lifecycle”- Request, unpaid. The payer requests the resource with no payment headers.
- HTTP 402 with an offer. The provider answers
402 Payment Required. ThePAYMENT-REQUIREDheader carriesPaymentRequirements: the scheme, the CAIP-2 network (eip155:<chainId>), the token contract (asset), the quotedamountin base units, a one-time burn address aspayTo(freshly derived for every 402, never reused),maxTimeoutSeconds, and SubEtha’sextrablock (challenge nonce, absolute expiry, optional gasless helper and deadline, optional fee disclosure). The provider cannot mint this offer itself: only the facilitator can derive a burn address, so the scheme plugin fetches a fresh challenge from the facilitator per 402. - The payment. The payer re-sends the request with
PAYMENT-SIGNATUREcarrying the full x402 v2 envelope:x402Version: 2, the received offer echoed verbatim asaccepted, and one of two payload shapes — the gasless permit (the payer only signs; the facilitator broadcasts the burn and pays its gas) or the self-transfer fallback (the payer broadcasts the burn transfer itself and includes the transaction hash). - Verify, serve, settle. The provider-side middleware verifies the
payment through the facilitator (a read-only check), runs the resource
handler with the response buffered, then settles. Settlement executes the
burn-confirmation path; on success the buffered resource is released with a
PAYMENT-RESPONSEheader. If settlement fails, the buffered response is discarded and the client receives a 402 — nothing is served on a failed settle.
accepted is not finalized
Section titled “accepted is not finalized”A zERC20 payment settles in two phases, and the two words are never interchangeable:
| phase | when | meaning |
|---|---|---|
| accepted | synchronously, inside the HTTP request | the burn transfer to the one-time payTo address is confirmed on-chain (receipt success, indexed transfer, balance). The resource MAY be served: the funds are already burned and the payer cannot reclaim them |
| finalized | asynchronously, minutes later, never on the request path | the proof-gated mint to the real recipient lands on-chain |
Mapping onto x402: /settle success == accepted. The mint stays
out-of-band, driven by the facilitator’s finalize loop. A SettleResponse
therefore carries extra: { phase: "accepted" }, and its transaction field
is the burn transaction hash (the on-chain event a payer can audit) — not
the mint.
What this construction hides is the correspondence between payer and provider on-chain; amounts are public. The full model, its caveats, and its limits are in the product repository’s overview until this site’s concepts pages land.
Offers expire; failures fail closed
Section titled “Offers expire; failures fail closed”- Offers carry an absolute expiry. A client must obtain a fresh 402 rather than reuse an old offer, and every 402 carries a new burn address.
- Verification is point-in-time, not a settlement guarantee: a payer can move its balance between verify and settle, in which case settle fails after the handler ran but before anything is served (the middleware replaces the buffered response with a 402). The funds themselves are never at risk for the provider; ordinary rate limiting covers the wasted compute.
- On the gasless path, if the burn is still unmined when settlement answers,
the facilitator responds
{ success: false, errorReason: "pending_burn" }; re-sending the same payload is idempotent and safe (the burn is never double-executed).
Where to go next
Section titled “Where to go next”- Wire protocol reference — normative field-by-field format, EIP-712 definitions, facilitator HTTP API.
- Configuration reference — facilitator config keys and payer environment variables.
- TypeScript API and Python API — the published client/provider surfaces.
- Versions & compatibility — protocol scope, package versions, and license boundaries.