Introduction
I run three types of trading bots on Hyperliquid, an on-chain order book DEX, 24/7 on a VPS: Grid Bots across 5 pairs, a Funding Rate arbitrage bot, and a Liquidation Cascade detector.
This is a technical deep dive into the stack, architecture decisions, and real operational challenges. No trading advice — just engineering.
Why Hyperliquid
Unlike AMM-based DEXes, Hyperliquid runs a full order book on its own L1 chain. For bot developers, this means:
- Limit orders (not just market orders like AMMs)
- Real-time order book data via WebSocket
- Maker fee: 0.01% (with VIP rebates)
- Taker fee: 0.035%
- Near-zero gas costs
- REST + WebSocket APIs, ccxt support
REST: https://api.hyperliquid.xyz
WebSocket: wss://api.hyperliquid.xyz/ws
Tech Stack
Runtime: Node.js
Exchange API: ccxt (Hyperliquid supported)
Tx Signing: ethers.js v5
Database: SQLite (WAL mode)
Process Manager: PM2
Server: Ubuntu 22.04 VPS
ccxt Integration
ccxt provides a unified API across 100+ exchanges. Switching between exchanges becomes trivial.
import ccxt from 'ccxt';
const exchange = new ccxt.hyperliquid({
walletAddress: process.env.HL_WALLET,
privateKey: process.env.HL_PRIVATE_KEY,
});
// Fetch balance
const balance = await exchange.fetchBalance();
console.log('USDC:', balance.USDC);
// Place a limit buy
const order = await exchange.createLimitBuyOrder(
'ETH/USDC:USDC', 0.01, 1800
);
// Fetch open orders
const open = await exchange.fetchOpenOrders('ETH/USDC:USDC');
Why ethers.js v5 (not v6)
Hyperliquid uses EIP-712 for transaction signing. There are known issues with ethers v6 and Hyperliquid's L1 auth flow. Stick with v5.
import { ethers } from 'ethers';
const wallet = new ethers.Wallet(process.env.HL_PRIVATE_KEY);
// EIP-712 domain for L1 auth
const domain = {
name: 'HyperliquidSignTransaction',
version: '1',
chainId: 1337,
verifyingContract: '0x' + '0'.repeat(40),
};
SQLite with WAL Mode
Trade history and bot state go into SQLite. WAL mode enables concurrent reads and writes.
import Database from 'better-sqlite3';
const db = new Database('grid-bot.db');
db.pragma('journal_mode = WAL');
db.pragma('busy_timeout = 5000');
db.exec(`
CREATE TABLE IF NOT EXISTS trades (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pair TEXT NOT NULL,
side TEXT NOT NULL,
price REAL NOT NULL,
size REAL NOT NULL,
pnl REAL DEFAULT 0,
created_at TEXT DEFAULT (datetime('now'))
)
`);
Why not Postgres/MySQL? For a solo bot operation, SQLite is enough. Single file backup, zero server management, and WAL handles concurrency fine.
PM2 Configuration
// ecosystem.config.cjs
module.exports = {
apps: [
{
name: 'grid-eth',
script: 'grid-bot.js',
env: { COIN: 'ETH', LEVERAGE: '3' },
max_memory_restart: '200M',
cron_restart: '0 */6 * * *',
},
{
name: 'grid-btc',
script: 'grid-bot.js',
env: { COIN: 'BTC', LEVERAGE: '3' },
max_memory_restart: '200M',
cron_restart: '0 */6 * * *',
},
],
};
PM2 is non-negotiable for 24/7 operation:
- Auto-restart on crash
-
max_memory_restartcatches memory leaks -
cron_restartfor periodic restarts -
pm2 logsandpm2 monitfor observability
Grid Bot Architecture
Core Logic
Grid width is ATR-based, not fixed. Adapts to volatility automatically.
function calculateATR(candles, period = 14) {
const trueRanges = candles.map((c, i) => {
if (i === 0) return c.high - c.low;
const prev = candles[i - 1];
return Math.max(
c.high - c.low,
Math.abs(c.high - prev.close),
Math.abs(c.low - prev.close)
);
});
return trueRanges.slice(-period)
.reduce((a, b) => a + b, 0) / period;
}
function calculateGridLevels(price, gridCount, atrMult, atr) {
const halfRange = atr * atrMult;
const upper = price + halfRange;
const lower = price - halfRange;
const step = (upper - lower) / gridCount;
const levels = [];
for (let i = 0; i <= gridCount; i++) {
levels.push(lower + step * i);
}
return { levels, upper, lower, step };
}
Order Placement
async function placeGridOrders(exchange, pair, levels, currentPrice, size) {
const orders = [];
for (const level of levels) {
if (level < currentPrice) {
orders.push(await exchange.createLimitBuyOrder(pair, size, level));
} else if (level > currentPrice) {
orders.push(await exchange.createLimitSellOrder(pair, size, level));
}
}
return orders;
}
Operational Challenges
429 Rate Limiting
Running 5 pairs simultaneously hammers the API. Exponential backoff is essential.
async function rateLimitedCall(fn, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (e) {
if (e.message?.includes('429')) {
const wait = 2 ** i * 1000 + Math.random() * 1000;
console.warn(`Rate limited, backing off ${Math.round(wait)}ms`);
await new Promise(r => setTimeout(r, wait));
} else throw e;
}
}
throw new Error('Rate limit retries exhausted');
}
The real fix: move from REST polling to WebSocket.
import WebSocket from 'ws';
const ws = new WebSocket('wss://api.hyperliquid.xyz/ws');
ws.on('open', () => {
ws.send(JSON.stringify({
method: 'subscribe',
subscription: { type: 'l2Book', coin: 'ETH' },
}));
ws.send(JSON.stringify({
method: 'subscribe',
subscription: { type: 'userFills', user: walletAddress },
}));
});
ws.on('message', (data) => {
const msg = JSON.parse(data);
// Handle order book updates and fill notifications
});
Leverage Creep
Grid bots accumulate positions at grid edges. Effective leverage can silently exceed your target.
async function checkLeverage(exchange, pair, max = 3) {
const positions = await exchange.fetchPositions([pair]);
const pos = positions.find(p => p.symbol === pair);
if (pos && pos.leverage > max) {
console.warn(`Leverage ${pos.leverage}x > max ${max}x`);
return false; // Stop placing new orders
}
return true;
}
SQLite Write Contention
Multiple bot processes writing to the same DB file will conflict. Solutions:
- Set
busy_timeout = 5000in every process - Or split into per-bot DB files (recommended)
VPS Issues I've Hit
- Provider maintenance causing downtime
- Disk full from log files (solved with PM2 log rotation)
- Memory leaks from long-running Node.js processes (solved with
cron_restart)
Project Structure
crypto-bot/
├── grid-bot/
│ ├── grid-bot.js
│ ├── ecosystem.config.cjs
│ └── grid-bot.db
├── funding-bot/
│ ├── funding-arb-cross.js
│ └── funding-arb.db
├── liquidation-ws/
│ └── hl-liquidation-ws.js
├── shared/
│ └── hl-client.js
└── package.json
Key Takeaways
- Hyperliquid is great for bot developers: low fees, solid API, on-chain transparency
- "Set and forget" is a myth. Constant parameter tuning, bug fixing, and infra maintenance required
- Leverage management is the #1 risk. One bad day can wipe your account
- PM2 + SQLite WAL + WebSocket is a solid stack for solo bot operations
- Start with $100 on testnet before going live
I wrote a more detailed article (in Japanese) about my background and how I got into bot trading here:
https://shuen-shukusai.com/media/crypto/cr-p2-hyperliquid-bot/
Top comments (0)