You open a block explorer to audit a wallet and it shows a long list of rows. You ask your own node for the same list and it returns nothing useful. Ethereum nodes store blocks, transactions and receipts. They do not keep a "transactions for this address" index. Every reliable web3 transaction history feature, from a portfolio tracker to a tax export, is built on an indexer you either rent or build.
This guide covers what a transaction record contains, five ways to fetch it, working Web3.js and Web3.py code, a comparison of Alchemy, QuickNode and Etherscan, plus batching, cross-chain aggregation, failed-transaction filtering and privacy.

Every full node stores blocks, and each block holds a list of transactions plus receipts describing their outcomes. The history for one address is the subset of those records where the address is the sender, the recipient, or a participant in an emitted event. That subset splits into four categories, and most bugs come from mixing them up:
| Category | What it is | Where it lives | Visible in a plain block scan? |
|---|---|---|---|
| Normal (external) transactions | Signed by an account, sent to an address or contract | Block body | Yes |
| Internal transactions | Value transfers triggered by contract execution | Execution traces | No, requires trace data |
| Token transfers (ERC-20) | Balance changes emitted as Transfer events | Receipt logs | No, requires log queries |
| NFT transfers (ERC-721/1155) | Ownership changes emitted as events | Receipt logs | No, requires log queries |
A wallet that only swaps tokens on a decentralized exchange may show a normal transaction to a router contract, while the real value movement sits in logs and internal calls. A tool that reads only the block body will miss it.
Fields come from two objects: the transaction itself and its receipt. You need both for a complete record.
| Field | Source | Meaning |
|---|---|---|
| hash | Transaction | Unique identifier of the transaction |
| nonce | Transaction | Sender's transaction counter; orders their transactions |
| from / to | Transaction | Sender and recipient; "to" is empty for contract creation |
| value | Transaction | Native currency amount in wei (1 ETH = 10^18 wei) |
| input | Transaction | Calldata; the first 4 bytes identify the function called |
| gas | Transaction | Gas limit the sender allowed |
| maxFeePerGas / maxPriorityFeePerGas | Transaction | EIP-1559 fee caps set by the sender |
| blockNumber | Transaction / receipt | Block that included it; null while pending |
| status | Receipt | 1 for success, 0 for reverted |
| gasUsed | Receipt | Gas actually consumed |
| effectiveGasPrice | Receipt | Price per gas actually paid |
| logs | Receipt | Events emitted, including token Transfer events |
alchemy_getAssetTransfers return transfers across categories in one call, with pagination keys.These use Web3.js v4, where numeric values are returned as bigint. Replace the RPC URL with your own endpoint.
import { Web3 } from 'web3';
const web3 = new Web3('YOUR_RPC_URL');
const target = '0xYourAddress'.toLowerCase();
async function scan(from, to) {
const found = [];
for (let n = from; n <= to; n++) {
const block = await web3.eth.getBlock(n, true);
for (const tx of block.transactions) {
if (tx.from?.toLowerCase() === target || tx.to?.toLowerCase() === target) {
const receipt = await web3.eth.getTransactionReceipt(tx.hash);
found.push({
hash: tx.hash,
block: n,
valueWei: tx.value,
status: receipt.status,
gasUsed: receipt.gasUsed
});
}
}
}
return found;
}
const TRANSFER = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';
const padded = '0x' + '0xYourAddress'.slice(2).toLowerCase().padStart(64, '0');
// Tokens received by the address
const incoming = await web3.eth.getPastLogs({
fromBlock: 20000000n,
toBlock: 20001000n,
topics: [TRANSFER, null, padded]
});
To get tokens sent by the address, put the padded value in the second topic position instead. Run both queries and merge the results.
from web3 import Web3
w3 = Web3(Web3.HTTPProvider("YOUR_RPC_URL"))
target = Web3.to_checksum_address("0xYourAddress")
def scan(start, end):
results = []
for n in range(start, end + 1):
block = w3.eth.get_block(n, full_transactions=True)
for tx in block["transactions"]:
if tx["from"] == target or tx["to"] == target:
receipt = w3.eth.get_transaction_receipt(tx["hash"])
results.append({
"hash": tx["hash"].hex(),
"block": n,
"value_wei": tx["value"],
"status": receipt["status"],
"gas_used": receipt["gasUsed"],
})
return results
TRANSFER = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
padded = "0x" + target[2:].lower().zfill(64)
logs = w3.eth.get_logs({
"fromBlock": 20000000,
"toBlock": 20001000,
"topics": [TRANSFER, None, padded],
})
const res = await fetch('https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0', id: 1,
method: 'alchemy_getAssetTransfers',
params: [{
fromBlock: '0x0', toBlock: 'latest',
fromAddress: '0xYourAddress',
category: ['external', 'erc20', 'erc721', 'erc1155'],
withMetadata: true
}]
})
});
const { result } = await res.json(); // result.pageKey for the next page
Pricing and limits change often, so confirm current figures on each vendor's pricing page. The table below sticks to stable, qualitative differences.
| Factor | Etherscan API | Alchemy | QuickNode |
|---|---|---|---|
| Core approach | Explorer-style REST endpoints by address | Node RPC plus enhanced methods such as alchemy_getAssetTransfers | Node RPC plus marketplace add-ons and token/NFT APIs |
| Ease of "history by address" | Very high (txlist, txlistinternal, tokentx) | High (single transfers call) | Medium (depends on add-on) |
| Internal transactions | Dedicated endpoint | — | Via trace methods or add-ons |
| Raw node access | No | Yes | Yes |
| Multichain | — | Separate endpoint per network | Separate endpoint per network |
| Pagination limit | — | pageKey cursor | Cursor or page, depends on method |
| Billing unit | Request-based tiers | Compute units per method | API credits per method |
| Best for | Quick wallet lookups, prototypes, reports | Apps needing mixed RPC and history | Teams already using QuickNode endpoints or add-ons |
| Your situation | Pick | Reason |
|---|---|---|
| One-off export for a few wallets | Etherscan API | Simplest endpoints, no infrastructure |
| Wallet app showing activity feed | Alchemy transfers call | One call covers native, token and NFT transfers |
| Need raw traces, custom filters, own parsing | QuickNode or Alchemy RPC plus your own code | Full control over what you decode |
| Thousands of wallets, repeated queries | Own indexer | Per-request pricing becomes the main cost |
| Private or custom chain | Block scanning or own indexer | Hosted indexers may not support it |

