x402 on the XRP Ledger

x402 turns HTTP's 402 Payment Required into a protocol: the server names a price, the client signs a payment and retries, the server settles it and serves the resource. On the XRP Ledger, the client signs a Payment it does not submit.

This page summarises x402 v2 and its XRPL exact scheme as pinned at x402-foundation/x402@6b6ee91fee02. That repository is canonical; coinbase/x402 is an older fork without the XRPL files. Where this page and the spec disagree, the spec wins.

The flow

x402 v2 payment flow on the XRP Ledger 1. The client requests the resource. 2. The merchant answers 402 with a PAYMENT-REQUIRED header listing what it accepts. 3. The client signs an XRP Ledger Payment without submitting it. 4. The client repeats the request with the signed blob in PAYMENT-SIGNATURE. 5 to 8. The merchant, directly or through a facilitator's verify and settle, submits it and waits for tesSUCCESS on a validated ledger. 9. The merchant reads the transaction on the ledger. 10. The merchant returns 200 with PAYMENT-RESPONSE and the resource. Client agent wallet Merchant API or MCP server Facilitator optional XRP Ledger 1. GET /report 2. 402 · PAYMENT-REQUIRED accepts: exact, xrpl:0, RLUSD 0.05 3. Sign an XRPL Payment not submitted; expires in ≤ 20 ledgers 4. GET /report · PAYMENT-SIGNATURE payload.signedTxBlob 5. /verify, then /settle 6. submit, wait for validated 7. tesSUCCESS 8. success, tx hash 9. read tx: delivered_amount, InvoiceID ledger truth 10. 200 · PAYMENT-RESPONSE the resource, and the receipt
x402 v2 over HTTP, with the XRPL exact scheme. The merchant may verify and submit itself instead of using a facilitator.

The three headers

HeaderDirectionCarries (standard base64 of UTF-8 JSON)
PAYMENT-REQUIREDServer → client, on 402{ x402Version: 2, error?, resource, accepts: [...], extensions? }
PAYMENT-SIGNATUREClient → server, on the retry{ x402Version: 2, resource?, accepted: <the chosen entry, unchanged>, payload: { signedTxBlob, invoiceId? }, extensions? }
PAYMENT-RESPONSEServer → client, with the result{ success, transaction, network, payer?, errorReason?, amount?, extensions? }

A failed payment is a 402 with PAYMENT-RESPONSE success: false; a malformed one is a 400. errorReason is an open string, not a closed list. XRPL facilitators do not send settlement_pending; they send transaction_failed: <code> with the hash.

x402 v1 (X-PAYMENT) has no XRPL encoding. OpenWallet refuses a v1 challenge with x402_v1_unsupported.

A requirement (accepts[] entry)

{
  "scheme": "exact",
  "network": "xrpl:0",
  "asset": "524C555344000000000000000000000000000000",
  "amount": "0.05",
  "payTo": "rMerchant…",
  "maxTimeoutSeconds": 60,
  "extra": {
    "areFeesSponsored": false,
    "issuer": "rMxCKbEDwqr76QuheSUMdEGf4B9xJ8m5De",
    "invoiceId": "inv_…",
    "destinationTag": 12345
  }
}
FieldRule
networkxrpl:{NetworkID}: xrpl:0 Mainnet, xrpl:1 Testnet, xrpl:2 Devnet (CAIP-2).
asset"XRP", a 3-character currency code, or 40 hex characters.
amountXRP: an integer string of drops (1 XRP = 1,000,000). Tokens: the exact value string, such as "0.05", not atomic units. There is no decimals.
extra.areFeesSponsoredRequired and false: the payer pays the XRPL fee.
extra.issuerRequired for tokens.
extra.invoiceIdOptional. Becomes the transaction's InvoiceID (below).
extra.destinationTagOptional uint32. Becomes DestinationTag.
extra.assetTransferMethodsequence (default) or ticketSequence.
extra.paymentFlowauthorization (default: settle after the handler runs) or upfront (settle first). A client must not pay a flow it does not recognise; OpenWallet skips escrow.

The signed Payment

  • TransactionType Payment, Destination = payTo, DestinationTag when given.
  • XRP: Amount is the drops string. No SendMax.
  • Tokens: Amount is {currency, issuer, value}, equal by exact decimal comparison, and a SendMax in the same currency and issuer of at least the amount (RLUSD has no transfer fee, so they are equal).
  • Never Paths, DeliverMin, Memos or Delegate, and never the partial-payment flag.
  • InvoiceID = upper-case hex SHA-256 of the UTF-8 invoiceId. Memos must not be used to bind a payment.
  • LastLedgerSequence above the current validated ledger and at most validated + ceil(maxTimeoutSeconds / 5) + 2. OpenWallet caps it lower, at 20 ledgers (about 80 s).
  • NetworkID is omitted for Mainnet, Testnet and Devnet (ids up to 1024).
  • One signature, by the master key or the account's RegularKey. The fee is the payer's; the reference facilitator caps it at 10,000 drops.

Sequence or tickets

With sequence, the payment uses the account's next sequence number, so the payer can have one payment pending at a time, and a payer who moves the sequence can make a payment fail after the merchant's handler ran. ticketSequence uses a ticket reserved in advance (sequence 0), which allows several pending payments and long handlers. OpenWallet uses tickets only when the grant allows them.

Separate accounts per network

Standard XRPL networks omit NetworkID from transactions, so a blob signed for Testnet is valid on Mainnet if the same account and sequence exist there. The spec says wallets should use separate accounts per network. OpenWallet derives Mainnet and test accounts on different paths, so they never share a key.

Settlement and facilitators

A facilitator exposes /verify, /settle and /supported. It re-verifies, refuses a hash it has seen, submits only to the named network, waits for validated, and counts only tesSUCCESS as success. The spec requires it to deduplicate on the transaction hash, atomically, until the payment's LastLedgerSequence passes.

FacilitatorNetworksNotes
x402.orgxrpl:1 (Testnet only)Follows the spec. Ignores an extra payload.invoiceId.
t54xrpl:0 and xrpl:1Needs payload.invoiceId and a SourceTag (asked for with extra.sourceTag). Omits areFeesSponsored in its 402s. Accepts Memos. Only its Testnet behaviour has been probed by us.

In one Testnet test on 30 September 2026, both public facilitators accepted a duplicate settlement of the same blob. That is a single sample, but it is why OpenWallet's merchant library verifies and submits by itself by default, and why it always reads the ledger before releasing a resource: a facilitator is at most a relay, and the ledger decides.

x402 over MCP

  • Payment required: the tool result has isError: true, structuredContent = the PaymentRequired object and content[0].text = its JSON. resource.url is conventionally mcp://tool/<name>.
  • Payment: the client retries the same tools/call with params._meta["x402/payment"] = the payload object (not base64).
  • Result: _meta["x402/payment-response"]. If settlement fails after the tool ran, the server returns only the payment error, never the tool's output.

Most MCP clients do not let a model set _meta, which is why openwallet_call_paid_mcp_tool makes the remote call itself. As a client, OpenWallet also parses shapes seen in the wild (_meta["x402/error"], JSON-RPC errors with code 402, or -32042 only when data carries x402Version and accepts).

Why upto is not here

An upto scheme (pay up to a maximum, settle the actual amount) is not standardised on the XRP Ledger. A draft based on payment channels is open and unmerged, and one facilitator's Testnet upto uses Checks and is its own design. OpenWallet does not pay upto challenges and refuses them with unsupported_scheme. The same goes for batch settlement.