Skip to content

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.

Division of responsibility
2
  • x402 v2

    PAYMENT-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-402

    discovery, 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.

plain-x402.tsRepository API
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. 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. 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. 3

    The client signs and retries

    A standard TransferWithAuthorization. The provider verifies through the facilitator, runs the handler, settles, and signs a receipt.
  4. 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

Headers
4
  • EPP-QUOTE

    client → 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-SESSION

    client → provider

    The session id, recorded in the extension and the receipt.
  • EPP-INTENT

    client → 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-RECEIPT

    provider → 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.

PAYMENT-REQUIRED, decodedExample values
{
  "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"
      }
    }
  }
}
extensions.epp402.info
5
  • version

    "1.0.0" · required

  • providerId

    ProviderId · required

  • quoteId

    Id · optional

    Always present for an EPP-402 provider, implicit quotes included.
  • sessionId

    Id · optional

    When the request carried EPP-SESSION.
  • intentId

    Id · 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.

EPP-INTENT, decodedExample values
{
  "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. 1

    intent_mismatch (409)

    if quoteId is not the quote the request pays for.
  2. 2

    intent_invalid_signature (400)

    if the EIP-712 signature does not recover to payer.
  3. 3

    intent_expired (410)

    if validBefore is not in the future.
  4. 4

    intent_mismatch (409)

    if amount, recipient, asset or network differ from the quote.
  5. 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.

Additions
4
  • Replay guard

    onBeforeSettle

    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.
  • Relay

    EPP402Settlement

    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.
  • Persistence

    Postgres

    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/:transactionId

    facilitator

    The facilitator's record of a settlement, refreshed from the chain while pending.

Status codes a client will meet

Codes
6
  • payment_required

    HTTP 402

    Also every x402 402, including settlement failures reported by the provider.
  • quote_required

    HTTP 428

    The service needs a quote first; details.quotesPath says where.
  • quote_expired, intent_expired, session_expired

    HTTP 410

    Request a new quote; a new intent is signed with it.
  • quote_mismatch, intent_mismatch, replay_detected, receipt_mismatch

    HTTP 409

    Something named does not match something signed. Do not retry blindly.
  • quote_invalid_signature, intent_invalid_signature, payment_invalid

    HTTP 400

    A signature or payload did not verify.
  • insufficient_funds, session_limit_exceeded

    HTTP 402

    The payer cannot cover the amount, or the session's budget would be exceeded.