Hyperliquid RPC: A Developer's Guide (Chain 999, 1s Blocks, and a Block Number That Does Nothing)

By John Sullivan · September 21, 2026 · 8 min read · #hyperliquid #rpc #developer guide #evm

Hyperliquid's EVM is a strange and interesting place to run a node: it finalises blocks about once a second, charges a base fee that never moves, and rejects eth_maxPriorityFeePerGas-style market pricing entirely. But the thing that will actually cost you time is quieter. Ask this endpoint for state at a historical block and it will answer — cheerfully, with an empty error field — using the latest state. There is no archive fallback to save you, because the request never looks like it failed.

Everything below was measured on 2026-09-21 through https://rpc.swiftnodes.io/rpc/hyperliquid. Every claim here is a call you can re-run in a minute; the one that matters most has the exact recipe below.

The essentials

Chain ID 999 (0x3e7) — net_version agrees, web3_clientVersion reports hyperliquid evm Mainnet
Block time ~0.985 s (measured over 600 blocks)
Head at time of writing 46,479,662
Gas baseFeePerGas of 100,000,000 wei (0.1 gwei) in every block sampled; gasLimit 3,000,000
eth_maxPriorityFeePerGas Returns 0x0 — there is no priority-fee market
Block history ~1.21M blocks (~14 days). Older heights error out
State history Not honoured — see the section below
WebSocket None. eth_subscribe-32601, no rpc_ws on the route
eth_getLogs Capped at 1,000 blocks and a 1 MiB response
Tracing debug_traceBlockByNumber, trace_*, txpool_* all -32601
HTTP https://rpc.swiftnodes.io/rpc/hyperliquid?key=YOUR_API_KEY

Connecting: the setup

Standard EVM JSON-RPC, and standard clients work — with the caveats below doing the real work.

curl

curl -s -X POST https://rpc.swiftnodes.io/rpc/hyperliquid?key=YOUR_API_KEY \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}'
# 0x2c5382e  -> 46,479,405

viem

import { createPublicClient, http, parseUnits } from 'viem';
import { mainnet } from 'viem/chains';

export const hyperliquid = {
  ...mainnet,
  id: 999,
  name: 'Hyperliquid',
  rpcUrls: { default: { http: ['https://rpc.swiftnodes.io/rpc/hyperliquid?key=YOUR_API_KEY'] } },
};

const client = createPublicClient({ chain: hyperliquid, transport: http() });
const block = await client.getBlockNumber();

ethers v6

import { JsonRpcProvider } from 'ethers';
const provider = new JsonRpcProvider(
  'https://rpc.swiftnodes.io/rpc/hyperliquid?key=YOUR_API_KEY', 999);
await provider.getNetwork();   // { chainId: 999n, name: 'unknown' }

eth_accounts returns -32601 Method not found rather than an empty array, so anything that probes for unlocked accounts will take an exception rather than a length-0 list.

The block number is decorative

This is the finding worth structuring your code around.

Take a contract that was deployed at block 46,479,401 — 261 blocks, about four minutes, before the head I was reading — and ask for its code at heights before that moment existed:

eth_getCode block param Returned
46,479,400 (1 block before deploy) 0x6080604052348015600e575f80fd5b50… — 342 chars
46,478,401 (1,000 blocks before) same 342 chars
46,379,401 (100,000 blocks before) same 342 chars
0x1 (height one) same 342 chars

A correct node must answer 0x for every height before the deployment, because the account did not exist. It answers with the current bytecode at all four. eth_getBalance and eth_getStorageAt behave the same way, and eth_getBlockReceipts at height 1 errors with invalid block height: 1 — so the node clearly knows block 1 is unavailable and still happily serves you "state" at block 1.

Read that precisely: block-tagged state reads are answered from the latest block. Not pruned-and-erroring, not approximate — silently wrong, and indistinguishable from a correct answer unless you test it.

The probe, so you can run it against any provider you are evaluating:

H=https://rpc.swiftnodes.io/rpc/hyperliquid?key=YOUR_API_KEY
# 1. find a contract deployed recently
R=$(curl -s -X POST $H -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"eth_getTransactionReceipt","params":["<recent creation tx hash>"]}' \
  | sed -E 's/.*"contractAddress":"(0x[a-fA-F0-9]{40})".*/\1/')
# 2. ask for its code at height 1
curl -s -X POST $H -H 'Content-Type: application/json' \
  -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"eth_getCode\",\"params\":[\"$R\",\"0x1\"]}"

If step 2 returns anything other than "result":"0x", historical state is not being honoured — no matter what the endpoint advertises, and no matter what a health check that only looks at HTTP status tells you.

Practical consequences:

  • Never use a historical block tag for a real balance or nonce here. If you need state as of a past block, you need an indexer that recorded it, not this RPC.
  • Reorg-safe nonce/balance logic that pins blockNumber "for consistency" gets no consistency guarantee — you are reading head under a label.
  • Indexers built on debug_*/archive traces cannot bootstrap from this endpoint. Event-log indexing is the path that works (see the caps below), because logs are fetched per block range and are block-accurate.

What's different from a mainstream EVM node

No WebSocket at all

