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.
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.protofiles 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);
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!