StreamRawBookDiffs - Real-time Book Diff Streaming
Stream incremental order book changes from Hyperliquid L1 Gateway via gRPC. Reconstruct the resting book from per-block additions, updates, and removals.
Stream incremental order book changes starting from a position, providing real-time access to every addition, size change, and removal in Hyperliquid's resting book.
Full Code Examples
Clone our gRPC Code Examples Repository for complete, runnable implementations in Go, Python, and Node.js.
When to Use This Method
StreamRawBookDiffs is essential for:
- Order Book Reconstruction - Maintain a live local book by applying per-block diffs
- Liquidity Analysis - Track depth, spread, and resting-order churn over time
- Microstructure Research - Study how the book evolves block by block
- Backtesting Inputs - Replay historical book changes from a chosen position
Method Signature
rpc StreamRawBookDiffs(Position) returns (stream BlockRawBookDiffs) {}Response Stream
message BlockRawBookDiffs {
// JSON-encoded object matching Hyperliquid's "node_raw_book_diffs_by_block" format.
bytes data = 1;
}The data field contains a JSON-encoded block envelope with:
- Block reference (
local_time,block_time,block_number) - An
eventsarray of incremental resting-book changes - Per-change order context (owner, order ID, coin, side, price)
Full Book Diff Spec
StreamRawBookDiffs emits a JSON payload matching Hyperliquid's node_raw_book_diffs_by_block format. Below is the exact structure.
Top-level keys (always present):
{
"local_time": "2026-04-20T00:00:00.040479928", // ISO-8601 timestamp when node processed the block (nanosecond precision)
"block_time": "2026-04-19T23:59:59.576040791", // ISO-8601 timestamp from block consensus
"block_number": 12345, // Block height containing these changes
"events": [ // Array of incremental book-change objects
{
"user": "0x1234567890abcdef1234567890abcdef12345678", // Order owner address
"oid": 12345, // Order identifier
"coin": "BTC", // Coin identifier
"side": "B", // "B" (buy) or "A" (sell/ask)
"px": "42000.0", // Price (numeric string)
"raw_book_diff": "remove" // Change applied to the book (see shapes below)
},
{
"user": "0x1234567890abcdef1234567890abcdef12345678",
"oid": 12346,
"coin": "BTC",
"side": "A",
"px": "42050.0",
"raw_book_diff": { "new": { "sz": "0.5" } } // Order added with size
},
{
"user": "0x1234567890abcdef1234567890abcdef12345678",
"oid": 12347,
"coin": "BTC",
"side": "A",
"px": "42100.0",
"raw_book_diff": { "update": { "newSz": "0.4", "origSz": "1.0" } } // Resting size changed
}
]
}Book diff event fields (events[i]):
| Field | Type | Description |
|---|---|---|
user | string | Order owner address |
oid | number | Order identifier. Not reliably unique across coins. See Order Identifiers |
coin | string | Coin identifier (e.g. "BTC", "ETH") |
side | string | "B" for buy, "A" for sell |
px | string | Price (numeric string) |
raw_book_diff | string or object | The change applied to the book. One of three shapes (below) |
The raw_book_diff value takes one of three shapes:
| Shape | Meaning |
|---|---|
"remove" (string literal) | Order removed from the book |
{ "new": { "sz": string } } | Order added with the given size |
{ "update": { "newSz": string, "origSz": string } } | Resting size changed from origSz to newSz |
Guarantees and alignment:
eventscontains all book changes from the block, across all coins and users.- Messages arrive in strictly increasing
block_number, one per on-disk block (including blocks with an emptyeventsarray). block_numbercorresponds toabci_block.roundin StreamBlocks;block_timealigns withabci_block.time.
Developer tips:
- Apply diffs in order: branch on the
raw_book_diffshape to add, resize, or remove the resting order. - Key your local book on
(coin, oid), never onoidalone. See Order Identifiers. - Normalize
px,sz,newSz, andorigSz(strings) to numeric types as appropriate.
Order Identifiers
oid is not reliably unique across coins. Consumers that index or join per-order state must key on the (coin, oid) tuple, never on oid alone.
Outcome markets (HIP-4) demonstrably reuse oids across the two sides of a single market (for example #30 and #31) within one block, so a diff for an oid on one coin is not necessarily the same logical order as a diff for that oid on another coin in the same block. Keying on (coin, oid) is safe whether or not oids turn out to be globally unique elsewhere.
Common Use Cases
1. Local Book Reconstruction
class BookReconstructor:
def __init__(self):
# (coin, oid) -> {"side", "px", "sz"}
self.orders = {}
def apply_block(self, block):
for event in block.get('events', []):
key = (event['coin'], event['oid'])
diff = event['raw_book_diff']
if diff == 'remove':
self.orders.pop(key, None)
elif 'new' in diff:
self.orders[key] = {
'side': event['side'],
'px': event['px'],
'sz': diff['new']['sz'],
}
elif 'update' in diff:
order = self.orders.get(key)
if order:
order['sz'] = diff['update']['newSz']2. Streaming Diffs in Python
import grpc
import json
import os
import hyperliquid_pb2
import hyperliquid_pb2_grpc
def stream_book_diffs():
endpoint = os.getenv('HYPERLIQUID_ENDPOINT')
api_key = os.getenv('API_KEY')
credentials = grpc.ssl_channel_credentials()
options = [('grpc.max_receive_message_length', 150 * 1024 * 1024)] # 150MB max
metadata = [('x-api-key', api_key)]
with grpc.secure_channel(endpoint, credentials, options=options) as channel:
client = hyperliquid_pb2_grpc.HyperliquidL1GatewayStub(channel)
# Empty Position means latest; set block_height or timestamp_ms to backfill
request = hyperliquid_pb2.Position()
for response in client.StreamRawBookDiffs(request, metadata=metadata):
block = json.loads(response.data)
print(f"block {block['block_number']}: {len(block.get('events', []))} diffs")Error Handling and Reconnection
Reconnect by replaying from the last processed block. Pass the next block_height in the Position so no diffs are skipped or double-applied.
def stream_with_resume(client, metadata, start_block):
next_block = start_block
while True:
request = hyperliquid_pb2.Position(block_height=next_block)
try:
for response in client.StreamRawBookDiffs(request, metadata=metadata):
block = json.loads(response.data)
process(block)
next_block = block['block_number'] + 1
except grpc.RpcError as e:
print(f"stream error, resuming from {next_block}: {e}")Best Practices
- Connection Management: Implement robust reconnection logic with exponential backoff
- Resumable State: Track the last applied
block_numberso you can resume from the next block - Stable Keys: Index resting orders on
(coin, oid)to stay correct across coins - Performance: Apply diffs asynchronously to avoid blocking the stream
- Resource Cleanup: Properly close streams and connections on shutdown
Current Limitations
- Backfill Window: Positioned requests are served from the node's on-disk feed; very old positions may fall outside the retained window. When the feed directory is not configured, positioned requests return
NotFoundand live tails end immediately. - Backpressure: High-volume periods may require careful handling to avoid overwhelming downstream systems
Resources
- GitHub: gRPC Code Examples - Complete working examples
- GetRawBookDiffs - Point-in-time book diff retrieval
- StreamOrderStatuses - The sibling block-envelope stream for order lifecycle events
- Archival Data - Historical
node_raw_book_diffs_by_blockdownloads
Need help? Contact our support team or check the Hyperliquid gRPC documentation.