Security · 2026-09-07
The Pre-Filter Pattern: Paying for Heavy Fraud Signals Only on the Suspicious Tail
By J. W. Bouckaert
The cost breakdown
A typical fraud stack at a fintech or marketplace runs three to five paid signals on every login, checkout or recovery request: device intelligence, phone intelligence, an email or IP reputation feed, a behavioural model, sometimes a document or liveness check. Each one bills per call, and each one is called on every request, because the stack has no way to know in advance which requests are worth the spend.
That gives you a cost line that is volume times the sum of the per-call prices, and a latency line that is either the slowest signal or the sum of them, depending on whether the calls are fanned out or chained. Neither line cares that most of the traffic was fine.
The pre-filter pattern changes the order of operations. One cheap, deterministic check runs first and sorts the traffic into three bins. The expensive signals run on one of the bins alone.
The three bins
The pre-filter has to answer a narrow question and answer it the same way every time. For account takeover the question is: has the carrier line tied to this account changed hands recently?
| Bin | What the pre-filter saw | What happens next |
|---|---|---|
| Hard fail | A SIM swap or port inside the risk window | Deny on the SMS and voice paths, route to a channel the attacker does not hold, and spend nothing on the heavy signals. |
| Clean | Stable line, no swap or port in the window, number reachable | Proceed. Record the result. The heavy signals do not run, except on a sampled slice (below). |
| Ambiguous | Signal unavailable, MVNO opacity, provider error, or a swap older than the window | Fan out to the existing engine exactly as today. Nothing changes for this bin. |
The savings come from the clean bin. The safety comes from two rules about it: the pre-filter must be a fact rather than a score, and the clean bin must be audited by sample so the savings are measured against what they cost in missed catches.
The pre-filter is a fact
A device fingerprint is a probability. A behavioural model is a probability. A probability makes a poor gate, because whatever threshold you pick, tuning it means re-running the heavy signals to see what the threshold missed, which is the spend you were trying to avoid.
A carrier-state check is a fact about a line at a moment. The line either changed SIM in the last seventy-two hours or it did not. The number either ported in the last thirty days or it did not. The provider that answers is the carrier data path itself, through Vonage Number Insight Advanced or Twilio Lookup v2, and the answer arrives as booleans and an age in hours, not as a score you have to calibrate against ninety days of chargebacks.
What our lookup returns, field by field, so you can write the rule against it:
| Field | Type | Meaning |
|---|---|---|
sim_swap_detected | boolean | The provider reports a SIM swap on the line. |
sim_swap_age_hours | integer or null | Hours since the last swap, when the provider reports it. |
ported | boolean | The number has moved carrier (Vonage reports it directly; on Twilio it is inferred from the current and original network codes). |
reachable | boolean | The line is reachable on the network. |
roaming | boolean | The line is roaming. |
carrier_name, carrier_type | string or null | Operator and line type. |
signal_type | string | sim_swap_detected when a swap was reported, otherwise carrier_check. |
risk_score | number, 0 to 1 | A small derived score: 0.60 for a swap, plus 0.20 if it is under 24 hours old or 0.10 if under 72, plus 0.15 for a port and 0.10 if unreachable. |
latency_ms | integer | Time spent waiting on the provider for this call. |
Write the pre-filter rule against the booleans and the age. The score exists for dashboards and for tenants who want a single number to plot; the gate should be the fact.
The rule table we recommend, and the one described in our credit union recovery piece, is short:
sim_swap_detectedandsim_swap_age_hoursinside your window, orportedinside your window: hard fail.- No swap, no port,
reachabletrue: clean. - Provider error, or a field your provider does not return for this line: ambiguous. The lookup returns a 502 with an explicit failure rather than a default, and writes a
carrier.lookup_failedaudit row, so the ambiguous bin is visible in your logs rather than silently folded into clean.
The window is yours to set. Twenty-four hours catches the overnight swap that drains an account before the member notices; seventy-two hours costs more ambiguous traffic and catches the patient attacker. We have written about why the age of the signal matters more than its strength; the short form is that a swap reported yesterday is a different fact from a swap reported thirty seconds ago, and your rule should say which one it means.
The arithmetic
Run this with your own numbers. The inputs are your monthly decision volume, the blended per-decision cost of the heavy bundle, the share of traffic that lands in each bin, and the size of the audit sample you keep sending through the heavy bundle from the clean bin.
The placeholder figures below are illustrative, chosen to make the arithmetic easy to follow, and none of them is a measurement of any customer.
| Input | Placeholder |
|---|---|
| Monthly decisions | 1,000,000 |
| Heavy bundle, blended cost per decision | $0.08 |
| Clean share after the pre-filter | 85 percent |
| Ambiguous share | 12 percent |
| Hard-fail share | 3 percent |
| Audit sample from the clean bin | 5 percent |
Before the pre-filter, the heavy bundle runs on everything: 1,000,000 times $0.08, or $80,000 a month.
After it, the heavy bundle runs on the ambiguous bin plus the audit sample of the clean bin: 12 percent plus 5 percent of 85 percent, which is 16.25 percent of traffic, or 162,500 decisions, at $0.08 each: $13,000.
The pre-filter itself runs on everything. Our Enterprise plan includes one million signals a month at $499 and meters further signals at $0.04 each, so at this volume the pre-filter costs $499; the pricing page has the tiers. If you bring your own provider keys, add your provider's per-lookup price on top; if you use ours, it is inside the signal price.
| Before | After | |
|---|---|---|
| Heavy bundle | $80,000 | $13,000 |
| Pre-filter | $0 | $499 |
| Total | $80,000 | $13,499 |
The saving is a function of the clean share and the heavy-bundle price, and the break-even is one line: the pre-filter pays for itself when the clean share times the heavy cost per decision exceeds the pre-filter cost per decision. At $0.04 against a $0.08 bundle, that is a clean share above fifty percent. At $0.0005 per signal, which is what the included allowance works out to when it is fully used, it is a clean share above one percent.
Two things this arithmetic does not do. It does not model fraud loss, in either direction. And it does not tell you the clean share; that number comes from running the lookup in shadow on your real traffic for a few weeks and counting, which is the first step of the rollout below.
Limits of the clean bin
A carrier-state check sees the line. It does not see the device, the session, the velocity of attempts across accounts, or a credential that was phished intact and replayed from the victim's own unchanged phone. A stack that drops every heavy signal on the clean bin will lose whatever those signals were catching there.
That is what the audit sample is for. Keep a random slice of the clean bin flowing through the full bundle, five percent in the placeholder table, and compare its hit rate with the hit rate the bundle used to produce on the same traffic. If the sample keeps catching things at the old rate, the clean bin is not clean on the dimensions the carrier check cannot see, and the heavy signal that is doing the catching should move back into the inline path for that segment. If the sample goes quiet, you have measured what you were paying for.
The sample also keeps the heavy vendors' models fed with labelled traffic, which matters if any of them learn from your outcomes.
Two more limits belong on the record. Carrier data coverage varies by operator and country, and an MVNO may report nothing at all for a line it resells; those lines land in the ambiguous bin, and if your customer base skews toward them, your clean share will be lower than a placeholder table suggests. And a check inside the request does not help with a swap that happens after the request; that is a monitoring problem, covered by our detection latency piece.
Latency
A pre-filter that sits inline adds its own time to every request, so the number that matters is the pre-filter's own latency, measured where you run.
Separate the legs, as our benchmarking guide says to. The transport leg is the round trip from your edge to ours. The decision leg is our own work: authenticate the key, resolve the tenant, keyed-hash the identifier, match a playbook, decide. It is recorded per request as decision_ms, separately from the full response time, and the docs publish its production p50 as sub-100ms on a live key. The provider leg is the carrier lookup itself, which belongs to Vonage or Twilio, is returned to you as latency_ms on every call, and is capped at fifteen seconds before we return the ambiguous failure instead of waiting.
The playground times the transport leg from your browser, against the production edge, with no signup. Run it from the region your traffic comes from before you believe any figure, ours included.
Wiring it in
Nothing in this pattern requires replacing the engine you have. The lookup is one POST with an API key, a tenant id and the phone number; the number is keyed-hashed with HMAC-SHA-256 under a server-held pepper before anything is stored, and the hashing is keyed precisely because phone numbers are enumerable. The response is the field table above. A rules engine consumes booleans without ceremony.
Two side effects happen on our side that you do not have to build. Each lookup writes an audit row carrying the provider, the signal type, the risk score and the provider latency, keyed by a prefix of the hashed number, so the decision can be reconstructed later without a phone number in the log. And when the lookup reports a swap, the result is forwarded into the ingest pipeline as a sim_swap_detected event, which is a hard signal type: any playbook you have attached to it runs immediately, bypassing trust scoring, and any agent delegates on the account are deactivated in the same pass.
The rollout order that keeps the risk on our side of the ledger:
- Shadow. Call the lookup on live traffic and log the bin without acting on it. This is where the clean share comes from, and where you find the MVNO coverage gap in your own base.
- Hard fail first. Turn on the deny for the hard-fail bin. It is the smallest bin and the one with the clearest loss attached, so the outcome is easy to read.
- Skip on clean, with the sample. Start routing the clean bin around the heavy bundle, keeping the audit sample. Compare hit rates monthly.
- Narrow the sample. As the sample stays quiet, reduce it, and move the saving onto the loss report where the finance team can see it.
The sequence matters because each step is reversible on its own and produces the number that justifies the next one.
Scope
We do not score sessions, learn from chargeback labels, or tell you whether a transaction is fraud. The pre-filter is one fact about one line, delivered fast enough to sit in front of the systems that do those things, and cheap enough to run on every request so that those systems do not have to. The fraud prevention API page describes where that fact sits in a stack alongside the engines that consume it.