Skip to main content

Hyperliquid WebSocket API

Critical Rules

  1. WebSocket URL: wss://hypercore.goldrushdata.com/ws?key=<GOLDRUSH_API_KEY> (note: hypercore, not hyperliquid; auth is a ?key= query parameter, not an Authorization header).
  2. Wire-compatible with wss://api.hyperliquid.xyz/ws for shared subscription types (e.g. l2Book).
  3. No 1000-subscription-per-IP cap. Multiplex many subscriptions on one connection.
  4. l2Book - aggregated price-level snapshots {px, sz, n}. coin is optional - omit it to stream every asset over a single subscription. When coin is omitted, marketTypes defaults to ["perp"] (perps only); pass ["spot"], ["outcome"], a mix, or ["*"] to opt into spot, outcome, and any future market types.
  5. l2BookDiff - GoldRush-native L2 diff transport. Same aggregated {px, sz, n} shape as l2Book, but emits one Snapshot per subscribed coin and then per-block Updates carrying only changed levels. coin accepts a single asset, an array of assets, or can be omitted for wildcard (same marketTypes default - perps only - applies when coin is omitted). Treat sz: "0" (with n: 0) as level removal. Not available on the public Hyperliquid WebSocket.
  6. l4Book - GoldRush-native order-level stream with user, oid, cloid, tif, and trigger metadata per order. coin is required. Emits a single Snapshot on subscribe, then per-block Updates (order_statuses + book_diffs). Not available on the public Hyperliquid WebSocket.

Available Subscriptions

ChannelSubscription bodyReturns
l2Book{"type":"l2Book","coin":"BTC"} (or omit coin)Full L2 book snapshot per tick with bids/asks aggregated by significant figures. Self-healing - every message is a complete snapshot.
l2BookDiff{"type":"l2BookDiff","coin":"HYPE"}, {"type":"l2BookDiff","coin":["HYPE","BTC","ETH"]}, or omit coin for wildcardInitial Snapshot per coin (aggregated {px, sz, n}), then per-block Updates with book_diffs grouped by coin carrying only changed levels. sz: "0" means level removed. GoldRush-native, no upstream equivalent.
l4Book{"type":"l4Book","coin":"BTC"}Initial Snapshot of every resting order, then per-block Updates with order_statuses (lifecycle) and book_diffs (per-order changes). GoldRush-native, no upstream equivalent.

Subscribe / Unsubscribe Pattern

// Subscribe
{"method":"subscribe","subscription":{"type":"l2Book","coin":"BTC"}}

// Unsubscribe (same subscription body, method swapped)
{"method":"unsubscribe","subscription":{"type":"l2Book","coin":"BTC"}}

When to use which channel

NeedUse
Top-of-book, spread, depth-weighted mid, slippage / impact estimatorl2Book
Stream every asset on one subscriptionl2Book (omit coin) or l2BookDiff (omit coin)
Aggregated {px, sz, n} book state with diff-only bandwidth, single coin / list / wildcardl2BookDiff
Multi-coin coverage with a fixed list of assets in one subscriptionl2BookDiff with coin: ["HYPE","BTC","ETH"]
Queue position, per-user flow attribution, microstructure analyticsl4Book
Reconstruct an L2-style aggregated view but keep order-level detaill4Book (aggregate client-side)
OHLCV candles instead of raw book stateStreaming API ohlcvCandlesForPair (see streaming.md)

The GoldRush Hyperliquid WebSocket API is a drop-in replacement for wss://api.hyperliquid.xyz/ws. Subscription payloads, channel names, and message shapes are byte-for-byte identical to the public Hyperliquid feed. The only difference is the connection URL - authentication is a required key query parameter, so no header changes are needed in your client.

Endpoint

wss://hypercore.goldrushdata.com/ws?key=
Your GoldRush API key. Passed as a query parameter at connection time.

Comparison with the public Hyperliquid WebSocket

Public WebSocketGoldRush
URLwss://api.hyperliquid.xyz/wswss://hypercore.goldrushdata.com/ws?key=
AuthNonekey query parameter (required)
Subscriptions per IP1000No cap
Wire compatibilityn/a (it’s the source)Byte-for-byte
Available channelsSee Hyperliquid DocsSee Available subscriptions

Available subscriptions

Order book

ChannelSubscription bodyReturns
l2Book{"type": "l2Book", "coin": "BTC"}Real-time L2 order book snapshots - bids and asks aggregated by significant figures. coin is optional - omit it to stream every asset on one subscription.
l2BookDiff{"type": "l2BookDiff", "coin": "HYPE"}GoldRush-native L2 diff transport - initial Snapshot per coin plus per-block Updates carrying only changed {px, sz, n} levels. coin accepts a single asset, an array of assets, or can be omitted to stream every asset. Not available on the public Hyperliquid WebSocket.
l4Book{"type": "l4Book", "coin": "BTC"}GoldRush-native order-level book stream - initial Snapshot of every resting order plus per-block Updates with order_statuses and book_diffs. Exposes user, oid, cloid, tif, and trigger metadata per order. coin is required. Not available on the public Hyperliquid WebSocket.

Wallet activity

ChannelSubscription bodyReturns
userFills{"type": "userFills", "addresses": ["0x…"]}Live trade fills for one or more wallets, batched per block as [address, fill] tuples. Aliases user / users also accepted.
orderUpdates{"type": "orderUpdates", "addresses": ["0x…"]}Live order lifecycle events (placements, fills, cancels, rejections) for one or more wallets, batched per block as an updates array of objects, each tagged with the originating user. Aliases user / users also accepted.
liquidationFills{"type": "liquidationFills"}Global stream of every liquidation fill on HyperCore. Same shape as userFills with a non-null liquidation object on every entry. GoldRush-native, no upstream equivalent.
allFills{"type": "allFills"}Global stream of every fill on HyperCore. Optional coin filter narrows to a single market. Same per-fill shape as userFills. GoldRush-native, no upstream equivalent.
builderFills{"type": "builderFills", "builder": "0x…"}Live attributed fills for one or more builder addresses, with builder and builderFee on every entry. Accepts builder (single) or addresses (array). GoldRush-native, no upstream equivalent.
userNonFundingLedgerUpdates{"type": "userNonFundingLedgerUpdates", "addresses": ["0x…"]}Live non-funding ledger events (deposits, withdrawals, transfers, liquidations, vault actions, staking, rewards) for one or more wallets.

Limits

No 1000-subscription-per-IP cap. On l2Book and l2BookDiff, filter parameters are optional - omit coin to stream the full L2 book across every asset on a single subscription. The wildcard defaults to perps only (marketTypes: ["perp"]); pass ["spot"], ["outcome"], a mix, or ["*"] to opt into spot, outcome, and future market types. l2BookDiff additionally accepts an array of coin symbols for a fixed multi-asset subscription. l4Book requires coin and is one-asset-per-subscription. See Limits & Connections for details. For richer real-time analytics (pre-decoded HyperCore fills, liquidations, vault events, OHLCV across every HIP-3/HIP-4 market), pair the WebSocket with the GraphQL Streaming API.
Moving from the public Hyperliquid WebSocket to GoldRush is one change:
  1. URL - replace wss://api.hyperliquid.xyz/ws with wss://hypercore.goldrushdata.com/ws?key=.
That’s it. Subscription payloads, channel names, and the streamed message shape are byte-for-byte identical. Authentication is a required key query parameter, so no header swap is needed in your client.

Side-by-side

wscat

Public Hyperliquid
wscat -c wss://api.hyperliquid.xyz/ws

> {"method":"subscribe","subscription":{"type":"l2Book","coin":"BTC"}}
GoldRush
wscat -c "wss://hypercore.goldrushdata.com/ws?key=$GOLDRUSH_API_KEY"

> {"method":"subscribe","subscription":{"type":"l2Book","coin":"BTC"}}

JavaScript / TypeScript

Public Hyperliquid
import WebSocket from "ws";

const ws = new WebSocket("wss://api.hyperliquid.xyz/ws");

ws.on("open", () => {
  ws.send(JSON.stringify({
    method: "subscribe",
    subscription: { type: "l2Book", coin: "BTC" },
  }));
});

ws.on("message", (raw) => {
  console.log(JSON.parse(raw.toString()));
});
GoldRush
import WebSocket from "ws";

const ws = new WebSocket(
  `wss://hypercore.goldrushdata.com/ws?key=${process.env.GOLDRUSH_API_KEY}`,
);

ws.on("open", () => {
  ws.send(JSON.stringify({
    method: "subscribe",
    subscription: { type: "l2Book", coin: "BTC" },
  }));
});

ws.on("message", (raw) => {
  console.log(JSON.parse(raw.toString()));
});

Python

Public Hyperliquid
import asyncio, json
import websockets

async def main():
    async with websockets.connect("wss://api.hyperliquid.xyz/ws") as ws:
        await ws.send(json.dumps({
            "method": "subscribe",
            "subscription": {"type": "l2Book", "coin": "BTC"},
        }))
        async for raw in ws:
            print(json.loads(raw))

asyncio.run(main())
GoldRush
import asyncio, json, os
import websockets

async def main():
    uri = f"wss://hypercore.goldrushdata.com/ws?key={os.environ['GOLDRUSH_API_KEY']}"
    async with websockets.connect(uri) as ws:
        await ws.send(json.dumps({
            "method": "subscribe",
            "subscription": {"type": "l2Book", "coin": "BTC"},
        }))
        async for raw in ws:
            print(json.loads(raw))

asyncio.run(main())

Behavioral notes

Things to be aware of when you cut over.

Stream payloads are byte-equal, modulo live drift

The channel name and data shape match Hyperliquid byte-for-byte - same keys, same nesting, same value types. Numeric fields update independently on each side, so a price level may differ by tens of milliseconds, but the schema is identical.

Auth errors close the connection

A missing or invalid key query param returns HTTP 401 on the upgrade handshake, so the WebSocket never opens. Public Hyperliquid has no auth and never rejects the handshake.

Filter parameters are optional on GoldRush

Parameters that the public Hyperliquid WebSocket requires (e.g. coin on l2Book) are optional on GoldRush. Omit them to stream the entire channel on a single subscription instead of fanning out one subscription per asset. See Limits.

Existing SDKs work after a wsUrl override

Most popular Hyperliquid SDKs accept a WebSocket base URL override - point them at wss://hypercore.goldrushdata.com/ws?key= and the rest of the SDK works unchanged. See SDK compatibility for the override snippets.

Authentication

The WebSocket API uses your standard GoldRush API key, passed as a key query parameter at connection time. The same key works against the Foundational API, the Streaming API, the Pipeline API, and the Info API. If you don’t have one yet, sign up here. Never hardcode keys in source. Use environment variables or a secrets manager.

What you gain

  • No subscription cap. No 1000-subscription-per-IP limit; multiplex hundreds of subscriptions on a single connection.
  • Wildcard subscriptions. Omit filter parameters to stream the entire channel - e.g. the full L2 order book across every asset on one subscription.
  • One key for everything Hyperliquid. The same API key unlocks Streaming, Pipeline, the Info API, and HyperEVM via the Foundational API.

The most popular Hyperliquid SDKs work against the GoldRush WebSocket API after a one-line URL override. Authentication is a key query parameter on the connection URL - no header injection is needed.

JavaScript / TypeScript: nomeida/hyperliquid

Install

npm
npm install hyperliquid
yarn
yarn add hyperliquid

Configure

import { Hyperliquid } from "hyperliquid";

const sdk = new Hyperliquid({
  // Point at GoldRush - REST and WebSocket
  baseUrl: "https://hypercore.goldrushdata.com",
  wsUrl: `wss://hypercore.goldrushdata.com/ws?key=${process.env.GOLDRUSH_API_KEY}`,
  // REST still needs the Authorization header
  headers: {
    Authorization: `Bearer ${process.env.GOLDRUSH_API_KEY}`,
  },
});

// Existing subscription methods work unchanged
sdk.subscriptions.subscribeToL2Book("BTC", (book) => {
  console.log(book.coin, book.time, book.levels[0][0]);
});

sdk.subscriptions.subscribeToL2Book("ETH", (book) => {
  console.log(book.coin, book.time, book.levels[0][0]);
});
Note: If your SDK version doesn’t expose a wsUrl option, instantiate the WebSocket client manually and pass it to the SDK, or patch the constant the SDK uses. See the override fallback below.

Manual WebSocket fallback

When the SDK doesn’t expose a wsUrl knob, bypass it and drive the raw socket yourself:
import WebSocket from "ws";

const ws = new WebSocket(
  `wss://hypercore.goldrushdata.com/ws?key=${process.env.GOLDRUSH_API_KEY}`,
);

ws.on("open", () => {
  ws.send(JSON.stringify({
    method: "subscribe",
    subscription: { type: "l2Book", coin: "BTC" },
  }));
});

ws.on("message", (raw) => {
  const msg = JSON.parse(raw.toString());
  if (msg.channel === "l2Book") {
    // Hand off to your application
  }
});

Python: hyperliquid-dex/hyperliquid-python-sdk

Install

pip install hyperliquid-python-sdk

Configure

import os
from hyperliquid.info import Info

