Query Historical EVM State Within Supported Windows

Distinguish authenticated state windows, keyless history and log spans. Choose a fixed block for eth_call and diagnose state_window errors.

Historical eth_call depends on the chain's state window, not its eth_getLogs block-range limit. Check the state window, the endpoint's authentication mode and the target block before reading an earlier contract value.

Three different historical limits

Field in GET /v1/chainsWhat it controlsWhat to check
state_window_blocksHow far back authenticated state reads such as eth_call, eth_getBalance, eth_getCode and eth_getStorageAt can queryWith head H and declared window W, a numbered block older than H − W is outside the window. Check methods.allow and methods.deny too.
public.history_blocksHistorical block references through the keyless public.urlUse only public.methods. For state reads, the smaller of the public history and the declared state window applies.
max_logs_block_rangeThe number of blocks in one authenticated eth_getLogs requestCount toBlock − fromBlock + 1. A permitted span does not establish that old contract state or logs are available.

These limits are in blocks, not days. A null or undeclared state window does not establish archive coverage. Keyless method availability is separate from authenticated method availability: a log span alone does not enable public eth_getLogs.

Compare per-chain state windows

The table shows published state windows, keyless history, log spans and declared Data API datasets from the public snapshot. For a request now, read GET /v1/chains and GET /v1/status again.

Developers and AI Agents should check state windows and log query spans separately. A null state window does not establish archive coverage. Public history applies only to the declared public methods.

ChainChain slugAuthenticated state window: state_window_blocks (blocks)Keyless history: public.history_blocks (blocks)Authenticated log query span: max_logs_block_range (blocks)Declared Data API datasets
Arbitrum Onearb_mainnet6,0001,0001,000blocks, transactions, address_transactions, transfers, token_metadata, freshness
Basebase_mainnet10,0001,0001,000blocks, transactions, address_transactions, transfers, token_metadata, freshness
BNB Smart Chainbsc_mainnet1001001,000blocks, transactions, address_transactions, transfers, token_metadata, freshness
Ethereumeth_mainnet250,0001,0001,000blocks, transactions, address_transactions, transfers, token_metadata, freshness
Ethereum Sepoliaeth_sepoliaNot declared1,0001,000blocks, transactions, address_transactions, transfers, token_metadata, freshness
HyperEVMhyperevm_mainnetNot declared1,0001,000blocks, transactions, address_transactions, transfers, token_metadata, balances, holders, nfts, freshness
Polygonpolygon_mainnet1261261,000blocks, transactions, address_transactions, transfers, token_metadata, freshness
Robinhood Chainrobinhood_mainnet9009001,000blocks, transactions, address_transactions, transfers, token_metadata, balances, holders, nfts, dex_swaps, dex_prices, stocks, traces, freshness
Robinhood Chain Testnetrobinhood_testnet1,0231,0001,000Data API unavailable

GET /v1/chains · Sampled (UTC):

GET /v1/status · Sampled (UTC):

Choose a block tag

Use latest for the current value. For a historical comparison, read eth_blockNumber once and convert a chosen block number to a hexadecimal quantity such as 0x18efa2f. Keep that number fixed for every call in the comparison; repeated latest calls can use different blocks.

For state reads, earliest, safe and finalized return -32011 under the state-window policy. Choose an explicit block number within the declared window instead. A block-hash form is not a way to obtain additional history: keyless state reads reject it, and an authenticated request still depends on available state.

A block number may refer to a different block after a reorganization. Record the block hash with eth_getBlockByNumber if you need to identify the result's block. An in-window number also needs a synced chain and an existing contract at that height.

Fixed-block contract read

On Ethereum, WETH at 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 exposes decimals() with selector 0x313ce567. A keyless call sampled on 2026-10-08 (UTC) at block 0x18efa2f returned HTTP 200 with this result:

Request to the chain's public.url:

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "eth_call",
  "params": [
    { "to": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", "data": "0x313ce567" },
    "0x18efa2f"
  ]
}

Response:

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": "0x0000000000000000000000000000000000000000000000000000000000000012"
}

The ABI-encoded integer is 18. The result is a decimals value, not a balance, and does not establish availability at other heights. That fixed block will age out of a bounded window; use a recent block when running the following example later.

Save the example as historical-state.mjs and run node historical-state.mjs with Node.js 24 or later and your BLOCKVECTRA_API_KEY environment variable set. It uses the authenticated endpoint, keeps the same contract and calldata, and compares latest, one fixed recent block and a block outside the published authenticated window. Each output includes the actual HTTP status and JSON-RPC body; an HTTP 200 can still contain an error. It stops on an unexpected response instead of treating it as a successful read.

