Skip to content

Accept x402 USDC payments on XRP Ledger

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…' })

The XRPL library is an optional peer dep. Install it once and the lazy import finds it:

Terminal window
npm install xrpl

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.

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).

TokenBuilt inNotes
'USDC'YesCircle USDC. Issuer Domain https://circle.com verified on mainnet.
'RLUSD'YesRipple’s RLUSD. Issuer Domain https://ripple.com/ verified on mainnet.
'native'YesXRP, 6 decimals (drops: 1 drop = 1e-6 XRP).
custom IOUn/aAny 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 codeWhat it meansFix
tecNO_DST_INSUF_XRP / tecNO_DSTpayTo isn’t activated (needs ≥1 XRP base reserve to exist)send the recipient ≥1 XRP to activate it
tecNO_LINE / tecPATH_DRYrecipient has no trustline for the IOUadd the trustline on the recipient
tecDST_TAG_NEEDEDrecipient 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.

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.

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/',
})

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.