Developers · x402
x402 on the wire, commerce on top.
EPP-402 changes nothing in x402 v2: the same three headers, the same exact scheme over EIP-3009, the same official packages. It adds four optional headers and one extension entry, and it decides how a 402 is priced.
Who does what
x402 moves money for one request. EPP-402 says what was bought, by whom, under which budget, and proves delivery.
x402 v2PAYMENT-REQUIRED, PAYMENT-SIGNATURE, PAYMENT-RESPONSE
Payment requirements, the EIP-3009 authorization, verification and settlement through a facilitator, the settle response. @x402/core, @x402/evm, @x402/hono, @x402/fetch, unmodified.EPP-402discovery, quote, session, intent, receipt
Signed objects around the payment, the epp402 extension entry, EPP-QUOTE, EPP-SESSION and EPP-INTENT on the request, EPP-RECEIPT on the response, the facilitator's /status endpoint and the optional EPP402Settlement relay.
A plain x402 client pays an EPP-402 provider
It never learns the difference. The 402 names an implicit list-price quote; the client echoes extensions unchanged; the receipt arrives in a header it may ignore.
import { x402Client } from "@x402/core/client";
import { registerExactEvmScheme } from "@x402/evm/exact/client";
import { wrapFetchWithPayment } from "@x402/fetch";
// A plain x402 v2 client. It knows nothing about EPP-402 and still pays an EPP-402 provider:
// the 402 names the implicit list-price quote, the client echoes extensions.epp402 unchanged.
const client = new x402Client();
registerExactEvmScheme(client, { signer, networks: ["eip155:99801"] });
const fetchWithPayment = wrapFetchWithPayment(fetch, client);
const res = await fetchWithPayment("https://inference.example.com/v1/inference", {
method: "POST",
body: JSON.stringify({ text: "hi" }),
});
// res.headers.get("PAYMENT-RESPONSE") → the settlement; res.headers.get("EPP-RECEIPT") → the signed receipt- 1
The client requests without paying
The provider mints an implicit quote at the service's list price (unless the service requires a quote, in which case it answers 428 quote_required with the quotes path) and answers 402. - 2
PAYMENT-REQUIRED is built from the quote
accepts[0] is the quote's x402 block: scheme exact, the network, the asset, the amount, payTo, maxTimeoutSeconds and the token's EIP-712 domain in extra. - 3
The client signs and retries
A standard TransferWithAuthorization. The provider verifies through the facilitator, runs the handler, settles, and signs a receipt. - 4
The response carries two headers
PAYMENT-RESPONSE from x402 and EPP-RECEIPT from EPP-402. A client that ignores the second has lost nothing it expected.
The headers EPP-402 adds
EPP-QUOTEclient → provider
The quote id the request pays for. The provider loads it, confirms it is for this service and unexpired, and builds the 402 from it.EPP-SESSIONclient → provider
The session id, recorded in the extension and the receipt.EPP-INTENTclient → provider
On the unpaid request, the intent id. On the paid request, the payer-signed Payment Intent as base64url JSON; see the rule below.EPP-RECEIPTprovider → client
The provider-signed Receipt as base64url JSON, on the successful paid response.
Header objects are base64url without padding of the object's JSON text (encodeHeaderJson). Receivers decode, parse and validate against the schema before using a field.
The epp402 extension
x402 reserves extensions for exactly this. The provider puts commerce references there; the client echoes them; the facilitator reads them.
{
"x402Version": 2,
"resource": {
"url": "https://inference.example.com/v1/inference",
"description": "Standard inference",
"mimeType": "application/json"
},
"accepts": [
{
"scheme": "exact",
"network": "eip155:99801",
"asset": "0x…EPPUSD",
"amount": "10000",
"payTo": "0xb1297Fea0b54A79BdBCfb8b057e39B226Cc2ebFc",
"maxTimeoutSeconds": 300,
"extra": {
"name": "EPP-402 Test USD",
"version": "1"
}
}
],
"extensions": {
"epp402": {
"info": {
"version": "1.0.0",
"providerId": "inference.example.com",
"quoteId": "qt_01k9r3w6zj8e2n0h4yv7c5b9ma"
}
}
}
}version"1.0.0" · required
providerIdProviderId · required
quoteIdId · optional
Always present for an EPP-402 provider, implicit quotes included.sessionIdId · optional
When the request carried EPP-SESSION.intentIdId · optional
When the request carried EPP-INTENT.
The facilitator attaches these ids to its settlement record and derives the relay's commerce reference, keccak256(abi.encode(quoteHash, intentHash, sessionHash)), from them.
The signed-intent rule
A bare id keeps plain clients compatible. A signed intent is verified before anything is fulfilled or settled.
{
"protocol": "epp-402",
"version": "1.0.0",
"intentId": "pi_01k9r3wq5v7r9cxm2d4h6j8k0n",
"sessionId": "ses_01k9r3v9p3b7qa2c6xd8e0f1gh",
"quoteId": "qt_01k9r3w6zj8e2n0h4yv7c5b9ma",
"payer": "0xB572b1950336e3c0b73B3E289929ceD2E4fda2b8",
"recipient": "0xb1297Fea0b54A79BdBCfb8b057e39B226Cc2ebFc",
"asset": "0x…EPPUSD",
"network": "eip155:99801",
"amount": "10000",
"validAfter": 0,
"validBefore": 1794100300,
"nonce": "0x…(the EIP-3009 nonce of the PAYMENT-SIGNATURE authorization)",
"scheme": "exact",
"status": "signed",
"createdAt": 1794100005,
"updatedAt": 1794100006,
"signature": "0x…"
}- 1
intent_mismatch (409)
if quoteId is not the quote the request pays for. - 2
intent_invalid_signature (400)
if the EIP-712 signature does not recover to payer. - 3
intent_expired (410)
if validBefore is not in the future. - 4
intent_mismatch (409)
if amount, recipient, asset or network differ from the quote. - 5
intent_mismatch (409)
if the EIP-3009 authorization in PAYMENT-SIGNATURE does not match the intent in from, to, value, validAfter, validBefore and nonce.
The registry applies the same signature check on POST /v1/intents and links the intent to the settlement with the same payer and nonce. The protocol page has the Payment Intent fields; the normative text is section 7 of the core specification.
What the facilitator adds
The hosted facilitator is a standard x402 facilitator for the exact scheme on one network, with four additions.
Replay guardonBeforeSettle
A (payer, nonce) pair that already produced a confirmed settlement is refused before any RPC call, and a nonce the token reports as used is refused with invalid_exact_evm_payload_authorization_nonce_already_used.RelayEPP402Settlement
When the relay is deployed and the asset is allowlisted, settlement goes through settleWithAuthorization, which emits Settled with the commerce reference, so a receipt can be checked against the chain without trusting the facilitator's database.PersistencePostgres
Every settlement attempt is recorded with its quote, intent and session ids; a retry after a receipt timeout reconciles instead of broadcasting twice.GET /status/:transactionIdfacilitator
The facilitator's record of a settlement, refreshed from the chain while pending.
Status codes a client will meet
payment_requiredHTTP 402
Also every x402 402, including settlement failures reported by the provider.quote_requiredHTTP 428
The service needs a quote first; details.quotesPath says where.quote_expired, intent_expired, session_expiredHTTP 410
Request a new quote; a new intent is signed with it.quote_mismatch, intent_mismatch, replay_detected, receipt_mismatchHTTP 409
Something named does not match something signed. Do not retry blindly.quote_invalid_signature, intent_invalid_signature, payment_invalidHTTP 400
A signature or payload did not verify.insufficient_funds, session_limit_exceededHTTP 402
The payer cannot cover the amount, or the session's budget would be exceeded.