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!