Anomaly Detection Dashboard on Solana by Employing the Goldrush Streaming API - (Part 2)

Rajdeep Singha
Content Writer
Anamoly Detection Dashboard on Solana (Part 2): Setting up the streaming connection and building the UI

Welcome back! In Part 1, we explored why real-time observability matters in Web3, dove into Solana's architecture, and discussed the theory behind anomaly detection. Now it's time to get our hands dirty and actually build something.

In this guide, we're going to create a production-ready anomaly detection system for Solana using the GoldRush Streaming API. By the end, you'll have a working application that monitors wallet activity, tracks token prices, and alerts you to suspicious behavior in real-time.

What we're building:

  • Real-time wallet transaction monitoring

  • OHLCV (price) data streaming for Solana tokens

  • Statistical anomaly detection algorithms

  • A beautiful React dashboard to visualize everything

Prerequisites:

  • Basic JavaScript/Node.js knowledge

  • A GoldRush API key (get one here)

Let's dive in! 🚀

Step 1: Setting Up Your Development Environment

First things first—let's get your project structure ready. We'll use Vite + React for the frontend because it's fast, modern, and plays nicely with real-time data.

# Create a new Vite + React project npm create vite@latest solana-anomaly-detector -- --template react cd solana-anomaly-detector # Install dependencies npm install npm install @covalenthq/client-sdk dotenv npm install -D tailwindcss postcss autoprefixer npx tailwindcss init -p

Why these choices?

  • Vite: Lightning-fast dev server and build tool

  • @covalenthq/client-sdk: Official GoldRush SDK for streaming blockchain data

  • Tailwind CSS: Utility-first styling that makes building UIs quick

Create a .env file in your project root:

VITE_GOLDRUSH_API_KEY=your_api_key_here

Before writing code, let's understand how everything fits together. Remember from Part 1 how we talked about the importance of granular, high-frequency data?
Here's how we achieve that:

Key insight from Part 1: Traditional polling (checking every few seconds) misses critical events. Streaming pushes data to you instantly—a flash loan attack happening in milliseconds gets caught, not missed.

Architecture

src/

├── components/

│   ├── AnomalyAlert.jsx       # Anomaly display component

│   ├── TokenBalances.jsx      # Token balance display

│   ├── TransactionTable.jsx   # Transaction history

│   ├── OHLCVMonitor.jsx       # OHLCV monitoring dashboard

│   └── WalletAnomalyDetector.jsx

├── hooks/

│   └── useGoldrushStreaming.js # Streaming hooks

├── services/

│   ├── anomalyDetection.js    # Anomaly detection logic

│   ├── dataProcessing.js      # Data transformation

│   └── goldrushClient.js      # GoldRush API client

├── App.jsx                     # Main application

└── main.jsx                    # Entry point

Key Technologies

  • React 19: Latest React with modern hooks

  • Vite: Fast build tool and dev server

  • Tailwind CSS: Utility-first styling

  • GoldRush SDK: Real-time blockchain streaming

  • Lucide React: Modern icon library

Step 2 : Building the Streaming Hook

Let's create our first custom hook to handle GoldRush streaming. This is the heart of our system.

Create src/hooks/useGoldrushStreaming.js:

import { useEffect, useState, useRef } from 'react'; import { initializeGoldrushClient, getGoldrushClient, disconnectGoldrushClient, } from '../services/goldrushClient'; import { StreamingChain, StreamingInterval, StreamingTimeframe } from '@covalenthq/client-sdk'; import { processOHLCVCandle, calculateOHLCVStatistics } from '../services/dataProcessing'; import { detectOHLCVAnomalies } from '../services/anomalyDetection'; export const useGoldrushStreaming = (walletAddress) => { const [isConnected, setIsConnected] = useState(false); const [transactions, setTransactions] = useState([]); const [balances, setBalances] = useState({}); const [error, setError] = useState(null); const unsubscribeRef = useRef({}); useEffect(() => { const setupStreaming = async () => { try { initializeGoldrushClient(); const client = getGoldrushClient(); setIsConnected(true); // Subscribe to wallet activity unsubscribeRef.current.wallet = client.StreamingService.subscribeToWalletActivity( { chain_name: StreamingChain.SOLANA_MAINNET, wallet_addresses: [walletAddress], }, { next: (data) => { setTransactions(prev => [ { id: Math.random(), hash: data.tx_hash, type: data.tx_type, timestamp: new Date(data.timestamp), ...data }, ...prev.slice(0, 99) ]); }, error: (err) => { console.error('Streaming error:', err); setError(err.message); } } ); } catch (err) { console.error('Setup error:', err); setError(err.message); setIsConnected(false); } }; setupStreaming(); return () => { Object.values(unsubscribeRef.current).forEach(unsub => { if (typeof unsub === 'function') unsub(); }); disconnectGoldrushClient(); }; }, [walletAddress]); return { isConnected, transactions, balances, error }; }; /** * Hook for OHLCV token streaming */ export const useOHLCVStreaming = (tokenAddress, options = {}) => { const { interval = StreamingInterval.ONE_MINUTE, timeframe = StreamingTimeframe.ONE_HOUR, maxHistory = 50 } = options; const [isConnected, setIsConnected] = useState(false); const [candles, setCandles] = useState([]); const [anomalies, setAnomalies] = useState([]); const [stats, setStats] = useState(null); const [error, setError] = useState(null); const [streamInfo, setStreamInfo] = useState({ startTime: null, dataCount: 0, lastUpdate: null }); const unsubscribeRef = useRef(null); const candleHistoryRef = useRef([]); useEffect(() => { if (!tokenAddress) return; const setupOHLCVStream = async () => { try { initializeGoldrushClient(); const client = getGoldrushClient(); setIsConnected(true); setStreamInfo(prev => ({ ...prev, startTime: new Date() })); console.log('📡 Subscribing to OHLCV stream for token:', tokenAddress); // Subscribe to OHLCV token data unsubscribeRef.current = client.StreamingService.subscribeToOHLCVTokens( { chain_name: StreamingChain.SOLANA_MAINNET, token_addresses: [tokenAddress], interval: interval, timeframe: timeframe, }, { next: (data) => { console.log('✓ OHLCV Data received:', data); console.log('📊 Data type:', typeof data, 'Is Array:', Array.isArray(data), 'Length:', data?.length); // Process the candle data const processedCandles = processOHLCVCandle(data); if (!processedCandles || processedCandles.length === 0) { console.warn('⚠️ No candles in this update - waiting for trading activity...'); console.log('💡 This is normal if the token has no recent trades. The stream is active and monitoring.'); // Update stream info even if no candles setStreamInfo(prev => ({ ...prev, dataCount: prev.dataCount + 1, lastUpdate: new Date() })); return; } // Process each candle processedCandles.forEach((candle) => { // Add to history candleHistoryRef.current = [...candleHistoryRef.current, candle].slice(-maxHistory); // Update state setCandles(candleHistoryRef.current); // Detect anomalies const history = candleHistoryRef.current.slice(0, -1); // All except current const detectedAnomalies = detectOHLCVAnomalies(candle, history); if (detectedAnomalies.length > 0) { console.log('⚠️ Anomalies detected:', detectedAnomalies); setAnomalies(prev => [...detectedAnomalies, ...prev].slice(0, 50)); } // Update stream info setStreamInfo(prev => ({ ...prev, dataCount: prev.dataCount + 1, lastUpdate: new Date() })); }); // Calculate statistics const newStats = calculateOHLCVStatistics(candleHistoryRef.current); setStats(newStats); }, error: (err) => { console.error('✗ OHLCV subscription error:', err); setError(err.message || 'Streaming error occurred'); }, complete: () => { console.log('✓ OHLCV stream completed'); setIsConnected(false); } } ); console.log('✓ OHLCV stream setup complete'); } catch (err) { console.error('✗ Failed to setup OHLCV stream:', err); setError(err.message); setIsConnected(false); } }; setupOHLCVStream(); return () => { if (unsubscribeRef.current && typeof unsubscribeRef.current === 'function') { console.log('🛑 Unsubscribing from OHLCV stream'); unsubscribeRef.current(); } }; }, [tokenAddress, interval, timeframe, maxHistory]); return { isConnected, candles, anomalies, stats, error, streamInfo }; };


Step 3: Implementing Anomaly Detection

Now for the fun part—detecting anomalies. In Part 1, we covered three approaches: threshold-based, statistical analysis, and ML. Let's implement the first two.

Create a file under this path src/services/anomalyDetection.js

export const ANOMALY_RULES = { LARGE_TRANSACTION: { threshold: 1000, type: 'THRESHOLD_BASED', severity: 'warning' }, MULTIPLE_FAILED_TXS: { threshold: 3, timeWindow: 60000, // 1 minute type: 'STATISTICAL', severity: 'error' }, UNUSUAL_TOKEN_ACTIVITY: { type: 'BEHAVIORAL', severity: 'info' }, PRICE_CRASH: { threshold: -15, // percent type: 'MARKET_BASED', severity: 'warning' } }; /** * Calculate statistics from price history (for OHLCV anomaly detection) */ export const calculateOHLCVStats = (history) => { if (history.length < 2) return null; const closes = history.map(c => parseFloat(c.close) || 0).filter(p => p > 0); const volumes = history.map(v => parseFloat(v.volume) || 0).filter(v => v > 0); if (closes.length < 2) return null; const avgClose = closes.reduce((a, b) => a + b) / closes.length; const avgVolume = volumes.length > 0 ? volumes.reduce((a, b) => a + b) / volumes.length : 0; const stdDev = Math.sqrt( closes.reduce((sq, n) => sq + Math.pow(n - avgClose, 2), 0) / closes.length ); return { avgClose, avgVolume, stdDev }; }; /** * Detect anomalies in OHLCV candle data */ export const detectOHLCVAnomalies = (candle, history) => { const anomalies = []; if (history.length < 3) return anomalies; const stats = calculateOHLCVStats(history); if (!stats) return anomalies; const currentPrice = parseFloat(candle.close) || 0; const currentVolume = parseFloat(candle.volume) || 0; const currentHigh = parseFloat(candle.high) || 0; const currentLow = parseFloat(candle.low) || 0; if (currentPrice <= 0) return anomalies; // Price spike detection (using standard deviation) const priceChange = ((currentPrice - stats.avgClose) / stats.avgClose) * 100; if (Math.abs(priceChange) > stats.stdDev * 2) { anomalies.push({ type: 'PRICE_SPIKE', severity: Math.abs(priceChange) > stats.stdDev * 3 ? 'CRITICAL' : 'HIGH', value: priceChange.toFixed(2) + '%', details: `Price: ${currentPrice.toFixed(6)} vs Avg: ${stats.avgClose.toFixed(6)}`, title: priceChange > 0 ? 'Price Surge Detected' : 'Price Drop Detected', message: `${Math.abs(priceChange).toFixed(2)}% ${priceChange > 0 ? 'increase' : 'decrease'} from average`, timestamp: new Date() }); } // Volume spike detection if (stats.avgVolume > 0) { const volumeChange = currentVolume / stats.avgVolume; if (volumeChange > 2) { anomalies.push({ type: 'VOLUME_SPIKE', severity: volumeChange > 5 ? 'CRITICAL' : 'HIGH', value: volumeChange.toFixed(2) + 'x', details: `Volume: ${currentVolume.toExponential(4)} vs Avg: ${stats.avgVolume.toExponential(4)}`, title: 'Volume Spike Detected', message: `${volumeChange.toFixed(2)}x higher than average volume`, timestamp: new Date() }); } } // High-Low spread anomaly (volatility) const spread = currentHigh - currentLow; if (history.length > 0) { const lastHigh = parseFloat(history[history.length - 1].high) || 0; const lastLow = parseFloat(history[history.length - 1].low) || 0; const avgSpread = (lastHigh - lastLow) || 0.00001; if (spread > avgSpread * 3) { anomalies.push({ type: 'HIGH_VOLATILITY', severity: 'MEDIUM', value: (spread / avgSpread).toFixed(2) + 'x', details: `Spread: ${spread.toFixed(6)} vs Avg: ${avgSpread.toFixed(6)}`, title: 'High Volatility Detected', message: `Price spread is ${(spread / avgSpread).toFixed(2)}x higher than normal`, timestamp: new Date() }); } } return anomalies; }; /** * Detect anomalies in transaction data */ export const detectAnomalies = (txData, previousData) => { const anomalies = []; // Threshold-based: Large transaction if (Math.abs(txData.value_change) > ANOMALY_RULES.LARGE_TRANSACTION.threshold) { anomalies.push({ type: 'LARGE_TRANSACTION', severity: 'warning', title: 'Large Transaction Detected', message: `Unusual amount: ${Math.abs(txData.value_change).toFixed(2)} ${txData.token_symbol}`, timestamp: new Date() }); } // Behavioral: Failed transactions if (txData.status === 'failed') { anomalies.push({ type: 'FAILED_TRANSACTION', severity: 'error', title: 'Transaction Failed', message: `TX ${txData.tx_hash} failed - possible security issue`, timestamp: new Date() }); } // Statistical: Activity burst if (previousData.recentTxCount > ANOMALY_RULES.UNUSUAL_TOKEN_ACTIVITY.threshold) { anomalies.push({ type: 'ACTIVITY_BURST', severity: 'info', title: 'Unusual Activity', message: `${previousData.recentTxCount} transactions in last minute`, timestamp: new Date() }); } return anomalies; };


Breaking it down:
Threshold-based (Simple but effective): If value > $1000, alert. If > $10,000, escalate to CRITICAL.
Statistical analysis (Smarter): Count transactions in last 60 seconds. More than 5? That's unusual—flag it.
Pattern recognition: Failed transactions might indicate someone trying to exploit a vulnerability.
Real-world example: In 2023, a Solana DeFi protocol was drained of $8M. The attack involved rapid-fire transactions that would've been caught by activity burst detection. This is why these algorithms matter.

Step 4 : Hook for managing GoldRush Steaming API

A simple, reusable hook to connect, manage, and disconnect the GoldRush streaming client — keeping your app’s blockchain data flow stable and efficient.

Create a file with this path src/hooks/goldrushClient.js:

import { GoldRushClient, StreamingChain, StreamingInterval, StreamingTimeframe, } from "@covalenthq/client-sdk"; let clientInstance = null; export const initializeGoldrushClient = (callbacks = {}) => { if (clientInstance) return clientInstance; const apiKey = import.meta.env.VITE_GOLDRUSH_API_KEY || import.meta.env.REACT_APP_GOLDRUSH_API_KEY; if (!apiKey || apiKey === 'your_goldrush_api_key_here') { throw new Error('Goldrush API key is missing. Please set VITE_GOLDRUSH_API_KEY in your .env file. Get your API key from: https://goldrush.dev/platform/apikey'); } const defaultCallbacks = { onConnecting: () => console.log("🔗 Connecting to GoldRush streaming service..."), onOpened: () => { console.log("✓ Connected to GoldRush Streaming API!"); console.log("📡 Monitoring for data..."); }, onClosed: () => { console.log("✓ Disconnected from GoldRush streaming service"); }, onError: (error) => { console.error("✗ GoldRush streaming error:", error); }, }; clientInstance = new GoldRushClient( apiKey, {}, { ...defaultCallbacks, ...callbacks } ); return clientInstance; }; export const getGoldrushClient = () => { if (!clientInstance) { throw new Error("Goldrush client not initialized. Call initializeGoldrushClient first."); } return clientInstance; }; export const disconnectGoldrushClient = async () => { if (clientInstance?.StreamingService) { await clientInstance.StreamingService.disconnect(); clientInstance = null; } };


Step 5: OHLCV Price Monitoring

Wallet activity is only half the story. Token prices often signal exploits before wallets show movement. Let's add OHLCV streaming.

Create a file with this path src/hooks/useGoldrushStreaming.js:

import { useEffect, useState, useRef } from 'react'; import { initializeGoldrushClient, getGoldrushClient, disconnectGoldrushClient, } from '../services/goldrushClient'; import { StreamingChain, StreamingInterval, StreamingTimeframe } from '@covalenthq/client-sdk'; import { processOHLCVCandle, calculateOHLCVStatistics } from '../services/dataProcessing'; import { detectOHLCVAnomalies } from '../services/anomalyDetection'; // export const useGoldrushStreaming = (walletAddress) => { const [isConnected, setIsConnected] = useState(false); const [transactions, setTransactions] = useState([]); const [balances, setBalances] = useState({}); const [error, setError] = useState(null); const unsubscribeRef = useRef({}); useEffect(() => { const setupStreaming = async () => { try { initializeGoldrushClient(); const client = getGoldrushClient(); setIsConnected(true); // Subscribe to wallet activity unsubscribeRef.current.wallet = client.StreamingService.subscribeToWalletActivity( { chain_name: StreamingChain.SOLANA_MAINNET, wallet_addresses: [walletAddress], }, { next: (data) => { setTransactions(prev => [ { id: Math.random(), hash: data.tx_hash, type: data.tx_type, timestamp: new Date(data.timestamp), ...data }, ...prev.slice(0, 99) ]); }, error: (err) => { console.error('Streaming error:', err); setError(err.message); } } ); } catch (err) { console.error('Setup error:', err); setError(err.message); setIsConnected(false); } }; setupStreaming(); return () => { Object.values(unsubscribeRef.current).forEach(unsub => { if (typeof unsub === 'function') unsub(); }); disconnectGoldrushClient(); }; }, [walletAddress]); return { isConnected, transactions, balances, error }; }; /** * Hook for OHLCV token streaming */ export const useOHLCVStreaming = (tokenAddress, options = {}) => { const { interval = StreamingInterval.ONE_MINUTE, timeframe = StreamingTimeframe.ONE_HOUR, maxHistory = 50 } = options; const [isConnected, setIsConnected] = useState(false); const [candles, setCandles] = useState([]); const [anomalies, setAnomalies] = useState([]); const [stats, setStats] = useState(null); const [error, setError] = useState(null); const [streamInfo, setStreamInfo] = useState({ startTime: null, dataCount: 0, lastUpdate: null }); const unsubscribeRef = useRef(null); const candleHistoryRef = useRef([]); useEffect(() => { if (!tokenAddress) return; const setupOHLCVStream = async () => { try { initializeGoldrushClient(); const client = getGoldrushClient(); setIsConnected(true); setStreamInfo(prev => ({ ...prev, startTime: new Date() })); console.log('📡 Subscribing to OHLCV stream for token:', tokenAddress); // Subscribe to OHLCV token data unsubscribeRef.current = client.StreamingService.subscribeToOHLCVTokens( { chain_name: StreamingChain.SOLANA_MAINNET, token_addresses: [tokenAddress], interval: interval, timeframe: timeframe, }, { next: (data) => { console.log('✓ OHLCV Data received:', data); console.log('📊 Data type:', typeof data, 'Is Array:', Array.isArray(data), 'Length:', data?.length); // Process the candle data const processedCandles = processOHLCVCandle(data); if (!processedCandles || processedCandles.length === 0) { console.warn('⚠️ No candles in this update - waiting for trading activity...'); console.log('💡 This is normal if the token has no recent trades. The stream is active and monitoring.'); // Update stream info even if no candles setStreamInfo(prev => ({ ...prev, dataCount: prev.dataCount + 1, lastUpdate: new Date() })); return; } // Process each candle processedCandles.forEach((candle) => { // Add to history candleHistoryRef.current = [...candleHistoryRef.current, candle].slice(-maxHistory); // Update state setCandles(candleHistoryRef.current); // Detect anomalies const history = candleHistoryRef.current.slice(0, -1); // All except current const detectedAnomalies = detectOHLCVAnomalies(candle, history); if (detectedAnomalies.length > 0) { console.log('⚠️ Anomalies detected:', detectedAnomalies); setAnomalies(prev => [...detectedAnomalies, ...prev].slice(0, 50)); } // Update stream info setStreamInfo(prev => ({ ...prev, dataCount: prev.dataCount + 1, lastUpdate: new Date() })); }); // Calculate statistics const newStats = calculateOHLCVStatistics(candleHistoryRef.current); setStats(newStats); }, error: (err) => { console.error('✗ OHLCV subscription error:', err); setError(err.message || 'Streaming error occurred'); }, complete: () => { console.log('✓ OHLCV stream completed'); setIsConnected(false); } } ); console.log('✓ OHLCV stream setup complete'); } catch (err) { console.error('✗ Failed to setup OHLCV stream:', err); setError(err.message); setIsConnected(false); } }; setupOHLCVStream(); return () => { if (unsubscribeRef.current && typeof unsubscribeRef.current === 'function') { console.log('🛑 Unsubscribing from OHLCV stream'); unsubscribeRef.current(); } }; }, [tokenAddress, interval, timeframe, maxHistory]); return { isConnected, candles, anomalies, stats, error, streamInfo }; };

This hook sets up and manages a singleton GoldRushClient instance to connect with the Covalent GoldRush Streaming API.

It ensures only one client runs, handles connection events (like connect, disconnect, and errors), and provides helper functions to initialize, access, and safely disconnect the client

Step 6: Building the UI Dashboard

Now let's make this data visible
Create a file with this path src/components/AnomalyAlert.jsx

import React from 'react'; import { AlertCircle, TrendingDown, AlertTriangle, Info } from 'lucide-react'; // export default function AnomalyAlert({ anomalies }) { const getSeverityColor = (severity) => { switch (severity) { case 'error': return 'bg-red-50 border-red-200 text-red-800 dark:bg-red-900/20 dark:border-red-700 dark:text-red-400'; case 'warning': return 'bg-yellow-50 border-yellow-200 text-yellow-800 dark:bg-yellow-900/20 dark:border-yellow-700 dark:text-yellow-400'; case 'info': return 'bg-blue-50 border-blue-200 text-blue-800 dark:bg-blue-900/20 dark:border-blue-700 dark:text-blue-400'; default: return 'bg-gray-50 border-gray-200 text-gray-800 dark:bg-gray-900/20 dark:border-gray-700 dark:text-gray-400'; } }; const getSeverityIcon = (severity) => { switch (severity) { case 'error': return <AlertCircle className="w-5 h-5 flex-shrink-0" />; case 'warning': return <AlertTriangle className="w-5 h-5 flex-shrink-0" />; case 'info': return <Info className="w-5 h-5 flex-shrink-0" />; default: return <TrendingDown className="w-5 h-5 flex-shrink-0" />; } }; return ( <div className="bg-slate-800/50 border border-slate-700/50 rounded-lg p-6"> <div className="flex items-center justify-between mb-4"> <h2 className="text-xl font-bold text-white">Detected Anomalies</h2> <span className="px-3 py-1 rounded-full bg-red-900/30 text-red-400 text-sm font-semibold"> {anomalies.length} alerts </span> </div> <div className="space-y-3 max-h-96 overflow-y-auto"> {anomalies.length === 0 ? ( <div className="flex items-center justify-center py-12"> <p className="text-slate-400 text-center"> ✓ No anomalies detected.<br /> <span className="text-xs">System operating normally.</span> </p> </div> ) : ( anomalies.map((anomaly) => ( <div key={anomaly.id} className={`p-4 rounded-lg border flex gap-3 transition-all ${getSeverityColor( anomaly.severity )}`} > {getSeverityIcon(anomaly.severity)} <div className="flex-1 min-w-0"> <p className="font-semibold text-sm">{anomaly.title}</p> <p className="text-xs opacity-80 break-words">{anomaly.message}</p> <p className="text-xs opacity-60 mt-1"> {anomaly.timestamp.toLocaleTimeString()} </p> </div> </div> )) )} </div> </div> ); }


Now building the OHLCV Monitor :
Create a file src/components/OHLCVMonitor.jsx

import React, { useState, useEffect, useRef } from 'react'; import { TrendingUp, TrendingDown, Activity, DollarSign, BarChart3, AlertTriangle } from 'lucide-react'; import { formatPrice, formatVolume } from '../services/dataProcessing'; export default function OHLCVMonitor({ candles, anomalies, stats }) { const [currentCandle, setCurrentCandle] = useState(null); const [streamStatus, setStreamStatus] = useState({ isActive: true, dataCount: candles.length, lastUpdate: null }); useEffect(() => { if (candles.length > 0) { setCurrentCandle(candles[candles.length - 1]); setStreamStatus(prev => ({ ...prev, dataCount: candles.length, lastUpdate: new Date() })); } }, [candles]); const getSeverityColor = (severity) => { switch (severity) { case 'CRITICAL': return 'bg-red-900/30 border-red-700 text-red-400'; case 'HIGH': return 'bg-orange-900/30 border-orange-700 text-orange-400'; case 'MEDIUM': return 'bg-yellow-900/30 border-yellow-700 text-yellow-400'; default: return 'bg-blue-900/30 border-blue-700 text-blue-400'; } }; const getPriceChangeColor = (change) => { if (change > 0) return 'text-green-400'; if (change < 0) return 'text-red-400'; return 'text-slate-400'; }; return ( <div className="space-y-6"> {/* Stream Status */} <div className="bg-slate-800/50 border border-purple-500/20 rounded-lg p-4"> <div className="flex items-center justify-between"> <div> <h3 className="text-lg font-bold text-white mb-1">OHLCV Stream Status</h3> <p className="text-slate-400 text-sm">Real-time token price monitoring</p> </div> <div className="flex items-center gap-2"> <div className={`px-3 py-1 rounded-full ${streamStatus.isActive ? 'bg-green-900/30 text-green-400' : 'bg-red-900/30 text-red-400'} text-sm font-semibold`}> {streamStatus.isActive ? '🟢 ACTIVE' : '🔴 INACTIVE'} </div> </div> </div> <div className="grid grid-cols-3 gap-4 mt-4"> <div className="bg-slate-700/30 rounded p-3"> <p className="text-slate-400 text-xs mb-1">Candles Received</p> <p className="text-white text-2xl font-bold">{streamStatus.dataCount}</p> </div> <div className="bg-slate-700/30 rounded p-3"> <p className="text-slate-400 text-xs mb-1">Anomalies</p> <p className="text-red-400 text-2xl font-bold">{anomalies.length}</p> </div> <div className="bg-slate-700/30 rounded p-3"> <p className="text-slate-400 text-xs mb-1">Last Update</p> <p className="text-white text-sm font-mono"> {streamStatus.lastUpdate ? streamStatus.lastUpdate.toLocaleTimeString() : 'Never'} </p> </div> </div> </div> {/* Current Candle */} {currentCandle ? ( <div className="bg-slate-800/50 border border-slate-700/50 rounded-lg p-6"> <div className="flex items-center justify-between mb-4"> <div> <h3 className="text-xl font-bold text-white"> {currentCandle.baseToken?.symbol || 'Token'} Price Data </h3> <p className="text-slate-400 text-sm"> {new Date(currentCandle.timestamp).toLocaleString()} </p> </div> {currentCandle.baseToken && ( <div className="text-right"> <p className="text-slate-400 text-xs mb-1">Token</p> <p className="text-white font-mono text-sm"> {currentCandle.baseToken.address.substring(0, 10)}... </p> </div> )} </div> {/* OHLCV Data */} <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6"> <div className="bg-blue-900/20 border border-blue-700/50 rounded p-4"> <div className="flex items-center gap-2 mb-2"> <BarChart3 className="w-4 h-4 text-blue-400" /> <p className="text-blue-400 text-xs font-semibold">OPEN</p> </div> <p className="text-white text-lg font-mono">{formatPrice(currentCandle.open)}</p> </div> <div className="bg-green-900/20 border border-green-700/50 rounded p-4"> <div className="flex items-center gap-2 mb-2"> <TrendingUp className="w-4 h-4 text-green-400" /> <p className="text-green-400 text-xs font-semibold">HIGH</p> </div> <p className="text-white text-lg font-mono">{formatPrice(currentCandle.high)}</p> </div> <div className="bg-red-900/20 border border-red-700/50 rounded p-4"> <div className="flex items-center gap-2 mb-2"> <TrendingDown className="w-4 h-4 text-red-400" /> <p className="text-red-400 text-xs font-semibold">LOW</p> </div> <p className="text-white text-lg font-mono">{formatPrice(currentCandle.low)}</p> </div> <div className="bg-yellow-900/20 border border-yellow-700/50 rounded p-4"> <div className="flex items-center gap-2 mb-2"> <Activity className="w-4 h-4 text-yellow-400" /> <p className="text-yellow-400 text-xs font-semibold">CLOSE</p> </div> <p className="text-white text-lg font-mono">{formatPrice(currentCandle.close)}</p> </div> </div> {/* Volume Data */} <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="bg-purple-900/20 border border-purple-700/50 rounded p-4"> <div className="flex items-center gap-2 mb-2"> <BarChart3 className="w-4 h-4 text-purple-400" /> <p className="text-purple-400 text-xs font-semibold">VOLUME</p> </div> <p className="text-white text-lg font-mono">{formatVolume(currentCandle.volume)}</p> </div> <div className="bg-cyan-900/20 border border-cyan-700/50 rounded p-4"> <div className="flex items-center gap-2 mb-2"> <DollarSign className="w-4 h-4 text-cyan-400" /> <p className="text-cyan-400 text-xs font-semibold">VOLUME USD</p> </div> <p className="text-white text-lg font-mono"> ${(currentCandle.volumeUsd || 0).toFixed(2)} </p> </div> </div> {/* Token Info */} {currentCandle.baseToken && ( <div className="mt-4 p-4 bg-slate-700/30 rounded"> <p className="text-slate-400 text-xs mb-2">TOKEN INFORMATION</p> <div className="grid grid-cols-2 gap-4 text-sm"> <div> <span className="text-slate-500">Name: </span> <span className="text-white font-semibold">{currentCandle.baseToken.name}</span> </div> <div> <span className="text-slate-500">Symbol: </span> <span className="text-white font-semibold">{currentCandle.baseToken.symbol}</span> </div> <div className="col-span-2"> <span className="text-slate-500">Decimals: </span> <span className="text-white font-semibold">{currentCandle.baseToken.decimals}</span> </div> </div> </div> )} </div> ) : ( <div className="bg-slate-800/50 border border-slate-700/50 rounded-lg p-12"> <div className="text-center"> <Activity className="w-12 h-12 text-purple-500 mx-auto mb-4 animate-pulse" /> <p className="text-white text-lg font-bold mb-2">🔄 Waiting for OHLCV data...</p> <p className="text-slate-400 text-sm mb-4"> The stream is connected and monitoring for trading activity </p> <div className="bg-blue-900/20 border border-blue-700/50 rounded-lg p-4 max-w-md mx-auto"> <p className="text-blue-400 text-xs font-semibold mb-2">📡 Stream Status</p> <div className="text-left text-xs space-y-1"> <p className="text-slate-300"> • Connection: <span className="text-green-400 font-semibold">Active ✓</span> </p> <p className="text-slate-300"> • Updates Received: <span className="text-yellow-400 font-semibold">{streamStatus.dataCount}</span> </p> <p className="text-slate-300"> • Waiting for: <span className="text-purple-400 font-semibold">Trading Activity</span> </p> </div> </div> <div className="mt-6 text-xs text-slate-500 space-y-1"> <p>💡 This is normal - OHLCV data appears when trades occur</p> <p>⏰ Data typically arrives within 1-5 minutes for active tokens</p> <p>🔧 Try a different token or check console for logs</p> </div> </div> </div> )} {/* Statistics */} {stats && stats.totalCandles > 0 && ( <div className="bg-slate-800/50 border border-slate-700/50 rounded-lg p-6"> <h3 className="text-lg font-bold text-white mb-4">Statistics</h3> <div className="grid grid-cols-2 md:grid-cols-4 gap-4"> <div> <p className="text-slate-400 text-xs mb-1">Avg Price</p> <p className="text-white font-mono text-sm">{stats.avgPrice}</p> </div> <div> <p className="text-slate-400 text-xs mb-1">Max Price</p> <p className="text-green-400 font-mono text-sm">{stats.maxPrice}</p> </div> <div> <p className="text-slate-400 text-xs mb-1">Min Price</p> <p className="text-red-400 font-mono text-sm">{stats.minPrice}</p> </div> <div> <p className="text-slate-400 text-xs mb-1">Price Change</p> <p className={`font-mono text-sm ${getPriceChangeColor(parseFloat(stats.priceChangePercent))}`}> {stats.priceChangePercent}% </p> </div> </div> </div> )} {/* Anomalies */} {anomalies.length > 0 && ( <div className="bg-slate-800/50 border border-red-700/50 rounded-lg p-6"> <div className="flex items-center gap-2 mb-4"> <AlertTriangle className="w-5 h-5 text-red-400" /> <h3 className="text-lg font-bold text-white">⚠️ Anomalies Detected</h3> <span className="ml-auto px-3 py-1 rounded-full bg-red-900/30 text-red-400 text-sm font-semibold"> {anomalies.length} alerts </span> </div> <div className="space-y-3"> {anomalies.slice(0, 5).map((anomaly, idx) => ( <div key={idx} className={`p-4 rounded-lg border ${getSeverityColor(anomaly.severity)}`} > <div className="flex items-start justify-between mb-2"> <p className="font-bold text-sm">[{anomaly.severity}] {anomaly.type}</p> <span className="text-xs opacity-75">{anomaly.value}</span> </div> <p className="text-xs opacity-90">{anomaly.details}</p> <p className="text-xs opacity-60 mt-1"> {anomaly.timestamp?.toLocaleTimeString()} </p> </div> ))} </div> {anomalies.length > 5 && ( <p className="text-center text-slate-400 text-sm mt-3"> +{anomalies.length - 5} more anomalies </p> )} </div> )} {/* Candle History */} {candles.length > 0 && ( <div className="bg-slate-800/50 border border-slate-700/50 rounded-lg p-6"> <h3 className="text-lg font-bold text-white mb-4">Candle History</h3> <div className="space-y-2 max-h-96 overflow-y-auto"> {candles.slice().reverse().slice(0, 20).map((candle, idx) => ( <div key={candle.id || idx} className="p-3 bg-slate-700/20 hover:bg-slate-700/40 rounded transition-colors" > <div className="flex items-center justify-between"> <div className="flex-1"> <p className="text-slate-400 text-xs mb-1"> {new Date(candle.timestamp).toLocaleString()} </p> <div className="grid grid-cols-4 gap-2 text-xs"> <div> <span className="text-slate-500">O:</span> <span className="text-white ml-1">{formatPrice(candle.open)}</span> </div> <div> <span className="text-slate-500">H:</span> <span className="text-green-400 ml-1">{formatPrice(candle.high)}</span> </div> <div> <span className="text-slate-500">L:</span> <span className="text-red-400 ml-1">{formatPrice(candle.low)}</span> </div> <div> <span className="text-slate-500">C:</span> <span className="text-white ml-1">{formatPrice(candle.close)}</span> </div> </div> </div> <div className="text-right"> <p className="text-slate-400 text-xs">Vol</p> <p className="text-purple-400 text-xs font-mono"> {formatVolume(candle.volume)} </p> </div> </div> </div> ))} </div> {candles.length > 20 && ( <p className="text-center text-slate-400 text-sm mt-3"> Showing 20 of {candles.length} candles </p> )} </div> )} </div> ); }

What it does: A comprehensive dashboard for real-time OHLCV and anomaly visualization. Merges live token analytics, anomaly detection, and historical tracking in one elegant UI. Perfect for blockchain traders, data scientists, or developers monitoring token performance streams.


Now lets build the Token Balance file :

Create a file src/components/TokenBalance.jsx

import React from 'react'; import { TrendingUp, TrendingDown } from 'lucide-react'; export default function TokenBalances({ balances }) { const sortedBalances = Object.entries(balances) .sort(([, a], [, b]) => Math.abs(b) - Math.abs(a)) .slice(0, 10); const getTrendIcon = (balance) => { if (balance > 0) { return <TrendingUp className="w-4 h-4 text-green-400" />; } else if (balance < 0) { return <TrendingDown className="w-4 h-4 text-red-400" />; } return null; }; const getBalanceColor = (balance) => { if (balance > 0) return 'text-green-400'; if (balance < 0) return 'text-red-400'; return 'text-slate-400'; }; return ( <div className="bg-slate-800/50 border border-slate-700/50 rounded-lg p-6"> <div className="flex items-center justify-between mb-4"> <h2 className="text-xl font-bold text-white">Token Balances</h2> <span className="px-3 py-1 rounded-full bg-purple-900/30 text-purple-400 text-sm font-semibold"> {sortedBalances.length} tokens </span> </div> <div className="space-y-2"> {sortedBalances.length === 0 ? ( <div className="flex items-center justify-center py-12"> <p className="text-slate-400 text-center"> Waiting for wallet data...<br /> <span className="text-xs">Real-time balances will appear here</span> </p> </div> ) : ( sortedBalances.map(([token, balance]) => ( <div key={token} className="flex items-center justify-between p-3 bg-slate-700/30 hover:bg-slate-700/50 rounded transition-colors" > <div className="flex items-center gap-2"> <div className="w-8 h-8 rounded-full bg-gradient-to-br from-purple-500 to-blue-500 flex items-center justify-center text-white text-xs font-bold"> {token.charAt(0)} </div> <span className="text-white font-mono font-semibold">{token}</span> </div> <div className="flex items-center gap-2"> {getTrendIcon(balance)} <span className={`font-mono font-semibold ${getBalanceColor(balance)}`}> {balance >= 0 ? '+' : ''}{balance.toFixed(4)} </span> </div> </div> )) )} </div> </div> ); }

What we have built till now :

  • A dynamic token balance tracker that visually communicates portfolio changes.

  • Highlights top-performing or declining tokens in real time.

  • Ideal for wallet dashboards, DeFi analytics, or portfolio monitoring tools

Now let’s build the Transaction table :

Create another file src/components/TransactionTable.jsx

import React, { useState } from 'react'; import { ChevronDown, ExternalLink } from 'lucide-react'; export default function TransactionTable({ transactions }) { const [expandedTx, setExpandedTx] = useState(null); const getStatusColor = (status) => { switch (status) { case 'success': return 'bg-green-900/30 text-green-400'; case 'failed': return 'bg-red-900/30 text-red-400'; case 'pending': return 'bg-yellow-900/30 text-yellow-400'; default: return 'bg-slate-900/30 text-slate-400'; } }; const getTypeColor = (type) => { switch (type) { case 'send': return 'text-red-400'; case 'receive': return 'text-green-400'; case 'swap': return 'text-blue-400'; default: return 'text-slate-400'; } }; const formatTime = (timestamp) => { if (!timestamp) return 'N/A'; const date = new Date(timestamp); return date.toLocaleTimeString(); }; const formatHash = (hash) => { if (!hash) return 'N/A'; return `${hash.substring(0, 8)}...${hash.substring(hash.length - 8)}`; }; return ( <div className="bg-slate-800/50 border border-slate-700/50 rounded-lg p-6 mt-6"> <div className="flex items-center justify-between mb-4"> <h2 className="text-xl font-bold text-white">Recent Transactions</h2> <span className="px-3 py-1 rounded-full bg-blue-900/30 text-blue-400 text-sm font-semibold"> {transactions.length} total </span> </div> <div className="overflow-x-auto"> {transactions.length === 0 ? ( <div className="flex items-center justify-center py-12"> <p className="text-slate-400 text-center"> No transactions yet...<br /> <span className="text-xs">Transactions will appear here in real-time</span> </p> </div> ) : ( <div className="space-y-2"> {transactions.map((tx, index) => ( <div key={tx.id || index} className="border border-slate-700/50 rounded-lg overflow-hidden"> <div className="p-4 bg-slate-700/20 hover:bg-slate-700/40 cursor-pointer transition-colors flex items-center justify-between" onClick={() => setExpandedTx(expandedTx === tx.id ? null : tx.id) } > <div className="flex items-center gap-4 flex-1 min-w-0"> <ChevronDown className={`w-5 h-5 text-slate-400 flex-shrink-0 transition-transform ${ expandedTx === tx.id ? 'rotate-180' : '' }`} /> <div className="flex-1 min-w-0"> <div className="flex items-center gap-2"> <span className={`font-mono text-sm font-semibold ${getTypeColor(tx.type)}`}> {tx.type.toUpperCase()} </span> <span className="text-slate-400">•</span> <span className="text-slate-300 text-sm font-mono"> {formatHash(tx.hash)} </span> </div> <p className="text-xs text-slate-500 mt-1"> {formatTime(tx.timestamp)} </p> </div> </div> <div className="flex items-center gap-4"> {tx.token && ( <span className="text-slate-300 font-semibold"> {tx.amount ? ( <> {tx.type === 'send' ? '-' : '+'} {Math.abs(tx.amount).toFixed(4)} {tx.token} </> ) : ( tx.token )} </span> )} <span className={`px-3 py-1 rounded text-xs font-semibold whitespace-nowrap ${getStatusColor( tx.status )}`} > {tx.status} </span> </div> </div> {expandedTx === tx.id && ( <div className="p-4 bg-slate-700/10 border-t border-slate-700/50"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm"> <div> <p className="text-slate-500 text-xs uppercase tracking-wide mb-1"> Transaction Hash </p> <p className="text-slate-200 font-mono break-all text-xs"> {tx.hash} </p> </div> <div> <p className="text-slate-500 text-xs uppercase tracking-wide mb-1"> Type </p> <p className={`capitalize font-semibold ${getTypeColor(tx.type)}`}> {tx.type} </p> </div> {tx.token && ( <div> <p className="text-slate-500 text-xs uppercase tracking-wide mb-1"> Token </p> <p className="text-slate-200 font-semibold">{tx.token}</p> </div> )} {tx.amount !== undefined && ( <div> <p className="text-slate-500 text-xs uppercase tracking-wide mb-1"> Amount </p> <p className="text-slate-200 font-mono"> {Math.abs(tx.amount).toFixed(4)} </p> </div> )} <div> <p className="text-slate-500 text-xs uppercase tracking-wide mb-1"> Status </p> <p className={`capitalize font-semibold ${getStatusColor(tx.status)}`} > {tx.status} </p> </div> <div> <p className="text-slate-500 text-xs uppercase tracking-wide mb-1"> Time </p> <p className="text-slate-200"> {new Date(tx.timestamp).toLocaleString()} </p> </div> </div> {tx.hash && ( <a href={`https://solscan.io/tx/${tx.hash}`} target="_blank" rel="noopener noreferrer" className="mt-4 inline-flex items-center gap-2 px-3 py-2 rounded bg-blue-900/30 hover:bg-blue-900/50 text-blue-400 text-sm font-semibold transition-colors" > View on Solscan <ExternalLink className="w-4 h-4" /> </a> )} </div> )} </div> ))} </div> )} </div> </div> ); }

Now we have built :

  • A responsive, interactive transaction tracker with expandable rows and color-coded statuses.

  • Helps users monitor on-chain activity in real time and quickly verify transactions via Solscan

At last we need to wrap up our progress in App.jsx :

Here's how to bring it all together in src/App.jsx

import React, { useState, useEffect, useRef, useCallback } from 'react'; import { Wifi, WifiOff, Settings, BarChart3 } from 'lucide-react'; import AnomalyAlert from './components/AnomalyAlert'; import TokenBalances from './components/TokenBalances'; import TransactionTable from './components/TransactionTable'; import OHLCVMonitor from './components/OHLCVMonitor'; import { initializeGoldrushClient, getGoldrushClient, disconnectGoldrushClient } from './services/goldrushClient'; import { StreamingChain } from '@covalenthq/client-sdk'; import { processTransactionData, processBalanceData, detectUnusualPatterns, calculateTransactionStats, } from './services/dataProcessing'; import { detectAnomalies } from './services/anomalyDetection'; import { useOHLCVStreaming } from './hooks/useGoldrushStreaming'; function App() { const [isConnected, setIsConnected] = useState(false); const [walletAddress, setWalletAddress] = useState( import.meta.env.VITE_SOLANA_WALLET_ADDRESS || import.meta.env.REACT_APP_SOLANA_WALLET_ADDRESS || 'JUP4Fb2cqiRiBcsFyH4qBLEV9DdroVHix7gpQtzLChE' ); const [tokenAddress, setTokenAddress] = useState( import.meta.env.VITE_TOKEN_ADDRESS || 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' ); const [balances, setBalances] = useState({}); const [transactions, setTransactions] = useState([]); const [anomalies, setAnomalies] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [showSettings, setShowSettings] = useState(false); const [stats, setStats] = useState(null); const [activeTab, setActiveTab] = useState('ohlcv'); // Changed default to 'ohlcv' const clientRef = useRef(null); const unsubscribeRef = useRef({}); const previousDataRef = useRef({ recentTxCount: 0, lastBalances: {}, }); // Use OHLCV streaming hook const { isConnected: ohlcvConnected, candles, anomalies: ohlcvAnomalies, stats: ohlcvStats, error: ohlcvError, streamInfo } = useOHLCVStreaming(tokenAddress); /** * Initialize Goldrush connection */ const initializeConnection = useCallback(async () => { try { setIsLoading(true); setError(null); initializeGoldrushClient(); clientRef.current = getGoldrushClient(); setIsConnected(true); console.log('Connected to Goldrush Streaming API'); } catch (err) { console.error('Failed to initialize Goldrush:', err); setError(`Connection failed: ${err.message}`); setIsConnected(false); } finally { setIsLoading(false); } }, []); /** * Add anomaly to list */ const addAnomaly = useCallback((title, message, severity = 'info') => { const newAnomaly = { id: Math.random(), title, message, severity, timestamp: new Date(), }; setAnomalies((prev) => [newAnomaly, ...prev.slice(0, 19)]); }, []); /** * Calculate statistics from transactions */ useEffect(() => { const newStats = calculateTransactionStats(transactions); setStats(newStats); detectUnusualPatterns(transactions); previousDataRef.current.recentTxCount = transactions.length; }, [transactions]); /** * Setup streaming on mount */ useEffect(() => { initializeConnection(); return () => { cleanup(); }; }, [initializeConnection]); /** * Cleanup function */ const cleanup = async () => { Object.values(unsubscribeRef.current).forEach((unsub) => { if (typeof unsub === 'function') { try { unsub(); } catch (err) { console.error('Error unsubscribing:', err); } } }); unsubscribeRef.current = {}; try { await disconnectGoldrushClient(); } catch (err) { console.error('Error disconnecting:', err); } setIsConnected(false); }; /** * Handle wallet address change */ const handleWalletAddressChange = (newAddress) => { setWalletAddress(newAddress); setTransactions([]); setBalances({}); setAnomalies([]); }; /** * Reconnect function */ const handleReconnect = () => { cleanup(); setTimeout(() => { initializeConnection(); }, 1000); }; return ( <div className="min-h-screen bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900"> {/* Header */} <header className="sticky top-0 z-50 border-b border-slate-700/50 bg-slate-900/80 backdrop-blur-sm"> <div className="max-w-7xl mx-auto px-6 py-4"> <div className="flex items-center justify-between"> <div> <h1 className="text-3xl font-bold text-white">Solana Anomaly Detector</h1> <p className="text-slate-400 text-sm mt-1">Real-time OHLCV monitoring with Goldrush Streaming</p> </div> <div className="flex items-center gap-4"> {/* Connection Status */} <div className={`flex items-center gap-2 px-4 py-2 rounded-lg border ${ ohlcvConnected ? 'bg-green-900/20 border-green-700/50 text-green-400' : 'bg-red-900/20 border-red-700/50 text-red-400' }`}> {ohlcvConnected ? ( <> <Wifi className="w-4 h-4 animate-pulse" /> <span className="text-sm font-semibold">Connected</span> </> ) : ( <> <WifiOff className="w-4 h-4" /> <span className="text-sm font-semibold">Disconnected</span> </> )} </div> {/* Settings Button */} <button onClick={() => setShowSettings(!showSettings)} className="p-2 hover:bg-slate-800 rounded-lg transition-colors" > <Settings className="w-5 h-5 text-slate-400 hover:text-white" /> </button> </div> </div> {/* Settings Panel */} {showSettings && ( <div className="mt-4 p-4 bg-slate-800/50 border border-slate-700/50 rounded-lg"> <div className="space-y-4"> <div> <label className="block text-sm font-semibold text-slate-300 mb-2"> Token Address (for OHLCV) </label> <input type="text" value={tokenAddress} onChange={(e) => setTokenAddress(e.target.value)} className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white placeholder-slate-500 font-mono text-sm" placeholder="Enter Solana token address" /> <p className="text-xs text-slate-500 mt-1"> Default: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v (USDC) </p> </div> <div className="flex gap-2"> <button onClick={handleReconnect} className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded font-semibold transition-colors" > Reconnect </button> <button onClick={() => setShowSettings(false)} className="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded font-semibold transition-colors" > Close </button> </div> </div> </div> )} </div> </header> {/* Main Content */} <main className="max-w-7xl mx-auto px-6 py-8"> {/* OHLCV Error Alert */} {ohlcvError && ( <div className="mb-6 p-4 bg-red-900/20 border border-red-700/50 rounded-lg text-red-400"> <p className="font-semibold mb-2">OHLCV Stream Error</p> <p className="text-sm">{ohlcvError}</p> <p className="text-xs mt-2 text-slate-400"> Note: OHLCV streaming may have limited availability. Contact [email protected] for Beta access. </p> </div> )} {/* Loading State */} {isLoading && ( <div className="flex items-center justify-center py-20"> <div className="text-center"> <div className="w-12 h-12 border-4 border-slate-700 border-t-purple-500 rounded-full animate-spin mx-auto mb-4"></div> <p className="text-slate-400">Connecting to Goldrush Streaming API...</p> </div> </div> )} {/* OHLCV Monitor Content */} {!isLoading && ( <> {/* Token Info Card */} <div className="bg-slate-800/50 border border-purple-500/20 rounded-lg p-6 mb-6"> <div className="flex items-center justify-between mb-4"> <div className="flex-1"> <p className="text-slate-400 text-sm mb-2">Monitoring Token</p> <p className="text-white text-xl font-mono font-semibold break-all">{tokenAddress}</p> </div> <div className={`px-4 py-2 rounded-lg border ${ ohlcvConnected ? 'bg-green-900/20 border-green-700/50 text-green-400' : 'bg-red-900/20 border-red-700/50 text-red-400' }`}> {ohlcvConnected ? ( <> <Wifi className="w-4 h-4 inline mr-2 animate-pulse" /> <span className="text-sm font-semibold">Streaming</span> </> ) : ( <> <WifiOff className="w-4 h-4 inline mr-2" /> <span className="text-sm font-semibold">Disconnected</span> </> )} </div> </div> {/* Quick Token Selector */} {candles.length === 0 && ( <div className="bg-blue-900/10 border border-blue-700/30 rounded-lg p-4"> <p className="text-blue-400 text-xs font-semibold mb-2">💡 Try Popular Tokens:</p> <div className="flex flex-wrap gap-2"> <button onClick={() => setTokenAddress('So11111111111111111111111111111111111111112')} className="px-3 py-1 bg-slate-700 hover:bg-slate-600 text-white rounded text-xs font-semibold transition-colors" > Wrapped SOL </button> <button onClick={() => setTokenAddress('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v')} className="px-3 py-1 bg-slate-700 hover:bg-slate-600 text-white rounded text-xs font-semibold transition-colors" > USDC </button> <button onClick={() => setTokenAddress('JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN')} className="px-3 py-1 bg-slate-700 hover:bg-slate-600 text-white rounded text-xs font-semibold transition-colors" > JUP </button> <button onClick={() => setTokenAddress('mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So')} className="px-3 py-1 bg-slate-700 hover:bg-slate-600 text-white rounded text-xs font-semibold transition-colors" > mSOL </button> </div> <p className="text-slate-500 text-xs mt-2"> Click a token to switch, or use Settings to enter a custom address </p> </div> )} </div> {/* OHLCV Stats */} <div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6"> <div className="bg-slate-800/50 border border-slate-700/50 rounded-lg p-4"> <p className="text-slate-400 text-sm mb-2">Candles</p> <p className="text-3xl font-bold text-blue-400">{candles.length}</p> </div> <div className="bg-slate-800/50 border border-slate-700/50 rounded-lg p-4"> <p className="text-slate-400 text-sm mb-2">Anomalies</p> <p className="text-3xl font-bold text-red-400">{ohlcvAnomalies.length}</p> </div> <div className="bg-slate-800/50 border border-slate-700/50 rounded-lg p-4"> <p className="text-slate-400 text-sm mb-2">Avg Price</p> <p className="text-2xl font-bold text-green-400 font-mono"> {ohlcvStats?.avgPrice || '0.000000'} </p> </div> <div className="bg-slate-800/50 border border-slate-700/50 rounded-lg p-4"> <p className="text-slate-400 text-sm mb-2">Price Change</p> <p className={`text-2xl font-bold font-mono ${ parseFloat(ohlcvStats?.priceChangePercent || 0) > 0 ? 'text-green-400' : 'text-red-400' }`}> {ohlcvStats?.priceChangePercent || '0.00'}% </p> </div> </div> {/* OHLCV Monitor Component */} <OHLCVMonitor candles={candles} anomalies={ohlcvAnomalies} stats={ohlcvStats} /> </> )} </main> </div> ); } export default App;

Step 7: Running Your System

Now run the following commands in your terminal:

# Start development server npm run dev # Build for production npm run build # Preview production build npm run preview

Open http://localhost:5173 and you should see:

  • Real-time transactions streaming in

  • Anomaly alerts appearing when detected

  • Connection status indicator

  • Smooth, responsive UI

Testing your system:

  1. Use Jupiter's aggregator wallet (constant activity)

  2. Wait 10-20 seconds for data to flow

  3. Watch for large transaction alerts

  4. Switch to OHLCV tab to see price monitoring

OUTPUT :


Understanding What You Built

Let's recap what makes this system production-ready:

1. Real-time Data Flow

  • WebSocket streaming (not polling)

  • Sub-second latency from blockchain to UI

  • Automatic reconnection on disconnect

2. Smart Anomaly Detection

  • Threshold-based (simple, reliable)

  • Statistical analysis (catches subtle patterns)

  • Multiple severity levels (prioritize alerts)

3. Scalable Architecture

  • React hooks for reusability

  • Service layer separation

  • State management that handles high-frequency updates

4. Production Considerations

  • Error handling and logging

  • Cleanup on unmount (no memory leaks)

  • Environment variable configuration


Wrapping Up

You now have a fully functional anomaly detection system for Solana. This isn't just a tutorial project—it's production-ready code you can deploy to monitor real wallets and tokens.

What you learned:

  • Setting up GoldRush Streaming API

  • Implementing statistical anomaly detection

  • Building responsive React dashboards

  • Handling real-time WebSocket data

  • Best practices for cleanup and error handling

Final Thought

In Part 1, we said: "A successful hack can drain a wallet in seconds. A network outage can prevent legitimate transactions. Users demand instantaneous visibility."

You've built that visibility. Part 3 will turn it into action.

The question isn't whether attacks will happen—it's whether you'll stop them in time.

Let's make sure you do


Resources to explore before Part 3:

  • Machine Learning on Blockchain Data - Primer on training models

  • Flash Loan Attack Patterns - Common exploitation techniques

  • Incident Response Frameworks - Best practices from traditional security

  • Goldrush Advanced Analytics Docs - API features we'll use

"In Web3, speed isn't just a feature. It's the difference between a warning and a postmortem."

See you in Part 3 .

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.