Transaction Velocity Algorithms: Detecting Impulse Spirals

When someone enters an impulse spiral, transaction frequency and amounts follow predictable patterns. Whistl's velocity algorithms detect these patterns in real-time, triggering intervention before significant harm occurs. This technical guide explains spending rate calculation, anomaly detection, and spiral prediction.

What Is Transaction Velocity?

Transaction velocity measures the rate of spending over time:

  • Frequency velocity: Number of transactions per hour/day
  • Amount velocity: Dollars spent per hour/day
  • Category velocity: Spending rate in specific categories
  • Acceleration: Rate of change in velocity (speeding up = danger)

Sudden increases in velocity often precede harmful spending episodes.

The Impulse Spiral Pattern

Research identifies a consistent pattern in compulsive spending episodes:

Four Phases of an Impulse Spiral

  1. Trigger: Emotional event, location, or time initiates urge
  2. Initial spending: First transaction (often small)
  3. Escalation: Rapid subsequent transactions, increasing amounts
  4. Crash: Exhaustion, regret, or external intervention

Velocity Profile

Time:     T-24h  T-12h  T-6h  T-2h  T-1h  T-30m  T-15m  T-0
Velocity:  1x     1x     1.2x  2x    4x    8x     15x    25x

# Normal baseline: 1 transaction/day
# Spiral onset (T-6h): Velocity increases 20%
# Escalation (T-2h): Velocity doubles
# Critical (T-30m): Velocity 8x normal
# Peak (T-0): Velocity 25x normal - INTERVENTION NEEDED

Velocity Calculation Algorithm

Whistl calculates velocity across multiple time windows:

Multi-Window Analysis

struct VelocityCalculator {
    private let timeWindows: [TimeInterval] = [
        300,      // 5 minutes
        900,      // 15 minutes
        1800,     // 30 minutes
        3600,     // 1 hour
        21600,    // 6 hours
        86400,    // 24 hours
        604800    // 7 days
    ]
    
    func calculateVelocity(
        transactions: [Transaction],
        category: String? = nil
    ) -> VelocityMetrics {
        var metrics = VelocityMetrics()
        
        for window in timeWindows {
            let windowStart = Date().addingTimeInterval(-window)
            let windowTransactions = transactions.filter {
                $0.date >= windowStart &&
                (category == nil || $0.category == category)
            }
            
            let frequency = Double(windowTransactions.count) / (window / 3600)
            let amount = windowTransactions.map { abs($0.amount) }.reduce(0, +)
            let amountRate = amount / (window / 3600)
            
            metrics.windows[window] = VelocityWindow(
                frequency: frequency,
                amountRate: amountRate,
                transactionCount: windowTransactions.count,
                totalAmount: amount
            )
        }
        
        return metrics
    }
}

Velocity Metrics Structure

struct VelocityMetrics {
    var windows: [TimeInterval: VelocityWindow] = [:]
    
    // Derived metrics
    var acceleration: Double {
        // Compare 1-hour velocity to 24-hour velocity
        let v1h = windows[3600]?.frequency ?? 0
        let v24h = windows[86400]?.frequency ?? 0
        return v24h > 0 ? v1h / v24h : 0
    }
    
    var escalationFactor: Double {
        // Compare 15-minute to 1-hour velocity
        let v15m = windows[900]?.frequency ?? 0
        let v1h = windows[3600]?.frequency ?? 0
        return v1h > 0 ? v15m / v1h : 0
    }
    
    var isSpiraling: Bool {
        // Spiral detected if acceleration > 3x AND escalation > 2x
        return acceleration > 3.0 && escalationFactor > 2.0
    }
}

Baseline Establishment

To detect anomalies, Whistl establishes personal baselines:

Baseline Calculation

class BaselineCalculator {
    private var historicalVelocities: [DayOfWeek: [Hour: Double]] = [:]
    
