Concepts · 8 min read

What is a wallet history API?

A wallet history API answers one question: given an address, what has it done? Transactions, token transfers, balances, and holdings, keyed by the account rather than by block. That re-keying is the whole problem, because the chain is not stored that way. This guide shows why a plain RPC endpoint cannot produce it, then the real queries that assemble a complete history from logs and traces.

Updated 2026-06-04 · By the SQD team

1. What is a wallet history API?

A wallet history API is a read interface keyed by address. The unit of the query is an account, and the response is everything that account was involved in: outbound and inbound transactions, token transfers, the balances it holds now, what it held at a past block, and the tokens and NFTs it owns. The activity feed and portfolio total in any wallet app are rendered from one.

The defining property is the access pattern. Blockchains record data by block and by transaction; a wallet history API re-organizes that same data so it can be retrieved by address. Everything below follows from that one mismatch between how the chain stores data and how the product needs to read it.

2. Why an RPC endpoint cannot serve it

A node's JSON-RPC interface is keyed by block and transaction. There is no standard eth_getTransactionsByAddress method, because the node does not maintain an address-to-transaction index. To answer "everything this address did" from RPC alone, you would have to scan every block since genesis and filter, which is not something you can do inside a single request.

eth_getLogs helps a little: you can filter event logs by topic, which surfaces ERC-20 and ERC-721 transfers that reference an address. But logs do not include native value transfers, do not include internal transactions, and still leave you assembling a coherent timeline yourself. The practical answer is to build the index ahead of time: ingest the chain once, store it keyed by address, and serve lookups in milliseconds. That is the indexer pattern, and the trade-off against calling RPC live is covered in RPC vs indexed data.

3. The queries that assemble a history

A complete history is the union of three data types, each answering part of "what touched this address." Here is how each looks against SQD's Portal, which lets you filter all three by address.

Token transfers. ERC-20 and ERC-721 both emit a Transfer event with the sender in topic1 and the recipient in topic2, each padded to 32 bytes. To capture both directions, run two log filters (an array of filters is OR logic) and leave out the contract address so it spans every token:

POST https://portal.sqd.dev/datasets/ethereum-mainnet/stream
Accept: application/x-ndjson

{
  "type": "evm",
  "fromBlock": 0,
  "toBlock": 21000000,
  "logs": [
    { "topic0": ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"],
      "topic1": ["0x000000000000000000000000ABCDEF...the address...padded"] },
    { "topic0": ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"],
      "topic2": ["0x000000000000000000000000ABCDEF...the address...padded"] }
  ],
  "fields": {
    "block": { "number": true, "timestamp": true },
    "log": { "address": true, "topics": true, "data": true, "transactionHash": true }
  }
}

The log's address in the response is the token contract; the amount is in data for ERC-20 and is the indexed tokenId for ERC-721. The two standards share a topic0 and are told apart by topic count, a detail covered in what an EVM indexer handles. ERC-1155 uses separate events and needs its own filters.

Native and internal value. This is the part logs cannot give you. ETH moved to or from the address, including value a contract forwarded mid-execution, lives in traces, not logs. Query the traces data type filtered by callFrom and callTo:

{
  "type": "evm",
  "fromBlock": 0,
  "toBlock": 21000000,
  "traces": [
    { "type": ["call"], "callFrom": ["0x...the address..."] },
    { "type": ["call"], "callTo": ["0x...the address..."] }
  ],
  "fields": {
    "block": { "number": true, "timestamp": true },
    "trace": { "type": true, "transactionIndex": true, "callFrom": true, "callTo": true, "callValue": true }
  }
}

callValue is the native value moved at each hop. An address that only ever received ETH through a contract (a withdrawal, a disbursement) shows nothing in the logs but appears here. The third piece, top-level transactions where the address is from or to, is a transactions query with the same address filter. Union the three by transaction hash and you have the complete timeline.

4. Balances, current and historical

