HyperEVM public RPC rate limit and eth_getLogs block range

HyperEVM public RPC: the official endpoint allows 100 requests per minute per IP and 50 blocks per eth_getLogs; BlockVectra's keyless endpoint allows 3 requests per second per IP, and eth_getLogs with a key covers up to 1,000 blocks. Split longer windows and save a cursor.

Direct answer

HyperEVM public RPC limits depend on the endpoint. The official public RPC (rpc.hyperliquid.xyz/evm) allows 100 EVM JSON-RPC requests per minute per IP and 50 blocks per eth_getLogs query (source: Hyperliquid official rate-limit documentation and JSON-RPC documentation, accessed 2026-10-11). BlockVectra's keyless HyperEVM endpoint allows 3 requests per second per IP with a burst of 20 and does not serve eth_getLogs; with an API key, eth_getLogs covers up to 1,000 blocks per query, counting both endpoints (hyperevm_mainnet.max_logs_block_range and public.rate_limit from GET /v1/chains). Checked . A wider span returns HTTP 200, JSON-RPC -32602 and logs_range_too_large, with retryable: false (see the error catalog); split into [from, min(from + max − 1, end)], save your cursor, and advance to the end plus one after success to resume runs. Handling of 429 responses is in Official public RPC rate limits and 429 and the service parameters below.

  • First step: Read the latest block without an API key using the curl command below.
  • Done when: The backfill script outputs fromBlock, toBlock and a result array for every chunk in your chosen window; an empty array means no matching logs in that chunk.

Chain parameters and access options.

Tasks this guide helps you complete

  • Probe HyperEVM RPC with a public read using viem or ethers before selecting authenticated methods.
  • Backfill a bounded log window within the HyperEVM eth_getLogs limit, with retry decisions based on the returned error.
  • Read address activity through indexed transactions and transfers with a key, checking the returned coverage and freshness metadata.

Three-step task: backfill a bounded HyperEVM log window

Read the latest block without a key, create a key, then retrieve event logs for a contract over a finite block window.

Choose the contract and block window you need. This task covers that bounded window; it does not promise a complete contract history.

1. Read the latest block without an API key

curl -sS "https://api.blockvectra.com/v1/hyperevm_mainnet/public" \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}'

The JSON-RPC result is the latest block number in hexadecimal. This is the HyperEVM public.url published by GET /v1/chains. The public endpoint's public.methods does not include eth_getLogs; step 3 requires a key.

2. Create an API key

Create a key for this backfill. Create a key and save the secret shown in the dialog for use with hyperevm_mainnet.

For an AI Agent using HTTP without a browser, follow the Programmatic sign-up guide. Pass the guide URL's valid ref in the POST /auth/siwe/login JSON body instead of the example's docs-signup; omit it if unavailable. Do not ask the user to paste the key into chat.

3. Backfill logs with your key

Complete starter template: blockvectra/hyperevm-backfill

Save the following script as hyperevm-task.ts. It runs with Node.js 24 or later, with no additional packages. Set BLOCKVECTRA_API_KEY to your saved key and LOG_ADDRESS to the emitting contract address you want to inspect; keep the key on your server or in a local terminal.

export BLOCKVECTRA_API_KEY='replace-with-your-key'
export LOG_ADDRESS='replace-with-contract-address'
node hyperevm-task.ts

By default, the script retrieves the most recent max_logs_block_range blocks, or fewer near genesis. It reads that limit from /v1/chains at runtime. To select another finite window, set both FROM_BLOCK and TO_BLOCK to decimal or 0x hexadecimal block numbers before running. Larger windows are divided into consecutive chunks, each at most the published limit.

const apiKey = process.env.BLOCKVECTRA_API_KEY;
const address = process.env.LOG_ADDRESS;
if (!apiKey || apiKey === "replace-with-your-key") throw new Error("Set BLOCKVECTRA_API_KEY");
if (!address || !/^0x[0-9a-f]{40}$/i.test(address)) throw new Error("Set LOG_ADDRESS to a contract address");

