The problem with rules, and the problem with replacing them
Banks move trillions of euros a day through SWIFT and SEPA rails. High-value cross-border payments are a prime fraud target precisely because they're valuable and because a corridor like Ireland→Nigeria or Germany→Kenya can look completely ordinary until you know what to look for. Rule-based systems have been the industry default for decades because they're auditable and fast, but they only catch patterns someone has already written a rule for. Pure black-box ML flips that weakness into a different one: it can catch novel patterns, but a compliance officer can't get a straight answer out of it about why a payment was flagged.
I built an end-to-end pipeline to test what happens when you take both seriously instead of picking one: real-time feature engineering on streaming transactions, a hybrid detection layer, and SHAP-generated reason codes attached to every alert. The results weren't uniformly clean — one part of the pipeline actively made things worse before the final design got it right, which is a more useful story than “everything worked.”
Building features that mean something for payments
Generic fraud features (transaction amount, hour of day) get you partway there. The features that actually separate fraud from noise in this domain are the ones that encode payment-specific behavior:
- ›
velocity_1h/velocity_24h— how many transactions a sender has made in rolling 1-hour and 24-hour windows, computed online with expiring deques - ›
amount_zscore_30d— how anomalous this transaction's amount is relative to the sender's own 30-day history, computed with a Welford online algorithm so it updates in O(1) per transaction without storing the full history - ›
corridor_risk— a lookup table scoring sender→receiver country pairs (EUR→Kenya and Ireland→Nigeria score 1.0; Germany→France scores the 0.1 default), reflecting real-world corridor risk patterns - ›
device_changed_24handip_mismatch— account takeover signals - ›
receiver_fan_in_24h— how many distinct senders have paid into this receiver account in the last 24 hours, which is the direct signature of a mule account collecting funds from multiple compromised sources - ›
benford_deviation— how far a sender's transaction amounts deviate from Benford's Law's expected leading-digit distribution, a classic forensic-accounting signal for fabricated numbers
Every one of these stateful features has a matching online implementation that has to produce numerically identical output to the offline batch version — verified in the test suite to within 1e-6 across 100 synthetic transactions with overlapping senders, receivers, and devices. That parity check matters more than it sounds: a feature pipeline that behaves differently in training than it does at serving time is a silent, hard-to-debug source of model degradation in production, and it's the kind of bug that doesn't show up until real traffic hits it.

Top-10 SHAP feature importance — mean absolute SHAP value across all transactions
When I ranked all 20 features by mean absolute SHAP value, the ranking confirmed the domain intuition: ip_mismatch and device_changed_24h — the two account-takeover signals — came out on top, followed closely by the night-plus-high-value rule feature and 24-hour velocity. is_cross_border alone, on its own, carried almost no signal (0.0005) — which makes sense, since nearly every transaction in a SWIFT/SEPA dataset is cross-border by definition. The lesson: a feature being domain-relevantdoesn't mean it's discriminativeonce you're already conditioning on more specific signals.
The hybrid model that wasn't
Here's the part I think is more useful than a clean success story. The obvious first approach — combine a simple rule (“payment > €1M at midnight”) with an XGBoost probability score — is the textbook definition of a “hybrid” detector. I built it. It performed worse than XGBoost alone.

5-model recall/precision comparison at 0.5% FPR operating point
XGBoost by itself reached 70.9% recall at a 0.5% false-positive rate and 95.0% precision in the top 1% of alerts — genuinely strong numbers. The naive rules+XGBoost combination collapsed recall to 0.0% and precision to 27.5%, worse than the rules-only baseline on its own. The reason is mechanical rather than mysterious: combining a hard rule threshold with a continuous probability score in the wrong way (e.g., requiring both to fire, or averaging a binary 0/1 with a continuous score) distorts the decision boundary in ways that are easy to get wrong and easy to miss if you only check aggregate accuracy rather than recall at a specific operating point.
What actually worked was adding an Isolation Forest anomaly layer scoped to the 8 most discriminative features and blending its normalized anomaly score with the XGBoost probability (70% XGBoost, 30% Isolation Forest) — pushing recall to 72.5%at the same 0.5% FPR operating point, with precision at 94.0%, while adding a genuinely independent detection signal that doesn't rely on the same decision boundary XGBoost already learned. Restricting Isolation Forest to the 8 strongest features rather than all 20 more than doubled its standalone PR-AUC (0.28 → 0.55) — high-dimensional noise genuinely hurts unsupervised anomaly detection in a way it doesn't hurt a supervised gradient-boosted model.
The takeaway I'd give any team building something similar: “hybrid” is not automatically better than either component alone. It depends entirely on how the signals are combined, and the only way to know is to measure recall and precision at your actual target operating point — not just look at whether the model runs.
Explaining every alert, not just scoring it
Article 13-style transparency requirements aside, a fraud analyst who gets a bare probability score with no explanation is going to either rubber-stamp it or ignore it. Every alert in this pipeline comes with up to three SHAP-derived reason codes, generated by ranking features by absolute SHAP value and applying a domain-specific template only when the underlying feature value clears a meaningful threshold — so a high SHAP value on a feature that's actually near zero doesn't produce a misleading reason.
A representative output for a flagged transaction:
“High-value payment at 02:30 (outside business hours)” · “New device seen within 24 hours” · “High-risk corridor IE→NG”
That's not a canned template — it's assembled per-transaction from whichever features actually drove that specific score, which is the difference between a compliance-shaped checkbox and something an analyst can actually act on.
What it costs to be fast, and what it's worth

p50/p95/p99 end-to-end scoring latency vs SLA targets
The full scoring path — feature lookup, XGBoost inference, Isolation Forest scoring, ensemble blending, SHAP reason generation — runs at a p50 of 17.1ms and p95 of 19.9ms, against an SLA target of under 1,000ms/2,000ms respectively. Throughput held at 57.4 transactions/secondin a single-process stress test against a target of 10/s. There's real headroom here before this would need horizontal scaling to handle production SWIFT/SEPA volumes.
To make the recall/precision numbers concrete, I modelled a hypothetical scenario at Citi-scale volume assumptions (500,000 daily transactions, 0.5% fraud rate, €250,000 average fraudulent amount) — purely illustrative, not a real institution's actual figures. At those assumptions, moving from a 40%-recall rules-only baseline to the 72.5%-recall hybrid model means catching roughly 813 more fraudulent transactions per day, while simultaneously cuttingfalse-positive review costs by more than half. Whatever the real numbers turn out to be at any actual institution's volume and fraud rate, the direction of that trade — more fraud caught, fewer wasted reviews — is the actual argument for why precision matters as much as recall in a fraud system: false positives aren't free, they cost analyst hours that could go toward the transactions that matter.
Keeping it honest after deployment
A model's job doesn't end at the training script. I built a drift-monitoring pass using the Population Stability Index (PSI)across the five most important features, comparing the first 30 days of data against the most recent 30 days. Every tracked feature came back under 0.10 (the “stable, no action needed” threshold) — corridor_risk and is_night barely moved at all (PSI of 0.0008 and 0.0002), while amount_zscore_30d showed the most drift at a still-comfortable 0.0603. The mean fraud score itself shifted by −0.0223, well under the 0.05 flag threshold. None of this is exciting, and that's the point: a monitoring pass that never has anything to report is a monitoring pass doing its job, and having it in place at all is what separates a demo from something built to run.
