Skip to main content

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.

Code Examples Repository

How to Subscribe#

Send a subscription message to the WebSocket endpoint:

{
"method": "subscribe",
"subscription": {
"type": "l4Book",
"coin": "BTC"
}
}

Subscription Parameters#

ParameterTypeRequiredDescription
typestringYesMust be "l4Book"
coinstringYesTrading pair symbol (e.g., "BTC", "ETH", "xyz:MSTR", "@150")
HIP3 Markets

For HIP3 (permissionless perpetuals) markets, use the full coin label format with the xyz: prefix. For example, use "xyz:MSTR" for the MicroStrategy perpetual, not just "MSTR". Standard perpetuals like BTC and ETH do not require a prefix.

Spot Markets

For spot markets, use the @{index} format where the index is the spot asset index. For example, use "@150" for a specific spot market.

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:

  1. Initial Snapshot - Complete order book state when you subscribe
  2. 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"
}
]
]
}
}
}
Snapshot Size

A full BTC order book snapshot typically contains 20,000-40,000+ orders and can exceed 5 MB. The example above shows one order per side for brevity. Many WebSocket libraries default to a 1 MB message limit. If your connection closes immediately after subscribing, increase the maximum message size (e.g., max_size=50 * 1024 * 1024 in Python's websockets library).

Incremental Update Response#

After the initial snapshot, you receive incremental updates wrapped in an Updates object containing order_statuses and book_diffs.

Example 1: New Order

This example shows a new order being added to the book:

{
"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"
}
}
}
]
}
}
}

Example 2: Order Size Update (Partial Fill)

This example shows an existing order being partially filled, with the size decreasing from 108.65 to 107.5:

{
"channel": "l4Book",
"data": {
"Updates": {
"time": 1767878902834,
"height": 854890880,
"order_statuses": [],
"book_diffs": [
{
"user": "0x97991003fd631e2923f40cab2a4fdc35e60dc807",
"oid": 316542552323,
"px": "84.371",
"coin": "SOL",
"raw_book_diff": {
"update": {
"origSz": "108.65",
"newSz": "107.5"
}
}
}
]
}
}
}

In this update:

  • The order at price 84.371 was partially filled
  • Original size (origSz): 108.65 SOL
  • New remaining size (newSz): 107.5 SOL

Response Field Reference#

This section provides a detailed breakdown of all fields in L4 messages. For specific field values like order statuses, see Order Status Values.

All L4 messages follow this top-level structure:

{
channel: "l4Book",
data: Snapshot | Updates
}

Message Type 1: Snapshot (Initial State)#

Structure: { channel: "l4Book", data: { Snapshot: {...} } }

Received once when you first subscribe. Contains the complete order book state.

Snapshot Object#

FieldTypeDescription
coinstringTrading pair symbol (e.g., "BTC", "ETH", "xyz:MSTR", "@150")
heightnumberHyperliquid block height
levels[Order[], Order[]]Two-element array: [bids, asks]. Each element is an array of Order objects.

Order Object (in levels arrays)#

Each order in the levels[0] (bids) and levels[1] (asks) arrays contains:

FieldTypeDescription
userstringWallet address of order owner (e.g., "0xf9109ada...")
coinstringTrading pair (e.g., "BTC")
side"B" | "A""B" = Bid (buy), "A" = Ask (sell)
limitPxstringLimit price (e.g., "90057")
szstringRemaining order size (e.g., "0.33289")
oidnumberUnique order ID (e.g., 289682065711)
timestampnumberOrder placement time in Unix milliseconds (e.g., 1767878782721)
triggerConditionstringTrigger condition type, or "N/A" if not a trigger order
isTriggerbooleanWhether this is a trigger/stop order
triggerPxstringTrigger price, or "0.0" if not a trigger order
isPositionTpslbooleanWhether this is a position take-profit/stop-loss order
reduceOnlybooleanWhether order can only reduce position
orderTypestringOrder type: "Limit", "Market", "Stop Market", "Stop Limit", "Scale", "TWAP"
tif"Gtc" | "Ioc" | "Alo"Time-in-force: "Gtc" (Good til Cancel), "Ioc" (Immediate or Cancel), "Alo" (Add Liquidity Only)
cloidstringClient order ID - hex string provided by user (e.g., "0x4c4617dbd8b94d35...")

Message Type 2: Updates (Incremental Changes)#

Structure: { channel: "l4Book", data: { Updates: {...} } }

Received continuously after the snapshot. Contains incremental changes to the order book.

Updates Object#

FieldTypeDescription
timenumberUnix timestamp in milliseconds (e.g., 1767878802703)
heightnumberHyperliquid block height - increments with each update
order_statusesOrderStatus[]Array of order status changes (new orders, fills, cancellations, rejections). See Order Status Values for status meanings.
book_diffsBookDiff[]Array of order book modifications (additions, removals, size changes)
L4 Book does not include individual fill details

