Learn to build Hyperliquid liquidation data to disk with TypeScript. Complete tutorial for building a file-based persistence system with position snapshots and historical tracking for DeFi liquidation monitoring using the Goldrush Streaming API.
In Part 1, we built a real-time Hyperliquid liquidation monitoring system that detects liquidation events with sub-second latency. But there was a critical problem: all our data was stored in memory. When you stopped and restarted the monitor, everything was lost – no history, no statistics, no patterns to analyze.
In Part 2, we're enhancing our monitor with file-based persistence that saves all hyperliquid liquidation events, position snapshots, and statistics to disk. This creates a historical record that survives restarts and forms the foundation for our upcoming analytics dashboard.
In this tutorial, you'll learn how to:
Convert memory-only data to persistent disk storage using the Node.js file system.
Connect your existing liquidation detector with the new persistence layer.
Structure data for easy querying and analysis.
Make sure your Part 1 project is ready. We'll be modifying your existing src/liquidator.ts file. Now, cd into the project, and let's get started.
cd hyperliquid-liquidation-monitorFirst, we need to add Node.js file system capabilities to our imports. Add these two imports at the top of your src/liquidator.ts:
import { GoldRushClient, StreamingChain } from "@covalenthq/client-sdk";
import WebSocket from "ws";
import * as dotenv from "dotenv";
import axios from "axios";
import fs from 'fs'; // File system operations
import path from 'path'; //Path manipulation
dotenv.config();
(global as any).WebSocket = WebSocket;
const HYPERLIQUID_API = "https://api.hyperliquid.xyz/info";
const API_KEY = process.env.COVALENT_API_KEY;We're adding two new imports: fs for file system operations and path for handling file paths. Both are built-in Node.js modules, so no additional installation is needed.
Your interfaces from Part 1 remain unchanged. Keep them as they are. These interfaces define the data structure for our liquidation monitoring system. Proper typing ensures data consistency across storage and analysis.
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;
}This is our new core component that handles all file operations. Add this class right after your interfaces:
class DataStorageManager {
private dataDir: string;
constructor() {
this.dataDir = path.join(process.cwd(), 'liquidation-data');
this.ensureDataDir();
}
private ensureDataDir(): void {
if (!fs.existsSync(this.dataDir)) {
fs.mkdirSync(this.dataDir, { recursive: true });
console.log(`Created data directory: ${this.dataDir}`);
}
}
saveLiquidation(detection: LiquidationDetection): void {
const date = new Date(detection.timestamp);
const dateStr = date.toISOString().split('T')[0];
const hour = date.getHours();
const dailyFile = path.join(this.dataDir, `liquidations-${dateStr}.json`);
const hourlyFile = path.join(this.dataDir, `liquidations-${dateStr}-${hour}.json`);
this.appendToFile(dailyFile, detection);
this.appendToFile(hourlyFile, detection);
console.log(`Saved liquidation to storage`);
}
savePositionSnapshot(wallet: string, positions: PositionSnapshot[]): void {
const timestamp = Date.now();
const snapshot = {
wallet,
timestamp,
positions,
marketSnapshot: {}
};
const dateStr = new Date().toISOString().split('T')[0];
const file = path.join(this.dataDir, `positions-${dateStr}.json`);
this.appendToFile(file, snapshot);
}
saveStatistics(stats: any): void {
const timestamp = new Date().toISOString();
const statistics = {
timestamp,
...stats
};
const file = path.join(this.dataDir, 'statistics.json');
this.appendToFile(file, statistics);
}
private appendToFile(filePath: string, data: any): void {
let existingData: any[] = [];
if (fs.existsSync(filePath)) {
try {
const fileContent = fs.readFileSync(filePath, 'utf-8');
existingData = JSON.parse(fileContent);
} catch (error) {
existingData = [];
}
}
existingData.push(data);
fs.writeFileSync(filePath, JSON.stringify(existingData, null, 2));
}
getRecentLiquidations(limit: number = 100): LiquidationDetection[] {
const dateStr = new Date().toISOString().split('T')[0];
const file = path.join(this.dataDir, `liquidations-${dateStr}.json`);
if (!fs.existsSync(file)) {
return [];
}
try {
const content = fs.readFileSync(file, 'utf-8');
const allLiquidations: LiquidationDetection[] = JSON.parse(content);
return allLiquidations.slice(-limit).reverse();
} catch (error) {
console.error('Error reading liquidations file:', error);
return [];
}
}
getAggregatedStats(days: number = 7): any[] {
const stats: any[] = [];
const today = new Date();
for (let i = 0; i < days; i++) {
const date = new Date(today);
date.setDate(date.getDate() - i);
const dateStr = date.toISOString().split('T')[0];
const file = path.join(this.dataDir, 'statistics.json');
if (fs.existsSync(file)) {
try {
const content = fs.readFileSync(file, 'utf-8');
const allStats = JSON.parse(content);
const dayStats = allStats.filter((s: any) =>
s.timestamp.includes(dateStr)
);
stats.push(...dayStats);
} catch (error) {
continue;
}
}
}
return stats;
}
getStorageInfo(): { files: string[]; totalLiquidations: number } {
const files = fs.readdirSync(this.dataDir);
const liquidationFiles = files.filter(f => f.startsWith('liquidations-'));
let totalLiquidations = 0;
liquidationFiles.forEach(file => {
const filePath = path.join(this.dataDir, file);
try {
const content = fs.readFileSync(filePath, 'utf-8');
const data = JSON.parse(content);
totalLiquidations += data.length;
} catch (error) {
}
});
return { files, totalLiquidations };
}
}DataStorageManager: What Each Method Does
dataDir: Stores the path to our data directory (./liquidation-data/).
ensureDataDir(): Creates the directory if it doesn't exist.
saveLiquidation(): Saves liquidation events to daily and hourly files.
savePositionSnapshot(): Saves current wallet positions for historical analysis.
saveStatistics(): Saves monitor statistics with timestamps.
appendToFile(): A helper method for safely appending data to JSON files.
getRecentLiquidations(): Reads today's liquidation events.
getAggregatedStats(): Gets statistics for multiple days.
getStorageInfo(): Returns information about stored data.
This class remains identical to Part 1 because it already efficiently fetches real-time prices from Hyperliquid. Changing it would introduce unnecessary complexity.
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;
}
}Now we integrate the storage manager with our existing detector. Here are the key changes:
Updated Class Properties
We're adding storageManager as a class property, making it available throughout our entire detector class.
class GoldRushLiquidationDetector {
private client: GoldRushClient;
private priceOracle: PriceOracle;
private storageManager: DataStorageManager; // NEW: Storage manager
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;Updated Constructor
The constructor now gives our liquidation detector permanent memory, automatically creating a data folder to save every liquidation event to disk instead of losing them when the program stops.
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();
this.storageManager = new DataStorageManager(); // NEW: Initialize storage
}loadWalletPositions Method
This remains the same as what we’ve got in our part 1 guide.
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");
}fetchPositions Method
Methods like fetchPositions() remain the same as in Part 1.
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;
}
}Updated startMonitoring Method
This helps informs users that our data persistence is active, so they know their liquidation history is being preserved, not just temporarily displayed.
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(`Mode 3: Data Persistence (file-based storage)`);
console.log(`Monitoring: ${wallets.length} wallet(s)`);
console.log("=".repeat(80) + "\n");
// GoldRush Stream
try {
const unsubscribe = this.client.StreamingService.subscribeToWalletActivity(
{
chain_name: chain,
wallet_addresses: wallets
},
{
next: (data: any) => {
this.goldRushEventCount++;
if (Array.isArray(data)) {
for (const tx of data) {
if (tx.from_address || tx.to_address) {
this.handleTransaction(tx, 'GOLDRUSH_STREAM');
}
}
} else {
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();
}PeriodicPolling Method
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);
}handleWalletActivity Method
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;
}
this.processingQueue.add(wallet);
try {
const currentPrices = await this.priceOracle.getCurrentPrices();
await this.detectLiquidation(wallet, currentPrices, txHash, source);
} finally {
this.processingQueue.delete(wallet);
}
}handleTransaction Method
private handleTransaction(tx: any, source: 'GOLDRUSH_STREAM' | 'PERIODIC_POLL'): void {
const wallet = tx.from_address || tx.to_address;
if (!wallet || !this.monitoredWallets.has(wallet)) {
return;
}
if (tx.decoded_type === 'FUNDING' && tx.value < 10) {
return;
}
this.handleWalletActivity({
wallet_address: wallet,
tx_hash: tx.tx_hash
}, source);
}Updated detectLiquidation Method
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) || [];
// Save position snapshot to storage
this.storageManager.savePositionSnapshot(wallet, newPositions);
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) {
this.monitoredWallets.set(wallet, newPositions);
return;
}
console.log(`\n Position 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);
// Save liquidation to storage
this.storageManager.saveLiquidation(liquidation);
}
}
this.checkRemainingPositions(wallet, newPositions, currentPrices);
this.monitoredWallets.set(wallet, newPositions);
}Here, we added two calls to storageManager:
savePositionSnapshot(): Saves current positions even if nothing is closed.
saveLiquidation(): Saves each liquidation event.
analyzeClosure Method
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
};
}checkRemainingPositions Method
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!`);
}
}
}displayLiquidationAlert Method
private displayLiquidationAlert(detection: LiquidationDetection): void {
const emoji = detection.type === 'LIQUIDATION' ? '🚨' : '📉';
const border = "=".repeat(80);
console.log(`\n${border}`);
console.log(`${emoji} ${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));
}Updated getStats Method
Added storage_files and stored_liquidations to track the amount of data we've saved.
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
};
// Get storage info
const storageInfo = this.storageManager.getStorageInfo();
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,
storage_files: storageInfo.files.length,
stored_liquidations: storageInfo.totalLiquidations,
recent: this.detectedLiquidations.slice(-5).reverse()
};
}Updated displayStats Method
saveStatistics(stats) to persist statistics, and also added a new "Storage Info" section to display saved data metrics.
displayStats(): void {
const stats = this.getStats();
// Save statistics to storage
this.storageManager.saveStatistics(stats);
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(`\nStorage Info:`);
console.log(` Data Files: ${stats.storage_files}`);
console.log(` Stored Liquidations: ${stats.stored_liquidations}`);
console.log("=".repeat(80) + "\n");
}New getStorageManager Method
This method allows external code (our part 3 API server) to access the storage manager.
// NEW: Get storage manager for API server
getStorageManager(): DataStorageManager {
return this.storageManager;
}
async disconnect(): Promise<void> {
if (this.pollingInterval) {
clearInterval(this.pollingInterval);
}
this.subscriptions.forEach(unsub => unsub());
await this.client.StreamingService.disconnect();
}
}// 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");
console.log(" Mode 3: File-based data persistence");
console.log("=".repeat(80) + "\n");
const detector = new GoldRushLiquidationDetector(API_KEY!);
const WALLETS_TO_MONITOR = [
“Input your Wallet addresses here..”
];
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");
console.log("Data is being saved to: ./liquidation-data/");
console.log("Start the API server in another terminal: npm run api\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, DataStorageManager };Now let's test our liquidation monitoring system. First, add some actual wallet addresses to monitor. You can find active Hyperliquid traders on their explorer
Once you’ve gotten the address you want to use, open your terminal and run:
npx tsx hyperliquid-liquidation-monitorExpected Console output
You should see output similar to this:

Generated File Structure
The system creates a data directory and saves every liquidation event as structured JSON. You'll get clean, formatted data with all the details needed, as shown below:

Congratulations! You've successfully enhanced your real-time liquidation monitor with permanent data persistence, transforming it from a temporary monitoring system into a historical data collection system. In Part 3, we'll build an API server that allows you to query and access this stored data programmatically, enabling you to build dashboards, run custom analyses, and integrate your liquidation insights. Happy Coding!