There is no ws/wss route for this chain on SwiftNodes, and eth_subscribe over HTTP returns -32601 Method not found. Nothing is half-open: subscriptions simply do not exist here. Poll at ~1 s to track ~1 block per poll, or use an external feed for low-latency updates. Do not assume the absence of a WS error means your subscription is working — check that you ever received a confirmation ID.

A base fee that barely moves

Across eight sampled blocks, baseFeePerGas was 100000000 wei in every one — 0.1 gwei — and gasLimit was pinned at 3,000,000. eth_feeHistory does show slight variation (0x651a00b ≈ 0.106 gwei alongside 0x5f5e100), so the mechanism exists; it is simply clamped to within a few percent and does not respond to demand the way an L1 does. eth_maxPriorityFeePerGas returns 0x0. So EIP-1559 fee estimation is effectively meaningless here: there is no market to estimate. Transactions in the blocks I sampled were type-2 with a user-chosen gasPrice of 1.6 gwei against a maxFeePerGas of 30 gwei, so senders are setting their own ceiling and the near-fixed base fee just gets burned.

The practical rule: hard-code a sane maxFeePerGas, skip fee estimators, and treat eth_gasPrice as a constant rather than a signal.

Block history is about two weeks

oldest readable block   ~45,269,661
head                     46,479,662
retained                  1,209,840 blocks   (~13.9 days at 0.985 s)

Heights older than that fail explicitly, which is the good case:

{"code":-32603,"message":"invalid block height: 1000000"}

earliest is not supported either — it comes back as invalid block height: 0. Anything that expects a canonical earliest tag needs a fallback to a numeric height.

eth_getLogs has two different ceilings

Range Result
100 blocks 1,055 logs, ~400 ms
300 blocks {"code":-32008,"message":"Response is too big","data":"Exceeded max limit of 1048576"}
1,000 blocks same -32008
5,000 blocks {"code":-32602,"message":"query exceeds max block range 1000"}

Two independent limits, and the byte cap bites first. A 1,000-block window is nominally allowed but fails on any busy stretch, so "1000 blocks" is not the number to design around — the number is whatever fits in 1 MiB, which on Hyperliquid means ~100 blocks during activity. Chunk by 50–100 blocks, and handle -32008 by halving the range rather than surfacing an error. Note also that the two error codes are different for the two causes, so a retry policy keyed on one will miss the other.

Tracing is absent

debug_traceBlockByNumberdoes not exist/is not available; trace_replayBlockTransactions and txpool_status-32601. No simulation-adjacent introspection, no mempool visibility — which fits a chain whose execution is driven by an order book rather than a public mempool. If your stack has a debug_traceTransaction fallback path, it will not fire here.

The transaction object carries an extra field

eth_getBlockByNumber with full transactions returns a blockTimestamp on each tx alongside the standard fields. It is additive, so most clients ignore it; if you decode transactions into a strict schema, allow for the extra key.

Not every upstream behaves the same

This route pools more than one Hyperliquid node. Against a busy eth_getLogs window one answered -32008 Response is too big and another answered -32005 rate limited; the same split showed on debug_traceBlockByNumber. If you are building retry or circuit-breaker logic, key it on the JSON-RPC code rather than assuming one error shape per condition.

What this means for your stack

Test the block tag before trusting it. One eth_getCode on a freshly deployed contract tells you whether an endpoint honours history. On this route it does not.

Design around ~100-block log chunks, and treat "response too big" as a normal control signal, not a failure.

Do not fee-estimate. Fixed 0.1 gwei base fee, priority fee of zero.

Plan without WebSocket or tracing. Poll at ~1 s; accept that mempool and debug surfaces do not exist.

And do not treat a 200 OK on a historical read as proof of anything — that is the entire lesson of this endpoint.

The short version

Hyperliquid's EVM is chain 999, blocks about every 0.985 s, a near-fixed 0.1 gwei base fee with a 3,000,000 gas limit, and no priority-fee market. It has no WebSocket, no debug_*/trace_*/txpool_*, and holds roughly 1.21M blocks (~14 days) of history, erroring cleanly beyond that. eth_getLogs is bounded by both a 1,000-block range and a 1 MiB response, and the byte limit arrives first at about 100 blocks of activity. The important part: block-tagged state reads are answered from the latest blocketh_getCode for a contract deployed seconds ago returns its full bytecode at height 1, so historical balances, nonces and storage slots cannot be recovered here and will not warn you.

For Hyperliquid RPC across load-balanced independent upstreams, grab a free API key and point your client at:

https://rpc.swiftnodes.io/rpc/hyperliquid?key=YOUR_API_KEY

One key covers all 86 chains, and the free tier is 2 requests per second — enough to run the eth_getCode probe above against this and every other endpoint you rely on. That probe takes about ninety seconds and it is the only way to know whether the "archive" node you are paying for is telling you the truth about the past.

J
John Sullivan
Infrastructure Writer, SwiftNodes

John Sullivan covers RPC infrastructure, node operations, and multi-chain development at SwiftNodes — what it actually takes to keep endpoints fast, fresh, and reliable across EVM and non-EVM networks.

Related posts

Try SwiftNodes free — multi-chain RPC across 86 networks, flat-rate pricing, pay by card or crypto, no KYC. Get an API key in 30 seconds →