Learn how to build a queryable Hyperliquid liquidation data API with Express.js using the Goldrush Streaming API.
Welcome to Part 3 of our series! In Part 2, we solved data loss by saving events to liquidations-{date}.json files. Now, we'll unlock that data's full potential by building a REST API server. This tutorial will guide you through creating a complete backend service that transforms your static JSON files into a live, queryable API, the essential foundation for building dashboards, trading tools, and automated alerts.
To follow this tutorial effectively, you should have completed:
Part 1: Build a Real-Time Liquidation Monitor.
Part 2: Add Data Persistence and store the data in a JSON file.
By the end, you will have a fully functional Hyperliquid liquidation API that enables you to:
Create a real-time data service that queries the goldrush streaming JSON files from Part 2.
Build 11 core endpoints to retrieve recent liquidations, analyze positions at risk, and track trends by coin.
Implement performance monitoring with health checks and system statistics for a robust service.
Installing Required Dependencies
Before building our endpoints, let's install the required dependencies. Open your terminal and install:
npm install express cors
npm install -D @types/express @types/cors
npm install -D concurrentlyOpen your package.json and update the "scripts" section:
{
"scripts": {
"monitor": "tsx src/liquidator.ts",
"api": "tsx src/api-server.ts",
"dev": "concurrently \"npm run monitor\" \"npm run api\"",
"start": "npm run dev"
}
}Once you've completed these prerequisites, we can start implementing our API endpoints.
Create a folder named `src` and add a new file called `api-server.ts` inside it. Then, move your `liquidator.ts` file from Part 2 into the `src` folder so both files are in the same directory.
cd hyperliquid-liquidation-monitor
touch src/api-server.tsNote: We'll build this file step by step, explaining each section. Don't worry if it seems like a lot – we'll break it down.
Now open src/api-server.ts and copy the complete code provided.
import express from 'express';
import cors from 'cors';
import fs from 'fs';
import path from 'path';
const app = express();
app.use(cors());
app.use(express.json());
const DATA_DIR = path.join(process.cwd(), 'liquidation-data');This sets up Express with CORS support and points to the data directory created in Part 2.
This creates the liquidation-data/ folder if it doesn't exist. This ensures the API server won't crash if started before the monitor makes the directory.
// Ensure data directory exists
if (!fs.existsSync(DATA_DIR)) {
fs.mkdirSync(DATA_DIR, { recursive: true });
}Since we'll read JSON files frequently, let's create a helper function that handles errors gracefully.
function readJsonFile(filePath: string): any[] {
if (!fs.existsSync(filePath)) {
return [];
}
try {
const content = fs.readFileSync(filePath, 'utf-8');
return JSON.parse(content);
} catch (error) {
console.error(`Error reading ${filePath}:`, error);
return [];
}
}Endpoint 1: Recent Liquidations
Let's start with the most useful endpoint - getting recent liquidations:
app.get('/api/liquidations/recent', (req, res) => {
const limit = parseInt(req.query.limit as string) || 100;
const dateStr = new Date().toISOString().split('T')[0];
const file = path.join(DATA_DIR, `liquidations-${dateStr}.json`);
const liquidations = readJsonFile(file);
res.json(liquidations.slice(-limit).reverse());
});What it does:
This returns today's liquidation events, newest first. You can use limit=5 to get only 5 results.
Endpoint 2: Liquidations by Date Range
What if you want more than just today's data? This endpoint lets you specify how many days to look back.
app.get('/api/liquidations/range', (req, res) => {
const days = parseInt(req.query.days as string) || 7;
const allLiquidations: any[] = [];
for (let i = 0; i < days; i++) {
const date = new Date();
date.setDate(date.getDate() - i);
const dateStr = date.toISOString().split('T')[0];
const file = path.join(DATA_DIR, `liquidations-${dateStr}.json`);
const dayLiquidations = readJsonFile(file);
allLiquidations.push(...dayLiquidations);
}
res.json(allLiquidations.reverse());
});What it does:
Loops through the specified number of days (today + past N-1 days). For each day, it reads the corresponding liquidation file. Combines all liquidations into one array and returns the combined array, newest first.
Endpoint 3: Today's Position Snapshots
This endpoint shows all the position snapshots saved today.
app.get('/api/positions/today', (req, res) => {
const dateStr = new Date().toISOString().split('T')[0];
const file = path.join(DATA_DIR, `positions-${dateStr}.json`);
const positions = readJsonFile(file);
res.json(positions);
});What it does:
Every position snapshot taken today. Remember, your monitor saves position snapshots periodically, so this could be many entries per wallet.
Endpoint 4: Latest Positions by Wallet
While the previous endpoint shows ALL snapshots, this one shows only the most recent snapshot for each wallet.
// Get latest position snapshot for each wallet
app.get('/api/positions/latest', (req, res) => {
const dateStr = new Date().toISOString().split('T')[0];
const file = path.join(DATA_DIR, `positions-${dateStr}.json`);
const allSnapshots = readJsonFile(file);
// Group by wallet and get the latest for each
const latestByWallet = new Map();
allSnapshots.forEach((snapshot: any) => {
const existing = latestByWallet.get(snapshot.wallet);
if (!existing || snapshot.timestamp > existing.timestamp) {
latestByWallet.set(snapshot.wallet, snapshot);
}
});
res.json(Array.from(latestByWallet.values()));
});What it does:
Reads all the latest position snapshots and groups them by wallet address using a Map. For each wallet, it keeps only the snapshot with the latest timestamp and returns an array of the latest snapshots.
Endpoint 5: Positions at Risk
This is one of the most valuable endpoints. It identifies positions that might get liquidated soon.
app.get('/api/positions/at-risk', (req, res) => {
const threshold = parseFloat(req.query.threshold as string) || 10; // % from liquidation
const dateStr = new Date().toISOString().split('T')[0];
const file = path.join(DATA_DIR, `positions-${dateStr}.json`);
const allSnapshots = readJsonFile(file);
// Get latest snapshot for each wallet
const latestByWallet = new Map();
allSnapshots.forEach((snapshot: any) => {
const existing = latestByWallet.get(snapshot.wallet);
if (!existing || snapshot.timestamp > existing.timestamp) {
latestByWallet.set(snapshot.wallet, snapshot);
}
});
const atRiskPositions: any[] = [];
latestByWallet.forEach((snapshot: any) => {
snapshot.positions.forEach((pos: any) => {
if (!pos.liquidationPrice) return;
// Calculate current price from unrealizedPnl and other data
// For now, we'll flag positions with negative PnL > $1000
if (pos.unrealizedPnl < -1000) {
atRiskPositions.push({
wallet: snapshot.wallet,
timestamp: snapshot.timestamp,
...pos
});
}
});
});
res.json(atRiskPositions);
});Fetch current prices to calculate the actual distance to liquidation. Use the threshold parameter meaningfully. Consider position size relative to account equity.
Endpoint 6: Latest Statistics
Your monitor saves statistics about its own performance. This endpoint gives you the latest stats.
app.get('/api/stats', (req, res) => {
const file = path.join(DATA_DIR, 'statistics.json');
const stats = readJsonFile(file);
if (stats.length === 0) {
res.json({
total_events: 0,
liquidations: 0,
high_confidence: 0,
total_loss: 0
});
} else {
res.json(stats[stats.length - 1]);
}
});Endpoint 7: Aggregated Statistics
While the previous endpoint gives you just the latest stats, this one gives you stats from multiple days:
app.get('/api/stats/aggregated', (req, res) => {
const days = parseInt(req.query.days as string) || 7;
const file = path.join(DATA_DIR, 'statistics.json');
const allStats = readJsonFile(file);
const now = new Date();
const cutoffTime = now.getTime() - (days * 24 * 60 * 60 * 1000);
const recentStats = allStats.filter((stat: any) => {
const statDate = new Date(stat.timestamp);
return statDate.getTime() > cutoffTime;
});
res.json(recentStats);
});Endpoint 8: Storage Information
This endpoint gives you metadata about your stored data.
app.get('/api/storage/info', (req, res) => {
const files = fs.readdirSync(DATA_DIR);
const liquidationFiles = files.filter(f => f.startsWith('liquidations-'));
const positionFiles = files.filter(f => f.startsWith('positions-'));
let totalLiquidations = 0;
let totalSnapshots = 0;
liquidationFiles.forEach(file => {
const data = readJsonFile(path.join(DATA_DIR, file));
totalLiquidations += data.length;
});
positionFiles.forEach(file => {
const data = readJsonFile(path.join(DATA_DIR, file));
totalSnapshots += data.length;
});
res.json({
files: files.length,
liquidationFiles: liquidationFiles.length,
positionFiles: positionFiles.length,
totalLiquidations,
totalSnapshots,
dataDirectory: DATA_DIR
});
});Endpoint 9: Top Losers Analytics
This endpoint finds the biggest liquidation losses.
app.get('/api/analytics/top-losers', (req, res) => {
const limit = parseInt(req.query.limit as string) || 10;
const days = parseInt(req.query.days as string) || 7;
const allLiquidations: any[] = [];
for (let i = 0; i < days; i++) {
const date = new Date();
date.setDate(date.getDate() - i);
const dateStr = date.toISOString().split('T')[0];
const file = path.join(DATA_DIR, `liquidations-${dateStr}.json`);
const dayLiquidations = readJsonFile(file);
allLiquidations.push(...dayLiquidations);
}
// Sort by loss amount
const sorted = allLiquidations
.sort((a, b) => b.lossAmount - a.lossAmount)
.slice(0, limit);
res.json(sorted);
});Endpoint 10: Liquidations by Coin
This endpoint analyzes which cryptocurrencies are getting liquidated most frequently and calculates the financial impact for each coin.
app.get('/api/analytics/by-coin', (req, res) => {
const days = parseInt(req.query.days as string) || 7;
const allLiquidations: any[] = [];
for (let i = 0; i < days; i++) {
const date = new Date();
date.setDate(date.getDate() - i);
const dateStr = date.toISOString().split('T')[0];
const file = path.join(DATA_DIR, `liquidations-${dateStr}.json`);
const dayLiquidations = readJsonFile(file);
allLiquidations.push(...dayLiquidations);
}
// Group by coin
const byCoin = new Map();
allLiquidations.forEach(liq => {
const coin = liq.coin;
if (!byCoin.has(coin)) {
byCoin.set(coin, {
coin,
count: 0,
totalLoss: 0,
avgLoss: 0
});
}
const data = byCoin.get(coin);
data.count++;
data.totalLoss += liq.lossAmount;
});
// Calculate averages
byCoin.forEach(data => {
data.avgLoss = data.totalLoss / data.count;
});
res.json(Array.from(byCoin.values()).sort((a, b) => b.totalLoss - a.totalLoss));
});Endpoint 11: Health Check
This is a health check endpoint – a simple way to verify that your API server is running and responding properly.
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});Finally, let’s add the server startup code with comprehensive logging.
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
console.log('\n' + '='.repeat(80));
console.log('Hyperliquid Liquidation API Server');
console.log('='.repeat(80));
console.log(`Server running on: http://localhost:${PORT}`);
console.log(`Data directory: ${DATA_DIR}`);
console.log('\nAvailable endpoints:');
console.log(' GET /api/liquidations/recent');
console.log(' GET /api/liquidations/range?days=7');
console.log(' GET /api/positions/today');
console.log(' GET /api/positions/latest');
console.log(' GET /api/positions/at-risk?threshold=10');
console.log(' GET /api/stats');
console.log(' GET /api/stats/aggregated?days=7');
console.log(' GET /api/storage/info');
console.log(' GET /api/analytics/top-losers?limit=10&days=7');
console.log(' GET /api/analytics/by-coin?days=7');
console.log(' GET /health');
console.log('='.repeat(80) + '\n');
});
export default app;If you've made it this far, congratulations! Now let's test whether our endpoints are working as expected. We'll use Postman to test the endpoints. If you don't have Postman installed, you can use curl in the terminal instead.
Now, open your terminal and run:
npm run devThis command will start both the real-time liquidation monitor and the local API server. Once your server has started, you can now start testing the endpoints. For this tutorial, we’ll only test one endpoint.
Testing with Postman
Open Postman.
Create a GET request to: `http://localhost:3001/api/positions/at-risk?threshold=10.`
Click "Send."
You should see the JSON response with at-risk positions.

Testing with curl
Open your terminal and run:
curl "http://localhost:3001/api/positions/at-risk?threshold=10"After running it, you should see the endpoint returning positions that are at risk of liquidation based on the wallet's current PnL.

And there you have it! You've built a complete API server that makes your liquidation data accessible and useful. Your JSON files are no longer just stored information; they're now an active service that any application can query. You can now get recent liquidations, check risky positions, analyze the biggest losses, and see which coins are getting liquidated most often. Everything you collected in Parts 1 and 2 is now available through simple API calls.
In Part 4, we'll build a simple dAPP to display this data visually. You'll create charts that update in real time and tables that show current positions. It's the final piece that brings everything together into something you can actually use and share.
See you in Part 4.