    func updateBaseline(with transaction: Transaction) {
        let dayOfWeek = Calendar.current.component(.weekday, from: transaction.date)
        let hour = Calendar.current.component(.hour, from: transaction.date)
        
        // Add to historical data
        historicalVelocities[dayOfWeek, default: [:]][hour, default: []]
            .append(transaction.velocityAtTime)
        
        // Keep last 30 days
        pruneOldData()
    }
    
    func getExpectedVelocity(dayOfWeek: Int, hour: Int) -> Double {
        let velocities = historicalVelocities[dayOfWeek]?[hour] ?? []
        guard !velocities.isEmpty else { return 1.0 }
        
        // Use median (more robust than mean)
        let sorted = velocities.sorted()
        let mid = sorted.count / 2
        return sorted[mid]
    }
    
    func getStandardDeviation(dayOfWeek: Int, hour: Int) -> Double {
        let velocities = historicalVelocities[dayOfWeek]?[hour] ?? []
        guard velocities.count > 2 else { return 1.0 }
        
        let mean = velocities.reduce(0, +) / Double(velocities.count)
        let variance = velocities.map { pow($0 - mean, 2) }.reduce(0, +) / Double(velocities.count - 1)
        return sqrt(variance)
    }
}

Personalised Thresholds

User TypeNormal VelocityAlert ThresholdCritical Threshold
Low spender1-2 txn/day5 txn/hour10 txn/hour
Moderate spender3-5 txn/day8 txn/hour15 txn/hour
High spender6-10 txn/day12 txn/hour20 txn/hour
Gambling pattern0-1 txn/day3 txn/hour5 txn/hour

Anomaly Detection

Statistical methods identify unusual spending patterns:

Z-Score Detection

func detectAnomaly(currentVelocity: Double, baseline: Baseline) -> AnomalyResult {
    let expected = baseline.expectedVelocity
    let stdDev = baseline.standardDeviation
    
    guard stdDev > 0 else {
        return .normal
    }
    
    // Calculate z-score
    let zScore = (currentVelocity - expected) / stdDev
    
    // Classify anomaly
    switch zScore {
    case ..<2.0:
        return .normal
    case 2.0..<3.0:
        return .moderate(zScore: zScore)
    case 3.0..<4.0:
        return .high(zScore: zScore)
    default:
        return .critical(zScore: zScore)
    }
}

// Example: Current velocity 15 txn/hour, baseline 3 ± 2
// z-score = (15 - 3) / 2 = 6.0 → CRITICAL

Exponential Moving Average (EMA)

EMA gives more weight to recent transactions for faster detection:

class ExponentialVelocityTracker {
    private var ema: Double = 0
    private let alpha: Double = 0.3  // Smoothing factor
    
    func update(with velocity: Double) -> Double {
        // EMA = α × current + (1-α) × previous_EMA
        ema = alpha * velocity + (1 - alpha) * ema
        return ema
    }
    
    func detectSpike(currentVelocity: Double) -> Bool {
        // Spike if current > 2x EMA
        return currentVelocity > (ema * 2.0)
    }
}

Category-Specific Velocity

Different categories have different velocity profiles:

Category Velocity Profiles

CategoryNormal PatternSpiral Pattern
Gambling0-1 txn/week10+ txn/hour, escalating amounts
Crypto1-2 txn/week5+ txn/hour during volatility
Shopping2-3 txn/week8+ txn/hour, multiple merchants
Food Delivery2-4 txn/week3+ txn/day (concerning but lower risk)
Groceries1-2 txn/weekRarely spirals

Category Velocity Calculator

func calculateCategoryVelocity(
    transactions: [Transaction],
    category: String
) -> CategoryVelocity {
    let categoryTransactions = transactions.filter { $0.category == category }
    
    // Calculate velocity for gambling-specific patterns
    if category == "gambling" {
        return GamblingVelocityAnalyzer.analyze(categoryTransactions)
    }
    
    // Calculate velocity for shopping-specific patterns
    if category == "shopping" {
        return ShoppingVelocityAnalyzer.analyze(categoryTransactions)
    }
    
    // Generic analysis for other categories
    return GenericVelocityAnalyzer.analyze(categoryTransactions)
}