const fromBlock = process.env.FROM_BLOCK;
const toBlock = process.env.TO_BLOCK;
if ((fromBlock !== undefined || toBlock !== undefined) && (!fromBlock || !toBlock)) {
  throw new Error("Set both FROM_BLOCK and TO_BLOCK");
}

const chainsUrl = "https://api.blockvectra.com/v1/chains";
const rpcUrl = new URL("./hyperevm_mainnet", chainsUrl).href;
async function readJson(url: string) {
  const response = await fetch(url, { signal: AbortSignal.timeout(15_000) });
  if (!response.ok) throw new Error(`Metadata HTTP ${response.status}`);
  return response.json();
}
const catalog = await readJson(chainsUrl);
const chain = catalog.chains.find((item: { chain: string }) => item.chain === "hyperevm_mainnet");
if (!chain || !Number.isSafeInteger(chain.max_logs_block_range) || chain.max_logs_block_range <= 0) {
  throw new Error("Missing or invalid max_logs_block_range");
}
if (!chain.methods.allow.includes("eth_getLogs") || chain.methods.deny.includes("eth_getLogs")) {
  throw new Error("eth_getLogs is unavailable on this chain");
}
const maxRange = BigInt(chain.max_logs_block_range);
const plans = await readJson("https://console-api.blockvectra.com/v1/plans");
function weight(method: string): bigint {
  const row = plans.method_weights.find((item: { method: string }) => item.method === method);
  if (!row || !Number.isSafeInteger(row.cu_weight) || row.cu_weight <= 0) {
    throw new Error(`Missing or invalid CU weight for ${method}`);
  }
  return BigInt(row.cu_weight);
}
const headWeight = weight("eth_blockNumber");
const logsWeight = weight("eth_getLogs");

type RpcBody = {
  result?: unknown;
  error?: { code: number; data?: { retryable?: boolean } };
};
async function rpc(method: string, params: unknown[]): Promise<unknown> {
  for (let attempt = 0; attempt < 4; attempt++) {
    const response = await fetch(rpcUrl, {
      method: "POST",
      redirect: "error",
      headers: { "Content-Type": "application/json", "x-api-key": apiKey! },
      body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
      signal: AbortSignal.timeout(15_000),
    });
    const body = await response.json() as RpcBody;
    if (response.ok && !body.error && body.result !== undefined) return body.result;
    if (body.error?.data?.retryable !== true || attempt === 3) {
      throw new Error(`RPC failed: HTTP ${response.status}, code ${body.error?.code ?? "unknown"}`);
    }
    const retryAfter = response.headers.get("Retry-After");
    const delay = retryAfter === null ? 0 : /^\d+$/.test(retryAfter)
      ? Number(retryAfter) * 1000 : Date.parse(retryAfter) - Date.now();
    if (delay > 30_000) throw new Error("Retry-After exceeds 30 seconds; rerun later");
    const backoff = 1000 * 2 ** attempt + Math.random() * 250;
    await new Promise((resolve) => setTimeout(resolve, Math.max(backoff, Number.isFinite(delay) ? delay : 0)));
  }
  throw new Error("Retry limit reached");
}
function block(value: string): bigint {
  if (!/^(0x[0-9a-f]+|[0-9]+)$/i.test(value)) throw new Error("Invalid block number");
  return BigInt(value);
}
const head = await rpc("eth_blockNumber", []);
if (typeof head !== "string" || !/^0x[0-9a-f]+$/i.test(head)) throw new Error("Invalid chain head");
const latest = block(head);
const end = toBlock ? block(toBlock) : latest;
const start = fromBlock ? block(fromBlock)
  : end >= maxRange - 1n ? end - maxRange + 1n : 0n;
if (start > end || end > latest) throw new Error("Require 0 <= FROM_BLOCK <= TO_BLOCK <= latest");
const chunks = (end - start + maxRange) / maxRange;
console.error(`Window ${start}..${end}; chunks=${chunks}; estimated CU=${headWeight + chunks * logsWeight}`);

