File size: 5,483 Bytes
de40b1a f667d47 de40b1a f667d47 de40b1a f667d47 de40b1a f667d47 de40b1a f667d47 de40b1a c6b6c96 de40b1a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | /**
* 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() })
}
|