Route payments across multiple currencies using the XRPL DEX. Learn pathfinding mechanics, prerequisites (trustlines, liquidity), and build cross-currency payment transactions.
What you'll learn
So far your payments have moved one currency from A to B: send XRP, receive XRP. But the XRP Ledger can do something more interesting in a single transaction — let the sender spend one currency while the receiver gets a different one, converting automatically along the way. You pay in USD, your friend receives EUR, and the ledger finds the cheapest route through its built-in exchange to make that happen.
That route-finding is pathfinding, and the converting happens on the XRPL's native decentralized exchange (DEX) — the order books and offers that anyone can post against. In this module you'll discover a route with the path_find API, read the path it computes, then submit a cross-currency Payment that delivers a different asset than it spends. By the end you'll understand why some payments "just work," why others fail with no path, and how to protect yourself from paying too much.
Think of it like booking a flight. You want to get from one city to another; the booking engine searches direct flights first, then connections through a hub if no direct route exists, and shows you the cheapest itinerary. Pathfinding is that engine for value: a direct order book if one exists, or a hop through an intermediate currency (often XRP, the ledger's natural bridge) when it doesn't.
A path is a sequence of steps that carries value from the sender's currency to the receiver's. The ledger continuously scans two sources of liquidity:
The engine evaluates the cost of each candidate and prefers the one that delivers your destination amount for the least source spend. Pathfinding is a suggestion, not a guarantee: liquidity moves between the moment you query and the moment you submit, so the path you found can shift or evaporate. That's why the transaction itself carries its own safety rails, which we'll get to.
Cross-currency payments fail far more often from missing setup than from buggy code. Before a single path can be found, three things must be true:
If any of these is missing, path_find returns no usable path and the payment fails with tecPATH_DRY or tecPATH_PARTIAL. When that happens, suspect setup before code.
path_find is a subscription-style request: you open it with the create subcommand, the server streams back the best route it can compute, and you close it when done. The key field in the response is paths_computed — an array of paths you'll hand straight to your payment.
Connect to the ledger as usual, then ask for a route. Here we want to deliver a specific amount of USD from a given issuer, spending XRP as the source:
import * as xrpl from 'xrpl'
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233')
async function findPath(
source: string,
destination: string,
destAmount: xrpl.IssuedCurrencyAmount,
) {
const response = await client.request({
command: 'path_find',
subcommand: 'create',
source_account: source,
destination_account: destination,
destination_amount: destAmount,
})
const paths = response.result.alternatives ?? []
console.log(`Found ${paths.length} route(s)`)
// Each alternative carries what it costs (source_amount)
// and the path itself (paths_computed).
return paths
}
Each entry in alternatives describes one viable route: source_amount tells you how much of the source currency that route would cost, and paths_computed is the step-by-step route. You pick the cheapest alternative — that's the engine's recommendation. When you're done listening, close the request with the close subcommand so you're not holding an open subscription.
Read the cost before you spend. If source_amount is wildly higher than you expect, the market is thin and you're about to pay heavy slippage — better to find out here than after submitting.
A cross-currency Payment looks like the XRP payment you already know, with two additions that do all the work:
Amount — what the destination receives. For an issued currency this is an object: { currency, issuer, value }. This is the amount the receiver is guaranteed to get.SendMax — the most the source is willing to spend to deliver that Amount. This is your slippage guardrail. The ledger will never spend more than SendMax; if delivering the full Amount would cost more, the payment fails rather than overcharging you.Paths — the paths_computed array from the alternative you chose, telling the ledger which route to take.Here's a payment that delivers USD to the receiver while the sender spends at most a capped amount of XRP, using the path we discovered:
async function sendCrossCurrency(
sender: xrpl.Wallet,
destination: string,
deliver: xrpl.IssuedCurrencyAmount, // what they receive, e.g. USD
sendMax: string, // cap in drops of XRP we'll spend
paths: unknown[], // paths_computed from path_find
) {
const tx: xrpl.Payment = {
TransactionType: 'Payment',
Account: sender.classicAddress,
Destination: destination,
Amount: deliver,
SendMax: sendMax,
Paths: paths as xrpl.Path[],
}
const result = await client.submitAndWait(tx, {
autofill: true,
wallet: sender,
})
const code = result.result.meta?.TransactionResult
if (code !== 'tesSUCCESS') {
throw new Error(`Cross-currency payment failed: ${code}`)
}
console.log('Delivered:', result.result.hash)
return result.result.hash
}
As with any payment, tesSUCCESS in the metadata is the only proof it landed. The codes you'll meet here are specific to pathfinding: tecPATH_DRY means the route had no liquidity by the time you submitted, and tecPATH_PARTIAL means the path could only deliver part of the Amount within SendMax.
That last code points at a deliberate design choice. By default a cross-currency Payment is all-or-nothing: either the receiver gets the exact Amount or the transaction fails. That protects the receiver — they can count on the number in Amount.
The optional partial payment flag (tfPartialPayment) flips that contract: it tells the ledger "deliver as much as you can with the SendMax I gave you, even if it's less than Amount." This is occasionally useful — for example, returning funds through a path where you want the fee to come out of the delivered amount rather than fail. But it's also a notorious footgun: a naive service that reads Amount instead of the metadata's delivered_amount can be tricked into crediting a full payment when only a sliver actually arrived. Rule of thumb: leave partial payments off unless you have a specific reason, and always trust delivered_amount from the transaction metadata over the requested Amount.
The deeper lesson is that cross-currency amounts are estimates until they're validated. Liquidity shifts, rates move, and SendMax is the only number you fully control. Set it deliberately — tight enough to cap your loss, loose enough to absorb normal market movement between discovery and submission.
A complete run reads like a short story: connect, confirm the receiver trusts the currency you're delivering, call path_find to discover a route and inspect its cost, choose the cheapest alternative, build a Payment with Amount / SendMax / Paths, submit, and verify tesSUCCESS plus the delivered_amount. On testnet you'll first need to manufacture the market — issue a token and post an offer — so a path can exist at all. That setup is the single most common thing standing between you and a working cross-currency payment.
You're done when you have a script that, in one run, discovers a route between two currencies with path_find and logs the available alternatives and their source_amount costs, then submits a cross-currency Payment that delivers a different currency than it spends — using Amount, SendMax, and the discovered Paths — and confirms the engine result is tesSUCCESS. Read delivered_amount from the metadata to prove the receiver got what you intended, and confirm the validated hash on the Testnet Explorer. Submit a repo or gist containing your cross-currency.ts (and the broadcast tx hash) for a peer to verify.
Assignments
0 of 3 complete