/** * FlowState AI Agent Engine — Autonomous churn detection & retention * 5-signal scoring: inactivity, volume decline, protocol diversity, streak, liquidation * Supports configurable thresholds and pre-churn early warning detection. */ import { ChurnRisk } from './types' import { sendCustomEvent } from './torque-mcp' export interface ScoringThresholds { inactivityCritical: number // days inactive → critical signal (default 30) inactivityHigh: number // days inactive → high signal (default 14) inactivityMedium: number // days inactive → medium signal (default 7) volumeDropCritical: number // % volume drop → critical signal (default 80) volumeDropHigh: number // % volume drop → high signal (default 50) volumeDropMedium: number // % volume drop → medium signal (default 25) preChurnDaysTrader: number // days quiet for trader type (default 3) preChurnDaysLP: number // days quiet for LP type (default 5) preChurnDaysStaker: number // days quiet for staker type (default 7) } export const DEFAULT_THRESHOLDS: ScoringThresholds = { inactivityCritical: 30, inactivityHigh: 14, inactivityMedium: 7, volumeDropCritical: 80, volumeDropHigh: 50, volumeDropMedium: 25, preChurnDaysTrader: 3, preChurnDaysLP: 5, preChurnDaysStaker: 7, } export type WalletType = 'trader' | 'lp' | 'staker' const TRADER_PROTOCOLS = new Set(['Jupiter', 'Raydium', 'Drift', 'Tensor']) const LP_PROTOCOLS = new Set(['Kamino', 'Marginfi', 'Meteora']) export function classifyWalletType(protocols: string[]): WalletType { const traderCount = protocols.filter(p => TRADER_PROTOCOLS.has(p)).length const lpCount = protocols.filter(p => LP_PROTOCOLS.has(p)).length if (traderCount >= 2) return 'trader' if (lpCount >= 1) return 'lp' return 'staker' } export function detectPreChurn( daysInactive: number, streak: number, protocols: string[], thresholds: ScoringThresholds = DEFAULT_THRESHOLDS ): { isPreChurn: boolean; walletType: WalletType; threshold: number } { const walletType = classifyWalletType(protocols) const threshold = walletType === 'trader' ? thresholds.preChurnDaysTrader : walletType === 'lp' ? thresholds.preChurnDaysLP : thresholds.preChurnDaysStaker // Pre-churn: below inactivity medium threshold but streak recently broken const isPreChurn = daysInactive >= threshold && daysInactive < thresholds.inactivityMedium && streak <= 2 return { isPreChurn, walletType, threshold } } export function calculateChurnScore( activity: { daysInactive: number; volumeDropPct: number; uniqueProtocols: number; currentStreak: number; hasLiquidation: boolean }, thresholds: ScoringThresholds = DEFAULT_THRESHOLDS ): { score: number; risk: ChurnRisk; signals: string[] } { let score = 0 const signals: string[] = [] // Signal 1: Inactivity (0-30pts) const inactScore = Math.min(activity.daysInactive * 3, 30) score += inactScore if (activity.daysInactive >= thresholds.inactivityMedium) { signals.push('Inactive ' + activity.daysInactive + ' days') } // Signal 2: Volume decline (0-25pts) if (activity.volumeDropPct > 0) { score += Math.min(activity.volumeDropPct / 4, 25) if (activity.volumeDropPct >= thresholds.volumeDropMedium) { signals.push('Volume dropped ' + activity.volumeDropPct.toFixed(0) + '%') } } // Signal 3: Protocol diversity (0-15pts) if (activity.uniqueProtocols <= 1) { score += 15; signals.push('Single protocol — low engagement') } else if (activity.uniqueProtocols <= 2) { score += 8; signals.push('Limited protocol diversity') } // Signal 4: Streak broken (0-15pts) if (activity.currentStreak === 0) { score += 15; signals.push('Activity streak broken') } else if (activity.currentStreak < 3) score += 5 // Signal 5: Liquidation (0-15pts) if (activity.hasLiquidation) { score += 15; signals.push('Recent liquidation event') } score = Math.min(Math.max(score, 0), 100) let risk: ChurnRisk if (score >= 80) risk = 'critical' else if (score >= 60) risk = 'high' else if (score >= 40) risk = 'medium' else if (score >= 20) risk = 'low' else risk = 'safe' return { score, risk, signals } } export function selectResponse(risk: ChurnRisk, wallet: string): { action: string; description: string }[] { const responses: { action: string; description: string }[] = [] responses.push({ action: 'detect', description: 'Detected ' + risk + ' churn risk for ' + wallet.slice(0, 8) + '...' }) switch (risk) { case 'critical': responses.push({ action: 'gift', description: 'Sending 0.5 SOL gift via Anti-Churn campaign' }) responses.push({ action: 'raffle', description: 'Enrolling in Comeback Raffle with 5x multiplier' }) break case 'high': responses.push({ action: 'raffle', description: 'Enrolling in Comeback Raffle with 3x multiplier' }) break case 'medium': responses.push({ action: 'rebate', description: 'Activating 2x rebate boost for 48 hours' }) break } return responses } export async function executeResponse(wallet: string, action: string) { const eventName = action === 'gift' || action === 'raffle' ? 'churn_risk_high' : action === 'rebate' ? 'churn_risk_medium' : 'inactivity_detected' return sendCustomEvent(wallet, eventName, { triggeredAction: action, detectedBy: 'flowstate-ai-agent', timestamp: new Date().toISOString() }) }