Building a Real-Time Hyperliquid Liquidation Monitor – Part 1

Habeeb Yinka
Content Writer
Learn how to track multiple wallets with sub-second latency and detect liquidation events before they happen using the Goldrush Streaming API.

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.

Latency in DeFi Monitoring

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

Architecture Overview: Covalent GoldRush & Hyperliquid

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

What You Will Build

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

What You Will Need

  • Covalent API Key 

  • Node.js v18+ 

  • TypeScript knowledge

  • Understanding of perpetual futures mechanics

Step 1: Project Setup and Dependencies

Initialize the project

mkdir hyperliquid-liquidation-monitor cd hyperliquid-liquidation-monitor npm init -y

Install Required  Dependencies

npm install @covalenthq/client-sdk ws axios dotenv npm install -D typescript @types/node @types/ws tsx

Configure 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

Step 2: Core Imports and Configuration

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; }

Step 3: Price Oracle Implementation

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

Step 4: Main Liquidation Detector Class

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();   } }

Step 5: Main Entry Point

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 };

Step 6: Running the Whale Tracker

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-monitor

You should see output similar to this:

Conclusion

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!

Get Started

Get started with GoldRush API in minutes. Sign up for a free API key and start building.

Support

Explore multiple support options! From FAQs for self-help to real-time interactions on Discord.

Contact Sales

Interested in our professional or enterprise plans? Contact our sales team to learn more.