In perpetual DEX trading, volatility is instantaneous. Price movements of 5-10% can occur in seconds, and liquidations cascade through the system in milliseconds. Traditional monitoring approaches that rely on RPC polling or block explorer scraping operate with inherent latency; by the time you detect a liquidation, the opportunity has passed, and you're merely observing historical data.
The competitive edge in DeFi trading no longer comes from observing what happened but from predicting what will happen next. This guide addresses that challenge by building a system that doesn't just monitor liquidations in real time but predicts them before they occur.
This is a three-part series on building a liquidation monitoring system for Hyperliquid. In Part 1, we're building the foundation of our liquidation monitoring system. We'll create a robust, dual-mode detector that can track multiple wallets simultaneously with sub-second latency. In Part 2, we'll improve on Part 1 by adding storage persistence and advanced analytics. Finally, in Part 3, we'll build a simple dApp with real-time visualizations.
Current approaches suffer from critical limitations:
RPC Polling Latency: Standard JSON-RPC calls introduce 2-5 second delays, missing rapid-fire liquidation events.
Historical Context Absence: Most systems view liquidations in isolation, without correlating them with market conditions or whale behavior.
Scalability Issues: Custom-built WebSocket solutions struggle under high-volume conditions.
Predictive Analytics with Real-Time Data
We're building a simple dApp that moves beyond simple monitoring to predictive analytics. At the end of this whole guide, the system will:
Visualize liquidation across Hyperliquid markets.
Correlate liquidations with market volatility and funding rates.
Predict cascading margin calls before they occur
Why Hyperliquid?
Hyperliquid has emerged as a leading perpetual DEX on its dedicated L1 chain. Its architecture offers:
Sub-second block times: Enables high-frequency trading strategies.
High throughput: Capable of processing thousands of transactions per second.
Sophisticated risk engine: Advanced liquidation mechanisms that create predictable patterns.
These characteristics make Hyperliquid an ideal environment for building and testing predictive liquidation models.
Why Covalent GoldRush?
Traditional approaches to blockchain data collection face significant challenges:
Limitations of RPC
Historical Context Gap: Standard RPCs provide the current state but lack the historical depth needed for pattern recognition
Event Parsing Complexity: Each protocol requires custom ABI parsing and event decoding
Rate Limiting and Reliability: Self-hosted nodes struggle with rate limits during market volatility
Cross-Chain Fragmentation: Monitoring multiple protocols requires maintaining separate infrastructure
Covalent GoldRush solves these challenges by providing:
RealTime WebSocket Streaming: Sub-second updates for immediate position monitoring
Historical Data Context: Multi-chain historical data for pattern analysis
Unified API: Consistent data structure across 200+ supported chains
A liquidation wallet monitoring on Hyperliquid that:
Tracks multiple wallets simultaneously in real-time using Covalent GoldRush WebSocket streams
Calculates liquidation proximity with dynamic confidence scoring (HIGH, MEDIUM, LOW)
Provides dual detection modes for maximum reliability
Create a dAPP that shows detailed analytics on liquidation patterns and total losses
Covalent API Key
Node.js v18+
TypeScript knowledge
Understanding of perpetual futures mechanics
Initialize the project
mkdir hyperliquid-liquidation-monitor
cd hyperliquid-liquidation-monitor
npm init -yInstall Required Dependencies
npm install @covalenthq/client-sdk ws axios dotenv
npm install -D typescript @types/node @types/ws tsxConfigure Environment Variables
Create your environment configuration file. You'll need a Covalent API key, which you can get for free from the website.
COVALENT_API_KEY=your_covalent_api_key
Now let's start building our monitoring engine. We'll begin by creating the main file and setting up our core interfaces. Create src/liquidator.ts and start with the basic imports and configuration:
import { GoldRushClient, StreamingChain } from "@covalenthq/client-sdk";
import WebSocket from "ws";
import * as dotenv from "dotenv";
import axios from "axios";
dotenv.config();
(global as any).WebSocket = WebSocket;
const HYPERLIQUID_API = "https://api.hyperliquid.xyz/info";
const API_KEY = process.env.COVALENT_API_KEY;
interface PositionSnapshot {
coin: string;
size: number;
entryPrice: number;
liquidationPrice: number | null;
unrealizedPnl: number;
positionValue: number;
}
interface LiquidationDetection {
type: 'LIQUIDATION' | 'POSITION_CLOSED' | 'MARGIN_CALL';
wallet: string;
coin: string;
side: 'LONG' | 'SHORT';
size: number;
entryPrice: number;
exitPrice: number;
liquidationPrice: number | null;
lossAmount: number;
confidence: 'HIGH' | 'MEDIUM' | 'LOW';
reason: string;
timestamp: string;
source: 'GOLDRUSH_STREAM' | 'PERIODIC_POLL';
txHash?: string;
}A liquidation monitor is only as good as its price data. We need accurate, real-time prices to calculate how close positions are to their liquidation thresholds. Let's build a PriceOracle that fetches current market prices with intelligent caching.
class PriceOracle {
private priceCache: Map<string, { price: number; timestamp: number }> = new Map();
async getCurrentPrices(): Promise<Map<string, number>> {
try {
const response = await axios.post(
HYPERLIQUID_API,
{ type: "allMids" },
{ timeout: 5000 }
);
const prices = new Map<string, number>();
const timestamp = Date.now();
for (const [coin, priceStr] of Object.entries(response.data)) {
const price = parseFloat(priceStr as string);
prices.set(coin, price);
this.priceCache.set(coin, { price, timestamp });
}
return prices;
} catch (error) {
console.error("Failed to fetch prices:", error);
const cachedPrices = new Map<string, number>();
this.priceCache.forEach((data, coin) => {
cachedPrices.set(coin, data.price);
});
return cachedPrices;
}
}
getPrice(coin: string): number | null {
const cached = this.priceCache.get(coin);
if (!cached) return null;
return cached.price;
}
}Price Oracle Features:
Real-time Price Fetching: Calls Hyperliquid's endpoint for current market prices
Intelligent Caching: Stores prices with timestamps to minimize API calls
Fallback: Returns cached prices if API call fails
Timeout Handling: 5-second timeout to prevent hanging requests
The GoldRushLiquidationDetector class orchestrates everything: monitoring wallets, detecting position changes, analyzing liquidation risk, and generating alerts.
Let's start with the constructor and basic setup:
class GoldRushLiquidationDetector {
private client: GoldRushClient;
private priceOracle: PriceOracle;
private monitoredWallets: Map<string, PositionSnapshot[]> = new Map();
private detectedLiquidations: LiquidationDetection[] = [];
private subscriptions: any[] = [];
private processingQueue: Set<string> = new Set();
private pollingInterval: NodeJS.Timeout | null = null;
private goldRushEventCount = 0;
constructor(apiKey: string) {
this.client = new GoldRushClient(
apiKey,
{},
{
onConnecting: () => console.log("Connecting to GoldRush..."),
onOpened: () => console.log("Connected to GoldRush"),
onClosed: () => console.log("Disconnected from GoldRush"),
onError: (error) => console.error("GoldRush error:", error),
}
);
this.priceOracle = new PriceOracle();
}
Position Loading Methods
Before we can monitor for changes, we need to know the current state. This function fetches all open positions for our target wallets:
async loadWalletPositions(wallets: string[]): Promise<void> {
console.log("\n" + "=".repeat(80));
console.log("Loading Initial Positions");
console.log("=".repeat(80) + "\n");
for (const wallet of wallets) {
const positions = await this.fetchPositions(wallet);
if (positions && positions.length > 0) {
this.monitoredWallets.set(wallet, positions);
const shortAddr = this.formatAddress(wallet);
console.log(`${shortAddr}: ${positions.length} position(s)`);
positions.forEach(pos => {
const side = pos.size > 0 ? 'LONG' : 'SHORT';
console.log(` ${pos.coin} ${side}: ${Math.abs(pos.size).toFixed(4)} @ $${pos.entryPrice.toFixed(2)}`);
if (pos.liquidationPrice) {
console.log(` Liquidation Price: $${pos.liquidationPrice.toFixed(2)}`);
}
});
console.log();
}
await this.sleep(300);
}
console.log("=".repeat(80) + "\n");
}
private async fetchPositions(wallet: string): Promise<PositionSnapshot[] | null> {
try {
const response = await axios.post(
HYPERLIQUID_API,
{
type: "clearinghouseState",
user: wallet
},
{ timeout: 10000 }
);
if (!response.data?.assetPositions) return null;
return response.data.assetPositions
.map((ap: any) => ap.position)
.filter((p: any) => parseFloat(p.szi) !== 0)
.map((p: any) => ({
coin: p.coin,
size: parseFloat(p.szi),
entryPrice: parseFloat(p.entryPx),
liquidationPrice: p.liquidationPx ? parseFloat(p.liquidationPx) : null,
unrealizedPnl: parseFloat(p.unrealizedPnl),
positionValue: parseFloat(p.positionValue)
}));
} catch (error) {
console.error(`Failed to fetch positions for ${wallet.slice(0, 10)}...`);
return null;
}
}Monitoring System
Here's where we try to make our liquidation system robust. Instead of relying on a single method, we implement two independent monitoring systems:
startMonitoring(chain: StreamingChain = StreamingChain.HYPERCORE_MAINNET): void {
const wallets = Array.from(this.monitoredWallets.keys());
if (wallets.length === 0) {
console.log("No wallets to monitor");
return;
}
console.log("\n" + "=".repeat(80));
console.log("Starting Dual-Mode Monitoring");
console.log("=".repeat(80));
console.log(`Mode 1: GoldRush Stream (real-time)`);
console.log(`Mode 2: Periodic Polling (every 30s backup)`);
console.log(`Monitoring: ${wallets.length} wallet(s)`);
console.log("=".repeat(80) + "\n");
try {
const unsubscribe = this.client.StreamingService.subscribeToWalletActivity(
{
chain_name: chain,
wallet_addresses: wallets
},
{
next: (data: any) => {
this.goldRushEventCount++;
// GoldRush sends an ARRAY of transactions
if (Array.isArray(data)) {
// Process each transaction in the batch silently
for (const tx of data) {
if (tx.from_address || tx.to_address) {
this.handleTransaction(tx, 'GOLDRUSH_STREAM');
}
}
} else {
// Single transaction
this.handleWalletActivity(data, 'GOLDRUSH_STREAM');
}
},
error: (error) => {
console.error("GoldRush stream error:", error);
},
complete: () => {
console.log("GoldRush stream completed");
}
}
);
this.subscriptions.push(unsubscribe);
console.log("GoldRush stream active\n");
} catch (error) {
console.error("Failed to subscribe to GoldRush:", error);
}
this.startPeriodicPolling();
}
private startPeriodicPolling(): void {
console.log("Starting periodic position polling (30s intervals)\n");
this.pollingInterval = setInterval(async () => {
const timestamp = new Date().toLocaleTimeString();
console.log(`\n[${timestamp}] Periodic Check (GoldRush events so far: ${this.goldRushEventCount})`);
const wallets = Array.from(this.monitoredWallets.keys());
const prices = await this.priceOracle.getCurrentPrices();
for (const wallet of wallets) {
if (this.processingQueue.has(wallet)) {
console.log(`Skipping ${this.formatAddress(wallet)} - already processing`);
continue;
}
await this.detectLiquidation(wallet, prices, undefined, 'PERIODIC_POLL');
await this.sleep(500);
}
console.log(`Periodic check complete\n`);
}, 30000); // Every 30 seconds
}Transaction and Activity Handlers
When we detect wallet activity, we need to handle it intelligently:
private async handleWalletActivity(data: any, source: 'GOLDRUSH_STREAM' | 'PERIODIC_POLL'): Promise<void> {
const wallet = data?.wallet_address;
const txHash = data?.tx_hash;
if (!wallet || !this.monitoredWallets.has(wallet)) {
return;
}
if (this.processingQueue.has(wallet)) {
return; // Skip if already processing
}
this.processingQueue.add(wallet);
try {
const currentPrices = await this.priceOracle.getCurrentPrices();
await this.detectLiquidation(wallet, currentPrices, txHash, source);
} finally {
this.processingQueue.delete(wallet);
}
}
private handleTransaction(tx: any, source: 'GOLDRUSH_STREAM' | 'PERIODIC_POLL'): void {
// Extract wallet address from from_address or to_address
const wallet = tx.from_address || tx.to_address;
if (!wallet || !this.monitoredWallets.has(wallet)) {
return;
}
// Filter noise: Only process non-funding or significant events
if (tx.decoded_type === 'FUNDING' && tx.value < 10) {
return; // Skip small funding payments
}
// Trigger position check (silently)
this.handleWalletActivity({
wallet_address: wallet,
tx_hash: tx.tx_hash
}, source);
}Core Liquidation Detection Logic
When we detect wallet activity, we compare the old positions with the new positions to see what changed:
private async detectLiquidation(
wallet: string,
currentPrices: Map<string, number>,
txHash?: string,
source: 'GOLDRUSH_STREAM' | 'PERIODIC_POLL' = 'PERIODIC_POLL'
): Promise<void> {
const oldPositions = this.monitoredWallets.get(wallet) || [];
const newPositions = await this.fetchPositions(wallet) || [];
const oldCoins = new Set(oldPositions.map(p => p.coin));
const newCoins = new Set(newPositions.map(p => p.coin));
const closedCoins = Array.from(oldCoins).filter(coin => !newCoins.has(coin));
if (closedCoins.length === 0) {
// Silent - no positions closed
this.monitoredWallets.set(wallet, newPositions);
return;
}
// ONLY OUTPUT: Position closed detected
console.log(`\nPosition change detected for ${this.formatAddress(wallet)}`);
for (const coin of closedCoins) {
const oldPos = oldPositions.find(p => p.coin === coin)!;
const currentPrice = currentPrices.get(coin);
if (!currentPrice) {
continue;
}
const liquidation = this.analyzeClosure(
wallet,
oldPos,
currentPrice,
txHash,
source
);
if (liquidation) {
this.detectedLiquidations.push(liquidation);
this.displayLiquidationAlert(liquidation);
}
}
this.checkRemainingPositions(wallet, newPositions, currentPrices);
this.monitoredWallets.set(wallet, newPositions);
}Liquidation Analysis Engine
Not every position closure is a liquidation. Traders close positions manually for various reasons. We want our analysis engine to distinguish between different types of closures:
private analyzeClosure(
wallet: string,
position: PositionSnapshot,
currentPrice: number,
txHash: string | undefined,
source: 'GOLDRUSH_STREAM' | 'PERIODIC_POLL'
): LiquidationDetection | null {
const side = position.size > 0 ? 'LONG' : 'SHORT';
const shortAddr = this.formatAddress(wallet);
let distanceToLiquidation = Infinity;
let wasNearLiquidation = false;
if (position.liquidationPrice) {
if (side === 'LONG') {
distanceToLiquidation = ((currentPrice - position.liquidationPrice) / currentPrice) * 100;
} else {
distanceToLiquidation = ((position.liquidationPrice - currentPrice) / currentPrice) * 100;
}
wasNearLiquidation = distanceToLiquidation < 10;
}
const isLosingPosition = position.unrealizedPnl < 0;
const significantLoss = Math.abs(position.unrealizedPnl) > 100;
let type: 'LIQUIDATION' | 'POSITION_CLOSED' | 'MARGIN_CALL' = 'POSITION_CLOSED';
let confidence: 'HIGH' | 'MEDIUM' | 'LOW' = 'LOW';
let reason = 'Position manually closed';
if (wasNearLiquidation && isLosingPosition && significantLoss) {
type = 'LIQUIDATION';
confidence = distanceToLiquidation < 2 ? 'HIGH' : 'MEDIUM';
reason = `Closed ${distanceToLiquidation.toFixed(2)}% from liquidation price with $${Math.abs(position.unrealizedPnl).toFixed(2)} loss`;
} else if (wasNearLiquidation) {
type = 'MARGIN_CALL';
confidence = 'MEDIUM';
reason = `Closed near liquidation price (${distanceToLiquidation.toFixed(2)}% away)`;
} else if (significantLoss) {
confidence = 'LOW';
reason = `Closed with $${Math.abs(position.unrealizedPnl).toFixed(2)} loss`;
}
if (type === 'POSITION_CLOSED' && !significantLoss) {
console.log(` ${position.coin} ${side} closed (likely manual, no significant loss)\n`);
return null;
}
return {
type,
wallet: shortAddr,
coin: position.coin,
side,
size: Math.abs(position.size),
entryPrice: position.entryPrice,
exitPrice: currentPrice,
liquidationPrice: position.liquidationPrice,
lossAmount: Math.abs(position.unrealizedPnl),
confidence,
reason,
timestamp: new Date().toISOString(),
source,
txHash
};
}
private checkRemainingPositions(
wallet: string,
positions: PositionSnapshot[],
currentPrices: Map<string, number>
): void {
for (const pos of positions) {
if (!pos.liquidationPrice) continue;
const currentPrice = currentPrices.get(pos.coin);
if (!currentPrice) continue;
const side = pos.size > 0 ? 'LONG' : 'SHORT';
const distanceToLiq = side === 'LONG'
? ((currentPrice - pos.liquidationPrice) / currentPrice) * 100
: ((pos.liquidationPrice - currentPrice) / currentPrice) * 100;
if (distanceToLiq < 5) {
const shortAddr = this.formatAddress(wallet);
console.log(`CRITICAL: ${shortAddr} - ${pos.coin} ${side} is ${distanceToLiq.toFixed(2)}% from liquidation!`);
}
}
}Alert System
When we detect a liquidation or high-risk position, we need to present the information clearly:
private displayLiquidationAlert(detection: LiquidationDetection): void {
const border = "=".repeat(80);
console.log(`\n${border}`);
console.log(`${detection.type} - ${detection.confidence} CONFIDENCE`);
console.log(border);
console.log(`Source: ${detection.source}`);
console.log(`Wallet: ${detection.wallet}`);
console.log(`Position: ${detection.coin} ${detection.side}`);
console.log(`Size: ${detection.size.toFixed(4)}`);
console.log(`Entry: $${detection.entryPrice.toFixed(2)} → Exit: $${detection.exitPrice.toFixed(2)}`);
if (detection.liquidationPrice) {
console.log(`Liquidation Price: $${detection.liquidationPrice.toFixed(2)}`);
}
console.log(`Loss: $${detection.lossAmount.toFixed(2)}`);
console.log(`Reason: ${detection.reason}`);
if (detection.txHash) {
console.log(`Tx: ${detection.txHash}`);
}
console.log(`Time: ${new Date(detection.timestamp).toLocaleString()}`);
console.log(border + "\n");
}
private formatAddress(addr: string): string {
return `${addr.slice(0, 6)}...${addr.slice(-4)}`;
}
private sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}Analytics
To understand our liquidation patterns and system performance, we need analytics of how our system is performing:
getStats(): any {
const highConfidence = this.detectedLiquidations.filter(l => l.confidence === 'HIGH');
const totalLoss = this.detectedLiquidations.reduce((sum, l) => sum + l.lossAmount, 0);
const bySource = {
goldrush: this.detectedLiquidations.filter(l => l.source === 'GOLDRUSH_STREAM').length,
polling: this.detectedLiquidations.filter(l => l.source === 'PERIODIC_POLL').length
};
return {
total_events: this.detectedLiquidations.length,
liquidations: this.detectedLiquidations.filter(l => l.type === 'LIQUIDATION').length,
high_confidence: highConfidence.length,
total_loss: totalLoss,
goldrush_events: this.goldRushEventCount,
detected_by_goldrush: bySource.goldrush,
detected_by_polling: bySource.polling,
recent: this.detectedLiquidations.slice(-5).reverse()
};
}
displayStats(): void {
const stats = this.getStats();
console.log("\n" + "=".repeat(80));
console.log("LIQUIDATION STATISTICS");
console.log("=".repeat(80));
console.log(`Total Events: ${stats.total_events}`);
console.log(`Liquidations: ${stats.liquidations}`);
console.log(`High Confidence: ${stats.high_confidence}`);
console.log(`Total Loss: $${stats.total_loss.toFixed(2)}`);
console.log(`\nDetection Sources:`);
console.log(` GoldRush Stream Events: ${stats.goldrush_events}`);
console.log(` Detected via GoldRush: ${stats.detected_by_goldrush}`);
console.log(` Detected via Polling: ${stats.detected_by_polling}`);
console.log("=".repeat(80) + "\n");
}
async disconnect(): Promise<void> {
if (this.pollingInterval) {
clearInterval(this.pollingInterval);
}
this.subscriptions.forEach(unsub => unsub());
await this.client.StreamingService.disconnect();
}
}Finally, let's create the main entry point that ties everything together:
// Main Execution
const main = async (): Promise<void> => {
console.log("\n" + "=".repeat(80));
console.log(" Dual-Mode Hyperliquid Liquidation Detector");
console.log("=".repeat(80));
console.log(" Mode 1: GoldRush real-time streaming");
console.log(" Mode 2: 30-second polling backup\n");
const detector = new GoldRushLiquidationDetector(API_KEY!);
const WALLETS_TO_MONITOR = [
"Enter the wallet addresses you want to monitor",
];
try {
await detector.loadWalletPositions(WALLETS_TO_MONITOR);
detector.startMonitoring(StreamingChain.HYPERCORE_MAINNET);
const runTime = 10 * 60 * 1000;
console.log("DUAL-MODE MONITORING ACTIVE");
console.log(`Will run for ${runTime / 60000} minutes`);
console.log("Press Ctrl+C to stop\n");
const statsInterval = setInterval(() => {
detector.displayStats();
}, 2 * 60 * 1000);
setTimeout(async () => {
clearInterval(statsInterval);
console.log("\nMonitoring complete");
detector.displayStats();
await detector.disconnect();
process.exit(0);
}, runTime);
process.on('SIGINT', async () => {
clearInterval(statsInterval);
console.log("\n\nStopping monitor...");
detector.displayStats();
await detector.disconnect();
process.exit(0);
});
} catch (error) {
console.error("Fatal error:", error);
await detector.disconnect();
process.exit(1);
}
};
if (require.main === module) {
main().catch(console.error);
}
export { GoldRushLiquidationDetector };Now let's test our liquidation monitoring system. First, add some actual wallet addresses to monitor. You can find an active Hyperliquid trader's wallet on their explorer
Once you’ve gotten the address you want to use, open your terminal and run:
npx tsx hyperliquid-liquidation-monitorYou should see output similar to this:

Congratulations! You've successfully built the foundation of a real-time liquidation monitoring system that tracks Hyperliquid positions with sub-second latency. You now have a working application that can detect liquidation events as they happen, provide confidence scoring, and offer predictive warnings for at-risk positions. In Part 2, we'll enhance this foundation by adding storage persistence for historical analysis and advanced analytics for pattern recognition. See you next time!