flowstate / src /lib /agent-engine.ts
muthuk1's picture
feat: recovery attribution, pre-churn warnings, recovery card, threshold editor, telegram alerts
f667d47
/**
* 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() })
}