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
- Trigger: Emotional event, location, or time initiates urge
- Initial spending: First transaction (often small)
- Escalation: Rapid subsequent transactions, increasing amounts
- 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 Type | Normal Velocity | Alert Threshold | Critical Threshold |
|---|---|---|---|
| Low spender | 1-2 txn/day | 5 txn/hour | 10 txn/hour |
| Moderate spender | 3-5 txn/day | 8 txn/hour | 15 txn/hour |
| High spender | 6-10 txn/day | 12 txn/hour | 20 txn/hour |
| Gambling pattern | 0-1 txn/day | 3 txn/hour | 5 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
| Category | Normal Pattern | Spiral Pattern |
|---|---|---|
| Gambling | 0-1 txn/week | 10+ txn/hour, escalating amounts |
| Crypto | 1-2 txn/week | 5+ txn/hour during volatility |
| Shopping | 2-3 txn/week | 8+ txn/hour, multiple merchants |
| Food Delivery | 2-4 txn/week | 3+ txn/day (concerning but lower risk) |
| Groceries | 1-2 txn/week | Rarely 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 Level Velocity Ratio Action
Normal <3x Passive monitoring
Moderate 3-5x Push notification: "Spending elevated"
High 5-8x SpendingShield YELLOW + AI check-in
Critical 8x+ SpendingShield RED + partner alert
Real-World Example
See how velocity detection works in practice:
Case Study: Marcus's Gambling Spiral
Time Transaction Velocity Action
8:00pm $20 TAB bet 1x (normal) None
8:15pm $50 Sportsbet 4x (elevated) Notification sent
8:22pm $100 Ladbrokes 8x (high) SpendingShield YELLOW
8:28pm $200 bet365 attempt 15x (critical) SpendingShield RED + intervention
8:30pm Blocked - 8-Step Negotiation activated
Outcome: Spiral interrupted at $170 instead of typical $800+ loss.
Performance Metrics
Velocity detection performance from production deployment:
Metric Result
Spiral Detection Rate 94%
Average Detection Time 12 minutes into spiral
False Positive Rate 4%
Intervention Acceptance 71%
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