Blockchain for Payments Professionals · Chapter 3 of 4Draft
Reading the chain with code
In Chapter 2 you read Ethereum by hand — one command at a time, like checking a nostro balance by logging into a portal. This chapter makes chain-reading programmable: same blocks, balances, receipts and event logs, now fetched by a program that can run on a schedule, filter, aggregate — and produce a number like “stablecoin volume,” which the last two lessons teach you to never take at face value again.
16 min readBy NewRemit Research
Typing commands works for looking; it doesn’t work for monitoring. Nobody builds a treasury dashboard by manually refreshing a screen. This chapter makes chain-reading programmable — and that’s where it earns its place in a payments curriculum: the moment you can aggregate on-chain data, you can produce a number like “stablecoin volume.” The last two lessons show why that number, taken raw, is one of the most misleading figures in the industry — and how to read it honestly.
What we deliberately skip in this chapter (you can build the monitor without them): writing or signing transactions, wallet and private-key management, DeFi protocol mechanics, indexer architecture, and Dune SQL. We stay entirely on the read side of the chain.
Lesson 3.1
From typing commands to writing programs
Why this matters: every serious payments operation automates its data access. This lesson shows that “programming against a blockchain” is structurally the same as programming against any payments API — a layered stack where each layer has one job.
The stack
In Chapter 2, the stack looked like this:
You typing a command (Cast)
↓
JSON-RPC ← the message standard
↓
RPC provider ← your gateway to the network
↓
EthereumIn this chapter, only the top layer changes:
Your program (TypeScript)
↓
viem ← a software library that writes the messages for you
↓
JSON-RPC
↓
RPC provider
↓
EthereumTranslation to familiar territory:
| Blockchain layer | Payments equivalent |
|---|---|
| JSON-RPC | The message standard — think ISO 20022 or SWIFT MT. A fixed format everyone agrees on. |
| RPC provider | Your gateway / service bureau. You don’t run your own SWIFT connection; most firms don’t run their own Ethereum node either. |
| viem | The vendor SDK that builds and parses the standard messages so your developers don’t hand-craft them. |
| Your program | Your application — the monitor, the dashboard, the reconciliation job. |
One clarification that saves confusion later: the RPC endpoint URL your program points at is not “Ethereum itself.” It’s one provider’s door into the network — exactly as your SWIFT gateway is not “SWIFT.”
The same stack, in real code
In Chapter 2, checking a balance was one command typed into a terminal — Cast hand-delivering a single JSON-RPC message:
cast balance 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 --rpc-url $ETH_RPC_URLIn this chapter, a program asks instead. The shared plumbing — the client every snippet below imports — is one real file:
import { createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'
export const client = createPublicClient({
chain: mainnet,
transport: http(process.env.ETH_RPC_URL),
})Read it against the diagram: viem is the SDK, http(process.env.ETH_RPC_URL) is the door to your provider, mainnet names the network. The whole five-layer stack, in six lines.
Interactive A
The request, layer by layer
Animated diagram of the five-layer read stack: pressing “Check a balance” sends a request token from your program down through viem, JSON-RPC and the RPC provider to Ethereum, then a response token travels back up the same layers.
Hover or tap a layer to pin its job — or run the request to watch all five do theirs in order.
Press “Check a balance” to watch one request travel down the five-layer stack and its response travel back up — each layer doing its single job on the way. Hover or tap a layer to pin its explanation.
Source: Illustrative — mechanics as described in this lesson; no live data.
Lesson 3.2
Two kinds of money, one balance inquiry
Why this matters: the single most common confusion for payments people entering this space is the difference between ETH and a stablecoin like USDC sitting “on Ethereum.” They live in different places and are read in different ways — and the difference explains what a stablecoin technically is.
Native balance vs contract balance
ETH is Ethereum’s native asset. Every account’s ETH balance is part of the chain’s base ledger. Reading it is one direct question:
import { formatEther } from 'viem'
import { client } from './client'
const balanceWei = await client.getBalance({
address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
})
console.log(balanceWei) // 6634031455680168742n — wei
console.log(formatEther(balanceWei)) // "6.634031455680168742"USDC is not part of that base ledger. It’s a balance inside the USDC smart contract — a program on Ethereum that maintains its own internal table of who holds what. To read a USDC balance, you don’t ask Ethereum; you ask the USDC contract:
import { erc20Abi, formatUnits } from 'viem'
import { client } from './client'
const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'
const balance = await client.readContract({
address: USDC, // which contract
abi: erc20Abi, // its API spec — the ABI
functionName: 'balanceOf', // which question
args: ['0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'], // about whom
})
console.log(balance) // 9739943715n — minor units
console.log(formatUnits(balance, 6)) // "9739.943715"(Cast can ask the identical question — same contract, same function, its signature typed inline:)
cast call 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 \
"balanceOf(address)(uint256)" \
0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 \
--rpc-url $ETH_RPC_URLThe payments translation: the USDC contract is the issuer’s ledger, published on a shared network. Ethereum’s base ledger records ETH; Circle’s contract records USDC claims. Same network, two different books.
Two precision points your analysts will care about:
- A balance inquiry is not a payment.
readContract()is read-only. It needs no key, no signature, no gas, and it never appears on-chain — like an account statement request, not a transfer instruction. - Amounts come back as exact integers in minor units. A balance arrives as something like
6634031455680168742— an integer, never a decimal. Payments people already know why: you never store money as floating point. On-chain amounts use the same discipline, just with more decimal places (USDC uses 6, ETH uses 18). The formatting into “6.63 ETH” happens in your program, explicitly, at display time.
The ABI — the contract’s API spec
How does your program know the USDC contract answers a question called balanceOf? From the contract’s ABI: a machine-readable description of its interface — the erc20Abi import in the snippet above, which in plain terms says:
balanceOf(address) → integer
decimals() → integer
symbol() → textThink of it as the API documentation for the issuer’s ledger. Under the hood, the library converts your typed request into raw bytes the contract understands, and decodes the raw bytes that come back. You’ll never do this by hand — but knowing it happens keeps the library from being magic.
Interactive B
Two ledgers, one balance inquiry
Split panel showing Ethereum's base ledger on the left and the USDC contract — the issuer's ledger — on the right. A toggle animates the balance inquiry landing on the correct ledger, and a switch flips the returned amount between exact minor units and its formatted display value.
Your program
client.getBalance({ address })
Ethereum base ledger
The chain's own book — ETH balances
- 0x51C4…9aF2128.4401 ETH
- 0xd8dA…60456.6340 ETH
- 0x28C6…97a10.0512 ETH
USDC contract
The issuer's ledger, published on the chain
- 0x51C4…9aF21,020.00 USDC
- 0xd8dA…60459,739.94 USDC
- 0x28C6…97a184,310.27 USDC
One direct question to Ethereum's base ledger. The chain itself keeps every account's ETH balance — no contract involved.
The chain returns
6634031455680168742wei · 18 decimals
Formatting is your job; exactness is the chain's.
Toggle between the two inquiries to watch the question land on the book that actually answers it — Ethereum's base ledger for ETH, the issuer's contract for USDC. Then flip the result between minor units and its formatted value.
Source: Illustrative — addresses and balances invented; mechanics as described in this lesson.
Lesson 3.3
The audit trail: transactions, receipts, and logs
Why this matters: everything you'll ever monitor — flows, freezes, corridor activity — comes from three record types. Confusing them produces wrong numbers. Payments people have an unfair advantage here, because the three map exactly onto concepts you already use daily.
Instruction vs confirmation
| On-chain object | What it answers | Payments equivalent |
|---|---|---|
| Transaction | What was requested? — sender, target, amount, attached data | The payment instruction (the MT103, the API request) |
| Receipt | What actually happened? — success/failure, fees consumed, events emitted | The confirmation / settlement status report |
These are separate objects, fetched separately:
import { client } from './client'
const hash = '0x…' // any transaction hash
const tx = await client.getTransaction({ hash }) // the instruction
const receipt = await client.getTransactionReceipt({ hash }) // the confirmationAn instruction existing proves intent; only the receipt proves execution. Every monitoring system reads receipts, not just transactions, for exactly this reason.
Logs — the statement entries
When a contract executes, it can emit events, which are stored as logs attached to the receipt. When USDC moves, the contract emits a Transfer event recording sender, recipient, and amount.
Logs are the closest thing on-chain to statement entries: the posting record of what a contract did. A monitoring program queries them like a database:
all logs
→ only from the USDC contract
→ only Transfer events
→ only this block range
→ decoded into structured recordsAnd the database analogy runs deeper. Each log has a few indexed fields (called topics) and a payload:
log ≈ a row
topics ≈ indexed, searchable columns (event type, from, to)
data ≈ the non-indexed payload (amount)Because sender and recipient are indexed, you can efficiently ask the network “give me every USDC transfer from this address” — without downloading everything and filtering yourself. That one design choice is what makes address-level monitoring practical.
The same funnel, as the real query:
import { parseAbiItem } from 'viem'
import { client } from './client'
const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'
const logs = await client.getLogs({
address: USDC, // only from the USDC contract
event: parseAbiItem(
'event Transfer(address indexed from, address indexed to, uint256 value)',
), // only Transfer events
args: { from: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' },
fromBlock: 23_000_000n, // only this block range
toBlock: 23_000_100n,
})A decoded event, as your program sees it:
{
eventName: 'Transfer',
args: {
from: '0x…',
to: '0x…',
value: 9739943715n, // 9,739.943715 USDC (6 decimals)
},
}One caveat before you trust a log: logs prove execution — they’re written only when a transaction actually ran. The instruction’s attached data proves only intent. Monitoring systems are built on logs for precisely this reason.
And one caveat before you trust an address: the chain tells you that address 0x51… received funds. It does not tell you the address belongs to Circle, Binance, or anyone. Mapping addresses to organizations requires external attribution data — explorer labels, entity datasets, verified documentation. On-chain: facts. Off-chain: names. Address ≠ wallet owner ≠ legal entity ≠ customer.
Interactive C
Filtering logs like a database
Table of twenty illustrative event logs. Three filter chips — contract, event type, and sender — narrow the table live, showing how a monitoring program queries logs like a database.
- USDCTransfer0xd8dA…6045 → 0x28C6…97a19,739.94 USDC
- WETHTransfer0x7c3F…88eD → 0x1f98…F98412.50 WETH
- USDCApproval0x9A77…4Cd1 → 0x6817…b3a950,000.00 USDC
- DAITransfer0x28C6…97a1 → 0x5d3a…C7e21,204.11 DAI
- USDCTransfer0x51C4…9aF2 → 0x8B21…D7c3250,000.00 USDC
- WETHDeposit0x9A77…4Cd1 → 0x9A77…4Cd15.00 WETH
- USDCTransfer0x28C6…97a1 → 0x9A77…4Cd174.10 USDC
- DAIApproval0x51C4…9aF2 → 0x6817…b3a910,000.00 DAI
- WETHTransfer0xd8dA…6045 → 0x7c3F…88eD2.00 WETH
- USDCTransfer0x8B21…D7c3 → 0x51C4…9aF212,400.00 USDC
- USDCApproval0x5d3a…C7e2 → 0x1f98…F984unlimited
- DAITransfer0x3fE8…12bB → 0x9A77…4Cd188,000.00 DAI
- USDCTransfer0xd8dA…6045 → 0x3fE8…12bB1,850.00 USDC
- WETHApproval0x28C6…97a1 → 0x1f98…F984unlimited
- USDCTransfer0x6817…b3a9 → 0x28C6…97a15,000.00 USDC
- USDCApproval0x7c3F…88eD → 0x6817…b3a92,500.00 USDC
- USDCTransfer0xd8dA…6045 → 0x5d3a…C7e2320.55 USDC
- DAIApproval0xd8dA…6045 → 0x1f98…F984400.00 DAI
- WETHTransfer0x1f98…F984 → 0x51C4…9aF20.75 WETH
- USDCApproval0x3fE8…12bB → 0x1f98…F984900.00 USDC
Twenty logs, three filters. Toggle the chips to narrow the table the way a monitoring program does — and notice which filter the network itself can run for you, because the field is indexed.
Source: Illustrative — all rows invented; truncated addresses and amounts are not real data.
Lesson 3.4
Why raw on-chain volume lies
Why this matters: this is the payoff of the entire chapter. Headline stablecoin volume figures are built by summing Transfer events. Once you've seen how those events are generated, you'll understand why raw sums overstate payment activity — and you'll never read a “$X trillion in stablecoin volume” claim the same way again.
One transaction, many transfers
Here is a real pattern from Ethereum mainnet:
ONE transaction
↓
contract execution
├── Transfer #1
├── Transfer #2
├── Transfer #3
└── Transfer #4A single instruction can trigger a chain of contracts, each moving tokens, each emitting its own Transfer event. You can see it in two lines:
const receipt = await client.getTransactionReceipt({ hash })
receipt.logs.length // 4 — four Transfer events from ONE instructionImmediately:
transaction count ≠ transfer countThe forensic case
Investigating one unusually large USDC Transfer event — pulling the transaction, its receipt, and all its logs — revealed that the “large transfer” was one leg of a complex multi-contract operation in which most of the token movements reversed before the transaction finished. Enormous gross flow; almost no net economic movement. All inside a single atomic transaction.
Payments professionals have seen this shape before: it’s the difference between gross ledger movement and customer payments. An RTGS system’s gross settlement flow dwarfs the underlying customer activity; interbank funding legs aren’t remittances. On-chain, the same distinction applies — but the headline numbers usually ignore it.
So the full inequality chain is:
transaction count ≠ transfer count ≠ payment count
gross transfer volume ≠ payment volumeWhat honest measurement requires
Summing Transfer events gives you accounting evidence of token movement — nothing more. Turning that into a payments figure requires classification: filtering out bot activity, exchange-internal shuffling, protocol round-trips, and self-transfers, then attributing what remains to real-world context. (This is exactly the adjustment ladder from Episode 2 of the stablecoin series: raw volume shrinks by orders of magnitude before it resembles payments.)
To be fair in both directions: this is not unique dishonesty by the crypto industry — incumbent payment networks also quote flattering gross figures. The difference is that on-chain data is public, so here the inflation is checkable. That’s a genuine advantage, if you know how to check.
Interactive D
One transaction, $40M of “volume”
Four-step walkthrough of a forensic case: a headline transfer event, the full log list of its transaction, the reversing legs cancelling out, and a final comparison of gross transfer volume against net economic movement.
Transfer event · USDC
$19.8M
0x51C4…9aF2 → 0x8B21…D7c3
One log, seen in isolation — the rest of the receipt not yet fetched.
One huge Transfer event lands in your monitor: $19.8M of USDC in a single log. Taken alone, it looks like a massive payment.
Step through the forensic case: the headline Transfer event, the zoom-out to its full receipt, the legs that reverse each other inside the same transaction, and what's actually left. Numbers are illustrative and internally consistent — not measured data.
Source: Illustrative walkthrough of the pattern described in this lesson; gross/net figures invented for teaching.
Lesson 3.5
No magic, and knowing what you don't need
Why this matters: two short habits separate analysts who use tools from analysts used by tools: verifying there's no magic in the stack, and consciously naming what you're choosing not to learn.
Proving there’s no magic
As a final check, we removed the library entirely and sent one raw JSON-RPC message to the provider — the equivalent of hand-writing a SWIFT message instead of using the vendor SDK:
const rpcUrl = process.env.ETH_RPC_URL
if (!rpcUrl) {
throw new Error('ETH_RPC_URL is not set')
}
const address = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'
const response = await fetch(rpcUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'eth_getBalance',
params: [address, 'latest'],
}),
})
const data = await response.json()
console.log('RAW JSON-RPC RESPONSE')
console.log(data)
if ('result' in data) {
const balanceWei = BigInt(data.result)
console.log('\nDecoded manually')
console.log('Hex:', data.result)
console.log('Wei:', balanceWei)
console.log('ETH:', Number(balanceWei) / 1e18)
}What it prints:
RAW JSON-RPC RESPONSE
{ jsonrpc: '2.0', id: 1, result: '0x5c10d075d8958326' }
Decoded manually
Hex: 0x5c10d075d8958326
Wei: 6634031455680168742n
ETH: 6.634031455680169(Look closely at the last line: dividing through Number rounded the final digits — the float trap this chapter keeps warning about, demonstrated live. Production code formats from the integer.)
The result matched the library exactly. Everything viem does is convenience: message construction, decoding, typing. Useful — but replaceable, inspectable, and never a source of truth on its own. Having proven that once, there’s no virtue in avoiding the library for real work.
Also learned in practice: your gateway has policies. Providers rate-limit, restrict deep historical queries, and require authentication tiers — your code can be perfectly correct while the provider refuses the request. Anyone who has worked with banking APIs will find this deeply familiar.
Named skips
Consistent with this series’ rule of saying what we’re not covering, this chapter deliberately excludes: writing or signing transactions, key and wallet management, DeFi protocol mechanics (the forensic case needed none of them), indexer and archive-node architecture, and large-scale entity attribution. They’re real topics; they’re simply not on the critical path to reading and measuring stablecoin activity honestly.
Self-test
You’ve absorbed this chapter if you can answer these without looking back.
Interactive E
Score yourself honestly
Seven self-test questions as cards. Reveal each model answer, score yourself honestly, and get a per-question pointer to the lesson worth rereading.
- Q1
Name the five layers between your program and blockchain data, and each layer's single job.
- Q2
Why does reading a USDC balance involve asking a contract, while reading an ETH balance doesn't?
- Q3
A balance arrives as 9739943715. Why an integer — and whose job is formatting?
- Q4
Transaction vs receipt: which proves intent, which proves execution?
- Q5
Why can you efficiently search logs by sender address?
- Q6
A single transaction emits four Transfer events totalling $40M, of which $39.6M reverses within the same transaction. What was the payment volume?
- Q7
The chain shows address 0x51… received funds. What can you not conclude, and what would you need?
0 of 7 answers revealed — reveal them all to score yourself.
Seven questions, no backend, nothing saved. Reveal each model answer, mark what you genuinely knew, and get a per-question pointer to the lesson worth rereading.
Source: This chapter's own lessons, compressed to model answers.
Optional hands-on lab (for readers who code)
A minimal TypeScript project reproduces everything in this chapter with the viem library: one shared read-only client (the client.ts from Lesson 3.1), then one small script per concept — latest block, ETH balance, USDC balanceOf, a transaction and its receipt, a Transfer log query with a sender filter, and a raw JSON-RPC call with fetch() as the no-magic proof. You’ll need a free RPC endpoint from any provider (Alchemy, Infura, or similar). The lab ships as a companion repository rather than inline scripts — this chapter stays readable for non-coders.
Glossary — Chapter 3 additions
| Term | Plain meaning |
|---|---|
| viem | A TypeScript library that builds and parses JSON-RPC messages — the vendor SDK of the stack. |
| RPC provider | Your gateway service into the network; think service bureau, not the network itself. |
| ABI | A contract’s machine-readable API spec — what questions it answers and in what types. |
readContract / balance inquiry | A read-only question to a contract. No key, no fee, never recorded on-chain. |
| Receipt | The confirmation record of what a transaction actually did — status, fees, events. |
| Log / event | A statement entry written by a contract during execution; the raw material of all monitoring. |
| Topic | An indexed, searchable field of a log (event type, sender, recipient). |
| Minor units / bigint | Exact-integer amount representation — the on-chain version of “never store money as a float.” |
| Entity attribution | Off-chain mapping from an address to an organization. The chain never provides it. |
Bridge to Chapter 4
Everything so far read generic chain data. Chapter 4 applies the identical stack — contract, ABI, events, log queries — to a question with direct compliance weight: when does a stablecoin issuer freeze an address? USDT’s administrative events (blacklisting, freezing) are just another event type to filter and decode. The result is a working Freeze Tracker v0.1 — the first genuinely payments-native artifact of this curriculum.
Next — Chapter 4: The Freeze Tracker. The same five layers, one new event type, and the first tool you’ll actually want to keep running.