Reading history costs no gas. Gas applies only when a transaction is written to the chain. But the history you read is full of gas data, and getting it wrong breaks accounting. Gas is the unit measuring computational effort on Ethereum, and the fee is gas consumed multiplied by the price per unit.
gasUsed x effectiveGasPrice, both from the receipt. Do not use the transaction's gas field (the limit) or maxFeePerGas (the cap).For background on fees, see the Ethereum gas fees explained guide.
This article does not publish benchmark figures, because latency depends on region, plan, chain and block range, and numbers copied from vendor marketing do not transfer to your workload. What can be said reliably is how the methods scale:
| Approach | Request count growth | Main bottleneck |
|---|---|---|
| Block scan with receipts | Linear in blocks, plus one per matching transaction | Round trips and provider rate limits |
| eth_getLogs by topic | Linear in number of block-range chunks | Provider range or result caps |
| Explorer or enhanced API | Linear in pages | Vendor rate limits and pagination caps |
| Own indexer | One-time ingest, then database queries | Storage and ingest throughput |
Practical optimizations that hold across providers:
eth_getLogs errors on range or result size, halve the range and retry.A wallet address works on every EVM chain, but each chain keeps a separate ledger. A unified history means querying each network and merging the results.
Block bodies include transactions that reverted. Only the receipt tells you the outcome, via status (1 or 0).
isError field, so filter client-side.For wider wallet hygiene, read the crypto wallet security guide.
| Use case | Data needed | Typical method |
|---|---|---|
| Portfolio tracker | Token transfers, current balances, prices | Enhanced API plus price feed |
| Tax reporting | All categories, fees, timestamps, fiat prices at execution time | Explorer or enhanced API, per chain; see crypto tax reporting tools |
| Compliance and investigations | Counterparty graph, internal transactions | Trace data and own indexer |
| Wallet activity feed | Recent transfers, decoded method names | Enhanced API with pagination |
| Trading research | Whale wallet flows, DEX swaps | Log queries and indexer; see on-chain analysis basics |
| DeFi position audit | Contract interactions, approvals | Block scan or logs filtered by contract |

Several errors recur when people first build this. Missing token activity almost always means the code read only the normal transaction list; ERC-20 transfers sit in logs, and Etherscan-style APIs expose them on a separate token endpoint. Missing ETH received through a swap or a multisig payout means internal transactions were not queried. In Web3.js v4, JSON.stringify throws on bigint values, so convert to strings first. A result-size or block-range error from eth_getLogs means you must shrink the range. Mixed-case addresses fail comparisons, so lowercase or checksum consistently. Finally, amounts are in the token's smallest unit: divide by 10^decimals, and read decimals from the token contract rather than assuming 18. As a trade-off, hosted APIs save engineering time but tie you to vendor limits and pricing, while a self-hosted indexer removes limits but adds operations work and reorg handling. For a broader view of that infrastructure trade-off, see our DeFi hub, and data from CoinDesk and similar outlets can help you track which chains are worth supporting.
It is the public on-chain record of transactions linked to a wallet address: native transfers, contract calls, token and NFT transfers, plus their fees and outcomes. Anyone can read it, and it cannot be edited.
Scan blocks with full transaction bodies and filter by sender or recipient, then fetch receipts for status and gas. For tokens, query Transfer event logs with the address as an indexed topic. For large ranges, use an indexed API instead.
Yes. Reading needs only a public address. The risks are privacy (your provider sees what you query) and scams that ask for private keys or seed phrases. Never share either.
Nodes store blocks and receipts without an address index, and the standard JSON-RPC has no method for it. You need to scan, query logs, use an indexer or use a vendor API.
Token transfers are emitted as events in receipt logs, not stored as normal transactions. Query logs or the token transfer endpoint of your API. Internal transactions also need a separate endpoint or trace data.
Etherscan suits fast address lookups and reports. Alchemy's transfers call suits apps needing one-call history. QuickNode suits teams wanting node access with add-ons. At large scale, an own indexer often costs less.
Related reading: More crypto articles and the complete DeFi guide.
Disclosure: Pro Trader Daily and xpertrax.com are operated by the same owner. Links and pop-ups pointing to xpertrax.com promote our own products. This article is for information only and is not financial advice.