DEX Integration - Order Books and Trading
Learning Objectives
Query order books using book_offers and interpret the response structure
Understand the taker_gets/taker_pays model and order book orientation
Create offers with appropriate flags (passive, immediate-or-cancel, fill-or-kill)
Calculate liquidity depth and effective prices for different trade sizes
Build market data aggregation for trading applications
Most blockchains require external exchanges or DEX protocols for trading. XRPL has exchange functionality built into the ledger itself:
XRPL DEX FEATURES:
- Orders stored in ledger
- Atomic matching
- No smart contracts needed
- XRP ↔ Any issued currency
- Issued currency ↔ Issued currency
- Auto-bridging through XRP
- Anyone can create offers
- No listing requirements
- No intermediary
- OfferCreate to place orders
- OfferCancel to remove
- Payments can cross offers
---
XRPL order books are described from the "taker's" perspective:
ORDER BOOK TERMINOLOGY:
taker_gets = What the taker receives (what offers are selling)
taker_pays = What the taker pays (what offers are buying)
- taker_gets: XRP (taker receives XRP)
- taker_pays: USD (taker pays USD)
This is the "buy XRP" side of the market.
Every trading pair has two order book sides:
XRP/USD TRADING PAIR:
BUY SIDE (Buy XRP, Sell USD):
taker_gets: XRP
taker_pays: USD
Offers: "I'll give you X XRP for Y USD"
SELL SIDE (Sell XRP, Buy USD):
taker_gets: USD
taker_pays: XRP
Offers: "I'll give you Y USD for X XRP"
// Query BUY side (taker receives XRP, pays USD)
const buyOrders = await client.request({
command: 'book_offers',
taker_gets: { currency: 'XRP' },
taker_pays: {
currency: 'USD',
issuer: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B' // Bitstamp
},
limit: 100
})
// Query SELL side (taker receives USD, pays XRP)
const sellOrders = await client.request({
command: 'book_offers',
taker_gets: {
currency: 'USD',
issuer: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B'
},
taker_pays: { currency: 'XRP' },
limit: 100
})
```
{
"result": {
"ledger_current_index": 87654321,
"offers": [
{
"Account": "rOfferCreator...",
"BookDirectory": "ABC...",
"BookNode": "0",
"Flags": 0,
"LedgerEntryType": "Offer",
"OwnerNode": "0",
"PreviousTxnID": "DEF...",
"PreviousTxnLgrSeq": 87654300,
"Sequence": 42,
"TakerGets": "100000000000", // 100,000 XRP in drops
"TakerPays": {
"currency": "USD",
"issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B",
"value": "50000"
},
"index": "GHI...",
"owner_funds": "150000000000", // Creator has 150,000 XRP
"quality": "0.0000005"
},
// ... more offers
],
"status": "success"
}
}FIELD MEANING
────────────────────────────────────────────────────────
TakerGets What you receive if you take this offer
TakerPays What you pay if you take this offer
Account Who created the offer
Sequence Offer ID (Account + Sequence uniquely identifies)
quality Price: TakerPays/TakerGets (lower is better for taker)
owner_funds How much the creator actually has available
Flags Offer flags (passive, sell, etc.)The quality field represents the exchange rate from the taker's perspective:
// For XRP (taker_gets) / USD (taker_pays) order book:
// quality = TakerPays / TakerGets
// quality = USD / XRP = price in USD per XRP
// Example offer:
// TakerGets: 100,000 XRP
// TakerPays: 50,000 USD
// quality: 0.5 (0.50 USD per XRP)
function getPrice(offer, takerGetsIsXRP) {
const gets = parseAmount(offer.TakerGets)
const pays = parseAmount(offer.TakerPays)
const price = pays / gets
if (takerGetsIsXRP) {
// Price is in quote currency per XRP
return price
} else {
// Price is XRP per unit of quote currency
return 1 / price
}
}
function parseAmount(amount) {
if (typeof amount === 'string') {
return parseInt(amount) / 1_000_000 // XRP drops to XRP
}
return parseFloat(amount.value) // Issued currency
}
The owner_funds field reveals how much the offer creator actually has:
function getEffectiveOffer(offer) {
const stated = parseAmount(offer.TakerGets)
const available = parseAmount(offer.owner_funds)
// Offer can only be filled up to creator's available funds
const effective = Math.min(stated, available)
// Scale TakerPays proportionally if limited by funds
const paysPer = parseAmount(offer.TakerPays) / stated
const effectivePays = effective * paysPer
return {
takerGets: effective,
takerPays: effectivePays,
isPartial: effective < stated,
fillRatio: effective / stated
}
}
class OrderBook {
constructor(client, base, quote) {
this.client = client
this.base = base // e.g., { currency: 'XRP' }
this.quote = quote // e.g., { currency: 'USD', issuer: '...' }
}
async fetchBothSides() {
const [bids, asks] = await Promise.all([
this.fetchSide('bids'),
this.fetchSide('asks')
])
return {
bids: this.processOffers(bids, 'bid'),
asks: this.processOffers(asks, 'ask'),
timestamp: Date.now()
}
}
async fetchSide(side) {
// Bids: people buying base (paying quote, getting base)
// Asks: people selling base (paying base, getting quote)
const request = {
command: 'book_offers',
limit: 100
}
if (side === 'bids') {
// Taker sells base, gets quote (takes bids)
request.taker_gets = this.quote
request.taker_pays = this.base
} else {
// Taker buys base, pays quote (takes asks)
request.taker_gets = this.base
request.taker_pays = this.quote
}
const response = await this.client.request(request)
return response.result.offers
}
processOffers(offers, side) {
return offers.map(offer => {
const effective = getEffectiveOffer(offer)
// Calculate price based on side
let price
if (side === 'bid') {
// Bid price: quote per base
price = effective.takerGets / effective.takerPays
} else {
// Ask price: quote per base
price = effective.takerPays / effective.takerGets
}
return {
price: price,
amount: side === 'bid' ? effective.takerPays : effective.takerGets,
total: side === 'bid' ? effective.takerGets : effective.takerPays,
account: offer.Account,
sequence: offer.Sequence,
isPartial: effective.isPartial
}
}).sort((a, b) => {
// Bids: highest price first
// Asks: lowest price first
return side === 'bid' ? b.price - a.price : a.price - b.price
})
}
}
// Usage
const orderBook = new OrderBook(client,
{ currency: 'XRP' },
{ currency: 'USD', issuer: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B' }
)
const { bids, asks } = await orderBook.fetchBothSides()
console.log('Best Bid:', bids[0]?.price)
console.log('Best Ask:', asks[0]?.price)
console.log('Spread:', asks[0]?.price - bids[0]?.price)
```
function calculateLiquidityDepth(offers, maxSpend) {
let totalGets = 0
let totalPays = 0
const levels = []
for (const offer of offers) {
const effective = getEffectiveOffer(offer)
const price = effective.takerPays / effective.takerGets
// How much can we take from this offer?
const remainingBudget = maxSpend - totalPays
if (remainingBudget <= 0) break
const canTake = Math.min(effective.takerGets, remainingBudget / price)
const willPay = canTake * price
totalGets += canTake
totalPays += willPay
levels.push({
price: price,
cumulative: totalGets,
spent: totalPays,
avgPrice: totalPays / totalGets
})
}
return {
totalGets,
totalPays,
avgPrice: totalGets > 0 ? totalPays / totalGets : null,
levels
}
}
// How much XRP can I get for 1000 USD?
const depth = calculateLiquidityDepth(asks, 1000)
console.log(1000 USD buys ${depth.totalGets} XRP at avg price ${depth.avgPrice})
```
const offer = {
TransactionType: 'OfferCreate',
Account: wallet.address,
TakerGets: xrpl.xrpToDrops('100'), // Selling 100 XRP
TakerPays: {
currency: 'USD',
issuer: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B',
value: '50' // For 50 USD
}
}
const result = await client.submitAndWait(offer, { wallet })
```
const OfferFlags = {
tfPassive: 0x00010000, // Don't cross existing offers
tfImmediateOrCancel: 0x00020000, // Fill immediately or cancel
tfFillOrKill: 0x00040000, // Fill completely or cancel
tfSell: 0x00080000 // Sell exact TakerGets amount
}
// Passive offer (market maker style - add liquidity, don't take)
const passiveOffer = {
TransactionType: 'OfferCreate',
Account: wallet.address,
TakerGets: xrpl.xrpToDrops('100'),
TakerPays: { currency: 'USD', issuer: '...', value: '50' },
Flags: OfferFlags.tfPassive
}
// Immediate-or-cancel (market order style)
const iocOffer = {
TransactionType: 'OfferCreate',
Account: wallet.address,
TakerGets: xrpl.xrpToDrops('100'),
TakerPays: { currency: 'USD', issuer: '...', value: '50' },
Flags: OfferFlags.tfImmediateOrCancel
}
// Fill-or-kill (all or nothing)
const fokOffer = {
TransactionType: 'OfferCreate',
Account: wallet.address,
TakerGets: xrpl.xrpToDrops('100'),
TakerPays: { currency: 'USD', issuer: '...', value: '50' },
Flags: OfferFlags.tfFillOrKill
}
```
When you create an offer, it can:
- Execute immediately - Crosses existing offers on the book
- Partially execute - Takes some liquidity, remainder goes on book
- Go to book - No immediate execution, waits for takers
- Fail - Fill-or-kill without enough liquidity
function interpretOfferResult(result) {
const meta = result.result.meta
// Check if offer was created (remains on book)
const createdOffer = meta.AffectedNodes.find(
node => node.CreatedNode?.LedgerEntryType === 'Offer'
)
// Check what was traded
const balanceChanges = extractBalanceChanges(meta)
return {
success: meta.TransactionResult === 'tesSUCCESS',
executed: balanceChanges.length > 0,
onBook: createdOffer !== undefined,
offerId: createdOffer?.CreatedNode?.LedgerIndex,
traded: balanceChanges
}
}
function extractBalanceChanges(meta) {
const changes = []
for (const node of meta.AffectedNodes) {
const modified = node.ModifiedNode
if (modified?.LedgerEntryType === 'AccountRoot') {
const prev = parseInt(modified.PreviousFields?.Balance || '0')
const final = parseInt(modified.FinalFields?.Balance || '0')
if (prev !== final) {
changes.push({
account: modified.FinalFields.Account,
change: (final - prev) / 1_000_000,
currency: 'XRP'
})
}
}
// Similar for RippleState (trust lines) for issued currencies
}
return changes
}
async function getMyOffers(client, account) {
const response = await client.request({
command: 'account_offers',
account: account,
ledger_index: 'validated'
})
return response.result.offers.map(offer => ({
sequence: offer.seq,
takerGets: offer.taker_gets,
takerPays: offer.taker_pays,
flags: offer.flags,
expiration: offer.expiration
}))
}
```
// Cancel by sequence number
const cancelTx = {
TransactionType: 'OfferCancel',
Account: wallet.address,
OfferSequence: 42 // Sequence of offer to cancel
}
await client.submitAndWait(cancelTx, { wallet })
// Cancel multiple offers
async function cancelAllOffers(client, wallet) {
const offers = await getMyOffers(client, wallet.address)
for (const offer of offers) {
const cancelTx = {
TransactionType: 'OfferCancel',
Account: wallet.address,
OfferSequence: offer.sequence
}
await client.submitAndWait(cancelTx, { wallet })
await sleep(100) // Rate limit
}
}
```
Create a new offer with the same OfferSequence to replace:
const replaceOffer = {
TransactionType: 'OfferCreate',
Account: wallet.address,
TakerGets: xrpl.xrpToDrops('100'),
TakerPays: { currency: 'USD', issuer: '...', value: '55' },
OfferSequence: 42 // Cancels offer 42, creates new one
}class MarketDataService {
constructor(client) {
this.client = client
this.orderBooks = new Map()
this.updateCallbacks = []
}
async subscribe(base, quote) {
const key = this.getPairKey(base, quote)
// Subscribe to order book updates
await this.client.request({
command: 'subscribe',
books: [
{ taker_gets: base, taker_pays: quote, both: true }
]
})
// Initial fetch
const orderBook = new OrderBook(this.client, base, quote)
const data = await orderBook.fetchBothSides()
this.orderBooks.set(key, data)
return data
}
getPairKey(base, quote) {
const baseKey = base.currency + (base.issuer || '')
const quoteKey = quote.currency + (quote.issuer || '')
return ${baseKey}/${quoteKey}
}
getOrderBook(base, quote) {
return this.orderBooks.get(this.getPairKey(base, quote))
}
getTicker(base, quote) {
const book = this.getOrderBook(base, quote)
if (!book) return null
const bestBid = book.bids[0]
const bestAsk = book.asks[0]
return {
bid: bestBid?.price || null,
ask: bestAsk?.price || null,
spread: bestAsk && bestBid ? bestAsk.price - bestBid.price : null,
spreadPercent: bestAsk && bestBid
? ((bestAsk.price - bestBid.price) / bestBid.price) * 100
: null,
bidDepth: book.bids.reduce((sum, o) => sum + o.amount, 0),
askDepth: book.asks.reduce((sum, o) => sum + o.amount, 0),
timestamp: book.timestamp
}
}
async getQuote(base, quote, side, amount) {
const book = this.getOrderBook(base, quote)
if (!book) return null
const offers = side === 'buy' ? book.asks : book.bids
return calculateLiquidityDepth(offers, amount)
}
}
// Usage
const market = new MarketDataService(client)
await market.subscribe(
{ currency: 'XRP' },
{ currency: 'USD', issuer: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B' }
)
const ticker = market.getTicker(
{ currency: 'XRP' },
{ currency: 'USD', issuer: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B' }
)
console.log(XRP/USD: ${ticker.bid} / ${ticker.ask} (${ticker.spreadPercent.toFixed(2)}% spread))
```
class OHLCVAggregator {
constructor() {
this.candles = new Map()
}
processTradeEvent(trade) {
const { price, amount, timestamp } = trade
const interval = 60000 // 1 minute candles
const candleTime = Math.floor(timestamp / interval) * interval
const key = candleTime.toString()
let candle = this.candles.get(key)
if (!candle) {
candle = {
time: candleTime,
open: price,
high: price,
low: price,
close: price,
volume: 0
}
this.candles.set(key, candle)
}
candle.high = Math.max(candle.high, price)
candle.low = Math.min(candle.low, price)
candle.close = price
candle.volume += amount
}
getCandles(count = 100) {
const sorted = [...this.candles.values()]
.sort((a, b) => b.time - a.time)
.slice(0, count)
return sorted.reverse()
}
}
```
XRPL automatically bridges through XRP for issued currency pairs:
Direct Path: USD → EUR (if direct offers exist)
Bridged Path: USD → XRP → EUR (through XRP as bridge)
The DEX automatically finds the best path.
async function getBridgedOrderBook(client, base, quote) {
// Direct path
const direct = await client.request({
command: 'book_offers',
taker_gets: quote,
taker_pays: base,
limit: 100
})
// Path through XRP
const baseToXrp = await client.request({
command: 'book_offers',
taker_gets: { currency: 'XRP' },
taker_pays: base,
limit: 50
})
const xrpToQuote = await client.request({
command: 'book_offers',
taker_gets: quote,
taker_pays: { currency: 'XRP' },
limit: 50
})
// Calculate effective bridged rates
const bridgedOffers = calculateBridgedOffers(
baseToXrp.result.offers,
xrpToQuote.result.offers
)
// Merge direct and bridged, sort by price
return mergeSortOffers(direct.result.offers, bridgedOffers)
}
```
✅ DEX is functional: Real liquidity exists for major pairs; trading works
✅ Order book queries are reliable: book_offers returns accurate current state
✅ Auto-bridging improves liquidity: XRP bridging often provides better rates
✅ Atomic execution: Trades execute completely or fail, no partial fills without explicit flags
⚠️ Liquidity depth varies: Some pairs have thin order books; large orders move price significantly
⚠️ Order book update latency: Subscriptions may have slight delays; stale data risk for fast markets
⚠️ Auto-bridging path selection: Complex paths may not always be optimal
🔴 Market orders without limits: IOC without price limits can execute at bad prices
🔴 Trusting stated amounts: owner_funds may limit actual fillable amount
🔴 Stale order book data: Always re-query before placing significant orders
🔴 Slippage on large orders: Price impact can be significant in thin markets
The XRPL DEX is a powerful feature but requires understanding its nuances. Order books are real, liquidity exists, but thin markets require careful execution. Use effective amounts, check owner_funds, and implement slippage protection for any production trading system.
Assignment: Build a market data service that provides real-time order book and ticker data.
Requirements:
Part 1: Order Book Management (40%)
- Fetch both sides of an order book
- Calculate effective amounts (using owner_funds)
- Sort orders by price
- Calculate bid/ask spread
Part 2: Ticker Service (30%)
- Best bid/ask prices
- Spread (absolute and percentage)
- Total depth on each side
- Mid-market price
Part 3: Quote Service (30%)
Average execution price
Total cost/proceeds
Price impact (vs best price)
Liquidity depth traversed
Order book accuracy: 30%
Effective amount calculation: 25%
Ticker correctness: 20%
Quote calculation: 15%
Code quality: 10%
Time Investment: 3-4 hours
Submission: Code with documentation and test results
1. Order Book Orientation (Tests Understanding):
You query book_offers with taker_gets: XRP, taker_pays: USD. What do the returned offers represent?
A) People selling XRP for USD
B) People buying XRP with USD
C) The sell side of the XRP/USD market
D) Both A and C
Correct Answer: B
Explanation: Offers in this query are from makers who will give you XRP (taker_gets) in exchange for USD (taker_pays). From the taker's perspective, these are "buy XRP" orders. The makers are selling XRP, but the taker is buying.
2. Effective Amount (Tests Application):
An offer has TakerGets: 1,000,000 XRP and owner_funds: 500,000 XRP. How much can actually be traded?
A) 1,000,000 XRP (stated amount)
B) 500,000 XRP (limited by funds)
C) 1,500,000 XRP (sum of both)
D) It depends on the taker's funds
Correct Answer: B
Explanation: The offer can only be filled up to the creator's available funds. Even though 1,000,000 XRP is offered, only 500,000 XRP is available, so that's the maximum fillable amount.
3. Offer Flags (Tests Knowledge):
Which flag should you use for a market order that should execute immediately at best available price?
A) tfPassive
B) tfImmediateOrCancel
C) tfFillOrKill
D) No flags
Correct Answer: B
Explanation: tfImmediateOrCancel (IOC) attempts to fill as much as possible immediately and cancels any unfilled portion. This is the market order behavior. tfPassive won't cross the book. tfFillOrKill requires complete fill or nothing. No flags would leave unfilled portion on the book.
4. Auto-Bridging (Tests Comprehension):
You want to trade EUR for JPY, but there are no direct EUR/JPY offers. What happens?
A) The trade fails
B) XRPL automatically routes through XRP (EUR→XRP→JPY)
C) You must manually create two separate trades
D) The trade executes at a default rate
Correct Answer: B
Explanation: XRPL's DEX automatically finds the best path, including bridging through XRP. If EUR→XRP→JPY provides better rates than any direct EUR→JPY offers, the trade will use that path automatically.
5. Spread Calculation (Tests Application):
Best bid is 0.50 USD/XRP, best ask is 0.52 USD/XRP. What is the percentage spread?
A) 2%
B) 4%
C) 0.04%
D) 0.02
Correct Answer: B
Explanation: Spread = (Ask - Bid) / Bid = (0.52 - 0.50) / 0.50 = 0.02 / 0.50 = 0.04 = 4%. The spread represents the cost of immediately buying and selling.
- book_offers: https://xrpl.org/book_offers.html
- account_offers: https://xrpl.org/account_offers.html
- OfferCreate: https://xrpl.org/offercreate.html
- OfferCancel: https://xrpl.org/offercancel.html
- Decentralized Exchange: https://xrpl.org/decentralized-exchange.html
- Auto-Bridging: https://xrpl.org/autobridging.html
- Offers: https://xrpl.org/offers.html
For Next Lesson:
Lesson 10 covers NFT and Token Operations—minting NFTs, creating offers, and managing tokenized assets on XRPL.
End of Lesson 9
Total words: ~4,700
Estimated completion time: 55 minutes reading + 3-4 hours for deliverable
Key Takeaways
Understand taker perspective:
taker_gets is what you receive, taker_pays is what you pay. Order book orientation depends on which side you're querying.
Check effective amounts:
The owner_funds field shows actual fillable quantity; stated TakerGets may be larger than what can actually be traded.
Use appropriate flags:
tfPassive for market making, tfImmediateOrCancel for market orders, tfFillOrKill for all-or-nothing.
Auto-bridging through XRP:
Many issued currency pairs trade through XRP as a bridge; the DEX handles this automatically.
Calculate liquidity depth:
Don't assume flat pricing; large orders move through multiple price levels. ---