eth_getLogs vs Token Transfers API: ERC-20 transfer history
Choose eth_getLogs for contract event logs or the Token Transfers API for indexed ERC-20 transfer history. Compare block ranges, pagination, coverage and finality.
For wallet history or ERC-20 transfer reconciliation, start with the Token Transfers API. Use eth_getLogs when you need contract event logs. Developers and AI agents can query indexed address transfers through the same blockchain data API. The wallet assets guide combines token balances, transfer history and metadata; the Data API reference defines request parameters and response schemas.
Tasks this guide helps you complete
- Query contract event logs through authenticated RPC in bounded block ranges for monitoring or log backfill.
- Query indexed ERC-20 transfer history through the blockchain data API by address or token contract, with cursor pagination and coverage checks.
Two ways to read logs and transfers
eth_getLogs is a JSON-RPC method: it returns block logs through the JSON-RPC endpoint. The Data API exposes token transfer history through two chain-scoped endpoints:
GET /{chain}/addresses/{address}/transfers— transfers involving an address.GET /{chain}/tokens/{token}/transfers— transfers for a single token contract.
Both use the same API key and are metered in CU by method weight (see the weights below). Which one fits depends on how recent the data is, whether you need a block window, and how you paginate.
Limits that apply to eth_getLogs
eth_getLogs is bounded by per-chain limits that the public GET /v1/chains response publishes:
- Block span:
max_logs_block_rangeis the maximum number of blocks a singleeth_getLogsrequest may span. It differs by chain — read it fromGET /v1/chains(chains are listed on Supported Chains) instead of hardcoding it. A wider range is rejected with JSON-RPC error-32602 eth_getLogs block range too large(not billed). - Node sync: while a chain's node is not synced,
eth_getLogsreturns-32010(not billed). - State window: the state window that
GET /v1/chainsreports asstate_window_blocksapplies to state-reading methods such aseth_callandeth_getBalance, not toeth_getLogs. - Node pruning: block and log reads are not limited by the state window, but they are limited by the node's retained history. Data that has been pruned returns
4444 pruned history unavailable(not billed).
When the fromBlock and toBlock filter fields are omitted or null, they default to latest.
For a history scan that exceeds one request, follow the eth_getLogs block-range guide to split the inclusive block window and advance only after each chunk succeeds.
Calling eth_subscribe over HTTP returns -32601 method not available. On chains where ws is true in /v1/chains, eth_subscribe is available over WebSocket (see Supported Chains); otherwise, poll eth_getLogs over the newest blocks.
What the Data API transfers endpoints provide
The two endpoints require different parameters:
| Endpoint | standard | Block window |
|---|---|---|
GET /{chain}/addresses/{address}/transfers | Required: erc20 or erc721. erc1155 returns 422 no_coverage | from_block and to_block are both required. Results are ordered by (block_number, log_index) descending. direction (in, out, or any; default any) filters by direction, and token optionally restricts the results to one contract. |
GET /{chain}/tokens/{token}/transfers | Required: erc20, erc721, or erc1155 | from_block and to_block are optional. An absent to_block defaults to as_of_block; an explicit to_block or from_block above it is a hard 409 not_indexed_yet, with no clamp escape. |
Pagination
Both endpoints are keyset-paginated:
limitdefaults to 50; values above 500 are clamped to 500, and0or a non-integer returns400 bad_request.- A page with
limitrows may includenext_cursoreven when no further rows remain. Follow each returned cursor until the key is absent; the final page may contain an emptydataarray. When there is no cursor, the key is omitted, nevernull. - Pass the returned value back as
cursor, unchanged, to fetch the next page. A cursor is valid only for the chain, endpoint, and query parameters that issued it.
Coverage and finality
For GET /{chain}/tokens/{token}/transfers, coverage is scoped to the token-transfer dataset of the requested standard. Its lower bound is computed from that dataset and can sit above the chain's coverage.from_block when the dataset's recorded build coverage starts later. A window entirely before that lower bound returns 422 no_coverage. A window that starts before it, or omits from_block, is served as far as the dataset goes and reports meta.coverage = "partial". meta.as_of_block is the newest fully written block (the write watermark), not proof that every block between the lower bound and it was indexed. The address endpoint still requires both from_block and to_block; do not apply this token-endpoint lower-bound rule to every transfers route. See Supported Chains for which chains expose the dataset.
During an ascending historical backfill, a gap between the backfilled tail and the live start is not detected: a window inside that gap can still return 200 with an empty data array. None of HTTP success, an empty result, exhausting every cursor, or a coverage field alone proves that an entire range is complete. Validate completeness against the data you expect or your own reconciliation.
Each transfer item contains token, standard, from, to, block_number, block_timestamp, tx_hash, tx_index, and log_index. ERC-20 items add amount; ERC-721 items add token_id; ERC-1155 items add operator, token_id, value, and batch_index.
Which one to use
| Typical task | Better fit | Why |
|---|---|---|
| Events in the most recent few hundred blocks | eth_getLogs | One request can cover a recent range as long as it does not exceed that chain's max_logs_block_range. |
| An address's historical transfers | GET /{chain}/addresses/{address}/transfers | Address-scoped query with a from_block/to_block window, direction and token filters, and cursor pagination; results serve up to as_of_block. |
| All transfers of a token | GET /{chain}/tokens/{token}/transfers | Token-contract-scoped query covering erc20, erc721, and erc1155, with an optional block window and cursor pagination across the range the dataset serves. |
| Live monitoring of new events | eth_subscribe (WebSocket chains) / eth_getLogs (polling) | Subscribe to new heads or logs over WebSocket where supported, or poll recent block ranges. |
Querying logs with eth_getLogs
export BLOCKVECTRA_API_KEY=rgw_your_api_key
# fromBlock / toBlock default to latest. Set an explicit recent range to follow
# new events, and keep its span within the chain's max_logs_block_range.
curl -s "https://api.blockvectra.com/v1/robinhood_mainnet" \
-H 'Content-Type: application/json' \
-H "x-api-key: $BLOCKVECTRA_API_KEY" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_getLogs",
"params": [{
"address": "0x1111111111111111111111111111111111111111",
"fromBlock": "latest",
"toBlock": "latest"
}]
}'Querying transfers with the Data API
export BLOCKVECTRA_API_KEY=rgw_your_api_key
# from_block / to_block are optional here; omitting to_block defaults to as_of_block.
curl -s "https://api.blockvectra.com/v1/data/robinhood_mainnet/tokens/0x1111111111111111111111111111111111111111/transfers?standard=erc20" \
-H "x-api-key: $BLOCKVECTRA_API_KEY"To query by address instead, from_block and to_block are required:
# clamp=true truncates a too-wide window, or a to_block above as_of_block,
# instead of returning 409.
curl -s "https://api.blockvectra.com/v1/data/robinhood_mainnet/addresses/0x1111111111111111111111111111111111111111/transfers?standard=erc20&from_block=0&to_block=73000000&direction=any&clamp=true" \
-H "x-api-key: $BLOCKVECTRA_API_KEY"CU per call
Every method is billed by its CU weight. The weights below are read from the platform plans API:
CU weight per call
| Method | CU per call |
|---|---|
eth_getLogs | 30 |
data.address_transfers | 25 |
data.token_transfers | 25 |
For current prices and top-up options, see the Pricing page.
Estimating calls and CU for a block window
Block windows are inclusive: N = to_block - from_block + 1. If each range request covers at most S blocks, a successful sweep takes at least ceil(N / S) requests. For example, 10,000,000 blocks at 5 blocks per request is 2,000,000 requests; at 10,000 blocks per request it is 1,000. These are arithmetic examples only, not a promise about any chain or provider limit.
Failures and retries add requests, and so do result-size limits. A smaller window does not automatically cost less: more calls can offset the narrower span, and CU is charged per call weight. Indexed transfers paginate by result, not by block count, so a sweep may need additional pages, including a possible final empty page; do not derive page count from the number of blocks. Multiply the per-call weights above by the expected number of billable calls to estimate CU, then check the Pricing page for current prices.
Next steps
- Browse the datasets directory to see every dataset BlockVectra indexes.
- See the free plan and pricing to check what your account includes.
- Log in to the console to create an API key.
Last updated:
Verify Agent Payments
Run a read-only Node.js script that checks one direct Priors LineKey spend receipt on Robinhood Chain mainnet against your invoice: pinned contracts, Paid, Borrowed and USDG Transfer logs, exact integer amounts.
eth_getLogs block range
With an API key, one eth_getLogs request may span 1,000 blocks on HyperEVM; each chain publishes its own max_logs_block_range. On logs_range_too_large, split the inclusive block interval within that limit and advance only after the chunk succeeds.