const key = process.env.BLOCKVECTRA_API_KEY;
if (!key) throw new Error('Set BLOCKVECTRA_API_KEY');
const chainsUrl = 'https://api.blockvectra.com/v1/chains';
const catalogResponse = await fetch(chainsUrl, { signal: AbortSignal.timeout(15_000) });
if (!catalogResponse.ok) throw new Error(`Chains HTTP ${catalogResponse.status}`);
const catalog = await catalogResponse.json();
const chain = catalog.chains.find(item => item.chain === 'eth_mainnet');
const matches = (method, pattern) => pattern.endsWith('*')
  ? method.startsWith(pattern.slice(0, -1)) : method === pattern;
if (!chain?.jsonrpc || !['eth_call', 'eth_blockNumber'].every(method =>
  chain.methods?.allow?.some(pattern => matches(method, pattern)) &&
  !chain.methods?.deny?.some(pattern => matches(method, pattern)))) {
  throw new Error('Required methods are unavailable');
}
const window = chain.state_window_blocks;
if (!Number.isSafeInteger(window) || window < 10) {
  throw new Error('This example needs a declared state window of at least 10 blocks');
}
const rpcUrl = new URL('./eth_mainnet', chainsUrl).href;
let id = 0;
async function rpc(method, params) {
  const response = await fetch(rpcUrl, {
    method: 'POST', redirect: 'error', signal: AbortSignal.timeout(15_000),
    headers: { 'Content-Type': 'application/json', 'x-api-key': key },
    body: JSON.stringify({ jsonrpc: '2.0', id: ++id, method, params }),
  });
  return { http: response.status, body: await response.json() };
}
const headResponse = await rpc('eth_blockNumber', []);
if (headResponse.http !== 200 || headResponse.body.error ||
    !/^0x[0-9a-f]+$/i.test(headResponse.body.result ?? '')) {
  throw new Error(`Cannot read head: ${JSON.stringify(headResponse)}`);
}
const head = BigInt(headResponse.body.result);
if (head <= BigInt(window)) throw new Error('Head is too low for an out-of-window block');
const hex = value => `0x${value.toString(16)}`;
const fixedBlock = hex(head - 10n);
const outsideBlock = hex(head - BigInt(window) - 1n);
const call = { to: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', data: '0x313ce567' };
for (const block of ['latest', fixedBlock, outsideBlock]) {
  const reply = await rpc('eth_call', [call, block]);
  console.log(JSON.stringify({ head: hex(head), block, ...reply }));
  if (block === outsideBlock) {
    if (reply.http !== 200 || reply.body.error?.code !== -32011 ||
        reply.body.error?.data?.reason !== 'state_window') {
      throw new Error('Expected state_window; inspect the actual response above');
    }
  } else if (reply.http !== 200 || reply.body.error ||
      reply.body.result !== '0x0000000000000000000000000000000000000000000000000000000000000012') {
    throw new Error('Expected the WETH decimals result; inspect the actual response above');
  }
}

The recorded response above uses public.url; the script uses an API key. To make a keyless read, take the URL directly from public.url, omit the key, and choose a block within public.history_blocks as well as the state window. Changing authentication can change the permitted history, even for the same contract and calldata.

Diagnose an out-of-window error

On the same keyless endpoint, a call sampled on 2026-10-08 (UTC) changing only the target block to 0x18ef650 (and the request ID) returned HTTP 200 with error.code: -32011, error.data.reason: state_window and error.data.retryable: false. Its message was block reference is outside the public history window. This is a public-history failure; the authenticated endpoint has its own state window.

Use these fields from the state_window error entry to recognize the failure rather than relying on a particular window number in the message:

FieldDocumented value or meaning
HTTP status200; inspect the JSON-RPC error even when HTTP succeeds
error.code-32011
error.messageAuthenticated state-window errors describe the most recent supported block count; public-history errors can use a different message
error.data.reasonstate_window
error.data.docs_urlLink to the error catalog's state_window explanation
error.data.retryablefalse: sending the same request later does not restore older state

Choose a newer numbered block or use latest if the task needs the current value. Reducing an eth_getLogs span does not recover historical eth_call state. Other -32011 reasons have different actions: range_not_indexed requires a covered range; history_not_ready allows retry after indexing catches up. Inspect error.data.reason, not only the numeric code.

Underlying state can also be unavailable with -32000, or pruned block history with 4444; see the error catalog. Do not retry an old block unchanged or assume a larger declared window guarantees every response.

Choose the next query

For a complete workload checklist and self-tests, start with How to choose an RPC provider.

When choosing a provider for repeated contract reads, compare daily and cycle budgets for EVM reads. Check the required historical blocks first, then plan the task's daily distribution and throughput; fitting a credit budget does not establish state coverage.

When comparing providers for historical reads, first confirm that both can serve the target block. The full-request overage comparison compares extra RU prices with method-based costs, separates included quota from extra usage, and explains full versus archive billing classes.

For earlier indexed blocks, transactions, transfers or other datasets, check the table's declared Data API datasets and the Data API reference. Indexed records do not provide arbitrary historical contract execution or imply that every chain has historical balances.

Last updated:

On this page