Accept x402 USDC payments on XRP Ledger
Introduction
Section titled “Introduction”The XRP Ledger (XRPL) is a non-EVM family that’s payment-native: 3 to 5s single-step finality, no
reorgs, with a built-in Memo and DestinationTag the protocol uses for proof binding. Name it with
chain: 'xrpl' and the driver auto-mounts on first use, so a pure-EVM or Solana install
never downloads the XRPL library. The protocol layer is unchanged; only the wallet shape and the
receive prerequisite differ.
import { requirePayment } from '@piprail/sdk'
requirePayment({ chain: 'xrpl', token: 'USDC', amount: '0.10', payTo: 'r…' })Install the peer dependency
Section titled “Install the peer dependency”The XRPL library is an optional peer dep. Install it once and the lazy import finds it:
npm install xrplThe wallet
Section titled “The wallet”An XRPL wallet is { key }, where key is an s… family secret seed, or a ready { wallet }
(an xrpl.js Wallet, if you built it yourself). payTo is a classic r… address.
import { PipRailClient } from '@piprail/sdk'
const client = new PipRailClient({ chain: 'xrpl', wallet: { key: process.env.XRPL_SEED }, // 's…' secret seed})The shape is checked synchronously at bind time, so passing an EVM, Solana, TON, or Stellar
wallet fails fast with a WrongFamilyError. See Wallets by
family for the full set of shapes, and
PipRailClient for the client surface.
Tokens
Section titled “Tokens”Name the symbol; the SDK fills in the issuer and currency code. Both built-in issuers were
verified live on mainnet before shipping (the on-ledger Domain and currency code read off
gateway_balances).
| Token | Built in | Notes |
|---|---|---|
'USDC' | Yes | Circle USDC. Issuer Domain https://circle.com verified on mainnet. |
'RLUSD' | Yes | Ripple’s RLUSD. Issuer Domain https://ripple.com/ verified on mainnet. |
'native' | Yes | XRP, 6 decimals (drops: 1 drop = 1e-6 XRP). |
| custom IOU | n/a | Any other issued currency via { issuer, currencyHex, decimals }. |
import { requirePayment } from '@piprail/sdk'
// A custom issued currency is { issuer, currencyHex, decimals }:requirePayment({ chain: 'xrpl', // 160-bit hex for a 4+ char code, e.g. 'MYTKN': token: { issuer: 'r…', currencyHex: '4D59544B4E000000000000000000000000000000', decimals: 6 }, amount: '0.10', payTo: 'r…',})Receive prerequisite: activation + a trustline
Section titled “Receive prerequisite: activation + a trustline”To receive an issued asset (USDC/RLUSD), the merchant (payTo) must (1) be an activated
account, holding the ~1 XRP base reserve to exist, and (2) hold a trustline (TrustSet) to
that issuer’s currency before its first IOU payment. Native XRP needs neither. The payer must
be activated and trustlined too.
When the recipient isn’t ready, PipRail raises RecipientNotReadyError
as a chain requirement rather than an SDK bug, echoing the raw XRPL engine code and the fix:
| Raw XRPL code | What it means | Fix |
|---|---|---|
tecNO_DST_INSUF_XRP / tecNO_DST | payTo isn’t activated (needs ≥1 XRP base reserve to exist) | send the recipient ≥1 XRP to activate it |
tecNO_LINE / tecPATH_DRY | recipient has no trustline for the IOU | add the trustline on the recipient |
tecDST_TAG_NEEDED | recipient requires a DestinationTag (PipRail sets one automatically) | none |
Reserves are locked, not spent (~1 XRP base + an owner reserve per trustline), so they are recoverable.
Check readiness before you spend with planPayment(): it
surfaces a not-ready recipient as a RECIPIENT_NOT_READY blocker rather than throwing, so an
agent branches on data instead of broadcasting into a stranded payment:
const url = 'https://api.example.com/report'const plan = await client.planPayment(url)// → PaymentPlan | null (null when the URL isn't 402-gated)
if (!plan) { await client.fetch(url) // not payment-gated, just fetch it} else if (plan.payable) { await client.fetch(url) // safe, we checked} else { console.log(plan.fundingHint) // e.g. "recipient has no trustline for USDC"}See planPayment() for the full readiness model and the
RECIPIENT_NOT_READY blocker.
Proof binding: Template A (memo-bound)
Section titled “Proof binding: Template A (memo-bound)”XRPL uses Template A: the challenge nonce rides in a Memo, which is
the cryptographic binding, plus a derived DestinationTag for deliverability (some accounts, like
the RLUSD issuer, set requireDestinationTag). verify() re-derives every checked field from
the trusted accept, never the client-supplied ref, so a forged echo can’t redirect it.
Server-side only
Section titled “Server-side only”Endpoints
Section titled “Endpoints”The default RPC is a public community cluster (https://xrplcluster.com/), rate-limited and not
for sustained business use. Pass your own rpcUrl in production:
import { requirePayment, PipRailClient } from '@piprail/sdk'
requirePayment({ chain: 'xrpl', token: 'USDC', amount: '0.10', payTo: 'r…', rpcUrl: 'https://your-xrpl-node/' })
const client = new PipRailClient({ chain: 'xrpl', wallet: { key: process.env.XRPL_SEED }, rpcUrl: 'https://your-xrpl-node/',})Swapping on the XRP Ledger
Section titled “Swapping on the XRP Ledger”Holding the wrong token? quoteSwap() prices a same-chain swap read-only and swap() is the
only call that moves anything. It is opt-in and never automatic: paying never swaps and
planning never swaps.
The ledger itself swaps, through the XRPL DEX + AMM, so there is no third party involved at all: no router to approve, no API key, and no integrator fee that could even be expressed.
2 real mainnet swaps back this route, and the transaction hashes are published so you can read them back off the chain yourself.
const quote = await client.quoteSwap({ from: 'native', to: 'USDC', wantAmount: '0.50' })if (quote) await client.swap(quote) // null means no route, never "no funds"Full guide: Swapping tokens. Every route, indexed by chain as well as by venue, is at piprail.com/swaps.