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

ModeWhat happensWhen
{ 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

  1. No payment header: create an invoice (inv_ + 128 random bits), store it, and answer 402 with PAYMENT-REQUIRED, the same JSON in the body, and Cache-Control: no-store.
  2. 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.
  3. 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.
  4. Settle, by itself or through the facilitator.
  5. Ledger truth, the only thing that releases the resource: the transaction is validated with tesSUCCESS, is a Payment to payTo with the right tag and InvoiceID, is not a partial payment, and delivered_amount equals the price exactly.
  6. Return PAYMENT-RESPONSE { success: true, transaction, network, payer }, or a 402 with success: 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 sequence and authorization, a payer who moves their sequence can make settlement fail after your handler ran. Use upfront or tickets for expensive handlers.