Spiral Prediction

Machine learning predicts spirals before they peak:

Prediction Features

struct SpiralPredictionFeatures {
    // Velocity features
    let currentVelocity: Double
    let velocityAcceleration: Double
    let escalationFactor: Double
    
    // Historical features
    let zScore: Double
    let deviationFromBaseline: Double
    
    // Context features
    let timeOfDay: Int
    let dayOfWeek: Int
    let daysSincePayday: Int
    
    // Category features
    let categoryRiskScore: Double
    let categoryBudgetUtilisation: Double
    
    // Recent behaviour
    let bypassAttemptsLast24h: Int
    let interventionsLast24h: Int
}

Prediction Model

class SpiralPredictor {
    private let model: NeuralNetwork  // Pre-trained model
    
    func predictSpiral(features: SpiralPredictionFeatures) -> SpiralPrediction {
        let probability = model.predict(input: features.toVector())
        
        return SpiralPrediction(
            probability: probability,
            riskLevel: getRiskLevel(probability),
            estimatedPeakTime: estimatePeakTime(probability),
            recommendedAction: getRecommendedAction(probability)
        )
    }
    
    private func getRiskLevel(_ probability: Double) -> RiskLevel {
        switch probability {
        case ..<0.3: return .low
        case 0.3..<0.6: return .moderate
        case 0.6..<0.8: return .high
        default: return .critical
        }
    }
}

Intervention Triggers

Velocity thresholds trigger escalating interventions:

Trigger Configuration

struct VelocityTriggers {
    // Moderate risk: 3x normal velocity
    static let moderateThreshold = 3.0
    // High risk: 5x normal velocity
    static let highThreshold = 5.0
    // Critical risk: 8x normal velocity
    static let criticalThreshold = 8.0
    
    static func getAction(for velocity: Double, baseline: Double) -> InterventionAction {
        let ratio = velocity / baseline
        
        switch ratio {
        case ..

         

Intervention Actions

Risk LevelVelocity RatioAction
Normal<3xPassive monitoring
Moderate3-5xPush notification: "Spending elevated"
High5-8xSpendingShield YELLOW + AI check-in
Critical8x+SpendingShield RED + partner alert

Real-World Example

See how velocity detection works in practice:

Case Study: Marcus's Gambling Spiral

TimeTransactionVelocityAction
8:00pm$20 TAB bet1x (normal)None
8:15pm$50 Sportsbet4x (elevated)Notification sent
8:22pm$100 Ladbrokes8x (high)SpendingShield YELLOW
8:28pm$200 bet365 attempt15x (critical)SpendingShield RED + intervention
8:30pmBlocked-8-Step Negotiation activated

Outcome: Spiral interrupted at $170 instead of typical $800+ loss.

Performance Metrics

Velocity detection performance from production deployment:

MetricResult
Spiral Detection Rate94%
Average Detection Time12 minutes into spiral
False Positive Rate4%
Intervention Acceptance71%
Average Loss Prevention$420 per spiral

User Testimonials

"Whistl caught me mid-spiral. I'd already lost $200 but it stopped me before I lost $1000. That notification saved me." — Marcus, 28

"The velocity tracking knows my patterns. When I start spending fast, Whistl knows something's wrong before I do." — Sarah, 34

"I didn't realise how fast I was clicking 'confirm' until Whistl showed me: 8 transactions in 20 minutes. Scary." — Jake, 31

Conclusion

Transaction velocity algorithms detect spending spirals by identifying rapid increases in transaction frequency and amounts. By comparing real-time velocity to personal baselines, Whistl intervenes early—often before the user is consciously aware they're spiraling.

Velocity detection is one of 27 risk signals that power Whistl's impulse prediction system, working alongside neural networks, biometrics, and location data to provide comprehensive protection.

Get Real-Time Spending Protection

Whistl's velocity algorithms detect spending spirals in real-time. Download free and connect your bank accounts for instant protection.

Download Whistl Free

Related: MCC Analysis | 27 Risk Signals | AI Financial Coach