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/chains | What it controls | What to check |
|---|---|---|
state_window_blocks | How far back authenticated state reads such as eth_call, eth_getBalance, eth_getCode and eth_getStorageAt can query | With 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_blocks | Historical block references through the keyless public.url | Use only public.methods. For state reads, the smaller of the public history and the declared state window applies. |
max_logs_block_range | The number of blocks in one authenticated eth_getLogs request | Count 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.
| Chain | Chain slug | Authenticated 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 One | arb_mainnet | 6,000 | 1,000 | 1,000 | blocks, transactions, address_transactions, transfers, token_metadata, freshness |
| Base | base_mainnet | 10,000 | 1,000 | 1,000 | blocks, transactions, address_transactions, transfers, token_metadata, freshness |
| BNB Smart Chain | bsc_mainnet | 100 | 100 | 1,000 | blocks, transactions, address_transactions, transfers, token_metadata, freshness |
| Ethereum | eth_mainnet | 250,000 | 1,000 | 1,000 | blocks, transactions, address_transactions, transfers, token_metadata, freshness |
| Ethereum Sepolia | eth_sepolia | Not declared | 1,000 | 1,000 | blocks, transactions, address_transactions, transfers, token_metadata, freshness |
| HyperEVM | hyperevm_mainnet | Not declared | 1,000 | 1,000 | blocks, transactions, address_transactions, transfers, token_metadata, balances, holders, nfts, freshness |
| Polygon | polygon_mainnet | 126 | 126 | 1,000 | blocks, transactions, address_transactions, transfers, token_metadata, freshness |
| Robinhood Chain | robinhood_mainnet | 900 | 900 | 1,000 | blocks, transactions, address_transactions, transfers, token_metadata, balances, holders, nfts, dex_swaps, dex_prices, stocks, traces, freshness |
| Robinhood Chain Testnet | robinhood_testnet | 1,023 | 1,000 | 1,000 | Data 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:
| Field | Documented value or meaning |
|---|---|
| HTTP status | 200; inspect the JSON-RPC error even when HTTP succeeds |
error.code | -32011 |
error.message | Authenticated state-window errors describe the most recent supported block count; public-history errors can use a different message |
error.data.reason | state_window |
error.data.docs_url | Link to the error catalog's state_window explanation |
error.data.retryable | false: 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.
- eth_call method reference for call parameters and return encoding.
- eth_getLogs block range and chunked queries for event-log history.
- Wallet custom RPC setup for wallet connections and dedicated keys.
- Supported chains for network availability and CU pricing for method costs.
Last updated: