# Backfilling and polling HyperEVM data under public RPC rate limits

> Original page: https://docs.blockvectra.com/en/guides/hyperevm-backfill/

When building applications or synchronizing chain data on HyperEVM, developers typically handle two core operational patterns: backfilling historical event logs and transactions, and polling for newly produced blocks and events in real time.

Official public RPC endpoints and third-party node providers operate under distinct rate limits, supported methods, and state retention policies. This guide details those parameters based on official documentation and published specifications, and demonstrates chunked backfilling, retryable error handling, querying structured Data API endpoints, and polling new blocks.

## Status quo: official public RPC limits

According to official Hyperliquid developer documentation, the public RPC operates under the following parameters and limits:

1. **Rate limits**:
   The official [Rate limits and user limits documentation](https://hyperliquid.gitbook.io/Hyperliquid-docs/for-developers/api/rate-limits-and-user-limits) specifies that for the public endpoint `rpc.hyperliquid.xyz/evm&#x60;, requests are subject to a limit of at most 100 EVM JSON-RPC requests per minute per IP address (&#x2A;"Maximum of 100 EVM JSON-RPC requests per minute for rpc.hyperliquid.xyz/evm"*).
2. **WebSocket support**:
   The official [HyperEVM overview](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/hyperevm) states that the public RPC endpoint `rpc.hyperliquid.xyz/evm&#x60; does not currently support WebSocket JSON-RPC (&#x2A;"There is currently no websocket JSON-RPC support for the RPC at rpc.hyperliquid.xyz/evm but other RPC implementations may support it"*).
3. **Chain parameters and endpoints**:
   * Mainnet: Chain ID `999`, public JSON-RPC endpoint `https://rpc.hyperliquid.xyz/evm`.
   * Testnet: Chain ID `998`, public JSON-RPC endpoint `https://rpc.hyperliquid-testnet.xyz/evm`.
4. **Hardfork and gas mechanics**:
   HyperEVM uses the Cancun hardfork with support for `MCOPY`, `TSTORE`, and `TLOAD`, but does not support blob transactions. EIP-1559 is enabled, but there are no priority fees (priority fees are burned and sent to the zero address balance, so transaction `maxFeePerGas` and `maxPriorityFeePerGas` must be equal).

Under a limit of 100 requests per minute per IP and without WebSocket support, broad historical log backfills and rapid block polling require deliberate chunking, retry handling, or polling logic.

## BlockVectra parameters and service rules

BlockVectra serves HyperEVM mainnet through JSON-RPC and REST Data API endpoints. Published parameters and service rules originate from public specifications:

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`**: Governed by the free-plan calls-per-second cap returned by the API (note: this limit represents the aggregated average across all API keys in the account).
   * **Default key limits**: Each API key has a CU bucket (`cu_per_sec` refill, `burst_cu` capacity — defaults are 400 CU/s and burst 1,600 CU). Methods are metered by Compute Unit (CU) weights (for example, `eth_getLogs` is 30 CU, `eth_blockNumber` is 1 CU, address transactions and transfers are 25 CU).
   * **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](https://blockvectra.com/en/pricing/).

## 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:

```json
{
  "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
    }
  }
}
```

### Code example: chunked queries and retries

**cURL**

```bash
export BLOCKVECTRA_API_KEY="rgw_your_api_key"

# 1. Fetch published max_logs_block_range dynamically (unauthenticated)
curl -s "https://api.blockvectra.com/v1/chains"

# 2. Query a single chunk within max_logs_block_range (e.g. 0x1 to 0x3e8)
curl -s "https://api.blockvectra.com/v1/hyperevm_mainnet" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "eth_getLogs",
    "params": [{
      "address": "0x2222222222222222222222222222222222222222",
      "fromBlock": "0x1",
      "toBlock": "0x3e8"
    }]
  }'
```


  **TypeScript (viem)**

```ts
import { createPublicClient, http, defineChain } from "viem";

const hyperevm = defineChain({
  id: 999,
  name: "HyperEVM",
  nativeCurrency: {
    decimals: 18,
    name: "Hyperliquid",
    symbol: "HYPE",
  },
  rpcUrls: {
    default: {
      http: ["https://api.blockvectra.com/v1/hyperevm_mainnet"],
    },
  },
});

const client = createPublicClient({
  chain: hyperevm,
  transport: http("https://api.blockvectra.com/v1/hyperevm_mainnet", {
    fetchOptions: {
      headers: { "x-api-key": process.env.BLOCKVECTRA_API_KEY! },
    },
  }),
});

// Read max_logs_block_range from the public /v1/chains endpoint
const chainsRes = await fetch("https://api.blockvectra.com/v1/chains");
const { chains } = (await chainsRes.json()) as {
  chains: Array<{ chain: string; max_logs_block_range: number }>;
};
const target = chains.find((c) => c.chain === "hyperevm_mainnet");
if (!target || !target.max_logs_block_range) {
  throw new Error("Target chain or max_logs_block_range not found");
}
const maxChunk = BigInt(target.max_logs_block_range);

// Helper that inspects retryable before retrying
async function fetchLogsWithRetry(fromBlock: bigint, toBlock: bigint) {
  const maxRetries = 3;
  let attempt = 0;

  while (true) {
    try {
      return await client.getLogs({
        address: "0x2222222222222222222222222222222222222222",
        fromBlock,
        toBlock,
      });
    } catch (err: unknown) {
      attempt++;
      const errorObj = err as { data?: { retryable?: boolean } };
      const isRetryable = errorObj?.data?.retryable ?? false;

      if (isRetryable && attempt <= maxRetries) {
        await new Promise((resolve) => setTimeout(resolve, attempt * 1000));
        continue;
      }
      throw err;
    }
  }
}

