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

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_range is the maximum number of blocks a single eth_getLogs request may span. It differs by chain — read it from GET /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_getLogs returns -32010 (not billed).
  • State window: the state window that GET /v1/chains reports as state_window_blocks applies to state-reading methods such as eth_call and eth_getBalance, not to eth_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:

EndpointstandardBlock window
GET /{chain}/addresses/{address}/transfersRequired: erc20 or erc721. erc1155 returns 422 no_coveragefrom_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}/transfersRequired: erc20, erc721, or erc1155from_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:

  • limit defaults to 50; values above 500 are clamped to 500, and 0 or a non-integer returns 400 bad_request.
  • A page with limit rows may include next_cursor even when no further rows remain. Follow each returned cursor until the key is absent; the final page may contain an empty data array. When there is no cursor, the key is omitted, never null.
  • 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 taskBetter fitWhy
Events in the most recent few hundred blockseth_getLogsOne request can cover a recent range as long as it does not exceed that chain's max_logs_block_range.
An address's historical transfersGET /{chain}/addresses/{address}/transfersAddress-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 tokenGET /{chain}/tokens/{token}/transfersToken-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 eventseth_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

MethodCU per call
eth_getLogs30
data.address_transfers25
data.token_transfers25

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

Last updated:

On this page