Skip to main content

Setting Up Node.js for OrderBook (L2/L4)

Updated on
Feb 24, 2026

Overview

This guide demonstrates how to stream real-time orderbook data from Hyperliquid using gRPC in Node.js. You'll learn to:

  • Stream L2 orderbook (aggregated price levels with depths)
  • Stream L4 orderbook (individual orders with full details)
  • Load Protocol Buffer definitions dynamically
  • Handle reconnections and errors gracefully

The orderbook streaming API uses a separate proto file (orderbook.proto) from the core streaming API, providing specialized methods optimized for market data.

Related Guides

For core streaming API (StreamData, StreamBlocks, Ping), see Node.js Setup Guide

Prerequisites

1. Install Node.js

Node.js 16 or later is required. Follow the official installation guide or verify your installation:

node --version
npm --version

Project Setup

Step 1: Create Project Directory

mkdir hyperliquid-orderbook-nodejs
cd hyperliquid-orderbook-nodejs
npm init -y

Your directory structure:

hyperliquid-orderbook-nodejs/
├── package.json
├── proto/ # Protocol buffer definitions
└── examples/ # Example scripts

Step 2: Install Dependencies

Install gRPC and Protocol Buffers packages:

npm install @grpc/grpc-js @grpc/proto-loader

These packages provide:

  • @grpc/grpc-js - Pure JavaScript gRPC implementation
  • @grpc/proto-loader - Loads .proto files at runtime

Step 3: Download Proto File

Create directories and download the orderbook protocol definition:

mkdir -p proto examples

curl -o proto/orderbook.proto \
https://raw.githubusercontent.com/quiknode-labs/hypercore-grpc-examples/main/proto/orderbook.proto

Verify the file was downloaded:

ls -la proto/
# Should show: orderbook.proto

Implementation

StreamL2Book Example

Create examples/stream_l2.js:

const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const path = require('path');

// Parse command-line arguments
const args = process.argv.slice(2);
let endpoint = 'your-endpoint.hype-mainnet.quiknode.pro:10000';
let token = 'your-auth-token';
let coin = 'BTC';
let levels = 20;

for (let i = 0; i < args.length; i++) {
if (args[i].startsWith('--endpoint=')) endpoint = args[i].split('=')[1];
else if (args[i].startsWith('--token=')) token = args[i].split('=')[1];
else if (args[i].startsWith('--coin=')) coin = args[i].split('=')[1];
else if (args[i].startsWith('--levels=')) levels = parseInt(args[i].split('=')[1]);
}

const PROTO_PATH = path.join(__dirname, '..', 'proto', 'orderbook.proto');

const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
});
const proto = grpc.loadPackageDefinition(packageDefinition).hyperliquid;

function createClient() {
return new proto.OrderBookStreaming(
endpoint,
grpc.credentials.createSsl(),
{ 'grpc.max_receive_message_length': 100 * 1024 * 1024 }
);
}

async function streamL2Orderbook() {
console.log('='.repeat(60));
console.log(`Streaming L2 Orderbook for ${coin}`);
console.log(`Levels: ${levels}`);
console.log('='.repeat(60) + '\n');

let retryCount = 0;
const maxRetries = 10;
const baseDelay = 2000;

while (retryCount < maxRetries) {
const client = createClient();
const metadata = new grpc.Metadata();
metadata.add('x-token', token);

const request = {
coin: coin,
n_levels: levels
};

try {
if (retryCount > 0) {
console.log(`\n🔄 Reconnecting (attempt ${retryCount + 1}/${maxRetries})...`);
} else {
console.log(`Connecting to ${endpoint}...`);
}

let msgCount = 0;
const call = client.StreamL2Book(request, metadata);

call.on('data', (update) => {
msgCount++;

if (msgCount === 1) {
console.log('✓ First L2 update received!\n');
retryCount = 0;
}

console.log('\n' + '─'.repeat(60));
console.log(`Block: ${update.block_number} | Time: ${update.time} | Coin: ${update.coin}`);
console.log('─'.repeat(60));

// Display asks (reversed for display)
if (update.asks && update.asks.length > 0) {
console.log('\n ASKS:');
update.asks.slice(0, 10).reverse().forEach(level => {
console.log(` ${level.px.padStart(12)} | ${level.sz.padStart(12)} | (${level.n} orders)`);
});
}

// Display spread
if (update.bids && update.bids.length > 0 && update.asks && update.asks.length > 0) {
const bestBid = parseFloat(update.bids[0].px);
const bestAsk = parseFloat(update.asks[0].px);
const spread = bestAsk - bestBid;
const spreadBps = (spread / bestBid) * 10000;
console.log('\n ' + '─'.repeat(44));
console.log(` SPREAD: ${spread.toFixed(2)} (${spreadBps.toFixed(2)} bps)`);
console.log(' ' + '─'.repeat(44));
}

// Display bids
if (update.bids && update.bids.length > 0) {
console.log('\n BIDS:');
update.bids.slice(0, 10).forEach(level => {
console.log(` ${level.px.padStart(12)} | ${level.sz.padStart(12)} | (${level.n} orders)`);
});
}

console.log(`\n Messages received: ${msgCount}`);
});

call.on('error', (err) => {
if (err.code === grpc.status.DATA_LOSS) {
console.log(`\n⚠️ Server reinitialized: ${err.message}`);
retryCount++;
if (retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount - 1);
console.log(`⏳ Waiting ${delay / 1000}s before reconnecting...`);
setTimeout(() => streamL2Orderbook(), delay);
}
} else {
console.error('\ngRPC error:', err.code, '-', err.message);
}
});

call.on('end', () => {
console.log('\nStream ended');
});

await new Promise((resolve) => {
call.on('end', resolve);
call.on('error', resolve);
});

break;

} catch (err) {
console.error('Error:', err.message);
break;
}
}
}

streamL2Orderbook().catch(console.error);
StreamL4Book Example

For individual order streaming with full details, see the StreamL4Book method documentation which includes complete code examples.

Run the Example

Execute the script with your credentials:

cd examples

node stream_l2.js \
--endpoint="your-endpoint.hype-mainnet.quiknode.pro:10000" \
--token="your-auth-token" \
--coin="BTC" \
--levels=20

Options:

  • --endpoint - gRPC endpoint with port 10000
  • --token - Authentication token from Quicknode
  • --coin - Trading pair (BTC, ETH, SOL, etc.)
  • --levels - Number of price levels (default: 20, max: 100)

Output:

============================================================
Streaming L2 Orderbook for BTC
Levels: 20
============================================================

Connecting to your-endpoint.hype-mainnet.quiknode.pro:10000...
✓ First L2 update received!

────────────────────────────────────────────────────────────
Block: 904333900 | Time: 1771916717433 | Coin: BTC
────────────────────────────────────────────────────────────

ASKS:
62916 | 1.03755 | (5 orders)
62915 | 2.8545 | (1 orders)
62914 | 0.05136 | (3 orders)
...

────────────────────────────────────────────
SPREAD: 1.00 (0.16 bps)
────────────────────────────────────────────

BIDS:
62906 | 4.42562 | (23 orders)
62905 | 0.20558 | (4 orders)
62904 | 0.11454 | (2 orders)
...

Messages received: 1

Understanding the Output:


  • Block/Time - Blockchain block number and timestamp
  • Asks - Sell orders (displayed high to low)
  • Spread - Difference between best bid and ask in basis points
  • Bids - Buy orders (displayed high to low)
  • Order Count - Number of individual orders at each price level

We ❤️ Feedback!

If you have any feedback or questions about this documentation, let us know. We'd love to hear from you!

Share this doc