Install
openclaw skills install @galleonlabs/hypergrok-hyperliquid-websocketSubscribe to live Hyperliquid data over WebSocket from the desk computer - mids, order book, trades, candles, best bid/offer, and per-account fills, order updates and events - with raw JSON, Python SDK and TypeScript examples, plus how to run a supervised watch that logs to a file and alerts. Read-only. Use for monitoring, fill notifications and any watch that polling would make expensive.
openclaw skills install @galleonlabs/hypergrok-hyperliquid-websocketRead-only, no key. Endpoints: wss://api.hyperliquid.xyz/ws (mainnet), wss://api.hyperliquid-testnet.xyz/ws (testnet). Prefer WebSocket over polling for anything continuous: fills, order updates, book and price watches.
Subscribe: {"method": "subscribe", "subscription": {...}}. Unsubscribe with "method": "unsubscribe". The server acknowledges with {"channel": "subscriptionResponse", ...} then streams {"channel": "<type>", "data": ...}. Send {"method": "ping"} periodically (the SDKs do it for you; the server expects activity within about a minute) and expect {"channel": "pong"}.
Subscription types the desk uses:
| Type | Subscription JSON | Data |
|---|---|---|
| Mids for all markets | {"type":"allMids"} (optional "dex") | {"mids": {"BTC": "97123.5", ...}} |
| Order book | {"type":"l2Book","coin":"ETH"} (optional nSigFigs, mantissa, fast: true for 5 levels) | {"coin","time","levels":[bids,asks]}, up to 20 levels a side, pushed on each block at least 0.5 s after the last push |
| Trades | {"type":"trades","coin":"ETH"} | array of {coin, side, px, sz, time, hash, tid, users} |
| Candles | {"type":"candle","coin":"ETH","interval":"1m"} | {t,T,s,i,o,c,h,l,v,n} updated in place until the bar closes |
| Best bid/offer | {"type":"bbo","coin":"ETH"} | {"coin","time","bbo":[bid, ask]} |
| Asset context | {"type":"activeAssetCtx","coin":"ETH"} | funding, OI, mark, oracle, premium, volume for one market |
| Account fills | {"type":"userFills","user":"0x..."} | {"user","isSnapshot","fills":[...]} (first message is a snapshot) |
| Order updates | {"type":"orderUpdates","user":"0x..."} | array of {order:{coin,side,limitPx,sz,oid,timestamp,origSz,cloid}, status, statusTimestamp} |
| Account events | {"type":"userEvents","user":"0x..."} | fills, funding, liquidation, non-user cancels; arrives on channel "user" |
| Account funding | {"type":"userFundings","user":"0x..."} | hourly funding payments |
| Per-market account data | {"type":"activeAssetData","user":"0x...","coin":"ETH"} | leverage setting, max trade sizes, available to trade, mark (perps only) |
| Account state stream | {"type":"clearinghouseState","user":"0x..."} / {"type":"openOrders","user":"0x..."} | {dex, user, clearinghouseState:{...REST shape...}} / {dex, user, orders:[...]} (order items carry the frontend fields: isTrigger, triggerPx, orderType, cloid), pushed |
| TWAP state | {"type":"twapStates","user":"0x...","dex":""} / {"type":"userTwapSliceFills","user":"0x..."} | running TWAPs and their slice fills |
| Frontend snapshot | {"type":"webData3","user":"0x..."} | positions, orders and context in one stream (heavy) |
Limits per IP: up to 10 connections, 30 new connections per minute, 1000 subscriptions, 10 distinct users across user subscriptions, 2000 messages per minute. The server closes a connection silent for 60 seconds. One connection per watch process is plenty.
You can also send /info requests over the socket: {"method":"post","id":1,"request":{"type":"info","payload":{"type":"allMids"}}} returns {"channel":"post","data":{"id":1,"response":{...}}}. Useful inside a watch to avoid mixing REST and WS.
import os, json, sys, time, signal
from hyperliquid.info import Info
from hyperliquid.utils import constants
NETWORK = os.environ.get("HYPERLIQUID_NETWORK", "testnet")
BASE = constants.MAINNET_API_URL if NETWORK == "mainnet" else constants.TESTNET_API_URL
ADDR = os.environ.get("HYPERLIQUID_ACCOUNT_ADDRESS")
LOG = open("/workspace/trading-desk/watch/ws.log", "a")
def on_msg(msg):
line = json.dumps({"ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "channel": msg.get("channel"), "data": msg.get("data")})
LOG.write(line + "\n"); LOG.flush()
if msg.get("channel") == "userFills" and not msg["data"].get("isSnapshot"):
for f in msg["data"]["fills"]:
print(f"FILL {f['coin']} {f['side']} {f['sz']} @ {f['px']} fee {f['fee']} oid {f['oid']} cloid {f.get('cloid')}", flush=True)
info = Info(BASE) # skip_ws=False starts the socket thread
info.subscribe({"type": "allMids"}, on_msg)
info.subscribe({"type": "l2Book", "coin": "ETH"}, on_msg)
if ADDR:
info.subscribe({"type": "userFills", "user": ADDR}, on_msg)
info.subscribe({"type": "orderUpdates", "user": ADDR}, on_msg) # one orderUpdates/userEvents subscription per Info
signal.signal(signal.SIGTERM, lambda *_: (info.disconnect_websocket(), sys.exit(0)))
while True:
time.sleep(60)
The SDK's manager pings for you but does not reconnect on drop; run it under a supervisor (see below) and treat a silent log as a dead watch. It also only routes these subscription types to your callback: allMids, l2Book, trades, candle, bbo, userEvents, userFills, orderUpdates, userFundings, userNonFundingLedgerUpdates, webData2, activeAssetCtx, activeAssetData. Others in the table (clearinghouseState, openOrders, twapStates, userTwapSliceFills, webData3, notification) are acknowledged by the server but silently dropped by the Python SDK; use the raw socket or the TypeScript client for those.
Run in the background from the desk computer:
mkdir -p /workspace/trading-desk/watch
nohup python3 /workspace/trading-desk/watch/ws_watch.py >> /workspace/trading-desk/watch/ws_watch.out 2>&1 &
echo $! > /workspace/trading-desk/watch/ws_watch.pid
Heartbeat check for a routine: tail -1 /workspace/trading-desk/watch/ws.log should be recent; if the pid is gone or the log is stale for more than a few minutes, restart it and note the gap.
@nktkas/hyperliquid)import { SubscriptionClient, WebSocketTransport } from "@nktkas/hyperliquid";
const isTestnet = (process.env.HYPERLIQUID_NETWORK ?? "testnet") !== "mainnet";
const transport = new WebSocketTransport({ isTestnet }); // auto-reconnect and re-subscribe by default
const subs = new SubscriptionClient({ transport });
const user = process.env.HYPERLIQUID_ACCOUNT_ADDRESS as `0x${string}`;
await subs.allMids((d) => console.log("mids", d.mids.ETH));
await subs.l2Book({ coin: "ETH" }, (d) => console.log("book", d.levels[0][0], d.levels[1][0]));
await subs.userFills({ user }, (d) => { if (!d.isSnapshot) console.log("fills", d.fills); });
const s = await subs.orderUpdates({ user }, (u) => console.log("orders", u), { onError: (e) => console.error(e) });
// await s.unsubscribe(); transport.close();
websocat wss://api.hyperliquid-testnet.xyz/ws <<'EOF'
{"method":"subscribe","subscription":{"type":"allMids"}}
EOF
A watch is a condition plus an alert (desk-monitoring). Structure every watch as: subscribe, log everything to a file, evaluate the condition on each message, post the alert once (with value, threshold, source, UTC time), then either exit or keep watching, and never call /exchange.
userFills message as new fills; it is a snapshot (isSnapshot: true).T reached) unless you want intrabar updates.@<index> on the wire (except a few like PURR/USDC); resolve via spotMeta.