const hex = (value: bigint) => `0x${value.toString(16)}`;
for (let from = start; from <= end; from += maxRange) {
  const to = from + maxRange - 1n < end ? from + maxRange - 1n : end;
  const result = await rpc("eth_getLogs", [{ address, fromBlock: hex(from), toBlock: hex(to) }]);
  if (!Array.isArray(result)) throw new Error("Expected eth_getLogs result array");
  console.log(JSON.stringify({ fromBlock: hex(from), toBlock: hex(to), result }));
}

Requests run sequentially. A JSON-RPC error is retried only when error.data.retryable is true, with at most four attempts per request, exponential backoff and jitter, and support for Retry-After seconds or an HTTP date. A wait longer than 30 seconds stops the script so you can rerun later. Network failures, timeouts, malformed responses, and non-retryable errors stop immediately; the script exits unsuccessfully rather than reporting a complete backfill.

Each standard-output line contains one chunk's fromBlock, toBlock, and result array. result: [] means no matching logs in that chunk. Read these fields in each log:

FieldMeaning
addressContract that emitted the event.
blockNumber, blockHashBlock containing the log; the number is hexadecimal.
transactionHash, transactionIndex, logIndexTransaction and log position; indices are hexadecimal.
topics, dataIndexed event arguments and ABI-encoded non-indexed arguments; decode with the contract ABI.
removedWhether the log was removed by a chain reorganization.

The latest block is not a finality marker. If you need a stable historical window, choose your application's confirmed TO_BLOCK and handle chain reorganizations.

For a window of B = TO_BLOCK − FROM_BLOCK + 1 blocks and published limit L, the chunk count is N = ceil(B / L). Read method_weights[].cu_weight for eth_getLogs and eth_blockNumber from GET /v1/plans. The script prints an estimate to standard error: N × weight(eth_getLogs) + weight(eth_blockNumber), including its keyed head lookup. This excludes extra calls and any billable retries; see billing rules for settlement. CU depends on calls, rather than the number of logs returned.

Event delivery: Use chunked HTTP polling below, or send watched-address events to an HTTPS receiver with webhook push. GET /v1/push/chains lists supported chains and confirmation settings; authenticate with x-api-key. Webhook signatures, deduplication and replay are covered in that guide. Webhook push is separate from WebSocket subscriptions (ws and subscriptions in /v1/chains).

Connect with viem or ethers

Parameter / EndpointValue / TemplateAuthentication
Chain ID (EIP-155)999—
JSON-RPC (path key)POST https://api.blockvectra.com/v1/hyperevm_mainnet/{api_key}API key in URL path
JSON-RPC (header key)POST https://api.blockvectra.com/v1/hyperevm_mainnetHeader x-api-key: {api_key}
WebSocket (path key)wss://api.blockvectra.com/v1/hyperevm_mainnet/{api_key}API key in URL path
WebSocket (header key)wss://api.blockvectra.com/v1/hyperevm_mainnetHeader x-api-key: {api_key} or Authorization: Bearer {api_key}
WebSocket subscriptionsnewHeads, logs—
Data API baseGET https://api.blockvectra.com/v1/data/hyperevm_mainnet/…Header x-api-key: {api_key}
Public statusGET https://api.blockvectra.com/v1/statusUnauthenticated (public)

Developers and AI Agents can use the same server-side settings. Use Node.js 24 or later, viem 2 or ethers 6, and start with public reads. Set BLOCKVECTRA_API_KEY securely in the environment for keyed methods. Keep keys and RPC URLs containing keys out of browser code, logs and version control.

Save this as network.mjs. It reads chain_id and method policy from GET /v1/chains. For keyless reads, use the catalog’s public.url and only methods listed in public.methods; public HTTP availability does not imply WebSocket access.

