Hyperliquid RPC: A Developer's Guide (Chain 999, 1s Blocks, and a Block Number That Does Nothing)
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_traceBlockByNumber → does 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 block — eth_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.
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
- Neutron RPC: A Developer's Guide (neutron-1, CometBFT 0.38, No eth_*)
How to query Neutron over JSON-RPC — CometBFT 0.38 on chain neutron-1, why there is no numeric chain ID, the header fields that disappeared in 0.38, a tx_search that times out instead of answering, a pruning floor that differs between nodes, and a block time that measured 7s this week against a 2s baseline.
- Arbitrum RPC: A Developer's Guide (Chain ID 42161, ~250ms Blocks, Fake Gas Limit)
How to connect to Arbitrum One over JSON-RPC — chain ID 42161, why the block gas limit is a sentinel value, what safe and finalized actually mean here, zero priority fees, and where archive reads stop working.
- Ethereum Classic RPC: A Developer's Guide (Chain ID 61, No EIP-1559)
How to connect to Ethereum Classic over JSON-RPC — chain ID 61, proof-of-work still live, why blocks carry no baseFeePerGas, what carries over from Ethereum unchanged, and where historical reads stop working.