A transaction history screen looks like a simple feature until you build it for real users.
Someone sends a token, and the list stays empty because you only tracked native transfers. Someone interacts with a contract, and the “to” address is a router, so the UI shows a meaningless entry. A third user sees duplicates after a reorg, and now support has to explain why a “confirmed” transfer moved down the list.
The uncomfortable truth is that most chains don’t offer a single, clean “give me everything that happened to this address” endpoint. You either rely on an indexer that already did the work, or you build your own event-driven view of activity. The right approach depends on what “history” means in your product.
What a transaction history list should include
Users don’t want “transactions.” They want a readable timeline of what changed, what it cost, and whether it’s final. Before you pick a data source, it helps to define the components your history entries must contain so the UI stays consistent even when the chain data is messy.
A stable identity for every entry
Every row in the list should be anchored to something the blockchain actually guarantees. On EVM chains, that anchor is usually the transaction hash, combined with the chain ID in multi-chain apps. If you also show token transfers as separate sub-items, you need a second stable identifier, such as the log index within the receipt, so you can deduplicate and preserve ordering when you merge multiple feeds.
A clear activity label that matches user intent
A history list becomes useful only when the user can answer “what happened” at a glance. A raw “contract interaction” label is sometimes unavoidable, yet you should aim to recognize the common cases: send, receive, approve, swap-like flows, and bridge-like flows. Block explorers split views into normal, internal, and token transfers because the raw transaction object often doesn’t describe the user-facing action by itself.
Assets and amounts in human units
Each entry should specify which asset moved and by how much, using the correct decimals for ERC-20 tokens. If the action involves multiple assets, such as swaps, the UI should either group them under one transaction or present a primary movement with expandable details. Users interpret “missing history” as “missing funds,” so asset coverage matters more than cosmetic formatting.
Direction and counterparties that are defensible
Users care about who they sent to and who sent to them. On-chain, the “from” and “to” fields can be misleading for contract-based flows because routers and proxies sit in the middle. Your UI should still show a counterparty, but it should do so carefully: show the direct counterparty from the transaction fields, then enhance it with decoded event data when you have high confidence. When you can’t be confident, it’s better to show “Contract” with an address than to guess incorrectly.
Status that reflects real chain behavior
The list should clearly represent pending, confirmed, and failed states. If you support pending transactions, they should have a distinct presentation because ordering can change once a transaction is mined. Reorgs are rare, yet they do happen, and your model should handle “mined then moved” without creating duplicate rows or silently changing history in a way that looks like a bug.
Time and ordering that won’t jump around
Confirmed history should be ordered by block number and transaction index for a stable, canonical sort. Pending history needs a separate strategy, so the list doesn’t reshuffle on every refresh. Many products pin pending items or place them in a separate section and merge them into the canonical order only after mining.
Fee visibility that matches what users pay
A history list should show fees in a way that matches the network’s fee model. Users will compare your fee number to explorers and wallets. If you show only gas limit or only max fee parameters, it won’t match what they think of as “fee paid.” A good entry shows what was actually paid, based on receipt data, and keeps advanced fee fields in the detail view.
Why raw JSON-RPC makes history harder than it sounds
Ethereum and most EVM chains do not provide a native “transaction history by address” method in JSON-RPC. That gap is widely documented because it forces teams to choose between scanning blocks or using indexed services.
Transactions live in blocks, and token transfers live in logs inside receipts. To build history yourself with raw JSON-RPC, you need to search across blocks for transactions that involve an address, and also query logs that mention it. eth_getLogs is a core method for this, but it has practical limits and complexity, especially over large block ranges. Many provider guides treat it as a “deep dive” topic for a reason.
So the “history problem” is really an indexing problem. Once you accept that, your options become clearer.
The main ways teams fetch history
There are three common approaches in production. They differ in cost, complexity, and the kind of UX you can support.
Explorer and indexer APIs for fast results
The quickest way to ship is to use a block explorer or indexing API that already tracks address activity. Etherscan’s API exposes endpoints for normal transactions, internal transactions, and token transfers with pagination.
This approach works well when you want a predictable development effort, and you accept a third-party dependency. The trade-off is merging. A “normal transactions” endpoint does not include token transfers, so you often have to combine multiple endpoints to approximate what users mean by “my history.”
Data APIs that return asset transfers directly
Some providers offer higher-level endpoints that return asset movements in a way that’s closer to wallet UX. Alchemy’s Transfers API is designed to fetch transfers without scanning blocks manually, covering multiple asset types in one approach.
The practical trade-off is coverage boundaries. Transfer APIs are usually derived from receipts and logs, so pending activity and certain internal movements may need separate handling. Provider docs and developer questions often highlight the difference between “transfer history” and a full “transaction history” that includes failures and pending entries.
Building your own history index from blocks and logs
If you need full control, strict data locality, or custom semantics, you can build your own indexer. This typically means ingesting blocks, extracting transactions and receipts, parsing logs, and storing a queryable history view. It’s a bigger investment, yet it gives you the most consistent behavior across chains and the most flexibility for future product features.
The real cost is operational. You own reorg handling, backfills, storage growth, and performance engineering. If your product roadmap includes deep history, analytics, or notifications, this investment can be worth it because it becomes shared infrastructure.
How to decode and label activity so it reads like a product
A history list becomes useful when entries answer three questions quickly: what happened, with what asset, and to whom. Raw on-chain fields rarely tell that story alone.
Separating native transfers from token transfers
Native transfers are visible in the transaction’s value field. Token transfers are usually visible through Transfer event logs. Explorers and APIs separate these streams because they originate from different on-chain data structures.
When you merge them into a single timeline, the transaction hash becomes the binding key. One transaction can include multiple token transfers, so you decide whether to show a grouped entry or multiple rows. Grouping tends to read better for swaps and complex contract actions.
Handling internal movements and contract-driven transfers
Contract execution can move ETH internally and trigger downstream token transfers. Explorers call these “internal transactions,” and developers explain them as execution effects rather than separate on-chain transactions.
If your product needs internal ETH movements, you typically rely on traces from an indexer or an explorer API that already computed them. Etherscan provides an endpoint for internal transactions that many teams use for this purpose.
Decoding calldata and event logs for human labels
A transaction sent to a router contract isn’t informative until you decode it. A practical approach is to recognize common events and known ABIs first, then fall back to a neutral label when decoding is uncertain. Overconfident decoding is worse than a generic “Contract interaction” because it creates false trust.
Developer write-ups on human-readable visualization emphasize that decoding and labeling are a distinct layer from fetching raw blockchain data.
How to sort, dedupe, and survive reorgs
A history list that looks correct on a calm day can still break when the chain behaves like a chain.
Step 1: Set a canonical ordering for confirmed entries
Confirmed history needs an ordering rule that never changes for the same chain state. The safest default is to sort by block number and then by transaction index within the block, because that matches how the block itself is ordered. Pending items don’t belong in that same sort, since they don’t have a stable block position yet. A practical approach is to keep pending entries pinned to the top or in a separate “Pending” section, and only move them into the confirmed timeline once they have a block number and transaction index.
Step 2: Deduplicate by identifiers that remain stable across merges
Once you combine sources, duplicates are inevitable. The same transaction hash can show up in a “normal transactions” feed and also produce multiple token transfer records. The safest way to prevent double rows is to treat the transaction hash as the primary key for a top-level entry, then use a secondary identifier for sub-events. For ERC-20 transfers, the log index inside the receipt is a reliable identifier for a specific transfer, which lets you merge and group activity without losing detail or creating collisions.
Step 3: Model confirmations so reorgs update the UI without drama
Reorgs can cause a mined transaction to move to a different block or temporarily disappear from the canonical chain. If your UI treats “mined once” as permanently confirmed, you will eventually show the wrong status or duplicate an entry when it reappears. A more defensible model is to treat confirmation depth as part of status: early confirmations look “settling,” and only deeper confirmations are treated as final for UI purposes. When the chain reorganizes, the history list can update calmly by adjusting block references and confirmation counts rather than pretending nothing happened.
Benefits of a well-built history layer
Faster support and fewer “missing funds” tickets
When your history layer merges native transfers, token transfers, and contract-driven movements into one coherent timeline, users stop interpreting gaps as lost funds. Clear, consistent statuses help too: “pending,” “failed,” and “confirmed” become understandable states rather than vague outcomes, which reduces the reflex to resend or panic. Stable identifiers and reliable explorer links turn support conversations from guesswork into verification, because a support engineer can follow one transaction hash, see the exact asset movements your UI derived, and explain discrepancies without asking the user to provide screenshots or repeat steps.
Better UX for swaps, bridges, and multi-step actions
Modern on-chain actions rarely look like “send from A to B.” Swaps, bridges, and staking flows often create multiple token movements inside a single transaction or across several transactions, with routers and contracts in the middle. A well-built history layer can group related transfers under one primary entry, label the action in plain language when decoding is confident, and still preserve detail for users who want to expand and inspect exact token movements. That structure makes complex actions feel understandable instead of chaotic, which reduces anxiety when balances change in multiple directions and prevents the common “why did it take my token and then give it back” confusion.
A clean foundation for notifications and analytics
Once you have a reliable internal model of “what happened” that normalizes assets, directions, counterparties, fees, and status, you can reuse it everywhere. Notifications become straightforward because you can trigger on meaningful events rather than on raw logs. Filters become accurate because you can distinguish receives from contract interactions and approvals. Analytics become credible because you can compute totals and trends from stable, deduplicated entries instead of stitching together inconsistent feeds. Most importantly, you avoid the painful rewrite that happens when a product tries to bolt on insights later and discovers that its “history” data is too messy to segment safely.
Conclusion
Creating a transaction history list is not just a UI exercise. It’s an indexing and modeling problem with UX consequences. Raw JSON-RPC makes it hard because there isn’t a native “history by address” endpoint, and logs and internal effects require indexing to become queryable.
You can ship quickly with explorer APIs, build a product-friendly feed with transfer-focused data APIs, or own the pipeline with your own indexer. What matters is defining what “history” means for your users, then implementing it in a way that stays consistent when the network isn’t.