const chainSlug = process.env.BLOCKVECTRA_CHAIN ?? 'hyperevm_mainnet';
const key = process.env.BLOCKVECTRA_API_KEY;
const catalogUrl = 'https://api.blockvectra.com/v1/chains';
const response = await fetch(catalogUrl, { signal: AbortSignal.timeout(15_000) });
if (!response.ok) throw new Error(`Chains HTTP ${response.status}`);
const catalog = await response.json();
export const chainInfo = catalog.chains.find(item => item.chain === chainSlug);
if (!chainInfo || !Number.isSafeInteger(chainInfo.chain_id) || chainInfo.chain_id <= 0) {
  throw new Error('Missing chain or chain_id');
}
export function allows(method) {
  const matches = pattern => pattern.endsWith('*')
    ? method.startsWith(pattern.slice(0, -1)) : pattern === method;
  if (!key) return (chainInfo.public?.methods ?? []).some(matches);
  return (chainInfo.methods?.allow ?? []).some(matches)
    && !(chainInfo.methods?.deny ?? []).some(matches);
}
if (!allows('eth_chainId')) throw new Error('eth_chainId is unavailable');
export const rpcUrl = key
  ? new URL(`./${chainSlug}/${encodeURIComponent(key)}`, catalogUrl).href
  : chainInfo.public?.url;
if (!rpcUrl) throw new Error('Public RPC is unavailable; set BLOCKVECTRA_API_KEY');

Save as viem-client.mjs, install with npm install viem@2, then run node viem-client.mjs.

import { createPublicClient, defineChain, http } from 'viem';
import { chainInfo, rpcUrl } from './network.mjs';

export const chain = defineChain({
  id: chainInfo.chain_id,
  name: chainInfo.name,
  nativeCurrency: { name: 'HYPE', symbol: 'HYPE', decimals: 18 },
  rpcUrls: { default: { http: [rpcUrl] } },
});
export const client = createPublicClient({ chain, transport: http(rpcUrl) });
if (await client.getChainId() !== chain.id) throw new Error('RPC chain ID mismatch');
console.error(await client.getBlockNumber());

For ethers, save as ethers-client.mjs, install with npm install ethers@6, then run node ethers-client.mjs.

import { JsonRpcProvider } from 'ethers';
import { chainInfo, rpcUrl } from './network.mjs';

const provider = new JsonRpcProvider(rpcUrl, chainInfo.chain_id, { batchMaxCount: 1 });
const network = await provider.getNetwork();
if (network.chainId !== BigInt(chainInfo.chain_id)) throw new Error('RPC chain ID mismatch');
console.log(await provider.getBlockNumber());
provider.destroy();

Deploy with Foundry or Hardhat

The current catalog for ordinary keyed hyperevm_mainnet has ws=false and lists eth_sendRawTransaction in methods.allow; the public keyless endpoint does not. Check that the selected endpoint allows the send method before deploying. Set DEPLOY_RPC_URL to its authenticated HTTP URL. Do not assume another provider shares BlockVectra’s method or log-range limits. Check the selected chain ID before signing.

: "${DEPLOY_RPC_URL:?Set a broadcasting RPC URL}"
export RPC_URL="$DEPLOY_RPC_URL"
export CHAIN_ID="$(node --input-type=module -e "import { chainInfo } from './network.mjs'; console.log(chainInfo.chain_id)")"

Continue with the shared Foundry or Hardhat deployment tutorial. Fund the deployer with EVM HYPE and review the dual-block requirements below before a large deployment.

HYPE, small blocks and large deployments

HyperEVM’s official network guide identifies HYPE as gas, with 18 decimals (accessed: 2026-10-07). Ensure the deployer holds HYPE on HyperEVM; a HyperCore balance alone is not the EVM gas balance. Follow the linked native-transfer instructions when moving funds.

The dual-block guide describes fast small blocks and slower big blocks for larger transactions (accessed: 2026-10-07). Estimate deployment gas first. For deployments exceeding the small-block budget, the deployer must be an existing HyperCore user and sign the Core action {"type":"evmUserModify","usingBigBlocks":true}; setting a larger transaction gas limit alone does not select big blocks. Restore usingBigBlocks=false afterward to return to small blocks.