# Point Info at GoldRush. skip_ws=False opens the WebSocket on init.
info = Info(
    base_url="https://hypercore.goldrushdata.com",
    skip_ws=False,
)

# Override the WebSocket URL on the underlying client so it includes the key
info.ws_manager.ws_url = (
    f"wss://hypercore.goldrushdata.com/ws?key={os.environ['GOLDRUSH_API_KEY']}"
)

# Inject the Authorization header for REST calls
info.session.headers.update({
    "Authorization": f"Bearer {os.environ['GOLDRUSH_API_KEY']}"
})

# Existing subscription methods work unchanged
def on_book(msg):
    print(msg["data"]["coin"], msg["data"]["time"], msg["data"]["levels"][0][:1])

info.subscribe({"type": "l2Book", "coin": "BTC"}, on_book)
Tip: The SDK’s Info class manages both REST and WebSocket. If you only need WebSocket, you can skip the session.headers.update(...) line. If you only need REST, pass skip_ws=True instead.

Verification

After cutover, confirm everything is wired correctly:
  1. Diff a known subscription - subscribe to l2Book for the same coin against both endpoints; the streamed channel and data shape (keys, nesting, types) should match exactly.
  2. Confirm auth - remove the key query parameter and confirm the WebSocket upgrade fails with HTTP 401. If the socket opens, your request isn’t reaching GoldRush.
  3. Confirm wildcard - subscribe to l2Book without a coin and confirm you receive book snapshots for multiple assets. This call would be rejected on the public Hyperliquid WebSocket.

Other SDKs

The pattern is the same for any WebSocket client: override the connection URL to wss://hypercore.goldrushdata.com/ws?key=. If you run into a specific SDK that doesn’t expose a URL override, email us - we’ll publish a recipe.

No subscription cap

The GoldRush Hyperliquid WebSocket API has no per-IP, per-key, or per-connection subscription cap. The 1000-subscription-per-IP limit on the public Hyperliquid WebSocket does not apply. You can:
  • Open as many concurrent subscriptions as your client supports.
  • Multiplex hundreds of l2Book subscriptions on a single connection.
  • Track every active wallet, market, and asset from one process.

Wildcard subscriptions

Filter parameters that the public Hyperliquid WebSocket requires are optional on GoldRush. Omit them to stream the entire channel on one subscription instead of fanning out one subscription per asset.
ChannelPublic HyperliquidGoldRush
l2Bookcoin required - one subscription per assetcoin optional - omit it to stream the full L2 order book across every asset on a single subscription
So a single connection can stream Hyperliquid’s full live book state without any client-side fan-out logic.

How streaming works

Messages are pushed directly from a live Hyperliquid ingestion pipeline - no polling, no cache delay. Latency from upstream Hyperliquid event to your client is dominated by network round-trip from our Tokyo nodes.
ChannelPush trigger
l2BookEvery L2 update on the subscribed coin (or every coin, if wildcard).

Connection management

You’re not rate-limited, but a few client-side defaults are worth tuning.
BehaviorRecommended client setting
ReconnectOn unexpected close, reconnect with exponential backoff capped at ~30 seconds. Re-send your subscription messages after the new socket opens.
HeartbeatSend an application-level ping every 30 seconds. The server replies with pong. Most WebSocket libraries handle this automatically; verify yours does.
Max message sizeL2 book snapshots for wildcard subscriptions can exceed 1 MB. Raise your client’s maxPayload (Node ws library) or max_size (Python websockets) if you’re receiving truncated messages.
BackpressureIf your handler can’t keep up with incoming messages, your client buffer will fill. Drain to a queue or downstream consumer; don’t block the read loop on application work.

Reconnect sketch

TypeScript
import WebSocket from "ws";

function connect() {
  const ws = new WebSocket(
    `wss://hypercore.goldrushdata.com/ws?key=${process.env.GOLDRUSH_API_KEY}`,
    { maxPayload: 8 * 1024 * 1024 },
  );

  ws.on("open", () => {
    ws.send(JSON.stringify({
      method: "subscribe",
      subscription: { type: "l2Book" }, // wildcard - all assets
    }));
  });

  ws.on("close", () => setTimeout(connect, Math.min(30_000, backoff *= 2)));
  ws.on("error", () => ws.close());
  ws.on("message", handle);
}

let backoff = 1_000;
connect();
Python
import asyncio, json, os
import websockets

async def consume():
    uri = f"wss://hypercore.goldrushdata.com/ws?key={os.environ['GOLDRUSH_API_KEY']}"
    backoff = 1
    while True:
        try:
            async with websockets.connect(uri, max_size=8 * 1024 * 1024, ping_interval=30) as ws:
                backoff = 1
                await ws.send(json.dumps({
                    "method": "subscribe",
                    "subscription": {"type": "l2Book"},  # wildcard
                }))
                async for raw in ws:
                    handle(json.loads(raw))
        except Exception:
            await asyncio.sleep(min(30, backoff))
            backoff *= 2

asyncio.run(consume())

Watch out for client-side limits

GoldRush has no caps, but the rest of your stack might:
  • OS file descriptor limits - if you’re opening many connections in parallel, raise ulimit -n.
  • Reverse proxy idle timeouts - if you’re terminating WS through nginx, HAProxy, or a cloud load balancer, set the idle timeout above your heartbeat interval (typically 60 seconds minimum).
  • Browser concurrency - browsers limit WebSocket connections per origin; one connection multiplexing many subscriptions is always preferable to many connections.

Network and TLS

  • HTTP/1.1 Upgrade to WSS is supported (standard WebSocket handshake).
  • TLS 1.2+ required.
  • Compression (permessage-deflate) is negotiated when offered by the client.

Need higher guarantees?

Enterprise SLA, dedicated capacity, regional pinning, and on-prem options are all available. Email sales.

WebSocket API Reference

allFills | Hyperliquid WebSocket API

Credit Cost: 1 per minute Processing: Realtime
Note: - GoldRush-native: No wss://api.hyperliquid.xyz/ws equivalent.
  • Filters coin and aggregateByTime are both optional. Omit coin to stream every market.
  • Live-only: No historical snapshot on subscribe. For windowed history, use the Info API userFillsByTime.
  • Same per-fill shape as userFills; channel name in messages is allFills.
Subscribe once and receive every fill on HyperCore as it executes, batched per block as [address, fill] tuples. Optional coin filter narrows the stream to a single market; aggregateByTime merges partial fills of the same order in a block.

Endpoint

wss://hypercore.goldrushdata.com/ws?key=
Your GoldRush API key. Passed as a query parameter at connection time - no Authorization header is used.

Subscribe

Send this JSON message after the connection is established:
ParameterTypeRequiredDescription
methodstringYesAlways "subscribe".
subscriptionobjectYes__RESPONSE_ROW__type string Always "allFills". __RESPONSE_ROW__coin string Restrict the stream to a single market (e.g. "BTC", "ETH", or spot @N form). Omit to receive every market on one subscription. __RESPONSE_ROW__aggregateByTime boolean When true, partial fills of the same order within the same block are merged into one. Default false.

Example

wscat
wscat -c "wss://hypercore.goldrushdata.com/ws?key=$GOLDRUSH_API_KEY"

> {"method":"subscribe","subscription":{"type":"allFills"}}
TypeScript
import WebSocket from "ws";

const ws = new WebSocket(
  `wss://hypercore.goldrushdata.com/ws?key=${process.env.GOLDRUSH_API_KEY}`,
);

ws.on("open", () => {
  ws.send(JSON.stringify({
    method: "subscribe",
    subscription: {
      type: "allFills",
    },
  }));
});

ws.on("message", (raw) => {
  const msg = JSON.parse(raw.toString());
  if (msg.channel === "allFills") {
    for (const [address, fill] of msg.fills) {
      console.log(address, fill.coin, fill.side, fill.sz, "@", fill.px);
    }
  }
});
Python
import asyncio, json, os
import websockets

async def main():
    uri = f"wss://hypercore.goldrushdata.com/ws?key={os.environ['GOLDRUSH_API_KEY']}"
    async with websockets.connect(uri) as ws:
        await ws.send(json.dumps({
            "method": "subscribe",
            "subscription": {"type": "allFills"},
        }))
        async for raw in ws:
            msg = json.loads(raw)
            if msg.get("channel") == "allFills":
                for address, fill in msg["fills"]:
                    print(address, fill["coin"], fill["side"], fill["sz"], "@", fill["px"])

asyncio.run(main())

Unsubscribe

Send the same subscription body with method: "unsubscribe":
{
  "method": "unsubscribe",
  "subscription": {
    "type": "allFills"
  }
}
Note: Unsubscribe matches subscriptions by exact body. A subscription created with {"type": "allFills", "coin": "BTC"} is a different subscription from a wildcard {"type": "allFills"} - they must be unsubscribed independently.

Streamed message

Each push has channel: "allFills" and a fills array of [address, fill] tuples - every fill from the same HyperCore block that matches the optional coin filter, with the executing wallet as the first element of each tuple.
{
  "channel": "allFills",
  "fills": [
    [
      "0x742d35cc6634c0532925a3b844bc9e7595f7f2e2",
      {
        "coin": "ETH",
        "px": "2150.5",
        "sz": "1.5",
        "side": "B",
        "time": 1704067200000,
        "startPosition": "1.5",
        "dir": "Open Long",
        "closedPnl": "125.5",
        "hash": "0xabc...def",
        "oid": 12345678,
        "crossed": false,
        "fee": "2.5",
        "tid": 87654321,
        "feeToken": "USDC",
        "twapId": null
      }
    ]
  ]
}
FieldTypeDescription
channelstringAlways "allFills".
fillsarrayTuples of [address, fill]. The address is the wallet that executed the fill; the fill object carries the trade details.
fills.coinstringAsset symbol - e.g. "BTC", "ETH" for perps; spot pairs use the @N form (e.g. "@107").
fills.pxstringFill execution price (decimal string).
fills.szstringFill size (decimal string).
fills.sidestring"B" for buy/long, "A" for ask/short.
fills.timeintUnix timestamp in milliseconds when the fill executed.
fills.startPositionstringSigned position size on the same coin immediately before this fill.
fills.dirstringHuman-readable direction label - "Open Long", "Open Short", "Close Long", "Close Short", "Buy", "Sell", or position-flip labels "Long > Short" / "Short > Long".
fills.closedPnlstringRealized PnL in USDC attributable to this fill (zero when the fill opens or extends a position).
fills.hashstringL1 transaction hash that included this fill.
fills.oidintParent order ID.
fills.crossedbooleantrue when the fill came from the taker side of the order, false when it was the maker side.
fills.feestringTrading fee paid for this fill, denominated in feeToken.
fills.tidintUnique trade ID.
fills.feeTokenstringSymbol the fee was paid in - typically "USDC".
fills.twapId`intnull`Parent TWAP order ID if this fill is a slice of a TWAP, otherwise null.
fills.cloidstringOptional. Client order ID (0x-prefixed 32-character hex) if one was set at order placement.
fills.builderstringOptional. Builder address the order was routed through.
fills.builderFeestringOptional. Builder fee paid for this fill, denominated in feeToken.
fills.liquidationobjectOptional. Present only when this fill closed a position as part of a liquidation event. __RESPONSE_ROW__liquidatedUser string The wallet whose position was liquidated. __RESPONSE_ROW__fills.markPx string Mark price at the time of liquidation. __RESPONSE_ROW__fills.method string Liquidation method - "market" or "backstop".

builderFills

stream live attributed fills for one or more builder addresses in real time.

liquidationFills

stream a global, market-wide feed of every liquidation fill on HyperCore.

userFills

stream real-time trade fills for one or more wallets as they execute on HyperCore. Last reviewed: 2026-06-19

builderFills | Hyperliquid WebSocket API

Credit Cost: 1 per minute Processing: Realtime
Note: - GoldRush-native: No wss://api.hyperliquid.xyz/ws equivalent.
  • Pass builder (string) for a single builder or addresses (string[]) for many. aggregateByTime optional.
  • Live-only: No historical snapshot on subscribe. For windowed history, use the Info API builderFillsByTime.
  • Same per-fill shape as userFills, plus builder and builderFee on every entry; channel name in messages is builderFills.
Subscribe to one builder address (builder) or many (addresses) and receive every fill routed through those builders as they execute, batched per block as [address, fill] tuples. Each fill carries the matched builder address and the corresponding builderFee.

Endpoint

wss://hypercore.goldrushdata.com/ws?key=
Your GoldRush API key. Passed as a query parameter at connection time - no Authorization header is used.

Subscribe

Send this JSON message after the connection is established. Provide either builder (single address) or addresses (array of addresses).
ParameterTypeRequiredDescription
methodstringYesAlways "subscribe".
subscriptionobjectYes__RESPONSE_ROW__type string Always "builderFills". __RESPONSE_ROW__builder string Single builder address (lowercase 0x-prefixed 42-character hex) to stream fills for. Mutually exclusive with addresses. __RESPONSE_ROW__addresses string[] Array of builder addresses to stream fills for - the multi-builder form of builder. Mutually exclusive with builder. __RESPONSE_ROW__aggregateByTime boolean When true, partial fills of the same order within the same block are merged into one. Default false.

Example

