Machine Learning ML-17.01
Design a fraud detection system
Design real-time fraud detection: rare labels, velocity features, rules plus a model under 100 ms, review queues, chargeback delay and adaptive fraudsters.
The problem
A payment processor must decide, for every card payment, whether to approve it, block it or send it for extra checks, in the fraction of a second between the customer pressing "pay" and the confirmation screen. Most payments are legitimate; a tiny share are made with stolen cards, and the business loses the money, a fee and sometimes the customer's trust when that happens. Block too much and honest customers are turned away; block too little and fraud losses grow.
What makes it hard is the combination: fraud is rare (Stripe puts it on the order of 1 in 1,000 payments), the decision has a hard latency budget, the true label arrives weeks later, and the "data distribution" includes people actively trying to look legitimate. This is the system design version of Anomaly detection (anomaly detection) and Imbalanced data , with a real-time serving path attached.
Requirements
Functional
- Score every payment and return approve, review (step-up check or manual review) or block.
- Let merchants and analysts add rules (allow lists, block lists, thresholds) that take effect immediately.
- Explain each decision with the top contributing signals, for support, analysts and merchants.
- Feed outcomes back: chargebacks, analyst decisions and customer disputes become labels.
Non-functional
- Latency: decision under 100 ms end to end, including feature lookups.
- Availability: payments must not stop if the model is down; fall back to rules or approve-with-limits.
- Quality: maximize fraud caught (recall, or dollars saved) at a fixed false-decline budget, because every wrongly blocked payment is a lost sale.
- Freshness: velocity signals ("payments on this card in the last hour") must reflect events from seconds ago.
Out of scope
- Merchant onboarding risk and account takeover on login, which are separate models with different labels.
- Anti-money-laundering, which is regulated and investigated differently.
Estimates
# Back-of-the-envelope numbers for a card-fraud system. All inputs are illustrative assumptions.
PAYMENTS_PER_DAY = 20e6 # payments scored per day
PEAK_FACTOR = 5 # peak minute vs daily average
FRAUD_RATE = 1 / 1000 # order of magnitude for card-not-present fraud
AVG_FRAUD_AMOUNT = 150 # dollars per fraudulent payment
ANALYSTS, HOURS, REVIEWS_PER_HOUR = 50, 8, 30
DAY = 86_400
avg = PAYMENTS_PER_DAY / DAY
print(f"scoring load: {avg:.0f} payments/s on average, {avg * PEAK_FACTOR:.0f}/s at peak")
fraud = PAYMENTS_PER_DAY * FRAUD_RATE
print(f"fraud attempts: {fraud:,.0f} per day, about {fraud * AVG_FRAUD_AMOUNT / 1e6:.1f} million dollars")
capacity = ANALYSTS * HOURS * REVIEWS_PER_HOUR
print(f"manual review capacity: {capacity:,} payments/day = {capacity / PAYMENTS_PER_DAY:.3%} of traffic")
# A 1% false-positive rate on legitimate payments, in absolute terms
print(f"1% of good payments wrongly blocked: {PAYMENTS_PER_DAY * (1 - FRAUD_RATE) * 0.01:,.0f} customers per day")
# Velocity features need counters per key, kept for the longest window
cards, ips, devices = 200e6, 100e6, 150e6
windows, bytes_per_counter = 4, 16 # 1 h, 24 h, 7 d, 30 d; value + timestamp bucket
print(f"online counters: {(cards + ips + devices) * windows * bytes_per_counter / 1e9:.0f} GB before replication")Output
scoring load: 231 payments/s on average, 1157/s at peak
fraud attempts: 20,000 per day, about 3.0 million dollars
manual review capacity: 12,000 payments/day = 0.060% of traffic
1% of good payments wrongly blocked: 199,800 customers per day
online counters: 29 GB before replication With illustrative inputs of 20 million payments a day:
- Scoring load is modest: about 231 payments per second on average, about 1,157 at a 5x peak. The difficulty is latency and feature freshness, not throughput.
- Fraud volume: 20,000 fraudulent attempts a day, about 3 million dollars, if fraud is 1 in 1,000 at 150 dollars each.
- Review capacity sets the review band: 50 analysts reviewing 30 payments an hour for 8 hours handle 12,000 payments a day, only 0.06 percent of traffic. The model cannot send more than that to people, whatever its scores say.
- False positives are counted in customers: a 1 percent false-positive rate on good payments would block 199,800 honest customers every day. That is why fraud models are tuned at very low false-positive rates.
- Velocity counters for 450 million cards, IPs and devices over four windows fit in about 29 GB before replication: a job for an in-memory key-value store.
Data and labels
The label is "was this payment fraudulent?", and it is slow and imperfect.
- Chargebacks: the cardholder disputes the payment with their bank. This is the main fraud label, and it arrives with label delayThe time between a prediction and the moment its true outcome is known; for card fraud, the weeks it takes for chargebacks to be filed.: often weeks after the payment. Training data from the last few weeks is incomplete; treating "no chargeback yet" as "legitimate" teaches the model that recent fraud is fine. Only train on payments old enough for most chargebacks to have arrived, or model the delay explicitly.
- Blocked payments have no label. If the model blocks a payment, you never learn whether it was fraud. Over time the training data only contains payments the previous model let through, a feedback loop ( Cold start, diversity and feedback loops ). The standard fix is to let a tiny random share of would-be blocks through, or to send them to step-up authentication, and to weight those examples so evaluation stays unbiased.
- Analyst decisions from the review queue are fast labels, but biased toward the uncertain middle of the score range.
- Friendly fraud: some chargebacks are filed by the real cardholder; some fraud is never disputed. Labels are noisy in both directions.
Features
Fraud signals are mostly about context and behaviour, not about the payment alone:
- Velocity: payments, distinct cards, amounts and declines per card, IP, device, email and merchant over sliding windows (1 hour, 24 hours, 7 days). Card testing, where fraudsters try many stolen cards with small amounts, shows up as bursts.
- Identity consistency: does the cardholder name match the email, does the billing country match the IP and the card's issuing country, is this a new device for this account.
- History: account age, past chargebacks for this card or email anywhere on the network, merchant's fraud rate.
- Payment: amount relative to the customer's and merchant's usual amounts, currency, product category.
A network that sees many merchants has an advantage: a card used fraudulently at one merchant is suspicious at the next. Stripe's post describes Radar assessing more than 1,000 characteristics of each payment and lists signals such as whether the cardholder's name matches the email and how many cards have been seen with an IP address. Velocity features must be computed identically in training and serving ( Framing ML problems for products frames the problem; the feature store lesson, ML-15.04, covers point-in-time correct joins): a feature computed with "the next hour" in training leaks the answer.
Model choice and training
- Rules encode known patterns, react within minutes of a new attack, and give merchants control. They are brittle and need maintenance, but they never go away.
- Gradient-boosted trees ( XGBoost, LightGBM and CatBoost in practice ) are the usual first model: they handle mixed tabular features, missing values and interactions out of the box, and train fast.
- Deep networks take over at very large scale. Stripe's post describes moving from logistic regression to an ensemble "Wide and Deep" model of XGBoost plus a DNN, and in mid-2022 to a DNN-only, multi-branch architecture inspired by ResNeXt.
Training handles the imbalance with class weights or downsampled negatives (then recalibrate, Probability calibration ), uses a time-based split, never a random one, and evaluates with precision-recall metrics, because ROC curves look excellent when 99.9 percent of examples are negative.
High-level design
- Synchronous scoring The risk API fetches velocity and history features from the online store, runs the rules engine and the model in parallel, and combines them into one decision inside the 100 ms budget.
- Fresh features Every payment is also published to an event stream. Stream processors update the per-card, per-IP and per-device counters within seconds, so the next payment sees them.
- Decisions and review Clear cases are approved or blocked. The uncertain band goes to step-up authentication or to analysts, whose decisions become fast, if biased, labels.
- Slow learning loop Chargebacks arrive weeks later. Training uses only matured labels, and new models go through shadow scoring and a canary before replacing the current one.
Two loops run at very different speeds. The synchronous path must finish in tens of milliseconds and degrade gracefully: if the model times out, the rules still run, and the decision can fall back to approving low-value payments. The learning loop runs on days and weeks, limited by label delay rather than compute.
Deep dives
Deep dive 1: Rules plus model, and three decision bands
A single threshold forces a bad choice in the overlap where fraud and legitimate scores mix. Two thresholds make three bands: block above the upper one, approve below the lower one, and send the middle to step-up authentication (for example a one-time code from the bank) or to analysts. The upper threshold is set by the acceptable false-decline rate, the lower one by review capacity. Rules sit beside the model: hard blocks for known-bad entities, allow lists for trusted customers, and fast patches during an attack.
import numpy as np
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.metrics import average_precision_score
rng = np.random.default_rng(17)
def payments(n, fraud_rate=0.003, adapted=False):
"""Synthetic card payments. Fraud: bigger amounts, card testing bursts, many cards per IP, new devices."""
f = rng.random(n) < fraud_rate
burst = f & (rng.random(n) < (0.15 if adapted else 0.6)) # adapted fraudsters slow down their bursts
X = np.c_[rng.lognormal(np.where(f, 4.3, 3.6), 1.0), # amount
rng.poisson(np.where(burst, 4.0, 0.3)), # payments on this card, last hour
1 + rng.poisson(np.where(f, 0.6 if adapted else 2.0, 0.1)), # distinct cards on this IP, 24 h
rng.random(n) < np.where(f, 0.45, 0.85), # name matches email
rng.random(n) < np.where(f, 0.55, 0.08), # new device for this account
rng.random(n) < np.where(f, 0.7 if adapted else 0.1, 0.05)] # shipping country != card country
return X, f.astype(int)
def top(s, frac): # flag exactly the top frac of scores
flag = np.zeros(len(s), bool); flag[np.argsort(-s)[:round(frac * len(s))]] = True; return flag
def report(name, y, flag):
print(f"{name:34s} flags {flag.mean():6.2%} precision {y[flag].mean():5.1%} recall {flag[y == 1].mean():5.1%}")
Xtr, ytr = payments(400_000); Xte, yte = payments(200_000)
rules = lambda X: (X[:, 1] >= 3) | (X[:, 2] >= 4) # hand-written velocity rules
report("rules (velocity)", yte, rules(Xte))
model = HistGradientBoostingClassifier(max_iter=200, learning_rate=0.1, random_state=0).fit(Xtr, ytr)
s = model.predict_proba(Xte)[:, 1]
print(f"model PR-AUC {average_precision_score(yte, s):.3f} (a random scorer gets {yte.mean():.3f})")
k = rules(Xte).mean() # same number of flags as the rules
report("model, same flag volume as rules", yte, top(s, k))
report("model: block (top 0.1%)", yte, top(s, 0.001)) # top 0.1% blocked
report("model: block + review (top 0.5%)", yte, top(s, 0.005)) # next 0.4% to manual review
Xad, yad = payments(200_000, adapted=True) # next month: fraudsters adapt
sa = model.predict_proba(Xad)[:, 1]
report("old model, adapted fraud, top 0.5%", yad, top(sa, 0.005))
report("rules, adapted fraud", yad, rules(Xad))
Xnew, ynew = payments(400_000, adapted=True) # labels from the new pattern arrive
new = HistGradientBoostingClassifier(max_iter=200, learning_rate=0.1, random_state=0).fit(
np.r_[Xtr, Xnew], np.r_[ytr, ynew])
sn = new.predict_proba(Xad)[:, 1]
report("retrained model, adapted, top 0.5%", yad, top(sn, 0.005))Output
rules (velocity) flags 0.57% precision 33.5% recall 63.7%
model PR-AUC 0.579 (a random scorer gets 0.003)
model, same flag volume as rules flags 0.57% precision 37.7% recall 71.7%
model: block (top 0.1%) flags 0.10% precision 84.5% recall 28.0%
model: block + review (top 0.5%) flags 0.50% precision 42.1% recall 69.7%
old model, adapted fraud, top 0.5% flags 0.50% precision 17.9% recall 29.2%
rules, adapted fraud flags 0.40% precision 10.2% recall 13.4%
retrained model, adapted, top 0.5% flags 0.50% precision 23.2% recall 37.8% The code simulates payments with 0.3 percent fraud (more than in reality, so a small sample has enough positives) and six features: amount, card velocity, cards per IP, name-email match, new device and a shipping-card country mismatch.
- Velocity rules flag 0.57 percent of payments with 33.5 percent precision and 63.7 percent recall.
- A gradient-boosted model reaches a PR-AUC of 0.579, where a random scorer gets 0.003. Flagging the same 0.57 percent of traffic, it lifts precision to 37.7 percent and recall to 71.7 percent, because it combines weaker signals the rules ignore.
- Bands: blocking only the top 0.1 percent is 84.5 percent precise but catches 28.0 percent of fraud; adding a review band to the top 0.5 percent catches 69.7 percent at 42.1 percent precision. The review band is where most of the recall comes from.
Deep dive 2: Fraudsters adapt
Fraud is adversarial: when a pattern gets blocked, fraudsters change it. Stripe's post notes that fraud on its network shifted over the years from mainly stolen-card fraud to a growing mix of card fraud and high-velocity card testing attacks.
In the code, next month's fraudsters slow their card-testing bursts, use fewer cards per IP, and instead ship to countries that do not match the card. The old model's recall at a 0.5 percent flag rate falls from 69.7 to 29.2 percent, and the velocity rules fall to 13.4 percent recall. Retraining once labels from the new pattern arrive lifts the model back to 37.8 percent: better, but the new pattern is simply harder to separate. Defences against adaptation:
- Fast feedback: analyst labels and rules cover the weeks before chargebacks arrive.
- Frequent retraining on the most recent matured data, with drift monitoring on scores and key features.
- Network features that are expensive for a fraudster to change (device, card history across merchants), rather than ones that are free to change (amount, time of day).
- Do not leak the model: decline reasons shown to the payer should not teach fraudsters which feature tripped.
Deep dive 3: Label delay and evaluation
Because chargebacks arrive weeks later, two things go wrong if you are careless. Training on recent data treats not-yet-disputed fraud as legitimate. And offline evaluation on a recent test set overstates precision, because some flagged payments that look like false positives today will be charged back next month. Use a test window old enough for labels to have matured, report metrics in dollars as well as counts, and compare new models against the current one on the same window.
Online, a new model is first run in shadow mode, scoring live payments without acting on them. Its would-be decisions are compared with the current model's, and a canary on a small share of traffic follows. A classic A/B test on fraud is slow because the outcome metric (chargebacks) is delayed; teams often A/B on fast proxies such as decline rate and step-up pass rate, and confirm on chargebacks later.
Monitoring
- Decision mix per merchant and country: approve, review and block rates. A jump usually means an attack or a broken feature.
- Score distribution and feature drift, especially for velocity counters, which break silently when the stream lags.
- Review queue health: queue length, time to decision, and analyst agreement with the model.
- Matured performance: precision, recall and dollars lost, by cohort as chargebacks arrive.
- Latency at p99 for the whole decision and for each feature lookup.
Bottlenecks and failure modes
| Failure | Impact | Mitigation |
|---|---|---|
| Model or feature store times out | No score inside 100 ms | Rules-only fallback; approve low-value payments with limits |
| Stream lag | Velocity counters stale; card testing passes | Alert on lag; conservative rules during lag |
| Training on immature labels | Model learns recent fraud is fine | Label maturity window; delay-aware training |
| Blocked payments never labelled | Feedback loop narrows the data | Small random holdout of would-be blocks; step-up instead of block |
| New attack pattern | Recall drops for weeks | Analyst rules, fast labels, frequent retraining, drift alerts |
| Review queue overflows | Uncertain payments wait or are auto-decided | Size the review band to capacity; prioritize by amount |
Trade-offs we made
| Option | Wins when | Loses when |
|---|---|---|
| Rules plus model (chosen) | Immediate control during attacks; model catches subtle combinations | Two systems to maintain; rules can conflict with the model |
| Model only | One system; learns everything from data | Slow to react before new labels exist; no instant patch |
| Three bands with review (chosen) | Most recall comes from the uncertain middle; fewer false declines | Costs analyst time; adds friction for some good customers |
| Gradient boosting first | Strong on tabular data, fast to train and explain | Harder to use embeddings and transfer learning at very large scale |
Extensions
- Graph features: link cards, devices, emails and addresses into a graph and score clusters, which exposes fraud rings.
- Per-merchant thresholds: merchants with high margins tolerate more risk than ones selling resellable goods.
- Cost-sensitive objective: optimize expected dollars saved minus false-decline cost, not a symmetric loss.
- Explanations for merchants using feature attributions ( Explainability ), without exposing the model to fraudsters.
In the wild
Stripe
How we built it: Stripe Radar (opens in a new tab)The 2023 post describes Radar assessing more than 1,000 characteristics of each payment and deciding in under 100 milliseconds, with fraud on the order of 1 in 1,000 payments. Stripe moved from logistic regression to a Wide and Deep ensemble of XGBoost and a DNN, then in mid-2022 to a DNN-only multi-branch architecture: simply dropping XGBoost would have cost 1.5 percent of recall, but the new design cut training time by over 85 percent, to under two hours, and a 10x increase in training data still improved the model. The post also stresses explanations: risk insights show users which features contributed to a decline.
Check yourself
-
Why is ROC AUC a poor headline metric for a fraud model?
Show answer
B. With 99.9 percent legitimate payments, the false-positive rate stays tiny even when false positives vastly outnumber caught fraud; precision-recall shows that At 1 in 1,000 fraud, a 1 percent false-positive rate means about ten false alarms for every fraudulent payment. PR curves expose this.
-
Your team trains on all payments up to yesterday, treating payments without a chargeback as legitimate. What goes wrong?
Show answer
B. Recent fraud whose chargebacks have not arrived is labelled legitimate, so the model learns that the newest fraud patterns are fine Label delay: train on a window old enough for most chargebacks to have arrived, or model the delay.
-
Analysts can review 12,000 payments a day out of 20 million. How should that shape the decision policy?
Show answer
B. Size the review band so it sends at most about 0.06 percent of traffic, the highest-value uncertain cases first Capacity, not the model, sets the review band.
-
In the code, why does the old model lose recall next month?
Show answer
B. Fraudsters changed behaviour: fewer velocity bursts and a new country-mismatch pattern the model had learned little about Recall at a 0.5 percent flag rate fell from 69.7 to 29.2 percent; retraining on the new labels recovered it to 37.8.
-
Why let a tiny random share of would-be blocked payments through, or send them to step-up checks?
Show answer
B. Blocked payments never get labels, so without this the training data only reflects what past models approved It breaks the feedback loop and keeps evaluation unbiased.
Recap
- Fraud detection decides approve, review or block in under 100 ms, for a rare, costly and adversarial class.
- Velocity, identity-consistency and network history features matter more than the payment alone; they must be fresh and computed identically in training and serving.
- Rules plus a model, with two thresholds: block the clear cases, review the uncertain band up to analyst capacity, approve the rest.
- Labels arrive weeks later through chargebacks, blocked payments are never labelled, and analyst labels are biased; design training and evaluation around all three.
- Fraudsters adapt; fast labels, rules, frequent retraining and drift monitoring keep recall from decaying.