Building  a Real-Time Hyperliquid Liquidation Dashboard using GoldRush - Part 4

Content Writer
Learn to visualize Hyperliquid liquidation data with React. Step-by-step tutorial building a crypto liquidation dashboard with real-time charts.

Complete the part 4 series by building a real-time Hyperliquid liquidation dashboard with React.

Welcome to the final part of our 4-part series! In Part 3, we built a powerful Express.js API that transforms raw JSON files into queryable endpoints. Now, we'll complete the process by creating a dashboard that visualizes this crypto liquidation data in real time.

What You'll Build

In this tutorial, you'll create a React dashboard with:

  • Live statistics overview showing total liquidations, losses, and system metrics

  • Interactive analytics charts for top losses and liquidation distribution

  • Real-time risk monitoring with positions nearing liquidation

Prerequisites 

Estimated time: 20-25 minutes

Before starting, verify you have:

  • Part 3 API running - Open http://localhost:3001/api/stats in your Postman. You should see JSON data. If not, go back and complete Part 3 first.

  •  Basic React knowledge - Understand components, props, and the useState hook.

  •  Two terminal windows - One for your API server, one for React development.

Step 1: Create Your React Application

Installation

Open a new terminal window (keep your API server running in the other one) and run:

npx create-react-app liquidation-dapp cd hyperliquid-dapp

Install Charting Libraries

Now add the visualization libraries:

npm install recharts lucide-react

Run the development server to see if everything is working using:

npm start

Your browser should open to http://localhost:3000, showing the default React page. Keep this running while we build the dashboard.

Step 2: Understand the Architecture

Before getting into code, here's our system flow:

Step 3: Build the Core Component

Now let's start building the actual dApp. We'll break this into digestible sections.

Understanding the Component Structure

Our App.js component has three main responsibilities:

  • State Management: Track all data from the API.

  • Data Fetching: Pull updates from 7 different endpoints.

  • Rendering: Displaying data in an organized, visual way.

Imports & Create Storage

Open src/App.js and delete everything. Start fresh with these imports:

import React, { useState, useEffect } from 'react'; import { BarChart, Bar, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; import { AlertCircle, TrendingDown, DollarSign, Activity, Database, Clock } from 'lucide-react'; import './App.css'; const API_URL = 'http://localhost:3001/api'; function App() { const [stats, setStats] = useState(null); const [recentLiquidations, setRecentLiquidations] = useState([]); const [latestPositions, setLatestPositions] = useState([]); const [atRiskPositions, setAtRiskPositions] = useState([]); const [topLosers, setTopLosers] = useState([]); const [byCoin, setByCoin] = useState([]); const [storageInfo, setStorageInfo] = useState(null); const [loading, setLoading] = useState(true); const [lastUpdate, setLastUpdate] = useState(new Date());

This imports React hooks for state management, charting components from Recharts, icons from Lucide, and points to our API from Part 3 to store state types.

Data Fetching Engine

const fetchData = async () => {     try {       const [statsRes, liqRes, posRes, riskRes, losersRes, coinRes, storageRes] = await Promise.all([         fetch(`${API_URL}/stats`),         fetch(`${API_URL}/liquidations/recent?limit=20`),         fetch(`${API_URL}/positions/latest`),         fetch(`${API_URL}/positions/at-risk?threshold=10`),         fetch(`${API_URL}/analytics/top-losers?limit=10&days=7`),         fetch(`${API_URL}/analytics/by-coin?days=7`),         fetch(`${API_URL}/storage/info`)       ]);       setStats(await statsRes.json());       setRecentLiquidations(await liqRes.json());       setLatestPositions(await posRes.json());       setAtRiskPositions(await riskRes.json());       setTopLosers(await losersRes.json());       setByCoin(await coinRes.json());       setStorageInfo(await storageRes.json());       setLastUpdate(new Date());       setLoading(false);     } catch (error) {       console.error('Error fetching data:', error);       setLoading(false);     }   };   useEffect(() => {     fetchData();     const interval = setInterval(fetchData, 30000);     return () => clearInterval(interval);   }, []);

This fetches ALL your liquidation data from 7 API endpoints simultaneously, then automatically refreshes every 30 seconds.

Data Formatting

const formatAddress = (addr) => `${addr?.slice(0, 6)}...${addr?.slice(-4)}`;   const formatCurrency = (val) => `$${val?.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;   const formatNumber = (val) => val?.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });   const COLORS = ['#ef4444', '#f97316', '#eab308', '#22c55e', '#3b82f6', '#8b5cf6', '#ec4899'];   if (loading) {     return (       <div className="loading-container">         <div className="loading-content">           <Activity className="loading-spinner" />           <p className="loading-text">Loading dashboard...</p>         </div>       </div>     );   }

Prepares your data for display formats, wallet addresses, and money values. Shows a loading screen while data is being fetched.

Header & Stats Cards

return (     <div className="dashboard">       {/* Header */}       <div className="header">         <div className="header-top">           <h1 className="title">Hyperliquid Liquidation Monitor</h1>           <div className="last-update">             <Clock style={{ width: 16, height: 16 }} />             <span>Last update: {lastUpdate.toLocaleTimeString()}</span>           </div>         </div>         <p className="subtitle">Real-time monitoring and analytics for Hyperliquid perpetual positions</p>       </div>       {/* Stats Cards */}       <div className="stats-grid">         <div className="stat-card">           <div className="stat-header">             <h3>Total Events</h3>             <Activity style={{ width: 20, height: 20, color: '#3b82f6' }} />           </div>           <p className="stat-value">{stats?.total_events || 0}</p>         </div>         <div className="stat-card">           <div className="stat-header">             <h3>Liquidations</h3>             <AlertCircle style={{ width: 20, height: 20, color: '#ef4444' }} />           </div>           <p className="stat-value" style={{ color: '#ef4444' }}>{stats?.liquidations || 0}</p>           <p className="stat-detail">{stats?.high_confidence || 0} high confidence</p>         </div>         <div className="stat-card">           <div className="stat-header">             <h3>Total Loss</h3>             <TrendingDown style={{ width: 20, height: 20, color: '#f97316' }} />           </div>           <p className="stat-value" style={{ color: '#f97316' }}>             {formatCurrency(stats?.total_loss || 0)}           </p>         </div>         <div className="stat-card">           <div className="stat-header">             <h3>Stored Snapshots</h3>             <Database style={{ width: 20, height: 20, color: '#22c55e' }} />           </div>           <p className="stat-value">{storageInfo?.totalSnapshots || 0}</p>           <p className="stat-detail">{storageInfo?.files || 0} files</p>         </div>       </div>

The header is sticky (we'll handle this in CSS), so users always see the update time, even when scrolling through data. It creates the top part of your dashboard, the title with update time, plus 4 cards showing key stats (total events, liquidations, losses, and storage info).

Charts & Data Tables
 

<div className="charts-grid">         {/* Top Losers Chart */}         <div className="chart-card">           <h2 className="chart-title">             <DollarSign style={{ width: 20, height: 20, color: '#ef4444' }} />             Top 10 Losses (7 Days)           </h2>           <ResponsiveContainer width="100%" height={300}>             <BarChart data={topLosers.slice(0, 10)}>               <CartesianGrid strokeDasharray="3 3" stroke="#374151" />               <XAxis dataKey="coin" stroke="#9ca3af" />               <YAxis stroke="#9ca3af" />               <Tooltip                 contentStyle={{ backgroundColor: '#1f2937', border: '1px solid #374151' }}                 formatter={(value) => formatCurrency(value)}               />               <Bar dataKey="lossAmount" fill="#ef4444" />             </BarChart>           </ResponsiveContainer>         </div>         {/* Liquidations by Coin */}         <div className="chart-card">           <h2 className="chart-title">Liquidations by Asset</h2>           <ResponsiveContainer width="100%" height={300}>             <PieChart>               <Pie                 data={byCoin.slice(0, 7)}                 dataKey="count"                 nameKey="coin"                 cx="50%"                 cy="50%"                 outerRadius={100}                 label={(entry) => `${entry.coin}: ${entry.count}`}               >                 {byCoin.slice(0, 7).map((entry, index) => (                   <Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />                 ))}               </Pie>               <Tooltip contentStyle={{ backgroundColor: '#1f2937', border: '1px solid #374151' }} />             </PieChart>           </ResponsiveContainer>         </div>       </div>       {/* At Risk Positions */}       {atRiskPositions.length > 0 && (         <div className="risk-card">           <h2 className="risk-title">             <AlertCircle style={{ width: 20, height: 20 }} />             Positions at Risk ({atRiskPositions.length})           </h2>           <div className="table-container">             <table className="data-table">               <thead>                 <tr>                   <th>Wallet</th>                   <th>Coin</th>                   <th>Side</th>                   <th>Size</th>                   <th>Entry Price</th>                   <th>Liq Price</th>                   <th>Unrealized PnL</th>                 </tr>               </thead>               <tbody>                 {atRiskPositions.slice(0, 10).map((pos, idx) => (                   <tr key={idx}>                     <td className="mono-font">{formatAddress(pos.wallet)}</td>                     <td className="bold-text">{pos.coin}</td>                     <td>                       <span className={pos.size > 0 ? 'badge-long' : 'badge-short'}>                         {pos.size > 0 ? 'LONG' : 'SHORT'}                       </span>                     </td>                     <td>{formatNumber(Math.abs(pos.size))}</td>                     <td>{formatCurrency(pos.entryPrice)}</td>                     <td>{pos.liquidationPrice ? formatCurrency(pos.liquidationPrice) : 'N/A'}</td>                     <td className={pos.unrealizedPnl < 0 ? 'text-red' : 'text-green'}>                       {formatCurrency(pos.unrealizedPnl)}                     </td>                   </tr>                 ))}               </tbody>             </table>           </div>         </div>       )}       {/* Recent Liquidations */}       <div className="liquidations-card">         <h2 className="section-title">Recent Liquidations</h2>         <div className="liquidations-list">           {recentLiquidations.length === 0 ? (             <p className="empty-message">No liquidations detected yet</p>           ) : (             recentLiquidations.map((liq, idx) => (               <div key={idx} className="liquidation-item">                 <div className="liquidation-badges">                   <span className={`badge-${liq.type.toLowerCase()}`}>{liq.type}</span>                   <span className={`badge-confidence-${liq.confidence.toLowerCase()}`}>{liq.confidence}</span>                   <span className="badge-source">{liq.source}</span>                 </div>                 <div className="liquidation-details">                   <div className="detail-item">                     <p className="detail-label">Wallet</p>                     <p className="detail-value mono-font">{liq.wallet}</p>                   </div>                   <div className="detail-item">                     <p className="detail-label">Position</p>                     <p className="detail-value bold-text">{liq.coin} {liq.side}</p>                   </div>                   <div className="detail-item">                     <p className="detail-label">Size</p>                     <p className="detail-value">{formatNumber(liq.size)}</p>                   </div>                   <div className="detail-item">                     <p className="detail-label">Loss</p>                     <p className="detail-value text-red bold-text">{formatCurrency(liq.lossAmount)}</p>                   </div>                 </div>                 <p className="liquidation-reason">{liq.reason}</p>                 <p className="liquidation-time">{new Date(liq.timestamp).toLocaleString()}</p>               </div>             ))           )}         </div>       </div>       {/* Current Positions */}       <div className="positions-card">         <h2 className="section-title">Current Positions</h2>         <div className="positions-list">           {latestPositions.map((snapshot, idx) => (             <div key={idx} className="position-snapshot">               <div className="snapshot-header">                 <h3 className="mono-font bold-text">{formatAddress(snapshot.wallet)}</h3>                 <span className="snapshot-time">{new Date(snapshot.timestamp).toLocaleString()}</span>               </div>               <div className="table-container">                 <table className="data-table">                   <thead>                     <tr>                       <th>Coin</th>                       <th>Side</th>                       <th>Size</th>                       <th>Entry</th>                       <th>Value</th>                       <th>PnL</th>                     </tr>                   </thead>                   <tbody>                     {snapshot.positions.slice(0, 5).map((pos, pidx) => (                       <tr key={pidx}>                         <td className="bold-text">{pos.coin}</td>                         <td>                           <span className={pos.size > 0 ? 'badge-long' : 'badge-short'}>                             {pos.size > 0 ? 'LONG' : 'SHORT'}                           </span>                         </td>                         <td>{formatNumber(Math.abs(pos.size))}</td>                         <td>{formatCurrency(pos.entryPrice)}</td>                         <td>{formatCurrency(pos.positionValue)}</td>                         <td className={pos.unrealizedPnl < 0 ? 'text-red bold-text' : 'text-green bold-text'}>                           {formatCurrency(pos.unrealizedPnl)}                         </td>                       </tr>                     ))}                   </tbody>                 </table>                 {snapshot.positions.length > 5 && (                   <p className="more-positions">+ {snapshot.positions.length - 5} more positions</p>                 )}               </div>             </div>           ))}         </div>       </div>     </div>   ); } export default App;

This creates charts (a bar chart for top losses and a pie chart for coin distribution) and a table showing positions at risk of liquidation. This is where users see the actual trading data.

Step 4: Styling

Create or open src/App.css in your project root. Delete any existing content and paste the CSS below. This stylesheet provides dark theme colors, responsive grid layouts that adapt to mobile, smooth animations for loading states, and color-coded badges for long/short positions.

Global Setup & Basics

body {   margin: 0;   font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', sans-serif;   background: #111827;   color: white; } /* Loading State */ .loading-container {   min-height: 100vh;   background: #111827;   color: white;   display: flex;   align-items: center;   justify-content: center; } .loading-content {   text-align: center; } .loading-spinner {   width: 64px;   height: 64px;   margin: 0 auto 16px;   color: #3b82f6;   animation: spin 1s linear infinite; } .loading-text {   font-size: 20px; } @keyframes spin {   from { transform: rotate(0deg); }   to { transform: rotate(360deg); } } /* Dashboard Container */ .dashboard {   min-height: 100vh;   background: #111827;   color: white;   padding: 24px; } /* Header */ .header {   margin-bottom: 32px; } .header-top {   display: flex;   align-items: center;   justify-content: space-between;   margin-bottom: 8px;   flex-wrap: wrap;   gap: 16px; } .title {   font-size: 36px;   font-weight: bold;   background: linear-gradient(to right, #3b82f6, #8b5cf6);   -webkit-background-clip: text;   -webkit-text-fill-color: transparent;   background-clip: text;   margin: 0; } .last-update {   display: flex;   align-items: center;   gap: 8px;   color: #9ca3af;   font-size: 14px; } .subtitle {   color: #9ca3af;   margin: 0; }

This set up the dashboard's foundation with a dark theme, loading screen, container layout, and the header with a cool gradient title.

Cards & Layouts

/* Stats Grid */ .stats-grid {   display: grid;   grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));   gap: 24px;   margin-bottom: 32px; } .stat-card {   background: #1f2937;   border-radius: 8px;   padding: 24px;   border: 1px solid #374151; } .stat-header {   display: flex;   align-items: center;   justify-content: space-between;   margin-bottom: 8px; } .stat-header h3 {   color: #9ca3af;   font-size: 14px;   font-weight: 500;   margin: 0; } .stat-value {   font-size: 30px;   font-weight: bold;   margin: 8px 0; } .stat-detail {   font-size: 14px;   color: #9ca3af;   margin: 4px 0 0 0; } /* Charts Grid */ .charts-grid {   display: grid;   grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));   gap: 24px;   margin-bottom: 32px; } .chart-card {   background: #1f2937;   border-radius: 8px;   padding: 24px;   border: 1px solid #374151; } .chart-title {   font-size: 20px;   font-weight: bold;   margin: 0 0 16px 0;   display: flex;   align-items: center;   gap: 8px; } /* Risk Card */ .risk-card {   background: #1f2937;   border-radius: 8px;   padding: 24px;   border: 2px solid #dc2626;   margin-bottom: 32px; } .risk-title {   font-size: 20px;   font-weight: bold;   margin: 0 0 16px 0;   color: #ef4444;   display: flex;   align-items: center;   gap: 8px; } /* Tables */ .table-container {   overflow-x: auto; } .data-table {   width: 100%;   font-size: 14px;   border-collapse: collapse; } .data-table th {   text-align: left;   padding-bottom: 8px;   border-bottom: 1px solid #374151;   font-weight: 500;   color: #9ca3af; } .data-table td {   padding: 8px 8px 8px 0;   border-bottom: 1px solid #374151; } .data-table tbody tr:last-child td {   border-bottom: none; } /* Badges */ .badge-long {   padding: 4px 8px;   border-radius: 4px;   font-size: 12px;   background: #065f46;   color: #6ee7b7;   display: inline-block; } .badge-short {   padding: 4px 8px;   border-radius: 4px;   font-size: 12px;   background: #7f1d1d;   color: #fca5a5;   display: inline-block; } .badge-liquidation {   padding: 4px 8px;   border-radius: 4px;   font-size: 12px;   font-weight: bold;   background: #7f1d1d;   color: #fca5a5;   display: inline-block; } .badge-margin_call {   padding: 4px 8px;   border-radius: 4px;   font-size: 12px;   font-weight: bold;   background: #78350f;   color: #fdba74;   display: inline-block; } .badge-position_closed {   padding: 4px 8px;   border-radius: 4px;   font-size: 12px;   font-weight: bold;   background: #1e3a8a;   color: #93c5fd;   display: inline-block; } .badge-confidence-high {   padding: 4px 8px;   border-radius: 4px;   font-size: 12px;   background: #7f1d1d;   color: #fca5a5;   display: inline-block; } .badge-confidence-medium {   padding: 4px 8px;   border-radius: 4px;   font-size: 12px;   background: #713f12;   color: #fde047;   display: inline-block; } .badge-confidence-low {   padding: 4px 8px;   border-radius: 4px;   font-size: 12px;   background: #374151;   color: #d1d5db;   display: inline-block; } .badge-source {   padding: 4px 8px;   border-radius: 4px;   font-size: 12px;   color: #9ca3af;   display: inline-block; }

Creates all the responsive layouts with 4 stat cards, 2 chart containers, a risk table, and data tables with proper spacing and borders.

Components & Utilities 

.liquidations-card {   background: #1f2937;   border-radius: 8px;   padding: 24px;   border: 1px solid #374151;   margin-bottom: 32px; } .section-title {   font-size: 20px;   font-weight: bold;   margin: 0 0 16px 0; } .liquidations-list {   display: flex;   flex-direction: column;   gap: 16px; } .liquidation-item {   background: #111827;   border-radius: 8px;   padding: 16px;   border: 1px solid #374151; } .liquidation-badges {   display: flex;   align-items: center;   gap: 8px;   margin-bottom: 8px;   flex-wrap: wrap; } .liquidation-details {   display: grid;   grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));   gap: 16px;   font-size: 14px;   margin-bottom: 8px; } .detail-item {   min-width: 0; } .detail-label {   color: #9ca3af;   margin: 0 0 4px 0;   font-size: 13px; } .detail-value {   margin: 0;   word-break: break-word; } .liquidation-reason {   font-size: 12px;   color: #9ca3af;   margin: 8px 0 4px 0; } .liquidation-time {   font-size: 12px;   color: #6b7280;   margin: 4px 0 0 0; } .empty-message {   color: #9ca3af;   text-align: center;   padding: 32px 0;   margin: 0; } /* Positions Section */ .positions-card {   background: #1f2937;   border-radius: 8px;   padding: 24px;   border: 1px solid #374151; } .positions-list {   display: flex;   flex-direction: column;   gap: 24px; } .position-snapshot {   background: #111827;   border-radius: 8px;   padding: 16px;   border: 1px solid #374151; } .snapshot-header {   display: flex;   align-items: center;   justify-content: space-between;   margin-bottom: 16px;   flex-wrap: wrap;   gap: 8px; } .snapshot-header h3 {   font-size: 18px;   margin: 0; } .snapshot-time {   font-size: 12px;   color: #9ca3af; } .more-positions {   font-size: 12px;   color: #9ca3af;   margin: 8px 0 0 0; } /* Utility Classes */ .mono-font {   font-family: 'Courier New', monospace;   font-size: 12px; } .bold-text {   font-weight: bold; } .text-red {   color: #ef4444; } .text-green {   color: #22c55e; } /* Responsive */ @media (max-width: 768px) {   .dashboard {     padding: 16px;   }   .title {     font-size: 28px;   }   .stats-grid {     grid-template-columns: 1fr;   }   .charts-grid {     grid-template-columns: 1fr;   }   .liquidation-details {     grid-template-columns: 1fr 1fr;   } }

Adds all the visual components with colored badges (LONG/SHORT, liquidation types), detail cards, and utility classes for text.

Step 5: Run and Test Your Dashboard

Open your terminal and run npm start; this will start our dapp on this local address: http://localhost:3000.

Now that your dapp is working, we need to see our data. To do this, start your API server from Part 3. You should see output similar to this:

Once the API server is running, your dashboard will automatically connect and display the live liquidation data. And that's it; you can now see liquidations occur in real time.

Conclusion

You've now built a complete real-time liquidation dashboard that brings all your data to life. What started as JSON files is now a live interface that displays liquidations as they occur, positions at risk, and market trends. This concludes our four-part series, which spans collecting blockchain data, building an API, and creating this crypto liquidation dashboard. 

From here, you can take this wherever you want. Add alerts for big liquidations, and throw in some filters to focus on specific coins or wallets. The foundation is solid, so whatever you build on top should fit in easily. Happy Coding!

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.