wscat
wscat -c "wss://hypercore.goldrushdata.com/ws?key=$GOLDRUSH_API_KEY"

> {"method":"subscribe","subscription":{"type":"builderFills","builder":"0xb84168cf3be63c6b8dad05ff5d755e97432ff80b"}}
TypeScript
import WebSocket from "ws";

const ws = new WebSocket(
  `wss://hypercore.goldrushdata.com/ws?key=${process.env.GOLDRUSH_API_KEY}`,
);

ws.on("open", () => {
  ws.send(JSON.stringify({
    method: "subscribe",
    subscription: {
      type: "builderFills",
      builder: "0xb84168cf3be63c6b8dad05ff5d755e97432ff80b",
    },
  }));
});

ws.on("message", (raw) => {
  const msg = JSON.parse(raw.toString());
  if (msg.channel === "builderFills") {
    for (const [address, fill] of msg.fills) {
      console.log(address, fill.coin, fill.side, fill.sz, "@", fill.px, "builderFee:", fill.builderFee);
    }
  }
});
Python
import asyncio, json, os
import websockets

async def main():
    uri = f"wss://hypercore.goldrushdata.com/ws?key={os.environ['GOLDRUSH_API_KEY']}"
    async with websockets.connect(uri) as ws:
        await ws.send(json.dumps({
            "method": "subscribe",
            "subscription": {
                "type": "builderFills",
                "builder": "0xb84168cf3be63c6b8dad05ff5d755e97432ff80b",
            },
        }))
        async for raw in ws:
            msg = json.loads(raw)
            if msg.get("channel") == "builderFills":
                for address, fill in msg["fills"]:
                    print(address, fill["coin"], fill["side"], fill["sz"], "@", fill["px"], "builderFee:", fill.get("builderFee"))

asyncio.run(main())

Unsubscribe

Send the same subscription body with method: "unsubscribe":
{
  "method": "unsubscribe",
  "subscription": {
    "type": "builderFills",
    "builder": "0xb84168cf3be63c6b8dad05ff5d755e97432ff80b"
  }
}
Note: Unsubscribe matches subscriptions by exact body. A subscription created with builder: "0xAAA" is a different subscription from one created with addresses: ["0xAAA"], even if they target the same builder. To narrow a multi-builder set, unsubscribe the original list in full, then resubscribe with the smaller list.

Streamed message

Each push has channel: "builderFills" and a fills array of [address, fill] tuples. The first element of each tuple is the trader who executed the fill; the fill object carries the trade details, including the builder it was routed through and the builderFee earned.
{
  "channel": "builderFills",
  "fills": [
    [
      "0x742d35cc6634c0532925a3b844bc9e7595f7f2e2",
      {
        "coin": "ETH",
        "px": "2150.5",
        "sz": "1.5",
        "side": "B",
        "time": 1704067200000,
        "startPosition": "1.5",
        "dir": "Open Long",
        "closedPnl": "125.5",
        "hash": "0xabc...def",
        "oid": 12345678,
        "crossed": false,
        "fee": "2.5",
        "tid": 87654321,
        "feeToken": "USDC",
        "twapId": null,
        "builderFee": "0.1",
        "builder": "0xb84168cf3be63c6b8dad05ff5d755e97432ff80b"
      }
    ]
  ]
}
FieldTypeDescription
channelstringAlways "builderFills".
fillsarrayTuples of [address, fill]. The address is the trader who placed the order; the fill object carries the trade details plus builder-attribution fields.
fills.coinstringAsset symbol - e.g. "BTC", "ETH" for perps; spot pairs use the @N form (e.g. "@107").
fills.pxstringFill execution price (decimal string).
fills.szstringFill size (decimal string).
fills.sidestring"B" for buy/long, "A" for ask/short.
fills.timeintUnix timestamp in milliseconds when the fill executed.
fills.startPositionstringSigned position size the trader held on this coin immediately before this fill.
fills.dirstringHuman-readable direction label - "Open Long", "Open Short", "Close Long", "Close Short", "Buy", "Sell", or position-flip labels "Long > Short" / "Short > Long".
fills.closedPnlstringRealized PnL in USDC attributable to this fill for the trader (zero when the fill opens or extends a position).
fills.hashstringL1 transaction hash that included this fill.
fills.oidintParent order ID.
fills.crossedbooleantrue when the fill came from the taker side of the order, false when it was the maker side.
fills.feestringTrading fee paid by the trader for this fill, denominated in feeToken.
fills.tidintUnique trade ID.
fills.feeTokenstringSymbol the fee was paid in - typically "USDC".
fills.twapId`intnull`Parent TWAP order ID if this fill is a slice of a TWAP, otherwise null.
fills.builderstringBuilder address the order was routed through - matches one of the subscribed builders.
fills.builderFeestringBuilder fee earned for this fill, denominated in feeToken.
fills.cloidstringOptional. Client order ID (0x-prefixed 32-character hex) if one was set at order placement.
fills.liquidationobjectOptional. Present only when this fill closed a position as part of a liquidation event. __RESPONSE_ROW__liquidatedUser string The wallet whose position was liquidated. __RESPONSE_ROW__fills.markPx string Mark price at the time of liquidation. __RESPONSE_ROW__fills.method string Liquidation method - "market" or "backstop".

Errors

A missing or malformed builder/addresses field returns an error message on the same channel:
{ "channel": "error", "data": "subscription builderFills requires a 'builder' field" }

allFills

stream every fill on HyperCore in real time for global market analytics and cross-wallet order-flow…

liquidationFills

stream a global, market-wide feed of every liquidation fill on HyperCore.

userFills

stream real-time trade fills for one or more wallets as they execute on HyperCore. Last reviewed: 2026-06-19

l2BookDiff | Hyperliquid WebSocket API

Credit Cost: 0.1 per coin per minute Processing: Realtime
Note: - GoldRush-native. l2BookDiff is not exposed on wss://api.hyperliquid.xyz/ws. Pointing a client at the public endpoint with this subscription type will fail.
  • Snapshot, then diffs. The first message is always a Snapshot; every message thereafter is an Updates. Clients must seed local book state from the snapshot and apply diffs from there. On reconnect, drop local state and re-seed from the next snapshot.
  • coin is optional. Omit it to stream the entire L2 order book across every asset on a single subscription.
  • When coin is omitted, the optional marketTypes filter selects which market families to include. It defaults to ["perp"] only.
  • Pass "marketTypes": ["spot"], or "marketTypes":["outcome"], or a mix (e.g. "marketTypes": ["perp","spot"]), or use the wildcard "marketTypes": ["*"] to opt into spot, perps, outcome, and any future market types.
  • No 1000-subscription-per-IP cap - multiplex hundreds of l2BookDiff subscriptions on a single connection.
  • For OHLCV candles instead of raw book state, use the Streaming API OHLCV streams.
Note: When coin is omitted, a credit rate of 10 credits per minute subscribed is applied.

Endpoint

wss://hypercore.goldrushdata.com/ws?key=
Your GoldRush API key. Passed as a query parameter at connection time - no Authorization header is used.

Subscribe