// Chunked sequential query loop
const startBlock = 100000n;
const endBlock = 103000n;
const allLogs = [];

for (let cur = startBlock; cur <= endBlock; cur += maxChunk) {
  const chunkEnd = cur + maxChunk - 1n < endBlock ? cur + maxChunk - 1n : endBlock;
  const logs = await fetchLogsWithRetry(cur, chunkEnd);
  allLogs.push(...logs);
}

console.log(`Backfill completed: fetched ${allLogs.length} logs`);
```


## 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` (defined in `openapi/data.yaml`), 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 `finalized_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`; OpenAPI defines that `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 (from OpenAPI specification)

Responses use standard envelope schemas:

* `data`: Array of records. Transactions include `hash`, `block_number`, `block_timestamp`, `from`, `to`, `value`, `tx_index`, `gas_limit`, `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`, `finalized_block`, `coverage` (`full` or `partial`), and `refreshed_at`.

### Code example: Data API queries

**cURL**

```bash
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"
```


  **TypeScript**

```ts
const apiKey = process.env.BLOCKVECTRA_API_KEY!;
const targetAddress = "0x2222222222222222222222222222222222222222";

let cursor: string | undefined;

do {
  const url = new URL(
    `https://api.blockvectra.com/v1/data/hyperevm_mainnet/addresses/${targetAddress}/transfers`,
  );
  url.searchParams.set("standard", "erc20");
  url.searchParams.set("from_block", "0");
  url.searchParams.set("to_block", "50000");
  url.searchParams.set("clamp", "true");
  if (cursor) {
    url.searchParams.set("cursor", cursor);
  }

  const res = await fetch(url, {
    headers: { "x-api-key": apiKey },
  });

  if (!res.ok) {
    throw new Error(`Data API HTTP ${res.status}`);
  }

  const body = (await res.json()) as {
    data: unknown[];
    next_cursor?: string;
  };

  console.log(`Fetched ${body.data.length} transfers`);
  cursor = body.next_cursor; // Loop terminates when cursor is absent
} while (cursor);
```


## Real-time tracking: polling new blocks

Because HyperEVM official public RPC does not offer WebSocket JSON-RPC support, and BlockVectra disables `eth_subscribe` under `methods.deny`, real-time block and event tracking is accomplished via polling.

### Polling flow

1. Issue periodic lightweight calls to `eth_blockNumber` (weighted at 1 CU) to inspect the latest chain head.
2. Compare the returned block number with the previously processed `lastSeenBlock`.
3. If `currentBlock > lastSeenBlock`, fetch new blocks or logs across `[lastSeenBlock + 1, currentBlock]` and update `lastSeenBlock`.
4. viem's `watchBlockNumber` or `watchBlocks` natively implements HTTP polling under an HTTP transport, allowing customization through the `pollingInterval` parameter (such as 1000 ms).

### Code example: polling blocks

**cURL**

```bash
export BLOCKVECTRA_API_KEY="rgw_your_api_key"

# Query the latest block height (1 CU)
curl -s "https://api.blockvectra.com/v1/hyperevm_mainnet" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY" \
  -d '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}'
```


  **TypeScript (viem)**

```ts
import { createPublicClient, http, defineChain } from "viem";

const hyperevm = defineChain({
  id: 999,
  name: "HyperEVM",
  nativeCurrency: {
    decimals: 18,
    name: "Hyperliquid",
    symbol: "HYPE",
  },
  rpcUrls: {
    default: {
      http: ["https://api.blockvectra.com/v1/hyperevm_mainnet"],
    },
  },
});

const client = createPublicClient({
  chain: hyperevm,
  transport: http("https://api.blockvectra.com/v1/hyperevm_mainnet", {
    fetchOptions: {
      headers: { "x-api-key": process.env.BLOCKVECTRA_API_KEY! },
    },
  }),
  pollingInterval: 1000, // Polling interval in milliseconds
});

// Watch for incoming blocks using watchBlockNumber
const unwatch = client.watchBlockNumber({
  onBlockNumber: (blockNumber) => {
    console.log("New block height:", blockNumber);
  },
  onError: (error) => {
    console.error("Polling error:", error);
  },
});
```


## Related guides and specifications

* For complete rules on `eth_getLogs` spans and chunking algorithms, see [eth\_getLogs block range limits and chunked queries](https://docs.blockvectra.com/en/guides/getlogs-block-range/).
* For comparing `eth_getLogs` against Data API transfers and understanding finalized block watermarks, see [Recent node data vs indexed history: when to use eth\_getLogs and when to use the transfers API](https://docs.blockvectra.com/en/guides/logs-vs-transfers/).
* For details on CU metering, non-billed errors, and retries, see [What is not billed: error codes and billing rules](https://docs.blockvectra.com/en/guides/billing-rules/).

## Next steps

* [Browse the datasets directory](https://blockvectra.com/en/data/) to see every dataset BlockVectra indexes.
* [See the free plan and pricing](https://blockvectra.com/en/pricing/#free) to check what your account includes.
* [Log in to the console](https://console.blockvectra.com/en/login/?next=%2Fen%2Fkeys%2F) to create an API key.
