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
Access to /hypercore (JSON-RPC and WebSocket) and HyperCore gRPC streaming requires a Quicknode Build plan or higher. View pricing to upgrade.
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!