Reading XRPL Transactions as Data
The XRP Ledger from an analyst's point of view: state versus history, the ledger objects that matter for following money, the transaction types that move value, how to read metadata and amounts, and the five traps that quietly corrupt an on-chain analysis.
What you'll learn
- Distinguish ledger state from transaction history and know which methods read each.
- Name the ledger objects and transaction types through which value moves on the XRPL.
- Read transaction metadata, and use delivered_amount and TransactionResult instead of the signed fields.
- Parse the three amount shapes (XRP drops, issued tokens, MPTs) and convert Ripple epoch time.
- Recognise partial payments, failed transactions, shared addresses, dust and non-Payment value flows before they corrupt your dataset.
Overview
In the last module you saw what a transaction is and how the ledger checks, orders and stores it. This module looks at the same transactions from the other side: as data. Your hackathon model will only be as good as your reading of the raw records, and the XRP Ledger has a few rules that trip up almost everyone the first time they turn transactions into a dataset.
No code yet. This is the mental model you need before you download anything. By the end you'll know which ledger objects matter for following money, which transaction types actually move value, how to read the metadata that records what really happened, and the handful of traps (partial payments, failed transactions, destination tags, dust) that quietly corrupt an analysis.
Two kinds of data: state and history
Every ledger version holds two things, and you'll query them differently:
- State is the current snapshot: every account, trust line, offer, escrow and pool as it stands right now. You read it with methods like
account_info,account_linesandaccount_objects. - History is the stream of transactions that produced that state, ledger after ledger. You read it with
ledger(everything in one ledger) andaccount_tx(everything touching one account).
For laundering detection you'll mostly work with history, because patterns live in how value moved over time. State is your enrichment layer: how old is this account, what tokens does it hold, does it require destination tags?
The objects that matter for following money
State is made of ledger entries, each with a type. You don't need all of them. These are the ones that show up in value flows:
| Ledger entry | What it represents | Why an analyst cares |
|---|---|---|
AccountRoot |
An account: its XRP balance, sequence, flags | Identity of every node in your graph, and its settings (RequireDest, DepositAuth) |
RippleState |
A trust line: a token balance between a holder and an issuer | Where issued tokens (stablecoins, IOUs) actually sit |
Offer |
A resting order on the built-in DEX | Value can change asset without a Payment to anyone |
AMM |
A liquidity pool for two assets | Swaps and deposits are another way to convert value |
Escrow, Check, PayChannel |
Value locked now and released later | Delayed or conditional transfers that break simple time windows |
NFTokenPage, NFTokenOffer |
NFTs and the offers to trade them | NFT sales can move value at an arbitrary price |
The account lifecycle
An address doesn't exist on the ledger until someone funds it with at least the base reserve (currently 1 XRP, plus 0.2 XRP per object the account owns). That has a useful consequence for analysts: every account has a parent, the account whose Payment created it. Following "who funded whom" is one of the oldest tricks in blockchain forensics, because a batch of fresh accounts funded by the same parent minutes apart often belongs to one operator.
Accounts can also leave. An AccountDelete transaction removes an eligible account and sends its remaining XRP to a destination. A short-lived account that is created, used once and deleted is worth noticing.
Which transactions move value
There are dozens of transaction types, but value moves through a small set:
- Payment: the workhorse. It can be direct (XRP to XRP, or a token to the same token) or cross-currency, where the sender pays in one asset and the receiver gets another, routed through the DEX or an AMM along a path.
- OfferCreate: trades on the DEX. When it crosses existing offers, balances change between parties who never sent each other a Payment.
- AMMDeposit / AMMWithdraw / AMMCreate: putting value into a pool and taking it out, possibly in a different asset.
- EscrowCreate / EscrowFinish, CheckCreate / CheckCash, PaymentChannelClaim: value that leaves now and arrives later.
- NFTokenAcceptOffer: settles an NFT sale, and the buyer pays the seller.
- AccountDelete: sweeps an account's remaining XRP to another account.
- Clawback: an issuer recovering issued tokens from a holder, where the issuer enabled it.
You'll also see Batch transactions on Mainnet: one outer transaction carrying several inner ones. Treat the inner transactions as the real value movements.
Metadata: what actually happened
The fields a user signs describe what they asked for. The metadata (meta) that the ledger adds after applying the transaction describes what actually happened. For analysis, trust the metadata.
The fields you'll use most:
TransactionResult: the outcome code.tesSUCCESSmeans it applied as intended.delivered_amount: the amount that actually reached the destination of a Payment.AffectedNodes: every ledger entry that was created, modified or deleted, with the balances before (PreviousFields) and after (FinalFields). This is the ground truth for balance changes, including DEX and AMM trades that have no explicit "Amount".
A simplified successful Payment looks like this:
{
"tx_json": {
"TransactionType": "Payment",
"Account": "rSender...",
"Destination": "rExchange...",
"DestinationTag": 4061234,
"DeliverMax": "250000000",
"Fee": "12"
},
"meta": {
"TransactionResult": "tesSUCCESS",
"delivered_amount": "250000000",
"AffectedNodes": [ "...balances before and after..." ]
},
"hash": "9EC0...",
"ledger_index": 107690955,
"close_time_iso": "2026-10-16T08:10:00Z"
}
Reading amounts correctly
An amount on the XRPL comes in one of three shapes, and your parser must handle all of them:
- XRP: a string of drops.
"250000000"is 250 XRP, because 1 XRP = 1,000,000 drops. - An issued token: an object
{ "currency": "USD", "issuer": "rIssuer...", "value": "100" }. The asset is the currency code and the issuer together:USDfrom one issuer is a completely different asset fromUSDfrom another. - A Multi-Purpose Token (MPT): an object with an
mpt_issuance_idand avalue.
Time has its own trap: raw ledger times are seconds since 1 January 2000 (the "Ripple epoch"), not Unix time. Add 946684800 to convert, or use the ready-made close_time_iso field.
Five traps that corrupt an analysis
1. Partial payments. A Payment with the tfPartialPayment flag can succeed while delivering much less than the amount the sender wrote. If you read Amount (or DeliverMax in API v2) instead of delivered_amount, you'll count money that never arrived. This trick has been used to defraud exchanges, so it belongs in your threat model too.
2. Failed transactions are still on the ledger. A transaction with a tec result (for example tecUNFUNDED_PAYMENT) failed, but it was still included in a ledger and still paid its fee. Filter on TransactionResult == "tesSUCCESS" before you count value flows, but keep the failures. A burst of failed attempts is a behaviour in its own right.
3. Shared addresses and destination tags. Exchanges and custodians receive deposits for thousands of customers at one address, and tell them apart with a destination tag (a 32-bit number). An exchange address in your graph is not one person. The pair (address, tag) is closer to "one customer", but only the exchange can link it to an identity. Accounts can set RequireDest to reject payments without a tag.
4. Dust and spam. A large share of Mainnet payments are tiny amounts (often a few drops, a millionth of an XRP each) sent to thousands of accounts, often to advertise something in a memo. They dominate a raw graph and will drown your model if you don't separate them.
5. Value that moves without a Payment. DEX trades, AMM swaps, escrows, checks and NFT sales all change balances. A model built only on Payments misses the asset conversions that layering relies on. Read AffectedNodes when you need the full picture.
Check
You're done when you can explain, in a sentence each: the difference between state and history, why every account has a funding parent, why delivered_amount beats Amount, why a tec transaction still appears in a ledger, and why an exchange address is not one person. Then take the quiz.
Resources
Assignments
0 of 2 complete