/** * Helius RPC client — fetches real Solana wallet activity for churn scoring. * Uses the enhanced transactions API to derive wallet signals without an indexer. */ const HELIUS_BASE = 'https://api.helius.xyz/v0' const KEY = process.env.HELIUS_API_KEY || '' export function isHeliusConfigured() { return KEY !== '' && !KEY.startsWith('your') } // Helius source → display protocol name const SOURCE_MAP: Record = { JUPITER: 'Jupiter', RAYDIUM: 'Raydium', DRIFT: 'Drift', KAMINO: 'Kamino', KAMINO_FINANCE: 'Kamino', MARGINFI: 'Marginfi', TENSOR: 'Tensor', ORCA: 'Orca', METEORA: 'Meteora', PHOENIX: 'Phoenix', LIFINITY: 'Lifinity', MARINADE: 'Marinade', } interface HeliusTx { timestamp: number source: string type: string nativeTransfers?: Array<{ amount: number }> tokenTransfers?: Array<{ tokenAmount: number }> } export interface WalletSignals { address: string daysInactive: number protocols: string[] uniqueProtocols: number currentStreak: number volumeDropPct: number hasLiquidation: boolean totalTxLast30d: number lastActiveDaysAgo: string source: 'helius' | 'mock' } export async function fetchWalletSignals(address: string): Promise { if (!isHeliusConfigured()) return null try { const res = await fetch( `${HELIUS_BASE}/addresses/${address}/transactions?api-key=${KEY}&limit=50`, { next: { revalidate: 60 } } ) if (!res.ok) return null const txs: HeliusTx[] = await res.json() if (!txs.length) return null const nowSec = Date.now() / 1000 const sorted = txs.sort((a, b) => b.timestamp - a.timestamp) // Days since last tx const daysInactive = Math.round((nowSec - sorted[0].timestamp) / 86400) // Protocols used const protocolSet = new Set() for (const tx of sorted) { const mapped = SOURCE_MAP[tx.source] if (mapped) protocolSet.add(mapped) } const protocols = Array.from(protocolSet) // Streak: count distinct calendar days active in last 7d const sevenDaysAgo = nowSec - 7 * 86400 const activeDays = new Set( sorted .filter(tx => tx.timestamp >= sevenDaysAgo) .map(tx => new Date(tx.timestamp * 1000).toDateString()) ) const currentStreak = activeDays.size // Volume drop: tx count in last 7d vs prior 7d (proxy for volume) const fourteenDaysAgo = nowSec - 14 * 86400 const recentCount = sorted.filter(tx => tx.timestamp >= sevenDaysAgo).length const priorCount = sorted.filter(tx => tx.timestamp >= fourteenDaysAgo && tx.timestamp < sevenDaysAgo).length const volumeDropPct = priorCount > 0 ? Math.round(Math.max(0, (priorCount - recentCount) / priorCount * 100)) : 0 // Liquidation check const hasLiquidation = sorted.some(tx => tx.type?.includes('LIQUIDAT')) // tx count last 30d const thirtyDaysAgo = nowSec - 30 * 86400 const totalTxLast30d = sorted.filter(tx => tx.timestamp >= thirtyDaysAgo).length const lastActiveDaysAgo = daysInactive === 0 ? 'today' : daysInactive === 1 ? '1d ago' : `${daysInactive}d ago` return { address, daysInactive, protocols, uniqueProtocols: protocols.length, currentStreak, volumeDropPct, hasLiquidation, totalTxLast30d, lastActiveDaysAgo, source: 'helius', } } catch { return null } }