A live, CEX-grade Hyperliquid L2 order book with a liquidation-radar overlay — a full book, every block, powered entirely by Quicknode HyperCore gRPC (
StreamL2Book). No REST bootstrap. No incremental diff-stitching. No sequence numbers to reconcile.
The pitch in one line: with StreamL2Book every message is a complete aggregated book for that block, so you replace local state wholesale on each update — there is nothing to stitch. That is what lets you stand up a real depth view in an afternoon.
On June 9, 2026 the Hyperliquid core team announced that several public WebSocket feeds will push less frequently after the next network upgrade. After the June 11 adjustment, the standard public l2Book feed:
Public WebSocket l2Book |
Levels | Cadence |
|---|---|---|
without fast: true |
20 | every 2s (→ 5s later) |
with fast: true |
5 | every 0.5s |
If your strategy quotes, sizes, cancels, or routes from public snapshots, that throttle changes your data plane. The gRPC alternative does not throttle:
Quicknode gRPC StreamL2Book |
Levels | Cadence |
|---|---|---|
StreamL2Book |
up to 100 | every block |
The angle here is completeness + freshness — a complete real-time book without building one from diffs — not a latency race.
- 📝 Blog: Hyperliquid Foundation WebSocket Changes
- 📚 Docs: Hyperliquid gRPC Streaming API ·
StreamData· Datasets - 🧪 Official examples (protos pulled from here):
quiknode-labs/hypercore-grpc-examples
npm install
npm run dev # replay mode — no credentials, no creditsThen open http://localhost:5173. That streams a bundled sample recording through the exact same backend → WebSocket → UI pipeline as live mode, so you can explore the whole app offline. To connect to a real endpoint, see Live mode.
Requires Node ≥ 20.
- Live ladder — asks (red) above, bids (green) below, a centred mid/spread row, and columns for price, size, cumulative size, order count (
n), and a depth bar per level. Up to 100 levels. - Depth chart — cumulative bid/ask curves, redrawn every block, with liquidation markers overlaid on the price axis (sized by USD notional, fading by age).
- Freshness HUD — the money shot: current block number, book age (ms since last update), and updates/sec. Visual proof the book is fresh every block, with an inline
l2BookvsStreamL2Bookcomparison. - Liquidation radar — a live feed of forced-liquidation fills (side, size, USD notional, price, age), flashing on arrival, with an optional sound toggle (off by default).
- Controls — coin selector (BTC / ETH / SOL / … + free input), depth slider (
n_levels), optional price bucketing (n_sig_figs/mantissa), and pause/resume (freezes the ladder while the HUD keeps proving the feed is live).
Browsers can't speak raw gRPC (HTTP/2 trailers + server streaming aren't reachable from the browser), so this is a two-tier app. Secrets live only in the backend.
TLS · port 10000 · x-token WebSocket (JSON)
┌────────────────┐ metadata header ┌───────────────────────────┐ book / liq / status ┌──────────────────┐
│ Quicknode │ ───────────────────► │ Backend (server/) │ ───────────────────────► │ Browser (web/) │
│ HyperCore gRPC │ │ ┌──────────────────────┐ │ │ ┌────────────┐ │
│ │ StreamL2Book ──────► │ │ book state (full snap)│ │ │ │ ladder │ │
│ │ │ │ replace wholesale │ │ │ │ depth chart │ │
│ │ StreamData(TRADES) ► │ │ liquidation extract │ │ ◄───── subscribe ────── │ │ HUD · radar │ │
│ │ │ │ zstd · reconnect/relay│ │ coin / levels │ │ controls │ │
└────────────────┘ │ └──────────────────────┘ │ │ └────────────┘ │
└───────────────────────────┘ └──────────────────┘
React + Vite frontend subscribes to the backend WebSocket — it never holds a secret or speaks gRPC.
A Source abstraction is the seam that makes replay possible: live and replay both implement the same interface and feed the identical relay → WebSocket pipeline, so the UI behaves the same with or without a connection.
server/src/
index.ts entry — parses mode/flags, wires a Source into the Relay
config.ts env loading + credential parsing (QN_HL_ENDPOINT / QN_HL_TOKEN / QN_HL_URL)
protocol.ts the server↔web wire contract (mirrored in web/src/protocol.ts)
relay.ts WebSocket relay; caches latest book/status; optional static serving
source.ts Source interface + subscription helpers
sources/live.ts gRPC streams → normalized messages
sources/replay.ts NDJSON fixture → same messages, re-stamped to stay "live"
record.ts capture live messages to an NDJSON fixture
book.ts normalize L2BookUpdate → compact wire frame (wholesale replace)
liquidations.ts pull liquidation fills out of TRADES JSON
grpc/
proto.ts load the official protos, typed via proto-loader-gen-types
client.ts TLS + x-token + channel options
l2book.ts StreamL2Book consumer (reconnect, DATA_LOSS backoff)
trades.ts StreamData(TRADES) consumer (bidi, ping keepalive, zstd)
generated/ TypeScript types generated from the protos (checked in)
tools/genFixture.ts deterministic synthetic-fixture generator
web/src/
useRelay.ts the store: WebSocket client + reconnect + freshness stats
derive.ts book math (cumulative depth, mid/spread)
components/ Ladder · DepthChart · FreshnessHUD · LiquidationRadar · Controls
proto/ orderbook.proto + hyperliquid.proto (pulled verbatim from the official repo)
fixtures/ sample-btc.ndjson (bundled, committed)
| Mode | Command | Needs credentials? | What it does |
|---|---|---|---|
| replay | npm run dev |
no | Streams the bundled fixture (zero-config default). |
| live | npm run dev:live |
yes | Connects to Quicknode HyperCore gRPC. |
| record | npm run record -w server -- … |
yes | Connects live and writes a fixture you can replay. |
-
Get an endpoint. HyperCore gRPC requires a Quicknode Build plan or higher with the Hyperliquid add-on. Create a Hyperliquid endpoint in the Quicknode dashboard — your HTTP provider URL looks like:
https://<endpoint-name>.hype-mainnet.quiknode.pro/<token>/ -
Configure
.env.cp .env.example .env
Then set either the split values or the full URL:
# derive from the HTTP URL above: QN_HL_ENDPOINT=<endpoint-name>.hype-mainnet.quiknode.pro:10000 # port 10000, TLS QN_HL_TOKEN=<token> # the path segment # …or just paste the whole URL and the server splits it: # QN_HL_URL=https://<endpoint-name>.hype-mainnet.quiknode.pro/<token>/
Use
.hype-testnet.quiknode.profor testnet. Never commit.env(it's git-ignored). -
Run it.
npm run dev:live
You can pick the starting market/depth with env (QN_HL_COIN, QN_HL_LEVELS) or change them live in the UI.
Capture a few minutes of live StreamL2Book + liquidation messages, then replay them — great for a reproducible demo without burning credits:
# capture (Ctrl-C to stop, or set a duration)
npm run record -w server -- --coin=BTC --levels=100 --duration=60s --out=fixtures/btc-demo.ndjson
# replay your recording
npm run replay -w server -- --fixture=fixtures/btc-demo.ndjson(Ad-hoc recordings under fixtures/ are git-ignored; only sample-btc.ndjson is committed.) Regenerate the bundled synthetic sample with npm run gen:fixture.
| Script | What it does |
|---|---|
npm run dev |
Replay backend + Vite dev server (default; no credentials). |
npm run dev:live |
Live backend + Vite dev server. Needs .env. |
npm run build |
Type-check the server and build the web bundle to web/dist. |
npm start |
Replay backend serving the built web on one port (http://localhost:8787). |
npm run replay -w server |
Replay backend only (headless). Add -- --fixture=…. |
npm run record -w server |
Record a fixture. Add -- --coin=… --duration=… --out=…. |
npm run proto:gen |
Regenerate server/src/generated from the protos. |
npm run gen:fixture |
Rebuild the bundled synthetic sample fixture. |
npm run typecheck |
Type-check both workspaces. |
Everything below is verified against the official protos (proto/) and the Quicknode docs — nothing is hand-waved.
StreamL2Book(orderbook.proto, serviceOrderBookStreaming): requestL2BookRequest { coin, n_levels (default 20, max 100), n_sig_figs?, mantissa? }; response stream ofL2BookUpdate { coin, time, block_number, bids[], asks[] }where eachL2Level = { px, sz, n }and index0is the best level. Each message is the complete book for that block → replace state wholesale.- Liquidations come from
StreamData(TRADES). Each TRADES fill optionally carries aliquidationobject —{ liquidatedUser, markPx, method }— present only on forced-liquidation fills. That is the marker this app keys on. (TheEVENTSstream'sledgerLiquidationis account-level and lacks per-fill price/size, so TRADES is the right feed; the official trading dashboard filters TRADES the same way.) Subscribe shape:Subscribe { stream_type: TRADES, filters: { coin: { values: [...] } } }. - Connection:
<endpoint>.hype-mainnet.quiknode.pro:10000, TLS, API token in thex-tokenmetadata header. Channel options match the official example:grpc.max_receive_message_length= 100MB,grpc.keepalive_time_ms= 30s. - zstd: payloads may be zstd-compressed (magic bytes
28 B5 2F FD). Billing is on uncompressed volume, so compression is free bandwidth — leave it on. Handled inserver/src/util/zstd.ts. DATA_LOSSstatus = "server reinitialized" → reconnect with exponential backoff (counter resets on the first healthy message). gRPC is real-time only: there's no historical backfill — on reconnect we just resubscribe and resume from the next snapshot, which is cheap because each snapshot is a full book.StreamL2BookvsStreamL2BookDiff: this demo usesStreamL2Book(full snapshot per block) on purpose — that's what eliminates stitching.StreamL2BookDiffis the incremental/sequence-number stream; it's a bandwidth-optimized advanced path, not the default here.
- Self-hosted assets only (system fonts, hand-rolled SVG depth chart) — no external CDN calls.
- The frontend never holds a secret and never speaks gRPC; all credentials stay in
server/. Next.jswould be a fine single-process alternative; this repo uses the Vite + Node split because it matches the team's stack and keeps the gRPC layer obvious.
MIT.
