intermediate 45 min

Pathfinding & Cross-Currency Payments

Route payments across multiple currencies using the XRPL DEX. Learn pathfinding mechanics, prerequisites (trustlines, liquidity), and build cross-currency payment transactions.

Prerequisites

Complete these before starting this module:

What you'll learn

  • Explain how XRPL pathfinding routes a payment across order books and intermediate currencies.
  • Use the path_find API to discover routes between two currencies and read paths_computed.
  • Build a cross-currency Payment with Amount, SendMax, and Paths to deliver one currency while spending another.
  • Set up the trustlines and liquidity prerequisites a cross-currency payment depends on.
  • Protect a payment with SendMax and reason about partial payments and slippage.
Complete this module by peer review. Jump to assessment

Overview

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.

How pathfinding works

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:

  • Direct offers — an order book that trades the source currency directly for the destination currency.
  • Multi-hop paths — when no direct book exists, the engine chains books together through an intermediate currency. USD to XRP to EUR is a classic two-hop route.

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.

Prerequisites: what a path needs to exist

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:

  • Active accounts. Both sender and receiver must be activated on the ledger and hold enough XRP for fees and reserves.
  • Trustlines. Any non-XRP currency on the XRPL is an issued token — a balance you hold toward an issuer. To hold or receive USD from some issuer, the account needs a trustline to that issuer for that currency. The receiver must trust the issuer of the currency they're being paid in; the sender must hold (via trustlines) the currency they're spending. XRP needs no trustline — it's the one asset everyone can hold natively.
  • Liquidity. There has to be a market. Someone must have posted offers on the DEX connecting your source and destination currencies, directly or through a hop. On mainnet these markets exist for popular pairs. On testnet they generally don't unless you (or a bot) create them — so to practice the full flow on testnet you'll often issue your own tokens and place your own offers first.

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.

Discovering a route with path_find

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.

Building the cross-currency payment

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.

Partial payments and slippage

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.

Putting it together

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.

Check

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