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! 🚀
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.
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
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
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
};
};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.
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;
}
};
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
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.
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
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
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;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:
Use Jupiter's aggregator wallet (constant activity)
Wait 10-20 seconds for data to flow
Watch for large transaction alerts
Switch to OHLCV tab to see price monitoring





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