Learn to retrieve account transaction history and subscribe to real-time transaction streams using WebSocket event listeners on the XRP Ledger.
What you'll learn
In the last module you wrote to the ledger — you sent a payment and watched it validate. This module is the other half of the conversation: reading what the ledger already recorded, and listening for what happens next. Those are two distinct moves, and almost every XRPL app you'll ever build leans on both.
The first move is history: "show me what already happened to this account." You ask the ledger a question, it hands you a list of past transactions, and you're done — a classic request/response. The second move is subscription: "tell me the moment anything new touches this account." You leave a WebSocket connection open and the server pushes events to you as they're validated, in real time, with no polling.
Think of it like email. Fetching history is opening your inbox and scrolling through messages that already arrived. Subscribing is leaving the app open so a notification pings the instant a new message lands. You'll build a small script for each, both pointed at the public testnet, both using xrpl.js.
Same one dependency as before. Create a fresh folder (or reuse your payments project) and install:
npm init -y
npm i xrpl
npm i -D typescript tsx @types/node
Both scripts open the same connection. Grab a testnet address you already funded in the previous module — or any address visible on the Testnet Explorer — and keep it handy. We'll call it your watched account.
import * as xrpl from 'xrpl'
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233')
const ACCOUNT = 'rEXAMPLE...' // <- a funded testnet address you control
To read what already happened, you send the ledger an account_tx request. It returns the transactions that affected a given account, newest first by default. You send raw requests with client.request(), passing a plain object whose command names the method:
async function history() {
await client.connect()
const response = await client.request({
command: 'account_tx',
account: ACCOUNT,
ledger_index_min: -1, // -1 means "from the earliest validated ledger"
ledger_index_max: -1, // -1 means "up to the latest validated ledger"
limit: 10, // cap how many we get back
})
const { transactions } = response.result
console.log(`Found ${transactions.length} transactions`)
await client.disconnect()
}
history()
A few parameters are worth knowing. limit caps how many transactions come back in one call. forward defaults to false (newest first); set it to true to read oldest first. And binary: true would return raw hex instead of JSON — handy for performance, but for learning we want the readable JSON, so leave it off.
Each element of the transactions array bundles two things you care about: the transaction itself, under tx_json, and the outcome, under meta. The tx_json tells you what was requested; the meta tells you what actually happened. Always read both — a transaction can be recorded on-ledger and still have failed.
for (const entry of transactions) {
const tx = entry.tx_json
const result = entry.meta?.TransactionResult
console.log({
hash: entry.hash,
type: tx?.TransactionType,
from: tx?.Account,
amount: tx?.Amount, // drops (string) for XRP payments
result, // e.g. 'tesSUCCESS'
})
}
Two gotchas show up immediately. First, Amount for an XRP payment is a string of drops, not XRP — divide by 1,000,000, or use xrpl.dropsToXrp() to convert. Second, not every transaction has an Amount: a TrustSet or an AccountSet won't, so guard with optional chaining (tx?.Amount) and don't assume the field exists.
To read more than limit transactions, account_tx is paginated. When more results exist, the response includes a marker. Pass that same marker back on your next request to pick up exactly where you left off:
let marker: unknown = undefined
do {
const page = await client.request({
command: 'account_tx',
account: ACCOUNT,
limit: 10,
marker,
})
// ...process page.result.transactions...
marker = page.result.marker
} while (marker)
When the response comes back with no marker, you've reached the end of the history.
History is a snapshot. To watch transactions arrive as they happen, you keep the connection open and ask the server to push events to you. This is the part that feels like magic the first time it works: you run the script, send a payment from somewhere else, and your terminal lights up instantly.
Two steps make it happen. First, register an event handler so xrpl.js knows what to do when a transaction message arrives. Then send a subscribe request naming the accounts you care about. The accounts parameter takes an array of addresses; the server will emit a transaction event whenever a validated transaction affects any of them.
async function watch() {
await client.connect()
// 1. React to every pushed transaction event
client.on('transaction', (event) => {
const tx = event.tx_json ?? event.transaction // field name varies by server version
console.log('Live transaction:', {
type: tx?.TransactionType,
from: tx?.Account,
to: tx?.Destination,
amount: tx?.Amount,
result: event.meta?.TransactionResult,
validated: event.validated,
})
})
// 2. Tell the server which accounts to watch
await client.request({
command: 'subscribe',
accounts: [ACCOUNT],
})
console.log(`Watching ${ACCOUNT}. Send it some XRP to see an event...`)
}
watch()
Notice we don't call disconnect() at the end — that's the whole point. The script stays alive, the socket stays open, and the client.on('transaction', ...) handler fires every time the testnet validates a transaction touching your account. Leave this running, then in a second terminal run your payment script from the previous module (sending XRP to ACCOUNT), and watch the event print.
The event object mirrors the history shape but flatter: meta carries the TransactionResult, validated is true for finalized transactions, and the transaction body lives under tx_json. One honest caveat: different rippled server versions have shipped this field as either tx_json or transaction, so the event.tx_json ?? event.transaction fallback above keeps your code working across endpoints. When in doubt, console.log(event) once and look at the real shape your server sends.
A subscription is a resource — leaving stray ones around wastes server connections and can leak events into your handlers. When you're finished watching, reverse the steps: unsubscribe from the accounts, remove the listener, and disconnect.
await client.request({ command: 'unsubscribe', accounts: [ACCOUNT] })
client.removeAllListeners('transaction')
await client.disconnect()
In a long-running app, wire this into your shutdown path (for example, a process.on('SIGINT', ...) handler) so pressing Ctrl-C closes the socket gracefully instead of yanking it.
You're done when you have two working scripts pointed at testnet. The first, history.ts, calls account_tx and prints the last 10 transactions for a funded account, showing each one's type, amount (converted from drops where present), and result code. The second, watch.ts, opens a subscribe on that account and stays alive; when you send XRP to it from a separate script, your handler logs the incoming transaction in real time. Submit a repo or gist with both files plus sample console output (or a short screen capture) of a live transaction being received, for a peer to verify.
Assignments
0 of 3 complete