L4 Order Book (Level 4)
The most detailed market data available - individual order visibility with user wallet addresses, order IDs, timestamps, and full order parameters. L4 data enables queue position tracking, whale watching, and advanced market microstructure analysis.
Clone our complete examples: github.com/dwellir-public/hyperliquid-orderbook-server-code-examples
How to Subscribe#
Send a subscription message to the WebSocket endpoint:
{
"method": "subscribe",
"subscription": {
"type": "l4Book",
"coin": "BTC"
}
}
Subscription Parameters#
| Parameter | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Must be "l4Book" |
coin | string | Yes | Trading pair symbol (e.g., "BTC", "ETH") |
Unlike L2, L4 subscriptions do not support nLevels or nSigFigs - you receive the complete order book with all individual orders.
Message Types#
L4 subscriptions produce two types of messages:
- Initial Snapshot - Complete order book state when you subscribe
- Incremental Updates - Changes as orders are placed, modified, or filled
Initial Snapshot Response#
When you first subscribe, you receive a complete snapshot of the order book wrapped in a Snapshot object:
{
"channel": "l4Book",
"data": {
"Snapshot": {
"coin": "BTC",
"height": 854890775,
"levels": [
[
{
"user": "0xf9109ada2f73c62e9889b45453065f0d99260a2d",
"coin": "BTC",
"side": "B",
"limitPx": "90057",
"sz": "0.33289",
"oid": 289682065711,
"timestamp": 1767878782721,
"triggerCondition": "N/A",
"isTrigger": false,
"triggerPx": "0.0",
"isPositionTpsl": false,
"reduceOnly": false,
"orderType": "Limit",
"tif": "Alo",
"cloid": "0x4c4617dbd8b94d358285c5c6d5a43df3"
}
],
[
{
"user": "0x13558be785661958932ceac35ba20de187275a42",
"coin": "BTC",
"side": "A",
"limitPx": "90058",
"sz": "0.37634",
"oid": 289682176026,
"timestamp": 1767878800615,
"triggerCondition": "N/A",
"isTrigger": false,
"triggerPx": "0.0",
"isPositionTpsl": false,
"reduceOnly": false,
"orderType": "Limit",
"tif": "Alo",
"cloid": "0x000000000814768000001999b6671c90"
}
]
]
}
}
}
A full BTC order book snapshot typically contains 20,000-40,000+ orders. The example above shows one order per side for brevity.
Incremental Update Response#
After the initial snapshot, you receive incremental updates wrapped in an Updates object containing order_statuses and book_diffs:
{
"channel": "l4Book",
"data": {
"Updates": {
"time": 1767878802703,
"height": 854890776,
"order_statuses": [
{
"time": "2026-01-08T13:26:42.703377851",
"user": "0xbc927e87d072dfac3693846a83fa6922cc6c5f2a",
"status": "open",
"order": {
"user": null,
"coin": "BTC",
"side": "B",
"limitPx": "90056.0",
"sz": "0.00014",
"oid": 289682192129,
"timestamp": 1767878802703,
"triggerCondition": "N/A",
"isTrigger": false,
"triggerPx": "0.0",
"isPositionTpsl": false,
"reduceOnly": false,
"orderType": "Limit",
"tif": "Alo",
"cloid": "0xa097c34ee13a42a1afeed2a5ce96b413"
}
}
],
"book_diffs": [
{
"user": "0xbc927e87d072dfac3693846a83fa6922cc6c5f2a",
"oid": 289682192129,
"px": "90056.0",
"coin": "BTC",
"raw_book_diff": {
"new": {
"sz": "0.00014"
}
}
}
]
}
}
}
Order Status Values#
The status field in order_statuses indicates the result of order processing:
| Status | Description |
|---|---|
open | Order successfully placed and resting on the book |
filled | Order fully executed |
canceled | Order canceled by user |
badAloPxRejected | ALO (Add Liquidity Only) order rejected - would have crossed the spread |
marginCanceled | Order canceled due to insufficient margin |
Response Field Reference#
Snapshot Fields#
| Field | Type | Description |
|---|---|---|
channel | string | Always "l4Book" for this subscription type |
data.Snapshot | object | Wrapper object for snapshot data |
data.Snapshot.coin | string | Trading pair symbol |
data.Snapshot.height | number | Hyperliquid block height |
data.Snapshot.levels | array | Two-element array: [bids, asks] |
Individual Order Fields#
| Field | Type | Description |
|---|---|---|
user | string | Wallet address of order owner |
coin | string | Trading pair |
side | string | "B" = Bid (buy), "A" = Ask (sell) |
limitPx | string | Limit price |
sz | string | Remaining order size |
oid | number | Unique order ID |
timestamp | number | Order placement time (Unix ms) |
orderType | string | "Limit", "Market", etc. |
tif | string | Time-in-force: "Gtc" (Good til Cancel), "Ioc" (Immediate or Cancel), "Alo" (Add Liquidity Only) |
cloid | string | Client order ID (hex string, user-provided) |
reduceOnly | boolean | Reduce-only order flag |
isTrigger | boolean | Trigger/stop order flag |
triggerPx | string | Trigger price ("0.0" if not a trigger order) |
triggerCondition | string | Trigger condition type ("N/A" if not a trigger order) |
isPositionTpsl | boolean | Position take-profit/stop-loss flag |
Incremental Update Fields#
| Field | Type | Description |
|---|---|---|
data.Updates | object | Wrapper object for update data |
data.Updates.time | number | Unix timestamp in milliseconds |
data.Updates.height | number | Hyperliquid block height |
data.Updates.order_statuses | array | Order status changes (new orders, fills, rejections) |
data.Updates.book_diffs | array | Order book modifications |
Order Status Fields#
| Field | Type | Description |
|---|---|---|
time | string | ISO-8601 timestamp with nanosecond precision |
user | string | Wallet address of order owner |
status | string | Order status (see Order Status Values above) |
order | object | Order details |
Book Diff Fields#
| Field | Type | Description |
|---|---|---|
user | string | Wallet address of order owner |
oid | number | Order ID |
px | string | Price level |
coin | string | Trading pair |
raw_book_diff | object | The modification details |
raw_book_diff.new | object | New order added (contains sz) |
raw_book_diff.modified | object | Order size changed (contains sz) |
raw_book_diff.removed | object | Order removed from book |
Code Examples#
- Python
- JavaScript
#!/usr/bin/env python3
"""
L4 Order Book Stream - Individual order visibility
Track whale orders, queue position, and market microstructure
"""
import asyncio
import json
import websockets
from collections import defaultdict
class L4OrderBook:
def __init__(self):
self.bids = {} # oid -> order
self.asks = {} # oid -> order
self.orders_by_user = defaultdict(list)
def apply_snapshot(self, snapshot_data):
"""Initialize from snapshot (data.Snapshot object)"""
self.bids.clear()
self.asks.clear()
self.orders_by_user.clear()
levels = snapshot_data.get("levels", [[], []])
bid_orders, ask_orders = levels
for order in bid_orders:
self.bids[order['oid']] = order
self.orders_by_user[order['user']].append(order['oid'])
for order in ask_orders:
self.asks[order['oid']] = order
self.orders_by_user[order['user']].append(order['oid'])
print(f"Snapshot loaded: {len(self.bids)} bids, {len(self.asks)} asks")
def apply_update(self, update_data):
"""Apply incremental update (data.Updates object)"""
# Process book_diffs for order book changes
for diff in update_data.get("book_diffs", []):
oid = diff["oid"]
user = diff["user"]
raw_diff = diff.get("raw_book_diff", {})
if "new" in raw_diff:
# New order added
order = {
"oid": oid,
"user": user,
"limitPx": diff["px"],
"sz": raw_diff["new"]["sz"],
"coin": diff["coin"]
}
# Determine side from order_statuses if available
self.bids[oid] = order # You may need to track side separately
self.orders_by_user[user].append(oid)
elif "modified" in raw_diff:
# Order size changed
new_sz = raw_diff["modified"]["sz"]
if oid in self.bids:
self.bids[oid]["sz"] = new_sz
elif oid in self.asks:
self.asks[oid]["sz"] = new_sz
elif "removed" in raw_diff:
# Order removed
if oid in self.bids:
del self.bids[oid]
elif oid in self.asks:
del self.asks[oid]
def get_best_bid(self):
if not self.bids:
return None
return max(self.bids.values(), key=lambda o: float(o['limitPx']))
def get_best_ask(self):
if not self.asks:
return None
return min(self.asks.values(), key=lambda o: float(o['limitPx']))
def get_orders_by_user(self, user_address):
"""Get all orders for a specific wallet"""
oids = self.orders_by_user.get(user_address, [])
orders = []
for oid in oids:
if oid in self.bids:
orders.append(self.bids[oid])
elif oid in self.asks:
orders.append(self.asks[oid])
return orders
def get_large_orders(self, min_size):
"""Find orders above a size threshold"""
large = []
for order in list(self.bids.values()) + list(self.asks.values()):
if float(order['sz']) >= min_size:
large.append(order)
return sorted(large, key=lambda o: float(o['sz']), reverse=True)
async def stream_l4_book():
ws_url = "wss://your-instance.dwellir.com/ws"
book = L4OrderBook()
async with websockets.connect(ws_url) as websocket:
await websocket.send(json.dumps({
"method": "subscribe",
"subscription": {"type": "l4Book", "coin": "BTC"}
}))
print("Subscribed to BTC L4 order book\n")
message_count = 0
async for message in websocket:
msg = json.loads(message)
if msg.get("channel") != "l4Book":
continue
data = msg["data"]
# Detect snapshot vs update by checking wrapper key
if "Snapshot" in data:
book.apply_snapshot(data["Snapshot"])
elif "Updates" in data:
book.apply_update(data["Updates"])
message_count += 1
# Display stats every 10 messages
if message_count % 10 == 0:
best_bid = book.get_best_bid()
best_ask = book.get_best_ask()
if best_bid and best_ask:
spread = float(best_ask['limitPx']) - float(best_bid['limitPx'])
print(f"\nBids: {len(book.bids)} | Asks: {len(book.asks)} | "
f"Spread: ${spread:.2f}")
# Show largest orders
large = book.get_large_orders(0.1)[:3]
if large:
print("Top orders:")
for o in large:
side = "BID" if o.get('side') == 'B' else "ASK"
print(f" {side} {o['sz']} @ ${o['limitPx']} "
f"by {o['user'][:10]}...")
if __name__ == "__main__":
asyncio.run(stream_l4_book())
const WebSocket = require('ws');
class L4OrderBook {
constructor() {
this.bids = new Map();
this.asks = new Map();
this.ordersByUser = new Map();
}
applySnapshot(snapshotData) {
// snapshotData is data.Snapshot object
this.bids.clear();
this.asks.clear();
this.ordersByUser.clear();
const [bidOrders, askOrders] = snapshotData.levels || [[], []];
for (const order of bidOrders) {
this.bids.set(order.oid, order);
this.trackUserOrder(order.user, order.oid);
}
for (const order of askOrders) {
this.asks.set(order.oid, order);
this.trackUserOrder(order.user, order.oid);
}
console.log(`Snapshot: ${this.bids.size} bids, ${this.asks.size} asks`);
}
trackUserOrder(user, oid) {
if (!this.ordersByUser.has(user)) {
this.ordersByUser.set(user, []);
}
this.ordersByUser.get(user).push(oid);
}
applyUpdate(updateData) {
// updateData is data.Updates object
for (const diff of (updateData.book_diffs || [])) {
const { oid, user, px, coin, raw_book_diff } = diff;
if (raw_book_diff.new) {
// New order added
const order = { oid, user, limitPx: px, sz: raw_book_diff.new.sz, coin };
this.bids.set(oid, order); // Track side from order_statuses if needed
this.trackUserOrder(user, oid);
} else if (raw_book_diff.modified) {
// Order size changed
const order = this.bids.get(oid) || this.asks.get(oid);
if (order) order.sz = raw_book_diff.modified.sz;
} else if (raw_book_diff.removed) {
// Order removed
this.bids.delete(oid);
this.asks.delete(oid);
}
}
}
getBestBid() {
let best = null;
for (const order of this.bids.values()) {
if (!best || parseFloat(order.limitPx) > parseFloat(best.limitPx)) {
best = order;
}
}
return best;
}
getBestAsk() {
let best = null;
for (const order of this.asks.values()) {
if (!best || parseFloat(order.limitPx) < parseFloat(best.limitPx)) {
best = order;
}
}
return best;
}
getLargeOrders(minSize) {
const large = [];
for (const order of [...this.bids.values(), ...this.asks.values()]) {
if (parseFloat(order.sz) >= minSize) {
large.push(order);
}
}
return large.sort((a, b) => parseFloat(b.sz) - parseFloat(a.sz));
}
}
const wsUrl = 'wss://your-instance.dwellir.com/ws';
const ws = new WebSocket(wsUrl);
const book = new L4OrderBook();
ws.on('open', () => {
ws.send(JSON.stringify({
method: 'subscribe',
subscription: { type: 'l4Book', coin: 'BTC' }
}));
console.log('Subscribed to BTC L4 order book\n');
});
let messageCount = 0;
ws.on('message', (rawData) => {
const msg = JSON.parse(rawData);
if (msg.channel !== 'l4Book') return;
const data = msg.data;
// Detect snapshot vs update by checking wrapper key
if (data.Snapshot) {
book.applySnapshot(data.Snapshot);
} else if (data.Updates) {
book.applyUpdate(data.Updates);
}
messageCount++;
if (messageCount % 10 === 0) {
const bestBid = book.getBestBid();
const bestAsk = book.getBestAsk();
if (bestBid && bestAsk) {
const spread = parseFloat(bestAsk.limitPx) - parseFloat(bestBid.limitPx);
console.log(`\nBids: ${book.bids.size} | Asks: ${book.asks.size} | Spread: $${spread.toFixed(2)}`);
}
}
});
ws.on('error', (error) => console.error('WebSocket error:', error));
Queue Position Tracker#
Track your order's position in the queue at a price level:
class QueuePositionTracker:
def __init__(self, book, your_address):
self.book = book
self.your_address = your_address.lower()
def get_queue_position(self, oid):
"""
Get position in queue for an order.
Returns (position, total_ahead) where:
- position: 1-indexed position in queue
- total_ahead: total size ahead of your order
"""
# Find the order
order = self.book.bids.get(oid) or self.book.asks.get(oid)
if not order:
return None, None
price = order['limitPx']
side = order['side']
timestamp = order['timestamp']
# Get all orders at same price, same side
if side == 'B':
same_price = [o for o in self.book.bids.values()
if o['limitPx'] == price]
else:
same_price = [o for o in self.book.asks.values()
if o['limitPx'] == price]
# Sort by timestamp (FIFO)
same_price.sort(key=lambda o: o['timestamp'])
# Find position
position = 0
size_ahead = 0
for o in same_price:
position += 1
if o['oid'] == oid:
return position, size_ahead
size_ahead += float(o['sz'])
return None, None
def get_your_orders(self):
"""Get all your orders in the book"""
return self.book.get_orders_by_user(self.your_address)
Whale Watcher#
Alert on large orders from specific wallets:
class WhaleWatcher:
def __init__(self, whale_addresses=None, size_threshold=1.0):
self.whale_addresses = set(a.lower() for a in (whale_addresses or []))
self.size_threshold = size_threshold
self.alerts = []
def check_order(self, order):
"""Check if order triggers whale alert"""
user = order['user'].lower()
size = float(order['sz'])
# Check if known whale
if user in self.whale_addresses:
self.alerts.append({
'type': 'KNOWN_WHALE',
'user': user,
'side': 'BUY' if order['side'] == 'B' else 'SELL',
'size': size,
'price': order['limitPx']
})
return True
# Check if large order
if size >= self.size_threshold:
self.alerts.append({
'type': 'LARGE_ORDER',
'user': user,
'side': 'BUY' if order['side'] == 'B' else 'SELL',
'size': size,
'price': order['limitPx']
})
return True
return False
def process_book_update(self, book_diffs):
"""Process incremental updates for whale activity"""
for diff in book_diffs:
if diff['action'] == 'add':
self.check_order(diff['order'])
def get_recent_alerts(self, limit=10):
return self.alerts[-limit:]
Use Cases#
Queue Position Optimization#
Understand your place in the order queue:
- Time priority: See exactly where your order sits at a price level
- Fill probability: Estimate likelihood of execution based on orders ahead
- Repositioning: Decide when to cancel and replace for better position
Whale Wallet Tracking#
Monitor large or notable traders:
- Address tracking: Follow specific wallet addresses
- Size alerts: Trigger on orders above threshold
- Pattern detection: Identify accumulation or distribution
Market Microstructure Research#
Analyze order flow dynamics:
- Order arrival rates: Study how orders enter the book
- Cancellation patterns: Track order lifetime and modification frequency
- Toxicity analysis: Measure adverse selection from order flow
Smart Order Routing#
Optimize order execution strategy:
- Liquidity mapping: Know exactly what size exists at each level
- Hidden liquidity: Detect when large orders are being worked
- Impact estimation: Model expected slippage from current book state
Bandwidth Considerations#
L4 data is significantly higher bandwidth than L2:
| Aspect | L2 | L4 |
|---|---|---|
| Data per level | 3 fields (px, sz, n) | 15+ fields per order |
| Orders visible | Aggregated count only | Every individual order |
| Update frequency | Per price level | Per order change |
| Typical message size | 1-5 KB | 10-100+ KB |
Consider subscribing to L4 only for coins where you need individual order visibility.
Get Access#
Ready to integrate real-time Hyperliquid L4 order book data?
- Volume Calculator - Estimate monthly message volume
- Contact Sales - Get your WebSocket credentials
- Dashboard - Manage your subscription
Stream institutional-grade Hyperliquid order book data with Dwellir's ultra-low latency infrastructure.