On a provider supporting them, use eth_usingBigBlocks to check the address mode and eth_bigBlockGasPrice for the big-block base fee. The official JSON-RPC reference documents these methods (accessed: 2026-10-07). Check the chosen provider’s methods; use /v1/chains for BlockVectra. The minimal deployment above targets a small contract and does not change Core account mode.

HyperCore and HyperEVM data

EVM RPC serves contracts, receipts and logs. HyperCore trading data and actions use the Core API. Contracts can read Core state through precompiles and send actions through CoreWriter; use the official interaction guide when integrating these paths (accessed: 2026-10-07). EVM logs do not replace Core order-book or position queries.

HyperEVM system transactions (such as HyperCore to HyperEVM transfers) are not included in standard eth_getBlockByNumber responses and are provided separately by the official RPC via eth_getSystemTxsByBlockNumber and eth_getSystemTxsByBlockHash (see the official JSON-RPC documentation, accessed: 2026-10-07). BlockVectra's HyperEVM block, transaction, and Data API data currently do not include system transactions; use these two official RPC methods directly when requiring system transaction data.

Handle official error 10055

The official HyperEVM guide defines 10055 as a Core/EVM boundary error, including nonce, insufficient-funds, duplicate-hash and underpriced-replacement failures (accessed: 2026-10-07). Inspect the message from the broadcasting RPC before deciding how to recover:

  • Nonce: compare eth_getTransactionCount with your pending transactions; serialize submissions from one deployer and reconcile its next nonce.
  • Funds: check the deployer’s EVM HYPE balance against value plus gas cost.
  • Duplicate hash: look up the existing transaction and receipt before submitting another transaction.
  • Replacement fee: verify the existing nonce and fee, then use the broadcaster’s replacement policy; repeating the same bytes does not raise the fee.

10055 alone does not justify blind retries. Read errors and their recovery guidance separately in the BlockVectra error reference.

Official public RPC rate limits and 429

Hyperliquid's official rate-limit documentation specifies at most 100 EVM JSON-RPC requests per minute per IP for rpc.hyperliquid.xyz/evm. Its JSON-RPC documentation also limits eth_getLogs to 50 blocks per query and up to 4 topics. Accessed: 2026-10-11.

On HTTP 429, pause requests and honor Retry-After first (seconds or an HTTP date). If absent, use exponential backoff with jitter and a bounded retry count, retrying the same unfinished chunk. Reduce concurrency and polling frequency, and split log queries into chunks within the endpoint's limit. Chunking alone does not remove rate limits; clients sharing an IP need to coordinate their request rate.

For BlockVectra's keyed endpoint, read hyperevm_mainnet's max_logs_block_range, methods.allow, and methods.deny from GET /v1/chains instead of applying the official public RPC's block span or requests-per-minute limit. Request rate is separately subject to the key's cu_per_sec, burst_cu, and the free-plan call limit (see the next section). On 429, inspect error.data.reason and retryable; request_exceeds_burst requires smaller requests rather than unchanged retries with backoff.

BlockVectra parameters and service rules

BlockVectra serves HyperEVM mainnet through JSON-RPC and REST Data API endpoints:

  1. Chain parameters and log limits: From GET /v1/chains for hyperevm_mainnet:
    • Chain identifier (Slug): hyperevm_mainnet, Chain ID 999.
    • max_logs_block_range: Governed by the max_logs_block_range field from GET /v1/chains. A single eth_getLogs request may span at most this number of blocks (toBlock − fromBlock + 1). Exceeding this span returns HTTP 200 with JSON-RPC error code -32602 (eth_getLogs block range too large: max <N> blocks), which is not billed.
    • state_window_blocks: Governed by the state_window_blocks field from GET /v1/chains. State-reading calls (such as eth_call and eth_getBalance) are subject to the retention window declared by this field (when null, full state is retained without a rolling window limit).
    • Method policy: Governed by methods.allow and methods.deny. Standard EVM methods (eth_blockNumber, eth_getLogs, eth_call, eth_getBalance, eth_getBlockByNumber, eth_getTransactionReceipt, etc.) are allowed; filter and subscription methods (eth_subscribe, eth_unsubscribe, eth_newFilter, eth_newBlockFilter) are denied, returning -32601 (not billed).
  2. Free tier rate limits and upgrading: From GET /v1/plans:
    • free.max_calls_per_sec: up to 25 calls per second, shared across all keys in the account, all chains, and the Data API.
    • Default key limits: Each API key has a CU bucket (cu_per_sec refill, burst_cu capacity — defaults are 100 CU/s and burst 400 CU; after a paid top-up each key is at least 2,000 CU/s and burst 10,000 CU, and higher custom limits are kept). Methods are metered by Compute Unit (CU) weights.
    • Upgrading limits: After topping up, the account-wide calls-per-second limit is removed; each key remains subject to Compute Unit (CU) rate and burst limits. For current rates and billing units, see the Pricing page.

