Docs

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

protobuf
rpc StreamRawBookDiffs(Position) returns (stream BlockRawBookDiffs) {}

Response Stream

protobuf
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 events array 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):

JSON
{
  "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]):

FieldTypeDescription
userstringOrder owner address
oidnumberOrder identifier. Not reliably unique across coins. See Order Identifiers
coinstringCoin identifier (e.g. "BTC", "ETH")
sidestring"B" for buy, "A" for sell
pxstringPrice (numeric string)
raw_book_diffstring or objectThe change applied to the book. One of three shapes (below)

The raw_book_diff value takes one of three shapes:

ShapeMeaning
"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:

  • events contains 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 empty events array).
  • block_number corresponds to abci_block.round in StreamBlocks; block_time aligns with abci_block.time.

Developer tips:

  • Apply diffs in order: branch on the raw_book_diff shape to add, resize, or remove the resting order.
  • Key your local book on (coin, oid), never on oid alone. See Order Identifiers.
  • Normalize px, sz, newSz, and origSz (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

Python
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

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.

Python
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

  1. Connection Management: Implement robust reconnection logic with exponential backoff
  2. Resumable State: Track the last applied block_number so you can resume from the next block
  3. Stable Keys: Index resting orders on (coin, oid) to stay correct across coins
  4. Performance: Apply diffs asynchronously to avoid blocking the stream
  5. 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 NotFound and live tails end immediately.
  • Backpressure: High-volume periods may require careful handling to avoid overwhelming downstream systems

Resources

Need help? Contact our support team or check the Hyperliquid gRPC documentation.