The order_statuses array contains status transitions (open, filled, canceled), not per-fill execution data. To get individual fill details (price, size, counterparty), subscribe to the Trades stream and correlate using oid. See Understanding Order Data Flow for details.

OrderStatus Object (in order_statuses array)#

FieldTypeDescription
timestringISO-8601 timestamp with nanosecond precision (e.g., "2026-01-08T13:26:42.703377851")
userstringWallet address of order owner
statusstringOrder status: "open", "filled", "canceled", "badAloPxRejected", etc. See Order Status Values for all possible values and their meanings.
orderOrderOrder object with same structure as Order Object above (but user field may be null)

BookDiff Object (in book_diffs array)#

The book_diffs array contains changes to individual orders in the book. Each diff describes what happened to a specific order - whether it was added (new), partially filled (update), modified (modified), or removed (remove).

Common fields present in all diff types:

FieldTypeDescription
userstringWallet address of order owner (e.g., "0xbc927e87...")
oidnumberUnique order ID being modified (e.g., 289682192129)
pxstringPrice level where order exists/existed (e.g., "90056.0")
coinstringTrading pair (e.g., "BTC")
raw_book_diffNewDiff | UpdateDiff | ModifiedDiff | "remove"Describes what changed. Can be: { new: { sz } } for new orders, { update: { origSz, newSz } } for partial fills, { modified: { sz } } for amendments, or "remove" for cancellations/complete fills. See BookDiff Types below for detailed specifications.

BookDiff Types (the raw_book_diff field)#

The raw_book_diff field indicates what happened to an order. It can take four forms:

TypeTypeScript DefinitionDescription
NewDiff{ new: { sz: string } }New order added to the book. The sz field contains the order size. Reference the corresponding entry in order_statuses with matching oid to get full order details (side, tif, etc.).
RemoveDiff"remove"Order completely removed from book. Can occur due to: full fill (check order_statuses for filled status), user cancellation (canceled), system cancellation, or order expiration.
UpdateDiff{ update: { origSz: string, newSz: string } }Order size changed (usually decreased due to partial fill). Contains both the original size (origSz) and the new remaining size (newSz).
ModifiedDiff{ modified: { sz: string } }Order size modified (typically from order amendments). The sz field contains the new remaining size. Unlike UpdateDiff, this only provides the new size without the original.

Order Status Values#

The status field in order_statuses indicates the result of order processing. Understanding these statuses is critical for debugging order placement issues.

StatusTypeDescriptionDebugging Notes
openSuccessOrder successfully placed and resting on the bookMost common successful status. Order is live and can be filled.
filledSuccessOrder fully executedOrder matched completely. Check book_diffs for removal. Note: the sz field in the order object may be non-zero even for filled status. This reflects remaining size at the time the status was emitted by the Hyperliquid node, not necessarily the final state. Use book_diffs for accurate size tracking.
triggeredSuccessTrigger/stop order activatedConditional order has been triggered and converted to regular order.
canceledCancellationOrder canceled by user or systemStandard cancellation. Check if user-initiated or system-triggered.
reduceOnlyCanceledCancellationReduce-only order was canceledPosition closed or order would have increased position instead of reducing.
selfTradeCanceledCancellationOrder canceled to prevent self-tradingSame user's buy and sell orders would have matched. Exchange prevented self-execution.
marginCanceledCancellationOrder canceled due to insufficient marginUser's margin balance insufficient to maintain the order.
openInterestCapCanceledCancellationOrder canceled due to open interest cap reachedMarket has reached maximum open interest limit. Try again later or use different market.
scheduledCancelCancellationOrder canceled on a scheduled basisOrder was automatically canceled based on time or condition schedule.
siblingFilledCanceledCancellationOrder canceled because sibling order filledPaired/bracket order canceled when the primary order executed. Common with OCO (One-Cancels-Other) orders.
badAloPxRejectedRejectionAdd-liquidity-only order rejected (never reached book)Most common rejection (~70% in production data). ALO order would have crossed spread and executed as taker. Price was too aggressive for maker-only order.
iocCancelRejectedRejectionImmediate-or-cancel order rejected (never reached book)IOC order couldn't fill immediately at specified price. No matching liquidity available.
perpMarginRejectedRejectionPerpetual futures order rejected (never reached book)Insufficient margin to open the position. Check account balance and leverage.
perpMaxPositionRejectedRejectionPerpetual order rejected - exceeds max position size (never reached book)Order would exceed maximum allowed position size for this market. Reduce order size or close existing positions.
minTradeNtlRejectedRejectionMinimum notional value rejected (never reached book)Order size (price Ɨ quantity) below exchange minimum. Increase order size.
reduceOnlyRejectedRejectionReduce-only order rejected (never reached book)Order marked reduce-only but would have increased position or no position exists to reduce.
insufficientSpotBalanceRejectedRejectionInsufficient spot token balance (never reached book)Not enough spot token balance to place order. Deposit more tokens or reduce order size.
oracleRejectedRejectionOrder rejected due to oracle price issues (never reached book)Oracle price feed unavailable or stale. Wait for oracle to update or check market status.
positionFlipAtOpenInterestCapRejectedRejectionPosition flip rejected at open interest cap (never reached book)Order would flip position direction when market is at OI cap. Close existing position first.
positionIncreaseAtOpenInterestCapRejectedRejectionPosition increase rejected at open interest cap (never reached book)Cannot increase position size when market has reached open interest limit.
tooAggressiveAtOpenInterestCapRejectedRejectionOrder too aggressive at open interest cap (never reached book)Order price too aggressive when market near OI cap. Use less aggressive limit price.