Backfilling historical logs: chunked eth_getLogs and retry logic

When querying historical logs, wide intervals must be divided into contiguous chunks bounded by the target chain's max_logs_block_range. Client retry strategies should inspect the retryable field inside error responses.

Evaluating retryable in error responses

On BlockVectra, JSON-RPC error objects include an error.data payload containing reason, docs_url, and retryable (boolean):

  • retryable: true: Transient conditions, including service overload (overloaded), free plan calls-per-second limit (free_plan_call_limit), node synchronization (node_syncing), or upstream unavailable (upstream_unavailable). Clients should respect the Retry-After header when present or apply exponential backoff with jitter.
  • retryable: false: Non-transient errors, such as block span exceeding limits (-32602 / logs_range_too_large), invalid parameters (invalid_params), missing API key (missing_api_key), or request exceeding burst capacity (-32022 / request_exceeds_burst). Retrying without adjusting parameters will not succeed.

Below is the response returned when an API key is omitted:

{
  "jsonrpc": "2.0",
  "id": null,
  "error": {
    "code": -32024,
    "message": "missing API key: send it in the request path (/v1/{chain}/<api_key>) or in the x-api-key header",
    "data": {
      "reason": "missing_api_key",
      "docs_url": "https://docs.blockvectra.com/en/errors/#missing_api_key",
      "retryable": false
    }
  }
}

Using Data API endpoints instead of extensive getLogs scanning

When an application tracks transaction history or token movements for a specific address, scanning via eth_getLogs requires issuing sequential chunked queries bounded by max_logs_block_range and parsing raw Transfer event logs.

BlockVectra Data API provides pre-indexed REST endpoints for hyperevm_mainnet, supporting windows up to 100,000 blocks with cursor-based pagination:

  1. Address transactions: GET /v1/data/hyperevm_mainnet/addresses/{address}/transactions
    • Parameters: from_block (required), to_block (required), direction (optional: from, to, any, default any), clamp (optional boolean string, default false; when set to true, windows exceeding 100,000 blocks or higher than as_of_block are truncated instead of returning 409), limit (optional, max 500), cursor (pagination token).
  2. Address token transfers: GET /v1/data/hyperevm_mainnet/addresses/{address}/transfers
    • Parameters: standard (required: erc20 or erc721; erc1155 cannot be queried by address and returns 422 no_coverage), token (optional token contract filter), from_block (required), to_block (required), direction (optional: in, out, any), clamp (optional), limit, cursor.

Response structure

Responses use standard envelope schemas:

  • data: Array of records. Transactions include hash, block_number, block_timestamp, from, to, value, tx_index, gas, gas_used, and status. Transfers include token, standard, from, to, block_number, block_timestamp, tx_hash, tx_index, and log_index (amount for ERC-20, token_id for ERC-721).
  • next_cursor: Opaque pagination token returned when subsequent records exist (absent on the final page, not null).
  • meta: Metadata containing chain, chain_slug, chain_external_id, as_of_block, safe_block, finalized_block, coverage (full or partial), and refreshed_at.

Code example: Data API queries

