Accept payments
@openwallet/x402-xrpl asks any x402 v2 client, OpenWallet agents included, to pay in XRP or RLUSD, and releases the resource only when the XRP Ledger shows the payment.
Install
npm install @openwallet/x402-xrpl
Node 20 or later, Deno, Bun and Cloudflare Workers. No runtime dependencies apart from OpenWallet's core compiled to WebAssembly (decode, offline verify, hash). Public source.
The API
import { createX402Xrpl, sqliteStore } from "@openwallet/x402-xrpl";
const x402 = createX402Xrpl({
network: "xrpl:0",
payTo: "rMerchant…",
rpc: "https://s1.ripple.com:51234", // needed in every mode, for ledger truth
settle: { mode: "self" }, // the default: verify, simulate, submit, wait
store: sqliteStore("./x402.db"), // the atomic single-use gate
maxTimeoutSeconds: 60,
paymentFlow: "authorization", // or "upfront": settle before the handler runs
});
// Express-style middleware
app.get("/v1/report",
x402.http({ price: { asset: "RLUSD", value: "0.05" }, description: "Daily report" }),
handler);
// Fetch-API style (Workers, Hono, Deno)
const r = await x402.handle(request, { price, description });
// → { paid: false, response } | { paid: true, receipt, finish(response) }
// A paid MCP tool
x402.mcpPaidTool(server, "summarise",
{ price: { asset: "XRP", drops: "20000" }, inputSchema, description },
handler);
Prices are { asset: "XRP", drops: "10000" } or { asset: "RLUSD", value: "0.01" }. Other options: destinationTag, assetTransferMethods: ["sequence", "ticketSequence"], and redisStore(client) or memoryStore().
Self or facilitator
| Mode | What happens | When |
|---|---|---|
{ mode: "self" } (default) | Verify offline, simulate, submit, and poll the ledger. No third party. | Recommended. |
{ mode: "facilitator", url: "https://xrpl-facilitator-mainnet.t54.ai", sourceTag: 804681468 } | /verify then /settle, then the ledger check regardless of the answer. | If you want t54 to submit on Mainnet. |
{ mode: "facilitator", url: "https://x402.org/facilitator" } | The same, Testnet only. | Testing. |
At start, and every 10 minutes, the library checks that the ledger server's network_id matches network, that payTo exists (with an RLUSD trust line if you charge RLUSD), and that a facilitator's /supported lists your network. A Mainnet configuration pointing at a known test server refuses to start.
What it does per request
- No payment header: create an invoice (
inv_+ 128 random bits), store it, and answer 402 withPAYMENT-REQUIRED, the same JSON in the body, andCache-Control: no-store. - A payment: decode it strictly, look up the invoice by
accepted.extra.invoiceId, and compare every field with the stored requirement. The requirement is rebuilt from the store, never from the client. - Single use: compute the transaction hash, then claim both the hash and the invoice id atomically in the store, kept until the payment's expiry plus 60 s. A second claim gets 402
duplicate_settlement. The hash is stored before anything is submitted. - Settle, by itself or through the facilitator.
- Ledger truth, the only thing that releases the resource: the transaction is validated with
tesSUCCESS, is a Payment topayTowith the right tag andInvoiceID, is not a partial payment, anddelivered_amountequals the price exactly. - Return
PAYMENT-RESPONSE{ success: true, transaction, network, payer }, or a 402 withsuccess: false.
With payment-identifier advertised, the same id and payload return the cached response; the same id with a different payload gets 409.
Paid MCP tools
mcpPaidTool returns the spec's shape (isError, structuredContent = PaymentRequired with resource.url mcp://tool/<name>) and accepts _meta["x402/payment"]. Because many hosts cannot set _meta, it also adds an optional x402_payment string argument (base64 payload) to the tool's input schema. That argument is an OpenWallet convention, not part of x402. After a settlement failure it never returns the tool's output.
What you should know
- RLUSD you receive can be frozen or clawed back by Ripple.
- XRP payments are final.
- With
sequenceandauthorization, a payer who moves their sequence can make settlement fail after your handler ran. Useupfrontor tickets for expensive handlers.