Send this JSON message after the connection is established:
ParameterTypeRequiredDescription
methodstringYesAlways "subscribe".
subscriptionobjectYes__RESPONSE_ROW__type string Always "l2BookDiff". __RESPONSE_ROW__coin string | string[] Asset filter. Accepts three shapes: - String - a single asset symbol (e.g. "HYPE", "BTC", "@107" for spot pairs). For HIP-3 markets, include the deployer prefix. - Array of strings - a fixed list of asset symbols (e.g. ["HYPE", "BTC", "ETH"]). Each listed coin gets its own initial Snapshot; subsequent Updates may bundle diffs for any subset of the list per block. - Omitted - wildcard. Streams the full L2 book across every perp asset by default on one subscription (see marketTypes to include spot or outcome markets). The server emits one Snapshot per live coin, then per-block Updates covering only the coins that changed. __RESPONSE_ROW__marketTypes string[] Optional. Selects which market families a wildcard subscription includes. Only valid when coin is omitted. Defaults to ["perp"] - when omitted, only perp markets stream. Spot and outcome markets require explicit opt-in. Accepted values: - "perp" (default) - vanilla perps and HIP-3 deployer-perps (e.g. BTC, ETH, HYPE, SOL, cash, abcd, USA500). - "spot" - @ spot markets and legacy spot pairs (e.g. @1, @107 for HYPE spot, PURR/USDC). Not included by default. - "outcome" - HIP-4 prediction-market outcomes (e.g. #700, #710, #741). Not included by default. - "*" - every current type ("perp" + "spot" + "outcome") and auto-opt-in to any future types the server adds. Mix and match in one subscription, e.g. ["perp", "outcome"]. A second subscribe with a different marketTypes value replaces the previous filter rather than coexisting with it.

Example

Pick the subscription shape that matches the coverage you want. Every subscription starts with one Snapshot per coin in scope, then per-block Updates carrying only changed levels:
Subscribe withWhat you receive
{"type":"l2BookDiff","coin":"HYPE"}Snapshot + diffs for HYPE only
{"type":"l2BookDiff","coin":["HYPE","BTC","ETH"]}Snapshot + diffs for a fixed list of coins
{"type":"l2BookDiff"}Snapshot + diffs for every perp coin (default: marketTypes: ["perp"])
{"type":"l2BookDiff","marketTypes":["spot"]}Snapshot + diffs for every spot coin
{"type":"l2BookDiff","marketTypes":["outcome"]}Snapshot + diffs for every HIP-4 outcome market
{"type":"l2BookDiff","marketTypes":["perp","spot"]}Snapshot + diffs for perps + spot
{"type":"l2BookDiff","marketTypes":["*"]}Snapshot + diffs for every coin (perp + spot + outcome, plus future types)
wscat
wscat -c "wss://hypercore.goldrushdata.com/ws?key=$GOLDRUSH_API_KEY"

> {"method":"subscribe","subscription":{"type":"l2BookDiff","coin":"HYPE"}}
TypeScript
import WebSocket from "ws";

const ws = new WebSocket(
  `wss://hypercore.goldrushdata.com/ws?key=${process.env.GOLDRUSH_API_KEY}`,
);

ws.on("open", () => {
  ws.send(JSON.stringify({
    method: "subscribe",
    subscription: { type: "l2BookDiff", coin: "HYPE" },
  }));
});

ws.on("message", (raw) => {
  const msg = JSON.parse(raw.toString());
  if (msg.channel !== "l2BookDiff") return;

  if (msg.data.Snapshot) {
    const { coin, time, block_height, levels: [bids, asks] } = msg.data.Snapshot;
    console.log("snapshot", coin, time, block_height, "bids:", bids.length, "asks:", asks.length);
  } else if (msg.data.Updates) {
    const { time, block_height, book_diffs } = msg.data.Updates;
    console.log("updates", time, block_height, "coins:", book_diffs.length);
  }
});
Python
import asyncio, json, os
import websockets

async def main():
    uri = f"wss://hypercore.goldrushdata.com/ws?key={os.environ['GOLDRUSH_API_KEY']}"
    async with websockets.connect(uri) as ws:
        await ws.send(json.dumps({
            "method": "subscribe",
            "subscription": {"type": "l2BookDiff", "coin": "HYPE"},
        }))
        async for raw in ws:
            msg = json.loads(raw)
            if msg.get("channel") != "l2BookDiff":
                continue
            data = msg["data"]
            if "Snapshot" in data:
                snap = data["Snapshot"]
                bids, asks = snap["levels"]
                print("snapshot", snap["coin"], snap["time"], snap["block_height"], "bids:", len(bids), "asks:", len(asks))
            elif "Updates" in data:
                upd = data["Updates"]
                print("updates", upd["time"], upd["block_height"], "coins:", len(upd["book_diffs"]))

asyncio.run(main())

Unsubscribe

Send the same subscription body with method: "unsubscribe":
{
  "method": "unsubscribe",
  "subscription": { "type": "l2BookDiff", "coin": "HYPE" }
}
Note: Unsubscribe matches subscriptions by exact body. A subscription created with coin: ["BTC", "ETH"] is a different subscription from one created with coin: "BTC" or coin: "ETH".
You cannot unsubscribe a partial set of coins from an existing multi-coin subscription. To narrow the set, unsubscribe the original coin array in full, then resubscribe with the smaller list:
// Drop ETH from a ["BTC","ETH"] subscription:
{ "method": "unsubscribe", "subscription": { "type": "l2BookDiff", "coin": ["BTC", "ETH"] } }
{ "method": "subscribe",   "subscription": { "type": "l2BookDiff", "coin": ["BTC"] } }

Streamed messages

Every message has channel: "l2BookDiff". The data payload contains exactly one of two variants: a Snapshot (emitted once per subscribed coin, immediately after subscribe) or an Updates (emitted on each subsequent HyperCore block where the book for at least one subscribed coin changed).

Initial snapshot

After subscribe, the server emits one Snapshot message per coin currently in scope. For a single-coin subscription that is one message; for a list or wildcard subscription that is one message per asset. Each entry in levels[0] (bids) and levels[1] (asks) is an aggregated price level, sorted best-first.
{
  "channel": "l2BookDiff",
  "data": {
    "Snapshot": {
      "coin": "HYPE",
      "time": 1779220051027,
      "block_height": 1002862373,
      "levels": [
        [
          { "px": "48.601", "sz": "51.26", "n": 1 }
        ],
        [
          { "px": "48.614", "sz": "12.34", "n": 1 }
        ]
      ]
    }
  }
}

Incremental updates

Subsequent messages carry only the levels that changed since the previous block, grouped by coin. A level entry with sz: "0" and n: 0 means the level at that price has been removed; any other entry replaces the current state at that px with the new {sz, n}.
{
  "channel": "l2BookDiff",
  "data": {
    "Updates": {
      "time": 1779220051224,
      "block_height": 1002862374,
      "book_diffs": [
        {
          "coin": "HYPE",
          "levels": [
            [
              { "px": "48.601", "sz": "60.00", "n": 2 }
            ],
            [
              { "px": "48.614", "sz": "0", "n": 0 }
            ]
          ]
        }
      ]
    }
  }
}

Response fields

FieldTypeDescription
channelstringAlways "l2BookDiff".
dataobjectContains exactly one of Snapshot or Updates.
data.coinstringAsset symbol the snapshot belongs to.
data.timeintHyperCore block timestamp in milliseconds.
data.block_heightintHyperCore block height the snapshot was taken at.
data.levelsarray>Tuple [bids, asks]. Each side is an array of aggregated Level objects in best-first order. __RESPONSE_ROW__px string Price for this level (decimal string). __RESPONSE_ROW__data.sz string Aggregate size resting at this level (decimal string, base units). __RESPONSE_ROW__data.n int Number of orders aggregated into this level. HyperCore block timestamp in milliseconds.
block_heightintHyperCore block height.
book_diffsarrayPer-coin lists of changed price levels. One entry per coin that had changes at this block.
book_diffs.coinstringAsset symbol the diff applies to.
book_diffs.levelsarray>Tuple [changed_bids, changed_asks]. Each entry replaces the current state at its px. An entry with sz: "0" and n: 0 removes the level at that price. __RESPONSE_ROW__px string Price for this level (decimal string). __RESPONSE_ROW__book_diffs.sz string New aggregate size at this level (decimal string). "0" means the level is removed. __RESPONSE_ROW__book_diffs.n int New number of orders aggregated into this level. 0 means the level is removed.

l2Book

Subscribe to real-time L2 order book snapshots for all Hyperliquid assets over WebSocket. Last reviewed: 2026-06-16

l2Book | Hyperliquid WebSocket API

Credit Cost: 0.5 per coin per minute Processing: Realtime
Note: - Wire-compatible with wss://api.hyperliquid.xyz/ws l2Book subscriptions - same channel name, same levels shape.
  • coin is optional on GoldRush. Omit it to stream the entire L2 order book across every asset on a single subscription. The public Hyperliquid API requires coin and locks each subscription to one asset at a time.
  • When coin is omitted, the optional marketTypes filter selects which market families to include. It defaults to ["perp"] only.
  • Pass "marketTypes": ["spot"], or "marketTypes":["outcome"], or a mix (e.g. "marketTypes": ["perp","spot"]), or use the wildcard "marketTypes": ["*"] to opt into spot, perps, outcome, and any future market types.
  • No 1000-subscription-per-IP cap - multiplex hundreds of l2Book subscriptions on a single connection.
  • For OHLCV candles instead of raw book state, use the Streaming API OHLCV streams.
Note: When coin is omitted, a credit rate of 50 credits per minute subscribed is applied.

Endpoint

wss://hypercore.goldrushdata.com/ws?key=
Your GoldRush API key. Passed as a query parameter at connection time - no Authorization header is used.

Subscribe

Send this JSON message after the connection is established:
ParameterTypeRequiredDescription
methodstringYesAlways "subscribe".
subscriptionobjectYes__RESPONSE_ROW__type string Always "l2Book". __RESPONSE_ROW__coin string Asset symbol - e.g. "BTC", "ETH", "@107" for spot pairs. For HIP-3 markets, include the deployer prefix. Omit to receive snapshots for all perp assets (the default; see marketTypes to include spot or outcome markets). __RESPONSE_ROW__marketTypes string[] Optional. Selects which market families a wildcard subscription includes. Only valid when coin is omitted. Defaults to ["perp"] - when omitted, only perp markets stream. Spot and outcome markets require explicit opt-in. Accepted values: - "perp" (default) - vanilla perps and HIP-3 deployer-perps (e.g. BTC, ETH, HYPE, SOL, cash, abcd, USA500). - "spot" - @ spot markets and legacy spot pairs (e.g. @1, @107 for HYPE spot, PURR/USDC). Not included by default. - "outcome" - HIP-4 prediction-market outcomes (e.g. #700, #710, #741). Not included by default. - "*" - every current type ("perp" + "spot" + "outcome") and auto-opt-in to any future types the server adds. Mix and match in one subscription, e.g. ["perp", "outcome"]. A second subscribe with a different marketTypes value replaces the previous filter rather than coexisting with it. __RESPONSE_ROW__nSigFigs int Significant figures used for price aggregation. One of 2, 3, 4, 5, or null for full precision. Defaults to null. __RESPONSE_ROW__mantissa int When nSigFigs is 5, controls the mantissa rounding. One of 1, 2, or 5. Not allowed for other nSigFigs values.

Example

Pick the subscription shape that matches the coverage you want:
Subscribe withWhat you receive
{"type":"l2Book","coin":"BTC"}L2 snapshots for BTC only
{"type":"l2Book"}L2 snapshots for every perp coin (default: marketTypes: ["perp"])
{"type":"l2Book","marketTypes":["spot"]}L2 snapshots for every spot coin
{"type":"l2Book","marketTypes":["outcome"]}L2 snapshots for every HIP-4 outcome market
{"type":"l2Book","marketTypes":["perp","spot"]}L2 snapshots for perps + spot
{"type":"l2Book","marketTypes":["*"]}L2 snapshots for every coin (perp + spot + outcome, plus future types)
wscat
wscat -c "wss://hypercore.goldrushdata.com/ws?key=$GOLDRUSH_API_KEY"

> {"method":"subscribe","subscription":{"type":"l2Book","coin":"BTC"}}
TypeScript
import WebSocket from "ws";

const ws = new WebSocket(
  `wss://hypercore.goldrushdata.com/ws?key=${process.env.GOLDRUSH_API_KEY}`,
);

ws.on("open", () => {
  ws.send(JSON.stringify({
    method: "subscribe",
    subscription: { type: "l2Book", coin: "BTC" },
  }));
});

ws.on("message", (raw) => {
  const msg = JSON.parse(raw.toString());
  if (msg.channel === "l2Book") {
    const { coin, time, levels: [bids, asks] } = msg.data;
    console.log(coin, time, "top bid:", bids[0], "top ask:", asks[0]);
  }
});
Python
import asyncio, json, os
import websockets

async def main():
    uri = f"wss://hypercore.goldrushdata.com/ws?key={os.environ['GOLDRUSH_API_KEY']}"
    async with websockets.connect(uri) as ws:
        await ws.send(json.dumps({
            "method": "subscribe",
            "subscription": {"type": "l2Book", "coin": "BTC"},
        }))
        async for raw in ws:
            msg = json.loads(raw)
            if msg.get("channel") == "l2Book":
                print(msg["data"]["coin"], msg["data"]["time"], msg["data"]["levels"][0][:1])

asyncio.run(main())

Unsubscribe

Send the same subscription body with method: "unsubscribe":
{
  "method": "unsubscribe",
  "subscription": { "type": "l2Book", "coin": "BTC" }
}
Note: Unsubscribe matches subscriptions by exact body. A subscription created with coin: ["BTC", "ETH"] is a different subscription from one created with coin: "BTC" or coin: "ETH".
You cannot unsubscribe a partial set of coins from an existing multi-coin subscription. To narrow the set, unsubscribe the original coin array in full, then resubscribe with the smaller list:
// Drop ETH from a ["BTC","ETH"] subscription:
{ "method": "unsubscribe", "subscription": { "type": "l2Book", "coin": ["BTC", "ETH"] } }
{ "method": "subscribe",   "subscription": { "type": "l2Book", "coin": ["BTC"] } }

Streamed message

Each message has channel: "l2Book" and a data payload with the current book snapshot for the subscribed coin.
{
  "channel": "l2Book",
  "data": {
    "coin": "BTC",
    "time": 1762450000123,
    "block_height": 996629014,
    "levels": [
      [
        { "px": "68210.0", "sz": "1.2345", "n": 3 },
        { "px": "68209.5", "sz": "4.5670", "n": 5 },
        { "px": "68209.0", "sz": "2.1100", "n": 2 }
      ],
      [
        { "px": "68215.0", "sz": "0.8900", "n": 2 },
        { "px": "68215.5", "sz": "3.4500", "n": 4 },
        { "px": "68216.0", "sz": "1.2300", "n": 1 }
      ]
    ]
  }
}
FieldTypeDescription
channelstringAlways "l2Book".
dataobject
data.coinstringAsset symbol the snapshot belongs to.
data.timeintHyperCore block timestamp in milliseconds.
data.block_heightintHyperCore block height the snapshot was taken at.
data.levelsarray>Tuple [bids, asks]. Each side is an array of price levels in best-first order. __RESPONSE_ROW__px string Price for this level (decimal string). __RESPONSE_ROW__data.sz string Aggregate size resting at this level (decimal string, base units). __RESPONSE_ROW__data.n int Number of orders aggregated into this level.

l2BookDiff

Subscribe to real-time L2 order book (initial snapshot + diffs) for all Hyperliquid assets over WebSocket. Last reviewed: 2026-06-16

l4Book | Hyperliquid WebSocket API

Credit Cost: 3 per coin per minute (except BTC which is 60 per minute) Processing: Realtime
Note: - GoldRush-native. l4Book is not exposed on wss://api.hyperliquid.xyz/ws. Pointing a client at the public endpoint with this subscription type will fail.
  • Perps only. No spot assets.
  • coin is required. Unlike l2Book and l2BookDiff, you cannot omit coin to stream every asset. Open one subscription per asset.
  • Snapshot, then diffs. The first message is always a Snapshot; every message thereafter is an Updates. Clients must seed local book state from the snapshot and apply diffs from there. On reconnect, drop local state and re-seed from the next snapshot.
  • Per-order detail. Each entry exposes useroidcloidtif, and trigger metadata - enabling queue-position reconstruction, per-trader flow attribution, and microstructure analytics that are not possible with l2Book.
  • See the L4 Order Book recipe for patterns to maintain book state, attribute flow by user, or reconstruct aggregated price levels.
Note: When "coin":"BTC" is used, a credit rate of 60 credits per minute subscribed is applied.

Endpoint

wss://hypercore.goldrushdata.com/ws?key=
Your GoldRush API key. Passed as a query parameter at connection time - no Authorization header is used.

Subscribe

Send this JSON message after the connection is established:
ParameterTypeRequiredDescription
methodstringYesAlways "subscribe".
subscriptionobjectYes__RESPONSE_ROW__type string Always "l4Book". __RESPONSE_ROW__coin string Asset symbol - e.g. "BTC", "ETH", "@107" for spot pairs. For HIP-3 markets, include the deployer prefix. Required - unlike l2Book, l4Book does not support wildcard subscriptions; each subscription is locked to a single asset.

Example

wscat
wscat -c "wss://hypercore.goldrushdata.com/ws?key=$GOLDRUSH_API_KEY"

> {"method":"subscribe","subscription":{"type":"l4Book","coin":"BTC"}}
TypeScript
import WebSocket from "ws";

const ws = new WebSocket(
  `wss://hypercore.goldrushdata.com/ws?key=${process.env.GOLDRUSH_API_KEY}`,
);

ws.on("open", () => {
  ws.send(JSON.stringify({
    method: "subscribe",
    subscription: { type: "l4Book", coin: "BTC" },
  }));
});

ws.on("message", (raw) => {
  const msg = JSON.parse(raw.toString());
  if (msg.channel !== "l4Book") return;

  if (msg.data.Snapshot) {
    const { coin, time, block_height, levels: [bids, asks] } = msg.data.Snapshot;
    console.log("snapshot", coin, time, block_height, "bids:", bids.length, "asks:", asks.length);
  } else if (msg.data.Updates) {
    const { time, block_height, order_statuses, book_diffs } = msg.data.Updates;
    console.log("updates", time, block_height, "statuses:", order_statuses.length, "diffs:", book_diffs.length);
  }
});
Python
import asyncio, json, os
import websockets

async def main():
    uri = f"wss://hypercore.goldrushdata.com/ws?key={os.environ['GOLDRUSH_API_KEY']}"
    async with websockets.connect(uri) as ws:
        await ws.send(json.dumps({
            "method": "subscribe",
            "subscription": {"type": "l4Book", "coin": "BTC"},
        }))
        async for raw in ws:
            msg = json.loads(raw)
            if msg.get("channel") != "l4Book":
                continue
            data = msg["data"]
            if "Snapshot" in data:
                snap = data["Snapshot"]
                bids, asks = snap["levels"]
                print("snapshot", snap["coin"], snap["time"], snap["block_height"], "bids:", len(bids), "asks:", len(asks))
            elif "Updates" in data:
                upd = data["Updates"]
                print("updates", upd["time"], upd["block_height"], "statuses:", len(upd["order_statuses"]), "diffs:", len(upd["book_diffs"]))

asyncio.run(main())

Unsubscribe

Send the same subscription body with method: "unsubscribe":
{
  "method": "unsubscribe",
  "subscription": { "type": "l4Book", "coin": "BTC" }
}

Streamed messages

Every message has channel: "l4Book". The data payload contains exactly one of two variants: a Snapshot (emitted once, immediately after subscribe) or an Updates (emitted on each subsequent HyperCore block where the book for coin changed).

Initial snapshot

The first message after subscribe carries the full resting book at the current block. Each entry in levels[0] (bids) and levels[1] (asks) is an individual order - not an aggregated price level.
{
  "channel": "l4Book",
  "data": {
    "Snapshot": {
      "coin": "BTC",
      "time": 1778865761968,
      "block_height": 997719816,
      "levels": [
        [
          {
            "user": "0xa62b923a112d50d03e1e096bbd53422490dac104",
            "coin": "BTC",
            "side": "B",
            "limitPx": "79242",
            "sz": "0.74831",
            "oid": 427632406005,
            "timestamp": 1778865761305,
            "triggerCondition": "N/A",
            "isTrigger": false,
            "triggerPx": "0.0",
            "isPositionTpsl": false,
            "reduceOnly": false,
            "orderType": "Limit",
            "tif": "Alo",
            "cloid": "0x00000000000000000000019e2c8b7d66"
          }
        ],
        [
          {
            "user": "0xfcf104006bfff47695c1dc21dad3e9de1e72098e",
            "coin": "BTC",
            "side": "A",
            "limitPx": "79250",
            "sz": "0.2961",
            "oid": 427632406032,
            "timestamp": 1778865761305,
            "triggerCondition": "N/A",
            "isTrigger": false,
            "triggerPx": "0.0",
            "isPositionTpsl": false,
            "reduceOnly": false,
            "orderType": "Limit",
            "tif": "Gtc",
            "cloid": null
          }
        ]
      ]
    }
  }
}

Incremental updates

Subsequent messages carry only what changed since the previous block. order_statuses describes order lifecycle events (open, etc.); book_diffs carries the corresponding price-level changes.
{
  "channel": "l4Book",
  "data": {
    "Updates": {
      "time": 1778865761768,
      "block_height": 997719813,
      "order_statuses": [
        {
          "time": "2026-05-15T17:22:41.768005701",
          "user": "0x31ca8395cf837de08b24da3f660e77761dfb974b",
          "status": "open",
          "order": {
            "user": null,
            "coin": "BTC",
            "side": "B",
            "limitPx": "79242.0",
            "sz": "0.00867",
            "oid": 427632416336,
            "timestamp": 1778865761768,
            "triggerCondition": "N/A",
            "isTrigger": false,
            "triggerPx": "0.0",
            "isPositionTpsl": false,
            "reduceOnly": false,
            "orderType": "Limit",
            "tif": "Alo",
            "cloid": null
          }
        }
      ],
      "book_diffs": [
        {
          "user": "0x31ca8395cf837de08b24da3f660e77761dfb974b",
          "oid": 427632416336,
          "px": "79242.0",
          "coin": "BTC",
          "raw_book_diff": { "new": { "sz": "0.00867" } }
        }
      ]
    }
  }
}

Response fields

FieldTypeDescription
channelstringAlways "l4Book".
dataobjectContains exactly one of Snapshot or Updates.
data.coinstringAsset symbol the snapshot belongs to.
data.timeintHyperCore block timestamp in milliseconds.
data.block_heightintHyperCore block height the snapshot was taken at.
data.levelsarray>Tuple [bids, asks]. Each side is an array of individual Order objects (see below), in queue order at their respective price.
data.timeintHyperCore block timestamp in milliseconds.
data.block_heightintHyperCore block height.
data.order_statusesarrayOrder lifecycle events at this block. __RESPONSE_ROW__time string ISO-8601 timestamp with nanosecond precision. __RESPONSE_ROW__data.user string Wallet address that owns the order. __RESPONSE_ROW__data.status string Lifecycle status (e.g. "open"). __RESPONSE_ROW__data.order Order The order, in the same shape as a snapshot entry. user inside this nested object is null because it duplicates the parent user. __RESPONSE_ROW__book_diffs array Per-order book changes at this block. __RESPONSE_ROW__book_diffs.user string Wallet address that owns the order. __RESPONSE_ROW__book_diffs.oid int Order id the diff applies to. __RESPONSE_ROW__book_diffs.px string Price level the diff applies to (decimal string). __RESPONSE_ROW__book_diffs.coin string Asset symbol. __RESPONSE_ROW__book_diffs.raw_book_diff object The change descriptor. Observed shape: { "new": { "sz": "" } } for a newly resting order. Other shapes may carry size deltas or cancellations - inspect the keys to discriminate.

Order object

The Order type appears inside Snapshot.levels[*][*] and Updates.order_statuses[*].order.
FieldTypeDescription
user`stringnull`Wallet address that owns the order. null when the order is nested inside an order_status (the parent already carries it).
coinstringAsset symbol.
sidestring"B" for bid, "A" for ask.
limitPxstringLimit price (decimal string).
szstringResting size (decimal string, base units).
oidintHyperliquid order id - stable for the lifetime of the order.
timestampintOrder-placement timestamp in HyperCore milliseconds.
triggerConditionstringTrigger condition string (e.g. "N/A" for plain limit orders).
isTriggerbooleanTrue if this is a stop / take-profit trigger order.
triggerPxstringTrigger price (decimal string, "0.0" for non-trigger orders).
isPositionTpslbooleanTrue if this is a position-level TP/SL.
reduceOnlybooleanTrue if the order is flagged reduce-only.
orderTypestringHyperliquid order type (e.g. "Limit").
tifstringTime-in-force (e.g. "Alo", "Gtc", "Ioc").
cloid`stringnull`Client-supplied order id (hex string), or null if none was provided.
Last reviewed: 2026-06-17

liquidationFills | Hyperliquid WebSocket API

Credit Cost: 1 per minute Processing: Realtime
Note: - GoldRush-native: No wss://api.hyperliquid.xyz/ws equivalent. The public WebSocket exposes per-wallet fills only; detecting liquidations there would require subscribing to every wallet and filtering.
  • Global stream: Every liquidation fill on HyperCore, across every wallet, on a single subscription. No addresses filter is accepted.
  • Every fill in this stream carries a non-null liquidation object. The rest of the payload mirrors userFills.
  • Live-only: No historical snapshot on subscribe. For historical liquidations, query the Streaming API HypercoreLedgerEvent type.
Subscribe with no addresses filter to receive every liquidation fill on HyperCore across every wallet, in real time. Same payload shape as userFills, with the liquidation object populated (with liquidatedUser, markPx, method) on every entry.

Endpoint

wss://hypercore.goldrushdata.com/ws?key=
Your GoldRush API key. Passed as a query parameter at connection time - no Authorization header is used.

Subscribe

Send this JSON message after the connection is established:
ParameterTypeRequiredDescription
methodstringYesAlways "subscribe".
subscriptionobjectYes__RESPONSE_ROW__type string Always "liquidationFills". __RESPONSE_ROW__aggregateByTime boolean When true, partial fills of the same liquidation within the same block are merged into one. Default false.

Example

wscat
wscat -c "wss://hypercore.goldrushdata.com/ws?key=$GOLDRUSH_API_KEY"

> {"method":"subscribe","subscription":{"type":"liquidationFills"}}
TypeScript
import WebSocket from "ws";

const ws = new WebSocket(
  `wss://hypercore.goldrushdata.com/ws?key=${process.env.GOLDRUSH_API_KEY}`,
);

ws.on("open", () => {
  ws.send(JSON.stringify({
    method: "subscribe",
    subscription: { type: "liquidationFills" },
  }));
});

ws.on("message", (raw) => {
  const msg = JSON.parse(raw.toString());
  if (msg.channel === "liquidationFills") {
    for (const [address, fill] of msg.fills) {
      console.log(
        "liquidated:", fill.liquidation.liquidatedUser,
        "via", fill.liquidation.method,
        "—", fill.coin, fill.sz, "@", fill.px,
      );
    }
  }
});
Python
import asyncio, json, os
import websockets

async def main():
    uri = f"wss://hypercore.goldrushdata.com/ws?key={os.environ['GOLDRUSH_API_KEY']}"
    async with websockets.connect(uri) as ws:
        await ws.send(json.dumps({
            "method": "subscribe",
            "subscription": {"type": "liquidationFills"},
        }))
        async for raw in ws:
            msg = json.loads(raw)
            if msg.get("channel") == "liquidationFills":
                for address, fill in msg["fills"]:
                    liq = fill["liquidation"]
                    print("liquidated:", liq["liquidatedUser"],
                          "via", liq["method"], "—",
                          fill["coin"], fill["sz"], "@", fill["px"])

asyncio.run(main())

Unsubscribe

Send the same subscription body with method: "unsubscribe":
{
  "method": "unsubscribe",
  "subscription": { "type": "liquidationFills" }
}

Streamed message

Each push has channel: "liquidationFills" and a fills array of [address, fill] tuples. The address is the wallet whose order was filled (i.e. the liquidator’s counterparty); liquidation.liquidatedUser is the wallet whose position was liquidated.
{
  "channel": "liquidationFills",
  "fills": [
    [
      "0x742d35cc6634c0532925a3b844bc9e7595f7f2e2",
      {
        "coin": "ETH",
        "px": "2150.5",
        "sz": "1.5",
        "side": "B",
        "time": 1704067200000,
        "startPosition": "1.5",
        "dir": "Open Long",
        "closedPnl": "125.5",
        "hash": "0xabc...def",
        "oid": 12345678,
        "crossed": false,
        "fee": "2.5",
        "tid": 87654321,
        "feeToken": "USDC",
        "liquidation": {
          "liquidatedUser": "0x...",
          "markPx": "2148.75",
          "method": "market"
        },
        "twapId": null
      }
    ]
  ]
}
FieldTypeDescription
channelstringAlways "liquidationFills".
fillsarrayTuples of [address, fill]. Same fill shape as userFills, with liquidation always populated.
fills.liquidationobjectAlways present on this stream. __RESPONSE_ROW__liquidatedUser string The wallet whose position was liquidated. __RESPONSE_ROW__fills.markPx string Mark price at the time of liquidation. __RESPONSE_ROW__fills.method string Liquidation method - "market" or "backstop". All other fields (coin, px, sz, side, time, startPosition, dir, closedPnl, hash, oid, crossed, fee, tid, feeToken, twapId, and optional cloid / builderFee / builder) match the userFills shape.

allFills

stream every fill on HyperCore in real time for global market analytics and cross-wallet order-flow…

builderFills

stream live attributed fills for one or more builder addresses in real time.

userFills

stream real-time trade fills for one or more wallets as they execute on HyperCore. Last reviewed: 2026-06-16

orderUpdates | Hyperliquid WebSocket API

Credit Cost: 1 per minute Processing: Realtime
Note: - Wire-compatible with wss://api.hyperliquid.xyz/ws orderUpdates subscription - same channel name, same per-update shape.
  • Up to 1,000 wallet addresses per subscription. Use addresses (string[]); aliases user(string) and users (string[]) are also accepted.
  • Messages are batched per HyperCore block; each push carries one or more updates entries, each with its own user field. Subscribe many wallets and receive one push per block containing every matching wallet’s events.
  • Live-only — no historical snapshot of resting orders on subscribe. For the current resting-order set, use the Info API  frontendOpenOrders. For historical fills, use userFillsByTime.
  • No 1,000-subscription-per-IP cap. Multiplex many orderUpdates subscriptions on a single connection.
Subscribe to one or more wallets and receive their live order status updates the moment they change when an order is placed, filled, canceled, or rejected. The server batches one push per HyperCore block, carrying every matching update as an updates array of objects, each tagged with the originating user. Supports up to 1,000 wallet addresses per subscription.

Endpoint

wss://hypercore.goldrushdata.com/ws?key=
Your GoldRush API key. Passed as a query parameter at connection time - no Authorization header is used.

Subscribe

Send this JSON message after the connection is established:
ParameterTypeRequiredDescription
methodstringYesAlways "subscribe".
subscriptionobjectYes__RESPONSE_ROW__type string Always "orderUpdates". __RESPONSE_ROW__addresses string[] One or more wallet addresses (lowercase 0x-prefixed hex) to stream order updates for. Aliases user (string) and users (string[]) are also accepted.

Example

wscat
wscat -c "wss://hypercore.goldrushdata.com/ws?key=$GOLDRUSH_API_KEY"

> {"method":"subscribe","subscription":{"type":"orderUpdates","addresses":["0x010461c14e146ac35fe42271bdc1134ee31c703a"]}}
TypeScript
import WebSocket from "ws";

const ws = new WebSocket(
  `wss://hypercore.goldrushdata.com/ws?key=${process.env.GOLDRUSH_API_KEY}`,
);

ws.on("open", () => {
  ws.send(JSON.stringify({
    method: "subscribe",
    subscription: {
      type: "orderUpdates",
      addresses: ["0x010461c14e146ac35fe42271bdc1134ee31c703a"],
    },
  }));
});

ws.on("message", (raw) => {
  const msg = JSON.parse(raw.toString());
  if (msg.channel === "orderUpdates") {
    for (const update of msg.updates) {
      console.log(
        update.user,
        update.order.coin,
        update.order.side,
        update.order.sz,
        "@",
        update.order.limitPx,
        "->",
        update.status,
      );
    }
  }
});
Python
import asyncio, json, os
import websockets

async def main():
    uri = f"wss://hypercore.goldrushdata.com/ws?key={os.environ['GOLDRUSH_API_KEY']}"
    async with websockets.connect(uri) as ws:
        await ws.send(json.dumps({
            "method": "subscribe",
            "subscription": {
                "type": "orderUpdates",
                "addresses": ["0x010461c14e146ac35fe42271bdc1134ee31c703a"],
            },
        }))
        async for raw in ws:
            msg = json.loads(raw)
            if msg.get("channel") == "orderUpdates":
                for update in msg["updates"]:
                    print(
                        update["user"],
                        update["order"]["coin"],
                        update["order"]["side"],
                        update["order"]["sz"],
                        "@",
                        update["order"]["limitPx"],
                        "->",
                        update["status"],
                    )

asyncio.run(main())

Unsubscribe

Send the same subscription body with method: "unsubscribe":
{
  "method": "unsubscribe",
  "subscription": {
    "type": "orderUpdates",
    "addresses": ["0x010461c14e146ac35fe42271bdc1134ee31c703a"]
  }
}
Note: Unsubscribe matches subscriptions by exact body. A subscription created with addresses: ["0xAAA", "0xBBB"] is a different subscription from two single-address subscriptions. To narrow the set, unsubscribe the original list in full, then resubscribe with the smaller list.

Streamed message

Each push has channel: "orderUpdates" and an updates array of objects - every order lifecycle event from the same HyperCore block that matches any subscribed wallet. Each entry is self-tagged with the originating user.
{
  "channel": "orderUpdates",
  "updates": [
    {
      "time": "2026-06-18T04:21:19.321641904",
      "user": "0x010461c14e146ac35fe42271bdc1134ee31c703a",
      "hash": "0xc2e340827d8e0749c45c043df991d1019b0058681881261b66abebd53c81e134",
      "status": "open",
      "builder": null,
      "order": {
        "coin": "W",
        "side": "B",
        "oid": 472501605004,
        "limitPx": "0.00963",
        "sz": "77502.4",
        "origSz": "77502.4",
        "timestamp": 1781756479321,
        "orderType": "Limit",
        "tif": "Alo",
        "reduceOnly": false,
        "isTrigger": false,
        "isPositionTpsl": false,
        "triggerCondition": "N/A",
        "triggerPx": "0.0",
        "cloid": null,
        "children": []
      }
    }
  ]
}
FieldTypeDescription
channelstringAlways "orderUpdates".
updatesobject[]Array of order lifecycle events from the same HyperCore block. Each entry carries its own user field, so a single push can contain events for multiple subscribed wallets.
updates.timestringISO 8601 timestamp (nanosecond precision) of the HyperCore block in which the status transition occurred.
updates.userstringWallet the update belongs to (lowercase 0x-prefixed hex).
updates.hashstringOptional. L1 transaction hash for the action that produced this update. Present on placements ("open"); typically absent on terminal transitions like "filled" and "canceled".
updates.statusstringOrder lifecycle state. Common values: "open", "filled", "canceled", "triggered", plus rejection codes ("iocCancelRejected", "selfTradeCanceled", "badAloPxRejected", "tickRejected", "minTradeNtlRejected", "reduceOnlyRejected", "marginCanceled", "vaultWithdrawalCanceled", "openInsufficientMargin", "perpMarginRejected", "badTriggerPxRejected", "marketOrderNoLiquidityRejected", "positionIncreaseAtOpenInterestCapRejected", "positionFlipAtOpenInterestCapRejected", "tooAggressiveAtOpenInterestCapRejected", "openInterestIncreaseRejected", "insufficientSpotBalanceRejected", "oracleNotFoundRejected", "perpMaxPositionRejected").
updates.builder`stringnull`Builder address the order was routed through, or null if the order was not routed through a builder code.
updates.orderobjectOrder snapshot at the moment of the status change. __RESPONSE_ROW__coin string Asset symbol - e.g. "BTC", "ETH" for perps; spot pairs use the @N form (e.g. "@107"). __RESPONSE_ROW__updates.side string "B" for buy/long, "A" for ask/short. __RESPONSE_ROW__updates.oid int Order ID. __RESPONSE_ROW__updates.limitPx string Limit price (decimal string). __RESPONSE_ROW__updates.sz string Current remaining size (decimal string). "0.0" after a full fill. __RESPONSE_ROW__updates.origSz string Original size at placement. __RESPONSE_ROW__updates.timestamp int Unix timestamp in milliseconds when the order was placed. __RESPONSE_ROW__updates.orderType string "Limit" or trigger variants (e.g. "Stop Market", "Stop Limit", "Take Profit Market", "Take Profit Limit"). __RESPONSE_ROW__updates.tif string Time-in-force - "Alo" (add-liquidity-only / post-only), "Ioc" (immediate-or-cancel), or "Gtc" (good-til-cancel). __RESPONSE_ROW__updates.reduceOnly boolean true if the order is constrained to reduce an existing position. __RESPONSE_ROW__updates.isTrigger boolean true if this is a trigger (stop / take-profit) order. __RESPONSE_ROW__updates.isPositionTpsl boolean true for position-level take-profit / stop-loss orders. __RESPONSE_ROW__updates.triggerCondition string Human-readable trigger condition; "N/A" for plain limit orders. __RESPONSE_ROW__updates.triggerPx string Trigger price (decimal string); "0.0" for plain limit orders. __RESPONSE_ROW__updates.cloid string | null Optional client order ID (0x-prefixed 32-character hex) if one was set at placement, otherwise null. __RESPONSE_ROW__updates.children object[] Child orders (e.g. TP/SL attached at placement); empty array if none.

userNonFundingLedgerUpdates

stream real-time non-funding ledger events (deposits, withdrawals, vault and staking activity) for one or… Last reviewed: 2026-06-18

userFills | Hyperliquid WebSocket API

Credit Cost: 1 per minute Processing: Realtime
Note: - Wire-compatible with wss://api.hyperliquid.xyz/ws userFills subscription - same channel name, same per-fill shape.
  • Up to 1,000 wallet addresses per subscription. Use addresses (string[]); aliases user(string) and users (string[]) are also accepted.
  • Messages are batched per HyperCore block as [address, fill] tuples; subscribe many wallets, receive one push per block per wallet that had activity.
  • Live-only - no historical snapshot on subscribe. For windowed history, use the Info API userFillsByTime.
  • No 1,000-subscription-per-IP cap. Multiplex many userFills subscriptions on a single connection.
Subscribe to one or more wallets and receive their live trade fills the moment they execute. The server batches one push per HyperCore block, carrying every matching fill as [address, fill] tuples. Supports up to 1,000 wallet addresses per subscription.

Endpoint

wss://hypercore.goldrushdata.com/ws?key=
Your GoldRush API key. Passed as a query parameter at connection time - no Authorization header is used.

Subscribe

Send this JSON message after the connection is established:
ParameterTypeRequiredDescription
methodstringYesAlways "subscribe".
subscriptionobjectYes__RESPONSE_ROW__type string Always "userFills". __RESPONSE_ROW__addresses string[] One or more wallet addresses (lowercase 0x-prefixed hex) to stream fills for. Aliases user (string) and users (string[]) are also accepted. __RESPONSE_ROW__aggregateByTime boolean When true, partial fills of the same order within the same block are merged into one. Default false.

Example

wscat
wscat -c "wss://hypercore.goldrushdata.com/ws?key=$GOLDRUSH_API_KEY"

> {"method":"subscribe","subscription":{"type":"userFills","addresses":["0x31ca8395cf837de08b24da3f660e77761dfb974b"]}}
TypeScript
import WebSocket from "ws";

const ws = new WebSocket(
  `wss://hypercore.goldrushdata.com/ws?key=${process.env.GOLDRUSH_API_KEY}`,
);

ws.on("open", () => {
  ws.send(JSON.stringify({
    method: "subscribe",
    subscription: {
      type: "userFills",
      addresses: ["0x31ca8395cf837de08b24da3f660e77761dfb974b"],
    },
  }));
});

ws.on("message", (raw) => {
  const msg = JSON.parse(raw.toString());
  if (msg.channel === "userFills") {
    for (const [address, fill] of msg.fills) {
      console.log(address, fill.coin, fill.side, fill.sz, "@", fill.px);
    }
  }
});
Python
import asyncio, json, os
import websockets

async def main():
    uri = f"wss://hypercore.goldrushdata.com/ws?key={os.environ['GOLDRUSH_API_KEY']}"
    async with websockets.connect(uri) as ws:
        await ws.send(json.dumps({
            "method": "subscribe",
            "subscription": {
                "type": "userFills",
                "addresses": ["0x31ca8395cf837de08b24da3f660e77761dfb974b"],
            },
        }))
        async for raw in ws:
            msg = json.loads(raw)
            if msg.get("channel") == "userFills":
                for address, fill in msg["fills"]:
                    print(address, fill["coin"], fill["side"], fill["sz"], "@", fill["px"])

asyncio.run(main())

Unsubscribe

Send the same subscription body with method: "unsubscribe":
{
  "method": "unsubscribe",
  "subscription": {
    "type": "userFills",
    "addresses": ["0x31ca8395cf837de08b24da3f660e77761dfb974b"]
  }
}
Note: Unsubscribe matches subscriptions by exact body. A subscription created with addresses: ["0xAAA", "0xBBB"] is a different subscription from two single-address subscriptions. To narrow the set, unsubscribe the original list in full, then resubscribe with the smaller list.

Streamed message

Each push has channel: "userFills" and a fills array of [address, fill] tuples - all fills from the same HyperCore block that match any subscribed wallet.
{
  "channel": "userFills",
  "fills": [
    [
      "0x31ca8395cf837de08b24da3f660e77761dfb974b",
      {
        "coin": "ETH",
        "px": "2150.5",
        "sz": "1.5",
        "side": "B",
        "time": 1704067200000,
        "startPosition": "1.5",
        "dir": "Open Long",
        "closedPnl": "125.5",
        "hash": "0xabc...def",
        "oid": 12345678,
        "crossed": false,
        "fee": "2.5",
        "tid": 87654321,
        "feeToken": "USDC",
        "twapId": null
      }
    ]
  ]
}
FieldTypeDescription
channelstringAlways "userFills".
fillsarrayTuples of [address, fill]. The address is the subscribed wallet the fill belongs to; the fill object carries the trade details.
fills.coinstringAsset symbol - e.g. "BTC", "ETH" for perps; spot pairs use the @N form (e.g. "@107").
fills.pxstringFill execution price (decimal string).
fills.szstringFill size (decimal string).
fills.sidestring"B" for buy/long, "A" for ask/short.
fills.timeintUnix timestamp in milliseconds when the fill executed.
fills.startPositionstringSigned position size on the same coin immediately before this fill.
fills.dirstringHuman-readable direction label - "Open Long", "Open Short", "Close Long", "Close Short", "Buy", "Sell", or position-flip labels "Long > Short" / "Short > Long".
fills.closedPnlstringRealized PnL in USDC attributable to this fill (zero when the fill opens or extends a position).
fills.hashstringL1 transaction hash that included this fill.
fills.oidintParent order ID.
fills.crossedbooleantrue when the fill came from the taker side of the order, false when it was the maker side.
fills.feestringTrading fee paid for this fill, denominated in feeToken.
fills.tidintUnique trade ID.
fills.feeTokenstringSymbol the fee was paid in - typically "USDC".
fills.twapId`intnull`Parent TWAP order ID if this fill is a slice of a TWAP, otherwise null.
fills.cloidstringOptional. Client order ID (0x-prefixed 32-character hex) if one was set at order placement.
fills.builderFeestringOptional. Builder fee paid for this fill, denominated in feeToken. Present only on fills routed through a builder code.
fills.builderstringOptional. Builder address the order was routed through.
fills.liquidationobjectOptional. Present only when this fill closed a position as part of a liquidation event. __RESPONSE_ROW__liquidatedUser string The wallet whose position was liquidated. __RESPONSE_ROW__fills.markPx string Mark price at the time of liquidation. __RESPONSE_ROW__fills.method string Liquidation method - "market" or "backstop".

allFills

stream every fill on HyperCore in real time for global market analytics and cross-wallet order-flow…

builderFills

stream live attributed fills for one or more builder addresses in real time.

liquidationFills

stream a global, market-wide feed of every liquidation fill on HyperCore.

userNonFundingLedgerUpdates

stream real-time non-funding ledger events (deposits, withdrawals, vault and staking activity) for one or… Last reviewed: 2026-06-16

userNonFundingLedgerUpdates | Hyperliquid WebSocket API

Credit Cost: 1 per minute Processing: Realtime
Note: - Wire-compatible with wss://api.hyperliquid.xyz/ws userNonFundingLedgerUpdatessubscription - same channel name, same delta shape per event.
  • Up to 1,000 wallet addresses per subscription. Use addresses (string[]); aliases user(string) and users (string[]) are also accepted.
  • Covers everything that moves USDC or token balances except funding payments - deposits, withdrawals, transfers, liquidations, vault actions, staking, rewards.
  • Live-only: isSnapshot is always false on this transport. For windowed history, use the Info API userNonFundingLedgerUpdates.
Subscribe to one or more wallets and receive every non-funding ledger event in real time: deposits, withdrawals, transfers, liquidations, vault deposits/withdrawals, staking, rewards, and more. Each push carries a list of {time, hash, delta} entries where delta.type identifies the event class and the remaining delta fields are type-specific. Supports up to 1,000 wallet addresses per subscription.

Endpoint

wss://hypercore.goldrushdata.com/ws?key=
Your GoldRush API key. Passed as a query parameter at connection time - no Authorization header is used.

Subscribe

Send this JSON message after the connection is established:
ParameterTypeRequiredDescription
methodstringYesAlways "subscribe".
subscriptionobjectYes__RESPONSE_ROW__type string Always "userNonFundingLedgerUpdates". __RESPONSE_ROW__addresses string[] One or more wallet addresses (lowercase 0x-prefixed hex) to stream events for. Aliases user (string) and users (string[]) are also accepted.

Example

wscat
wscat -c "wss://hypercore.goldrushdata.com/ws?key=$GOLDRUSH_API_KEY"

> {"method":"subscribe","subscription":{"type":"userNonFundingLedgerUpdates","addresses":["0x31ca8395cf837de08b24da3f660e77761dfb974b"]}}
TypeScript
import WebSocket from "ws";

const ws = new WebSocket(
  `wss://hypercore.goldrushdata.com/ws?key=${process.env.GOLDRUSH_API_KEY}`,
);

ws.on("open", () => {
  ws.send(JSON.stringify({
    method: "subscribe",
    subscription: {
      type: "userNonFundingLedgerUpdates",
      addresses: ["0x31ca8395cf837de08b24da3f660e77761dfb974b"],
    },
  }));
});

ws.on("message", (raw) => {
  const msg = JSON.parse(raw.toString());
  if (msg.channel === "userNonFundingLedgerUpdates") {
    const { user, nonFundingLedgerUpdates } = msg.data;
    for (const update of nonFundingLedgerUpdates) {
      console.log(user, update.delta.type, update.delta);
    }
  }
});
Python
import asyncio, json, os
import websockets

async def main():
    uri = f"wss://hypercore.goldrushdata.com/ws?key={os.environ['GOLDRUSH_API_KEY']}"
    async with websockets.connect(uri) as ws:
        await ws.send(json.dumps({
            "method": "subscribe",
            "subscription": {
                "type": "userNonFundingLedgerUpdates",
                "addresses": ["0x31ca8395cf837de08b24da3f660e77761dfb974b"],
            },
        }))
        async for raw in ws:
            msg = json.loads(raw)
            if msg.get("channel") == "userNonFundingLedgerUpdates":
                data = msg["data"]
                for update in data["nonFundingLedgerUpdates"]:
                    print(data["user"], update["delta"]["type"], update["delta"])

asyncio.run(main())

Unsubscribe

Send the same subscription body with method: "unsubscribe":
{
  "method": "unsubscribe",
  "subscription": {
    "type": "userNonFundingLedgerUpdates",
    "addresses": ["0x31ca8395cf837de08b24da3f660e77761dfb974b"]
  }
}
Note: Unsubscribe matches subscriptions by exact body. To narrow the wallet set, unsubscribe the original addresses list in full, then resubscribe with the smaller list.

Streamed message

Each push has channel: "userNonFundingLedgerUpdates" and a data object keyed to a single wallet. Multi-wallet subscriptions receive one push per affected wallet.
{
  "channel": "userNonFundingLedgerUpdates",
  "data": {
    "user": "0x31ca8395cf837de08b24da3f660e77761dfb974b",
    "isSnapshot": false,
    "nonFundingLedgerUpdates": [
      {
        "time": 1781560864370,
        "hash": "0xd49c1a45a99fa0b4d615043dce4a050000d7322b4492bf867864c59868937a9f",
        "delta": {
          "type": "send",
          "amount": "0.012417",
          "destination": "0x6043daf4fbd6bd60601d277312a0664551302e70",
          "usdcValue": "0.012417",
          "nativeTokenFee": "0.0"
        }
      }
    ]
  }
}
FieldTypeDescription
channelstringAlways "userNonFundingLedgerUpdates".
dataobject
data.userstringThe wallet address this batch of updates belongs to.
data.isSnapshotbooleanAlways false on this live stream. The field is preserved for parity with the public Hyperliquid WebSocket contract.
data.nonFundingLedgerUpdatesarrayOne entry per ledger event in the current block. __RESPONSE_ROW__time int Unix timestamp in milliseconds when the event was recorded. __RESPONSE_ROW__data.hash string L1 transaction hash that produced the event. __RESPONSE_ROW__data.delta object Event payload. delta.type identifies the event class; remaining delta fields are type-specific - see Delta types below.

Delta types

delta.type is one of:
ValueDescription
depositUSDC deposit into the perp account from an external chain.
withdrawUSDC withdrawal to an external chain.
internalTransferUSDC transfer between Hyperliquid accounts.
subAccountTransferTransfer between a master account and one of its sub-accounts.
accountClassTransferTransfer between perp and spot accounts on the same wallet.
spotTransferSpot token transfer between wallets.
liquidationPosition closed by liquidation.
vaultCreateVault was created.
vaultDepositDeposit into a vault.
vaultWithdrawWithdrawal from a vault.
vaultDistributionVault PnL distribution to depositors.
vaultLeaderCommissionCommission paid to a vault leader.
spotGenesisInitial token allocation at spot deployment.
rewardsClaimClaim of accrued rewards.
accountActivationGasGas fee paid to activate a new account.
perpDexClassTransferTransfer between different perp DEXes on the same wallet.
deployGasAuctionGas-auction settlement for HIP-3 / HIP-4 deployments.
sendNative HYPE / token send.
cStakingTransferTransfer of staked HYPE between wallets.
borrowLendBorrow or lend operation in the spot borrow-lend market.
The remaining keys on delta vary by type and follow the public Hyperliquid info API userNonFundingLedgerUpdates payload. Common fields include amount (decimal string), usdcValue (decimal string), coin (string), destination / source (address strings), and nativeTokenFee (decimal string).

orderUpdates

stream real-time order lifecycle events (placements, fills, cancels, and rejections) for one or more wallets…

userFills

stream real-time trade fills for one or more wallets as they execute on HyperCore. Last reviewed: 2026-06-17

Recipes

The l2Book channel on wss://hypercore.goldrushdata.com/ws?key= is wire-equal to the public Hyperliquid feed but with coin made optional and the per-IP subscription cap removed. This recipe shows how to turn that stream into common trading and analytics building blocks. For the raw subscription shape see the l2Book reference; for the connection model see the WebSocket API overview.

What you get

  • Complete snapshots, not diffs. Every l2Book message contains the current time, coin, and a full [bids, asks] tuple in best-first order, with px / sz / n per level. Consume each message in isolation - no sequence numbers to track, no diff replay buffer, no REST snapshot to bootstrap.
  • Self-healing on packet loss. Drop a message, reconnect mid-session, or restart your process - the next message arrives with the full book state, so your in-memory view is correct on the very next tick.
  • Upstream-compatible aggregation knobs. nSigFigs accepts 2, 3, 4, 5, or null (full precision). mantissa accepts 1, 2, or 5, and is only valid when nSigFigs is 5.
  • Wildcard coverage. Omit coin to stream every asset’s book over a single subscription, instead of fanning out one subscription per asset. The wildcard defaults to perps only (marketTypes: ["perp"]); pass ["spot"], ["outcome"], a mix, or ["*"] to opt into spot, outcome, and future market types.

Subscribe and hold book state

The pattern below keeps a Map in memory. Each incoming message replaces the entry for its coin, so the map is always current and never needs reconciliation.
TypeScript
import WebSocket from "ws";

type Level = { px: string; sz: string; n: number };
type Snapshot = { time: number; bids: Level[]; asks: Level[] };

const books = new Map();

const ws = new WebSocket(
  `wss://hypercore.goldrushdata.com/ws?key=${process.env.GOLDRUSH_API_KEY}`,
);

ws.on("open", () => {
  // Omit `coin` to stream every asset on one subscription.
  ws.send(JSON.stringify({
    method: "subscribe",
    subscription: { type: "l2Book", coin: "BTC" },
  }));
});

ws.on("message", (raw) => {
  const msg = JSON.parse(raw.toString());
  if (msg.channel !== "l2Book") return;

  const { coin, time, levels: [bids, asks] } = msg.data;
  books.set(coin, { time, bids, asks });

  console.log(coin, time, "bid:", bids[0]?.px, "ask:", asks[0]?.px);
});
Python
import asyncio, json, os
import websockets

books: dict[str, dict] = {}

async def main():
    uri = f"wss://hypercore.goldrushdata.com/ws?key={os.environ['GOLDRUSH_API_KEY']}"
    async with websockets.connect(uri) as ws:
        await ws.send(json.dumps({
            "method": "subscribe",
            "subscription": {"type": "l2Book", "coin": "BTC"},
        }))
        async for raw in ws:
            msg = json.loads(raw)
            if msg.get("channel") != "l2Book":
                continue
            data = msg["data"]
            coin, time = data["coin"], data["time"]
            bids, asks = data["levels"]
            books[coin] = {"time": time, "bids": bids, "asks": asks}
            print(coin, time, "bid:", bids[0]["px"], "ask:", asks[0]["px"])

asyncio.run(main())

Patterns

Top-of-book tracker

Read bids[0] and asks[0] directly from each message. The spread is Number(asks[0].px) - Number(bids[0].px). No state required - every message is self-contained, so a single-line transformation gives you a live ticker.

Depth-weighted mid quote

Sum px * sz across the first K levels on each side, then average. This produces a fair-value mid that’s robust to thin top-of-book liquidity, and is useful as a hedging or pricing reference.
TypeScript
function depthWeightedMid(snap: Snapshot, k = 5): number {
  const side = (levels: Level[]) => {
    let num = 0, den = 0;
    for (const { px, sz } of levels.slice(0, k)) {
      const p = Number(px), s = Number(sz);
      num += p * s;
      den += s;
    }
    return den > 0 ? num / den : NaN;
  };
  return (side(snap.bids) + side(snap.asks)) / 2;
}

Slippage / impact estimator

Walk levels on the relevant side until cumulative sz covers the requested notional. Return the size-weighted average fill price - the difference vs the top-of-book is your expected slippage.
TypeScript
function estimateFill(levels: Level[], targetSize: number): number {
  let remaining = targetSize, notional = 0;
  for (const { px, sz } of levels) {
    const take = Math.min(remaining, Number(sz));
    notional += take * Number(px);
    remaining -= take;
    if (remaining  0 ? NaN : notional / targetSize;
}

// Buying 10 BTC against current asks:
const avgFill = estimateFill(books.get("BTC")!.asks, 10);

Liquidity heatmap

On each message, append [time, coin, side, px, sz] rows to your time-series store (Clickhouse, TimescaleDB, Parquet). Because every message is a complete snapshot of the top levels, the heatmap rebuilds correctly from any contiguous slice of history - you don’t need a separate “initial book” record to seed the visualisation.

Handling reconnects

Reconnect logic is a one-liner: open a new socket and resend the same subscribe payload. The first message after subscribe is a full book snapshot, so your books map is correct on the next tick - there’s nothing to replay, nothing to buffer, and no sequence numbers to reconcile against a separately-fetched REST snapshot.
TypeScript
function connect() {
  const ws = new WebSocket(
    `wss://hypercore.goldrushdata.com/ws?key=${process.env.GOLDRUSH_API_KEY}`,
  );
  ws.on("open", () => ws.send(JSON.stringify({
    method: "subscribe",
    subscription: { type: "l2Book", coin: "BTC" },
  })));
  ws.on("close", () => setTimeout(connect, 1000));
  return ws;
}
  • l2Book API reference - full subscription and message schema.
  • WebSocket API overview - endpoint URL, auth, and limits.
  • clearinghouseState - pair the live book with per-account position and margin state.
  • OHLCV pairs stream - candles instead of raw book state.

The l2BookDiff channel on wss://hypercore.goldrushdata.com/ws?key= is a GoldRush-exclusive stream - it has no equivalent on wss://api.hyperliquid.xyz/ws. After subscribing, the server emits one Snapshot per subscribed coin, then per-block Updates carrying only the price levels that changed. The shape per level is the same {px, sz, n} you already get from l2Book, so book-state code only needs to learn how to apply diffs - and coin accepts a single asset, a list, or can be omitted to stream every asset on one subscription. For the raw subscription shape see the l2BookDiff reference; for the connection model see the WebSocket API overview.

What you get

  • Snapshot + diff transport. One full Snapshot per subscribed coin on subscribe, then per-block Updates containing only the levels that changed. Apply diffs to local state.
  • Aggregated {px, sz, n} shape. Identical to l2Book levels - reuse your existing aggregated-book types and just add a level-apply function.
  • Multi-coin / wildcard friendly. coin accepts a single asset, an array of assets, or can be omitted to stream every asset on one subscription. When coin is omitted, marketTypes defaults to ["perp"] - pass ["spot"], ["outcome"], a mix, or ["*"] to opt into spot, outcome, and future market types. l4Book is one-coin-per-subscription; l2Book requires per-asset fan-out on the public feed.
  • Bandwidth proportional to change. Quiet markets cost almost nothing; only the levels that actually moved arrive over the wire, instead of a full re-snapshot every tick.
  • GoldRush-exclusive. Public Hyperliquid has no L2-diff transport - l2Book is full-snapshot only.

Subscribe and maintain book state

The pattern below keeps a Map, asks: Map }>. Each Snapshot seeds the per-coin entry; each book_diff inside Updates either deletes a level (when sz === "0") or replaces it with the new {sz, n}.
TypeScript
import WebSocket from "ws";

type Level = { px: string; sz: string; n: number };
type Side = Map;
type Book = { bids: Side; asks: Side };

const books = new Map();

function applyLevels(side: Side, levels: Level[]) {
  for (const lvl of levels) {
    if (lvl.sz === "0") side.delete(lvl.px);
    else side.set(lvl.px, lvl);
  }
}

const ws = new WebSocket(
  `wss://hypercore.goldrushdata.com/ws?key=${process.env.GOLDRUSH_API_KEY}`,
);

ws.on("open", () => {
  ws.send(JSON.stringify({
    method: "subscribe",
    subscription: { type: "l2BookDiff", coin: "HYPE" },
  }));
});

ws.on("message", (raw) => {
  const msg = JSON.parse(raw.toString());
  if (msg.channel !== "l2BookDiff") return;

  if (msg.data.Snapshot) {
    const { coin, levels: [bids, asks] } = msg.data.Snapshot;
    const book: Book = { bids: new Map(), asks: new Map() };
    for (const l of bids) book.bids.set(l.px, l);
    for (const l of asks) book.asks.set(l.px, l);
    books.set(coin, book);
    return;
  }

  if (msg.data.Updates) {
    for (const diff of msg.data.Updates.book_diffs) {
      const book = books.get(diff.coin);
      if (!book) continue;
      const [bidLevels, askLevels] = diff.levels;
      applyLevels(book.bids, bidLevels);
      applyLevels(book.asks, askLevels);
    }
  }
});
Python
import asyncio, json, os
import websockets

books: dict[str, dict] = {}

def apply_levels(side: dict, levels: list[dict]) -> None:
    for lvl in levels:
        if lvl["sz"] == "0":
            side.pop(lvl["px"], None)
        else:
            side[lvl["px"]] = lvl

async def main():
    uri = f"wss://hypercore.goldrushdata.com/ws?key={os.environ['GOLDRUSH_API_KEY']}"
    async with websockets.connect(uri) as ws:
        await ws.send(json.dumps({
            "method": "subscribe",
            "subscription": {"type": "l2BookDiff", "coin": "HYPE"},
        }))
        async for raw in ws:
            msg = json.loads(raw)
            if msg.get("channel") != "l2BookDiff":
                continue
            data = msg["data"]

            if "Snapshot" in data:
                snap = data["Snapshot"]
                bids, asks = snap["levels"]
                books[snap["coin"]] = {
                    "bids": {l["px"]: l for l in bids},
                    "asks": {l["px"]: l for l in asks},
                }
                continue

            if "Updates" in data:
                for diff in data["Updates"]["book_diffs"]:
                    book = books.get(diff["coin"])
                    if not book:
                        continue
                    bid_levels, ask_levels = diff["levels"]
                    apply_levels(book["bids"], bid_levels)
                    apply_levels(book["asks"], ask_levels)

asyncio.run(main())

Patterns

Single coin, list, or wildcard

All three subscription shapes share the same handler - only the payload sent at subscribe time changes. Use whichever matches your coverage requirements.
TypeScript
// Single coin
ws.send(JSON.stringify({
  method: "subscribe",
  subscription: { type: "l2BookDiff", coin: "HYPE" },
}));

// Fixed list of coins
ws.send(JSON.stringify({
  method: "subscribe",
  subscription: { type: "l2BookDiff", coin: ["HYPE", "BTC", "ETH"] },
}));

// Every perp asset on one subscription (perps is the default when coin is omitted)
ws.send(JSON.stringify({
  method: "subscribe",
  subscription: { type: "l2BookDiff" },
}));

// Spot only, or any mix; pass ["*"] for perps + spot + outcomes plus auto-opt-in to future types
ws.send(JSON.stringify({
  method: "subscribe",
  subscription: { type: "l2BookDiff", marketTypes: ["spot"] },
}));
ws.send(JSON.stringify({
  method: "subscribe",
  subscription: { type: "l2BookDiff", marketTypes: ["*"] },
}));
A list or wildcard subscription receives one Snapshot per live coin before diffs begin, so the books map fills in over the first few messages rather than all at once. marketTypes is only valid when coin is omitted; it defaults to ["perp"], so spot and outcome markets require explicit opt-in. A second subscribe with a different marketTypes value replaces the previous filter rather than coexisting with it.

Reconstruct a top-of-book stream

After each diff is applied, the best bid is the highest px in book.bids and the best ask is the lowest px in book.asks. Emit only when the top level changes to avoid noise.
TypeScript
function bestLevels(book: Book) {
  let bestBid: string | null = null;
  for (const px of book.bids.keys()) if (!bestBid || Number(px) > Number(bestBid)) bestBid = px;
  let bestAsk: string | null = null;
  for (const px of book.asks.keys()) if (!bestAsk || Number(px)  ws.send(JSON.stringify({
    method: "subscribe",
    subscription: { type: "l2BookDiff", coin: "HYPE" },
  })));
  ws.on("close", () => {
    books.clear();
    setTimeout(connect, 1000);
  });
  return ws;
}
  • l2BookDiff API reference - full subscription, snapshot, and update schema.
  • l2Book reference - full-snapshot transport when diff replay isn’t desirable.
  • l4Book reference - order-level stream with user, oid, cloid, tif, and trigger metadata.
  • WebSocket API overview - endpoint URL, auth, and limits.

The l4Book channel on wss://hypercore.goldrushdata.com/ws?key= is a GoldRush-exclusive stream - it has no equivalent on wss://api.hyperliquid.xyz/ws. After subscribing, the server sends a single Snapshot of every resting order, then per-block Updates carrying lifecycle events and per-order book changes. Each order arrives with its user, oid, cloid, tif, and trigger metadata, so you can reconstruct queue position, attribute flow to specific wallets, and run microstructure analytics that l2Book’s aggregated {px, sz, n} view hides. For the raw subscription shape see the l4Book reference; for the connection model see the WebSocket API overview.

What you get

  • Per-order visibility. Every level in the snapshot is an individual order keyed by oid, with user, cloid, tif, orderType, and trigger metadata attached. l2Book only exposes {px, sz, n} per price level.
  • Snapshot + diff transport. One full Snapshot on subscribe, then per-block Updates containing order_statuses (lifecycle events) and book_diffs (per-order changes). Apply diffs to local state.
  • Per-block cadence. Updates fire on each HyperCore block where the book for the subscribed coin changed. The time and block_height fields anchor each message to a specific block.
  • GoldRush-exclusive. Not available on the public Hyperliquid WebSocket - this stream surfaces user attribution and per-order metadata the public feed never exposes.
  • One coin per subscription. Unlike l2Book, coin is required. To cover multiple assets, open one l4Book subscription per asset on the same connection.

Subscribe and maintain book state

The pattern below keeps a Map per coin. The snapshot seeds the map; each Updates message applies book_diffs against it. Reconnects drop the map and re-seed from the next snapshot.
TypeScript
import WebSocket from "ws";

type Order = {
  user: string | null;
  coin: string;
  side: "B" | "A";
  limitPx: string;
  sz: string;
  oid: number;
  timestamp: number;
  triggerCondition: string;
  isTrigger: boolean;
  triggerPx: string;
  isPositionTpsl: boolean;
  reduceOnly: boolean;
  orderType: string;
  tif: string;
  cloid: string | null;
};

const orders = new Map();

const ws = new WebSocket(
  `wss://hypercore.goldrushdata.com/ws?key=${process.env.GOLDRUSH_API_KEY}`,
);

ws.on("open", () => {
  ws.send(JSON.stringify({
    method: "subscribe",
    subscription: { type: "l4Book", coin: "BTC" },
  }));
});

ws.on("message", (raw) => {
  const msg = JSON.parse(raw.toString());
  if (msg.channel !== "l4Book") return;

  if (msg.data.Snapshot) {
    orders.clear();
    const [bids, asks] = msg.data.Snapshot.levels;
    for (const o of [...bids, ...asks]) orders.set(o.oid, o);
    console.log("seeded from snapshot:", orders.size, "orders");
    return;
  }

  if (msg.data.Updates) {
    const { order_statuses, book_diffs } = msg.data.Updates;

    for (const s of order_statuses) {
      // Re-attach the parent `user` since the nested order has user=null.
      orders.set(s.order.oid, { ...s.order, user: s.user });
    }

    for (const d of book_diffs) {
      const existing = orders.get(d.oid);
      if (!existing) continue;
      if (d.raw_book_diff.new) {
        orders.set(d.oid, { ...existing, sz: d.raw_book_diff.new.sz });
      }
      // Other raw_book_diff shapes (deletes, modifies) belong here.
    }
  }
});
Python
import asyncio, json, os
import websockets

orders: dict[int, dict] = {}

async def main():
    uri = f"wss://hypercore.goldrushdata.com/ws?key={os.environ['GOLDRUSH_API_KEY']}"
    async with websockets.connect(uri) as ws:
        await ws.send(json.dumps({
            "method": "subscribe",
            "subscription": {"type": "l4Book", "coin": "BTC"},
        }))
        async for raw in ws:
            msg = json.loads(raw)
            if msg.get("channel") != "l4Book":
                continue
            data = msg["data"]

            if "Snapshot" in data:
                orders.clear()
                bids, asks = data["Snapshot"]["levels"]
                for o in bids + asks:
                    orders[o["oid"]] = o
                print("seeded from snapshot:", len(orders), "orders")
                continue

            if "Updates" in data:
                for s in data["Updates"]["order_statuses"]:
                    o = {**s["order"], "user": s["user"]}
                    orders[o["oid"]] = o
                for d in data["Updates"]["book_diffs"]:
                    existing = orders.get(d["oid"])
                    if not existing:
                        continue
                    new = d["raw_book_diff"].get("new")
                    if new:
                        existing["sz"] = new["sz"]

asyncio.run(main())

Patterns

Track individual orders by oid

oid is the Hyperliquid order id, stable for the lifetime of the order - use it as the primary key in your local map. cloid is the client-supplied id (may be null); index on it when you need to correlate fills back to a specific trading bot’s instructions.

Per-user flow attribution

Every order entry carries user. Group orders by wallet to surface market-maker behavior, identify spoofing patterns, or build a per-trader heatmap of resting size. Pair this with clearinghouseState for per-user position and margin context.
TypeScript
function sizeByUser(orders: Map) {
  const totals = new Map();
  for (const o of orders.values()) {
    if (!o.user) continue;
    totals.set(o.user, (totals.get(o.user) ?? 0) + Number(o.sz));
  }
  return totals;
}

Reconstruct aggregated price levels

If a downstream consumer expects an l2Book-style aggregated view, sum sz across all orders sharing a limitPx on the same side. Going the other way isn’t possible - l2Book collapses the per-order detail you’d lose.
TypeScript
function aggregate(orders: Map) {
  const bids = new Map();
  const asks = new Map();
  for (const o of orders.values()) {
    const book = o.side === "B" ? bids : asks;
    book.set(o.limitPx, (book.get(o.limitPx) ?? 0) + Number(o.sz));
  }
  return { bids, asks };
}

Handling reconnects

On reconnect, resend the same subscribe payload. The first message back is always a fresh Snapshot - drop your local orders map and re-seed from it. Do not attempt to replay missed Updates - the snapshot is authoritative and supersedes anything you held before the disconnect.
TypeScript
function connect() {
  const ws = new WebSocket(
    `wss://hypercore.goldrushdata.com/ws?key=${process.env.GOLDRUSH_API_KEY}`,
  );
  ws.on("open", () => ws.send(JSON.stringify({
    method: "subscribe",
    subscription: { type: "l4Book", coin: "BTC" },
  })));
  ws.on("close", () => {
    orders.clear();
    setTimeout(connect, 1000);
  });
  return ws;
}
  • l4Book API reference - full subscription, snapshot, and update schema.
  • l2Book reference - aggregated price-level snapshots when per-order detail isn’t needed.
  • WebSocket API overview - endpoint URL, auth, and limits.
  • clearinghouseState - pair per-user resting orders with position and margin state.