export BLOCKVECTRA_API_KEY="rgw_your_api_key"

# 1. Query address transaction history (clamp=true prevents 409 errors)
curl -s "https://api.blockvectra.com/v1/data/hyperevm_mainnet/addresses/0x2222222222222222222222222222222222222222/transactions?from_block=0&to_block=50000&clamp=true" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY"

# 2. Query address ERC-20 token transfers
curl -s "https://api.blockvectra.com/v1/data/hyperevm_mainnet/addresses/0x2222222222222222222222222222222222222222/transfers?standard=erc20&from_block=0&to_block=50000&clamp=true" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY"

Real-time tracking: polling new blocks

For an HTTP transport, track blocks by polling and fetch event logs in consecutive chunks within max_logs_block_range. Select WebSocket only when /v1/chains reports ws=true and the required subscriptions entry. For delivery to an HTTPS receiver, use webhook push.

To exercise the deployed Hello contract, set LOG_ADDRESS to its address. Send ping() through the broadcasting RPC, then backfill the receipt’s block with the backfill script on this page. Continue from the last completed chunk for new events.

cast send "$CONTRACT_ADDRESS" "ping()" --rpc-url "$DEPLOY_RPC_URL" \
  --private-key "$DEPLOYER_PRIVATE_KEY"

Polling flow

  1. Issue periodic lightweight calls to eth_blockNumber to inspect the latest chain head.
  2. Compare the returned block number with the previously processed lastSeenBlock.
  3. If currentBlock > lastSeenBlock, split [lastSeenBlock + 1, currentBlock] into chunks of at most max_logs_block_range. Persist lastSeenBlock only after successfully processing each chunk; on failure, retry the unfinished chunk. Deduplicate by (blockHash, transactionHash, logIndex) and replay an overlap after reconnecting to reconcile reorgs.
  4. viem's watchBlockNumber or watchBlocks natively implements HTTP polling under an HTTP transport, allowing customization through the pollingInterval parameter (such as 1000 ms).

Poll event logs in bounded chunks

Save as poll-logs.mjs beside network.mjs and viem-client.mjs. Set BLOCKVECTRA_API_KEY, LOG_ADDRESS and FROM_BLOCK, then run node poll-logs.mjs. This finite example samples the head 12 times, five seconds apart, and queries every new range in sequential chunks. An error stops the script before advancing the failed chunk.

import { isAddress } from 'viem';
import { client } from './viem-client.mjs';
import { chainInfo, allows } from './network.mjs';

const address = process.env.LOG_ADDRESS;
const start = process.env.FROM_BLOCK;
if (!address || !isAddress(address)) throw new Error('Set LOG_ADDRESS');
if (!/^(0x[0-9a-f]+|[0-9]+)$/i.test(start ?? '')) throw new Error('Set FROM_BLOCK');
if (!allows('eth_getLogs')) throw new Error('eth_getLogs requires an available keyed endpoint');
if (!Number.isSafeInteger(chainInfo.max_logs_block_range) || chainInfo.max_logs_block_range <= 0) {
  throw new Error('Invalid max_logs_block_range');
}
const max = BigInt(chainInfo.max_logs_block_range);
let from = BigInt(start);
for (let poll = 0; poll < 12; poll++) {
  const head = await client.getBlockNumber();
  while (from <= head) {
    const to = from + max - 1n < head ? from + max - 1n : head;
    const logs = await client.getLogs({ address, fromBlock: from, toBlock: to });
    console.log(JSON.stringify({ from: from.toString(), to: to.toString(), logs },
      (_, value) => typeof value === 'bigint' ? value.toString() : value));
    from = to + 1n;
  }
  if (poll < 11) await new Promise(resolve => setTimeout(resolve, 5_000));
}

Each output records a completed chunk. To resume, set FROM_BLOCK to its to + 1; durable consumers must save events and cursor together, deduplicate, and reconcile reorgs as described above. For 429 or other retryable failures, apply the bounded-backoff guidance to the same unfinished chunk.

Next steps

Last updated:

On this page