Once the transfer history above is indexed, balances fall out of it. A token balance is the sum of every transfer in minus every transfer out for that address and token; the native balance is the same arithmetic over the value-bearing traces and transactions. Because the history is already keyed by address, this is a query against your own store, not a fresh scan of the chain.

Historical balances ("what did this address hold last March") are the same sum truncated at a block height, which is why a transfer-derived balance is more flexible than a live balanceOf call: the call only tells you the balance now. Reading balanceOf directly is still useful as a reconciliation check, but doing it for every token an address might hold is expensive, so production wallet APIs compute from transfers and verify against state selectively.

5. Where it gets hard

  • Trace coverage. The native-and-internal query above is the one most data sources skip. A feed built on logs alone returns a history that silently omits contract-forwarded value, which is a correctness bug, not a gap in coverage.
  • Multi-chain aggregation. Users hold assets across many networks but want one feed. That means running the same log and trace queries against each dataset and normalizing the results, the problem described in multi-chain indexing.
  • Token metadata and spam. There are thousands of tokens, many airdropped purely to appear in wallets. Useful history needs metadata resolution and a way to separate real holdings from spam.
  • High-activity addresses. Exchange and contract addresses can have millions of transfers. Pagination, ordering, and query performance stop being trivial at that volume.
  • Standard sprawl. ERC-20, ERC-721, and ERC-1155 differ in event shape, and some tokens deviate from the standards. A complete feed handles all three plus the exceptions.

6. Wallet history with SQD

The three queries above are the actual interface. SQD's Portal lets you filter logs, traces, and transactions by address in the same request shape, so the data a complete history needs (including the internal value a log-only source misses) is reachable in one place rather than stitched from several providers.

Multi-chain is a one-line change: point the same queries at base-mainnet or arbitrum-one instead of ethereum-mainnet, across the networks listed at sqd.dev/chains. For a product you would not query the Portal per request; the Squid and Pipes SDKs stream this data into your own address-indexed store so lookups return in milliseconds. The wallets and payments solution page walks through that setup.

Frequently asked questions

Can I get wallet history from an RPC endpoint?
Not directly. A node's JSON-RPC is keyed by block and transaction, not by address, and there is no standard method that returns "every transaction for this address." You can filter event logs by topic to find token transfers touching an address, but that misses native transfers and internal transactions and does not give a clean unified timeline. A wallet history API solves this by indexing the chain ahead of time and keying it by address, so a lookup returns in milliseconds instead of scanning the chain at request time.
How do I get all token transfers for an address?
ERC-20 and ERC-721 Transfer events both put the sender in topic1 and the recipient in topic2 (each padded to 32 bytes). To capture transfers in both directions you run two log filters on the Transfer topic0: one matching topic1 (outgoing) and one matching topic2 (incoming), with no contract address filter so it spans every token. ERC-1155 uses separate TransferSingle and TransferBatch events and needs its own filters.
How do internal transactions show up in wallet history?
Internal transactions are value movements that happen inside a contract during execution, for example a contract forwarding ETH to an address. They do not appear in the transaction list or in event logs, only in execution traces. A complete wallet history queries the traces data type filtered by callTo and callFrom for the address, which is the only way to capture contract-forwarded native value. A log-only source returns an incomplete history that silently omits it.
How do I get an address's token balances?
Balances are derived from indexed transfer history: sum every transfer in and out of the address per token. The same history makes historical balances possible by summing only up to a chosen block. Current balances can also be read directly from contract state (an ERC-20 balanceOf call), but doing that for every token an address might hold is expensive, which is why most wallet APIs compute balances from transfers.
Can one API return history across multiple chains?
Yes, if the underlying data is indexed per chain and normalized into a shared shape. With SQD the same log and trace queries run against every dataset (ethereum-mainnet, base-mainnet, arbitrum-one, and the rest), so a multi-chain wallet history is the same queries pointed at several datasets and merged. The work is normalization: the same concept has different event shapes on different chains.

Building a wallet or payments product?

See how full address history, balances, and transfers come together across networks on the wallets and payments solution page.