Understanding Order Data Flow#

Hyperliquid separates order data across two channels. Understanding this separation is essential for building correct trading systems.

DataChannelWhat you get
Order status transitions (open, filled, canceled)L4 BookWhen an order changes state, not how it was filled
Book mutations (new, update, remove)L4 BookSize changes at each price level per order
Individual fill executions (price, size, counterparty)TradesEach execution with price, size, hash, and both wallet addresses

For complete order visibility, subscribe to both channels and correlate events using the oid (order ID) field:

# Subscribe to both channels for full order tracking
await websocket.send(json.dumps({
"method": "subscribe",
"subscription": {"type": "l4Book", "coin": "ETH"}
}))
await websocket.send(json.dumps({
"method": "subscribe",
"subscription": {"type": "trades", "coin": "ETH"}
}))

# Correlate: l4Book order_statuses use "oid" in the order object,
# trades use "tid" (trade ID) but can be matched to orders by
# tracking which orders are open at each block height.

Why "only 2 events" is normal#

A common question is why an order shows only open then filled with no events in between. This is expected behavior. The L4 Book order_statuses array reports state transitions, not individual fills.

If an order receives 5 partial fills before completing, the L4 Book reports:

  1. open - order placed on the book
  2. filled - order fully executed

The 5 individual fills appear on the Trades stream, not in order_statuses. The book_diffs array tracks the size decreases from partial fills as update diffs.

Order Lifecycle Patterns#

Based on production data for ETH, these are the most common order lifecycle patterns on the L4 Book channel:

PatternFrequencyDescription
Single rejection event~88% of all ordersOrders rejected immediately (badAloPxRejected, perpMarginRejected, etc.). Never reach the book.
open then canceled~98.9% of multi-eventOrder placed and later canceled by the user or system
open then filled~1.1% of multi-eventOrder placed and fully executed (may happen in the same block)
triggered then filledRareStop/trigger order activated and then filled
open then triggered then filledVery rareOrder transitions through triggered state before filling

Most orders on Hyperliquid are ALO (Add Liquidity Only) orders from market makers that get rejected because the price crossed the spread. This is normal high-frequency trading behavior.

Code Examples#

#!/usr/bin/env python3

import asyncio
import json
import os
import websockets
from dotenv import load_dotenv
from pathlib import Path

# Load environment variables
env_path = Path(__file__).parent.parent / '.env'
load_dotenv(env_path)


async def main():
ws_url = os.getenv("WEBSOCKET_URL", "wss://api-hyperliquid-mainnet-orderbook.n.dwellir.com/API_KEY/ws")

print(f"Connecting to: {ws_url}\n")

async with websockets.connect(ws_url, max_size=50 * 1024 * 1024) as websocket:
# Subscribe to BTC L4 orderbook
subscribe = {
"method": "subscribe",
"subscription": {
"type": "l4Book",
"coin": "BTC"
}
}

await websocket.send(json.dumps(subscribe))
print("āœ… Subscribed to BTC L4 orderbook\n")
print("=" * 60)

message_count = 0

# Listen for messages
async for message in websocket:
message_count += 1
data = json.loads(message)

# Determine message type
if 'data' in data:
if 'Snapshot' in data['data']:
snapshot = data['data']['Snapshot']
height = snapshot.get('height', 'N/A')
bids = len(snapshot.get('bids', []))
asks = len(snapshot.get('asks', []))
print(f"šŸ“ø Snapshot #{message_count} | Height: {height} | Bids: {bids} | Asks: {asks}")

elif 'Updates' in data['data']:
updates = data['data']['Updates']
height = updates.get('height', 'N/A')
num_updates = len(updates.get('order_statuses', []))
print(f"šŸ”„ Update #{message_count} | Height: {height} | Orders: {num_updates}")
else:
print(f"ā„¹ļø Message #{message_count}: {data.get('channel', 'unknown')}")

print("-" * 60)


if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\n\nāœ‹ Stopped by user")

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:

AspectL2L4
Data per level3 fields (px, sz, n)15+ fields per order
Orders visibleAggregated count onlyEvery individual order
Update frequencyPer price levelPer order change
Typical message size1-5 KB10-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?


Stream institutional-grade Hyperliquid order book data with Dwellir's ultra-low latency infrastructure.