Design a trading system that prevents fraud
"Fraud prevention here is not one system, it is a decision taken at three different latencies, and the mistake is trying to take all of it inline. The order path gets ten milliseconds and can only look things up. The stream gets seconds and does the real detection. The graph gets hours and finds the collusion no single account reveals. And the reason that split is safe is not that an order can be undone, because a filled trade cannot be: it is that an order's loss is BOUNDED and the position is still an asset I can liquidate, while a payout's loss is total and final. So the strong gate goes where the money leaves, not where the trading happens."
Understanding the problem
5 minFunctional Requirements
- Every order should pass a risk decision before it is routed, inside the trading latency budget, with the mandatory pre-trade controls applied without exception.
- The system should detect fraudulent patterns across accounts, sessions and devices in near real time, and be able to freeze an account.
- Withdrawals and other irreversible egress should be gated on a stronger, slower check than orders are.
- Every decision should be explainable and auditable years later, and an analyst should be able to review and reverse it.
Below the line (out of scope):
- Identity verification at onboarding. It is a vendor call and a different problem: I am defending accounts that already exist and have already passed KYC.
- The matching engine, market data, and order routing. Say the cut out loud, because it is what lets me treat "allowed" as the end of my responsibility.
- Margin and credit risk. Those are market risk, not fraud, and conflating them is a common way to lose half the hour.
- Tax reporting and statements, and the analyst tooling's own UI.
Non-Functional Requirements
- Do the two-rate comparison first, because it decides where the expensive checks are allowed to live.
- 50M orders a day is about 580 a second averaged over 24 hours, but the market is open 6.5 hours, so the in-session average is about 2,100 a second, and the open concentrates it further: call it 5,000 a second at peak.
- 200K withdrawals a day is 2.3 a second. Flat, all day, no open-bell spike.
- That is 250 to 1 by daily count, and far wider at peak. Anything I want to spend 500 ms on is affordable on the withdrawal path and unthinkable on the order path.
- So the design is not "how do I make fraud checks fast". It is "which checks have to be on the fast path at all", and the answer is: almost none.
- Then the loss asymmetry, which is the other half of the same idea. Say it as bounded versus total, not as reversible versus irreversible, because a filled trade is not reversible.
- A bad order costs market exposure. An unfilled one can be cancelled; a filled one generally cannot be undone, since clearly-erroneous busts are a narrow venue remedy for off-market prices and fraud is not a ground for one.
- What makes it survivable is that the loss is BOUNDED, by the notional cap times the move over the seconds until detection, and the position is still an asset that can be liquidated.
- A completed withdrawal is gone. There is no recall on a wire, and an ACH pull can be returned against me for up to 60 days after I have already paid out.
- So the two paths get opposite failure policies, which is the single most useful sentence on this page and the thing I would lead the dives with.
- Latency: p99 under 100 ms for an order end to end, and the risk decision gets 10 ms of it. That is a hard budget handed to me, not one I get to negotiate, because the rest belongs to routing and the venue.
- Say which entity you are in one sentence, because the obligations differ: I am the retail broker, the matching venue is downstream and not mine. That decides who owns 15c3-5, who can bust a trade (the venue, and not for fraud), and where self-trade prevention actually executes.
- Availability: the risk system must not be able to stop customers trading. A broker that stops accepting orders because a fraud service is down has caused a larger incident than the fraud would have, and has handed an attacker a denial-of-service lever.
- Explainability is a functional requirement wearing a non-functional hat. Every decision stores its inputs, the rule version and the score version. A regulator will ask why this account was frozen, and "the model said so" is not an answer anyone accepts.
- Retention: 7 years, immutable. SEC 17a-4 is six years for the core books and records and the BSA is five for SAR-related material, so seven is a covering envelope rather than a magic number. At roughly 1 KB a decision that is 50 GB a day and about 130 TB, which is cheap.
- Feedback latency: a confirmed fraud case must change behaviour in minutes. If the loop from "analyst confirms" to "the inline path knows" is a nightly model retrain, the same attacker runs all night.
- False positives have a real, asymmetric cost. Blocking a legitimate trade during a market move loses the customer money and generates a complaint I will have to answer in writing. This is not a system where you turn the threshold up and call it safety.
Entities + API
5 minDefining the Core Entities
- Account is the customer relationship: id, status (active, restricted, frozen), tier, and the KYC record it was opened against.
- Session and Device are separate on purpose.
- A device fingerprint outlives a session and is what links accounts an attacker is running together.
- Binding a session to a device is what makes "this order came from somewhere new" a signal rather than a guess.
- Order is an intent to trade: account, instrument, side, quantity, and the session and device it came from. Carrying the device on the order is what lets the stream correlate later without a join.
- Transfer covers deposits and withdrawals, and it carries the thing that matters most here:
settledAt. A deposit that has landed is not a deposit that is final. - Beneficiary is a withdrawal destination: bank account or wallet, with
addedAt. The age of this row is one of the strongest signals in the system. - RiskDecision is immutable and is the audit trail: the verdict, the features it saw, the rule version, the score version, and the latency it took.
- It records the INPUTS, not just the answer. Re-deriving why a decision was made two years later is impossible otherwise, because the score has moved a thousand times since.
- Case is the analyst's unit of work: the accounts involved, the evidence, the state, and the outcome. A closed case is also a training label, which is the only place clean labels come from.
- Rule is versioned and deployable on its own, with a shadow mode. Fraud moves faster than a release train, so rules cannot be code.
API or System Interface
POST /v1/orders // the hot path, 10 ms of risk budget
// → 201 { orderId } | 202 { stepUpRequired } | 403 { reason, decisionId }
POST /v1/transfers/withdrawals // { amount, beneficiaryId }
// → 202 { transferId, status: HELD | RELEASED, eligibleAt }
POST /v1/beneficiaries // → 201 { beneficiaryId, withdrawableAt } // starts the cool-off
POST /v1/accounts/{id}/freeze // internal, and reversible on purpose
GET /v1/cases?state=open&sort=exposure
POST /v1/cases/{id}/resolve // { outcome, notes } -> becomes a training label
POST /v1/rules // { definition, mode: shadow | enforcing }
- The order endpoint has three outcomes, not two, and the middle one is the useful one.
- Allow and block are the obvious pair. Step-up (re-authenticate, then retry) is what lets the system act on suspicion without eating a false positive.
- Most real signals are ambiguous. A design with only allow and block forces every ambiguous case into one of two wrong answers.
- Withdrawal returns 202 with a status, always. There is no synchronous success. Making the held state normal rather than exceptional is what stops "held" from reading as an outage to the client.
- Adding a beneficiary returns
withdrawableAtin the response. The cool-off is visible at the moment it starts, not discovered later when a withdrawal is refused, which is the difference between a considered policy and a bug. decisionIdcomes back on a block. Support can look up exactly what happened without an engineer, and the customer gets an answer.- Rules deploy in shadow mode first. A new rule runs and logs its verdict without enforcing it, so its false-positive rate is measured on real traffic before it can refuse anybody.
- What the API deliberately does not say: a user under investigation is never told. Tipping off is itself an offence in most jurisdictions, so the product surface for a SAR-flagged account looks like an ordinary hold.
High-level design
10 min, end to end, no dives yet1) A risk decision on every order, inside the budget

- The inline path computes almost nothing, and that is the design rather than a compromise.
- It reads one precomputed score for the account out of a feature store, about 2 ms.
- It runs deterministic rules that need no joins: velocity caps, watchlist membership, self-trade prevention, notional limits. About 1 ms.
- It returns allow, step-up, or block, and writes the decision asynchronously.
- No model runs here, and be precise about why, because the sloppy version of this claim is easy to puncture.
- It is not that inference is slow. A compiled gradient-boosted tree scores a prepared feature vector in tens of microseconds in-process, and card networks score every authorisation inline.
- What is unaffordable is computing the FEATURES, which needs joins over history, and taking a network hop to an inference service, which adds a tail you do not control.
- So the rule is: no feature computation and no RPC on the order path. A model co-located in the decisioner reading precomputed features is fine and fits the budget, which is a better answer than banning models outright.
- The score being minutes stale is accepted, out loud. Staleness is what the stream in the next section exists to cover, and pretending the inline path is authoritative is how designs end up with a 200 ms order.
- The decision log is written for the regulator, not for the engineer. Inputs, rule version, score version, verdict, latency. Immutable, 7 years, roughly 130 TB.
2) Near-real-time detection over the event stream

- Everything publishes to one stream: orders, logins, device changes, transfers, beneficiary additions. One stream rather than several is what makes cross-signal patterns expressible at all, and the account takeover sequence is precisely a cross-signal pattern.
- Carry order MODIFICATIONS and CANCELS, not just fills, and say why explicitly. Spoofing and layering are patterns in orders that were never meant to execute, so a feed of fills alone cannot see them. Adding cancel events roughly triples the stream volume and is the price of being able to answer a manipulation question at all.
- Windowed aggregates do the work the inline path had no time for: orders per minute per account, distinct instruments per hour, logins per device, failed step-ups per IP.
- The correlator is the part people leave out.
- It joins accounts that share a device fingerprint, an IP, or a beneficiary bank account.
- A per-account rule cannot see a ring by construction, no matter how good it is, because each individual account looks ordinary.
- The output is two levers, and they are deliberately different in strength.
- Open a case for an analyst, ranked by exposure, when the pattern is suggestive.
- Pull the freeze lever automatically when it is unambiguous. That is defensible only because a freeze is reversible: an analyst can undo it in minutes, and the customer gets an apology rather than a loss.
- The loop closes back into the feature store. A confirmed pattern updates the score that the 10 ms path reads, so detection at second N changes inline behaviour at second N plus a few. That loop is the product.
3) The egress gate, where the system is allowed to be slow

- Four checks, any one of which can hold, and none of which would be affordable on the order path:
- The settlement clock. Funds from an unsettled deposit are not withdrawable. This alone kills the deposit-buy-withdraw cycle.
- Destination cool-off. A beneficiary added in the last 72 hours cannot receive a withdrawal, because "change the bank account, then withdraw" is the account takeover playbook.
- Step-up authentication on a device that has not been seen before.
- The current risk score and any open case, the same score the order path reads, used here with a much lower tolerance.
- Hold, do not reject. A hold is reversible and an analyst can release it in minutes. A rejection is a support ticket, a complaint, and a customer who now believes the platform lost their money.
- Size the hold rate, because every hold ends at a human. At a 1 percent hold rate that is 2,000 holds a day and roughly 20 analysts a shift at five minutes each; at 0.1 percent it is two people. The rate is therefore a staffing decision, and the queue is ranked by exposure so the scarce hour goes to the largest number.
- Be explicit about instant buying power, because it is the interesting product tension. Letting a customer trade against an unsettled deposit is the platform lending its own balance sheet, capped, knowingly, and priced as a fraud loss. It is a business decision the system implements, not an oversight.
- The ACH return window outlasts everything. A deposit can be pulled back for up to 60 days, long after the trade settled and the customer left. That is why exposure is capped per account rather than merely delayed.
4) Offline: the graph, the labels, and the regulator

- Entity resolution links accounts by what they share: device, IP, bank account, beneficiary, address. The output is a graph, and fraud rings are communities in it.
- The UNLINKABLE ring is only visible here. A pair that shares a device or a bank account is caught by the correlator in seconds; what needs the graph is the ring whose only shared attribute is behaviour. Either way there is no per-order rule that catches it, because each account is individually ordinary.
- Self-trade prevention splits into two problems with two homes, and the mechanism is not mine even though the obligation is.
- Same account both sides: tag the order with a self-match-prevention identifier so the VENUE cancels one side, plus an inline check against that account's own resting orders. An inline check alone only sees what routes through me, and the resting order may be at another venue.
- FINRA Rule 5210.02 makes it my obligation to have policies reasonably designed to prevent self-trades across accounts under common beneficial ownership, which is why the graph below is a compliance artefact and not just a fraud tool.
- Three accounts with one beneficial owner: a graph query that runs overnight.
- Training labels come from closed cases, not from guesses. An analyst's confirmed outcome is the only clean label the system produces, which makes the case queue a data pipeline as much as a workflow.
- Compliance files a SAR on a confirmed case, and the customer is not told. Hours of latency is acceptable here precisely because the egress gate already stopped the money moving.
Potential deep dives
~20 min, interviewer steers1) What can you actually decide in 10 ms?
- Pull the account's recent history, join it to device and IP reputation, run the model, decide.
- That is several round trips and an inference call. It is 100 ms on a good day and unbounded on a bad one.
- At 5,000 orders a second it also means the fraud system is now the highest-QPS consumer of the trading database, which is the last thing that database needs at market open.
- Put the history and the reputation lookups behind a cache and keep the model call. Better, and it is where most designs stop.
- The model call is still there, so the tail is still owned by someone else's inference queue.
- A cache miss on a cold account is now the worst case, and cold accounts are exactly the ones worth checking.
- You have made the common case fast and left the tail, which is what the p99 is measuring.
- The account's risk score is computed offline and written to a feature store. The inline path reads one key. There is no miss to handle, because every account has a score, even if it is the default one.
- Size the write side, because "move it off the request path" only counts if you say where it went. 20M accounts refreshed blindly every 5 minutes is about 67K writes a second, an order of magnitude more work than the 5,000 reads a second it exists to serve. So recompute on EVENT, not on a timer: only accounts that did something get rescored, which is a few thousand a second at peak and tracks the actual order rate.
- Inline rules are restricted to things that need no join: velocity counters kept in the same store, watchlist membership, notional caps, same-account self-trade.
- The budget, out loud: 2 ms for the read, 1 ms for the rules, 1 ms for an async log write, and 6 ms of headroom for the tail. Naming the headroom is what shows you have actually thought about p99 rather than p50.
- The staleness is the price and it is worth paying. A score minutes old catches the account that was already suspicious. The stream catches the one that just became suspicious. Neither alone is enough, and trying to do both in 10 ms gets you neither.
- Say what this gives up: an attack that is entirely novel and entirely fast gets its first orders through. The answer is that the egress gate is what stops those orders from becoming money.
2) Fail open or fail closed?
- Risk service unavailable, so no orders are accepted.
- A fraud-service outage is now a trading halt. Customers cannot exit positions during a market move, which turns an internal incident into customer losses and a regulatory event.
- It also creates a denial-of-service target: whoever can knock over the risk service can stop the market.
- The safe-sounding answer is the dangerous one here, and saying that plainly is most of the value of this dive.
- If risk cannot answer in 10 ms, allow the order. Correct for the order path, and it is the right instinct.
- But applied uniformly it also opens the withdrawal path, which is precisely when an attacker wants it open.
- An outage becomes the ideal moment to cash out, and a sophisticated attacker will cause one.
- Split the order path in two first, because "orders fail open" is not something a regulated broker may say without qualification.
- Mandatory pre-trade controls NEVER fail open. SEC Rule 15c3-5 requires them applied automatically, pre-trade, under the broker's exclusive control, with no bypass. That covers credit and capital thresholds, erroneous-order collars, restricted lists and blackout windows, sanctions screening, Reg SHO locates on short sales, and account status.
- They live in the gateway, need nothing external, and are the reason a risk-service outage does not become a compliance breach.
- The discretionary fraud score fails OPEN. Its loss is bounded, the position stays visible, and the stream catches it seconds later. Halting the market is the larger harm.
- Account status is the one that people put in the wrong place. A freeze pulled by the correlator has to be a locally cached hard block at the gateway, not a lookup, or the strongest lever in the system is defeated by exactly the outage an attacker would cause.
- Egress fails CLOSED. 2.3 withdrawals a second means an hour of downtime queues about 8,000 withdrawals, which is a backlog to work through rather than a loss. Letting money out during a blind window is unrecoverable.
- Degrade in stages rather than flipping a single switch:
- Feature store unavailable: fall back to the last cached score, then to the account's tier default.
- Rules engine unavailable: keep the hard deterministic caps, which live in the gateway and need nothing external.
- Everything unavailable: allow orders under a reduced notional cap, with the mandatory controls and the cached freeze list still enforced. Still trading, with the blast radius bounded.
- A manual conservative posture, one switch, for when you know you are under attack. It tightens caps and holds all egress. Someone senior owns it and it is exercised in a drill, or it will not work the day it is needed.
- Say the meta-point, because it generalises beyond this question: "fail open or closed" is the wrong question. The right question is which actions are reversible, and each one gets its own answer.
3) Catching collusion between accounts
- Flag an order that looks like the other side of a recent order.
- Each account in a ring is individually unremarkable. The signal does not exist in any single order, so no per-order rule can hold it.
- Adding a third account defeats it entirely, and adding a third account is free for the attacker.
- Track concentration per account: same instrument repeatedly, unusual volume in an illiquid name, round-trip patterns. Genuinely useful and it catches the lazy version.
- It still reasons about one account at a time, so it sees a suspicious account rather than a ring.
- It cannot answer the question the regulator will actually ask, which is who is on both sides.
- Build the graph from what accounts share: device fingerprint, IP, bank account, beneficiary, address, and funding source. Edges are cheap to derive and the join keys already exist on the events.
- Then the questions become graph queries: is there a cycle where the same component is on both sides of a trade, is this cluster's internal volume out of proportion to its external volume, did these accounts all open in the same week.
- Split the problem by where it can be answered:
- Same account both sides: inline, trivial, do it now.
- Two accounts sharing a device: the stream correlator, seconds.
- A ring of five with no shared attribute except behaviour: offline graph analytics, overnight.
- Overnight is fine here and you should say why rather than apologising: the egress gate already holds the money, so the cost of finding out tomorrow is that the ring keeps trading, not that it gets paid.
- The honest limit: a sophisticated ring shares nothing, and behavioural community detection has a real false-positive rate. That is why the output is a case for a human, not an automatic freeze.
4) The deposit-buy-withdraw cycle
- The ACH arrived, so credit it and let them trade and withdraw.
- An ACH debit can be returned for insufficient funds or as unauthorised, and the unauthorised window runs to 60 days for consumer accounts.
- The complete attack is three steps and needs no sophistication at all: deposit, withdraw, dispute. The platform funds it.
- No trading and no withdrawing until the deposit is final. Safe, and a genuinely defensible product for some businesses.
- It also makes the product uncompetitive: nobody waits two days to buy in a market that moves in minutes.
- And it does not actually close the window, because the return right outlives settlement by weeks.
- Instant buying power, capped, is the platform lending its own money against an unsettled deposit. Name it as a priced decision rather than a feature, because that framing is what makes the cap defensible.
- Withdrawal availability follows settlement, always. Funds traceable to an unsettled deposit are not withdrawable, and that single rule removes the attacker's exit.
- Tier the cap by account history, so a five-year customer gets instant availability and a five-day-old account gets very little. The score already exists; this is just another consumer of it.
- Track the exposure as a number someone owns. Total unsettled credit extended, per account and in aggregate, with an alert and a hard ceiling. Fraud loss becomes a line item that is monitored rather than a surprise discovered at quarter end.
- The 60-day return window is the part people miss. Settlement is not finality. So a returned deposit on an account that has already withdrawn creates a negative balance and a collections problem, and the design should say what happens: the account restricts, the loss is booked, and the pattern feeds the score.
5) False positives, and the loop that fixes them
- Fraud is up, so raise sensitivity. Complaints are up, so lower it.
- A single threshold trades one harm for the other with no way to improve both, and it oscillates on whichever one is loudest this week.
- The two harms are also not comparable in the same unit, so there is no threshold that is correct.
- Hold out labelled data, plot the curve, choose an operating point with the business. This is the right vocabulary and most of the way there.
- But the labels come from cases analysts opened, which came from the current rules, so the data only contains fraud the system already catches.
- Nothing in this measures the fraud that never got flagged, which is exactly the population that matters.
- Step-up is the release valve. Most ambiguous signals should trigger re-authentication, not refusal. A real customer is mildly inconvenienced; an attacker without the second factor is stopped. It converts a binary decision into a graded one.
- Every new rule runs in shadow first. It logs the verdict it would have given against real traffic for a week. You learn its false-positive rate before it can refuse a customer, and most rules die here.
- Estimate recall from a holdout, but hold out on STEP-UP, never on hard blocks.
- Deliberately releasing transactions you believe are fraudulent is not merely a dollar cost: you still owe a SAR on them, and where the fraud is account takeover you have left a customer unprotected on purpose.
- Downgrading a block to a step-up on a random slice gives you the same measurement with the attacker still stopped, since an attacker without the second factor fails it.
- Shadow mode is the other natural experiment, and between them you get an honest recall estimate without a compliance argument.
- Closed cases are the labels, so analyst throughput is a modelling constraint. Rank the queue by exposure, not by arrival, and treat analyst time as the scarce resource it is.
- Feed the outcome back in minutes. A confirmed case updates the score immediately rather than at the next retrain, because a fraud ring operating tonight does not wait for the batch job.
- Track the cost of both errors in the same unit, money, so the operating point is a business decision made on purpose instead of an engineering default nobody signed off.
6) Manipulation: spoofing, layering, and marking the close
- Watch fills for suspicious patterns.
- Spoofing is the placement and rapid cancellation of orders with no intent to execute. The manipulative orders never fill, so a feed of fills is blind to the entire behaviour.
- This is also why the event stream in section 2 has to carry placements, modifications and cancels: the design decision comes before the detection rule.
- Flag accounts that cancel most of what they place. A real signal and easy to compute.
- But market makers legitimately cancel well over 90 percent of their orders, so the ratio alone flags exactly the participants you least want to accuse.
- It also says nothing about intent, which is what the offence actually turns on.
- Spoofing has a shape: size posted away from the touch on one side, a genuine order on the other side, and the large side cancelled within milliseconds of the small side filling. It is the CORRELATION between the cancel and the fill that carries the intent, not either alone.
- Layering is the same idea stacked across several price levels, and marking the close is concentrated aggressive volume in the last minutes against a benchmark the account has a position in.
- Detect on the stream in seconds, but escalate to a human always. Intent is a legal finding and an automated freeze on a manipulation signal will eventually freeze a legitimate market maker.
- Say the honest framing: as a retail broker this is mostly a supervision obligation under FINRA 3110 and the venue also surveils it, whereas account takeover and deposit fraud are where my actual losses are. Spending the same effort on both would be misallocating.
- Pump-and-dump has a different shape and a different home: concentrated retail buying in a microcap plus promotional activity, which is a cross-account and cross-channel question for the graph rather than the stream.
7) Account takeover, which is where the losses actually are
- Alert on a new-device login, on a password change, on a new beneficiary.
- Every one of those is something real customers do constantly, so each alone is mostly noise and gets tuned down until it is worthless.
- The attacker performs the same steps a customer does. What distinguishes them is the ORDER and the compression in time, and a per-step rule throws exactly that away.
- Re-authenticate on a beneficiary change and on a withdrawal. Right, and it is the single highest-value control here.
- But if the second factor is SMS it is defeated by a SIM swap, which is the standard escalation once an attacker has the password.
- And if the attacker also controls the email, every notification you send lands in their inbox.
- The sequence is recognisable and should be a single stream pattern: login from a new device, then a credential or email change, then a new beneficiary, then liquidate everything, then withdraw. Each step is ordinary; the chain inside an hour is not, and it should raise the score sharply for a few hours.
- Liquidating an entire portfolio is itself the loudest signal on the page, because it is rare for real customers and mandatory for the attacker, who has to convert positions to cash before extracting.
- Passkeys or a push to an enrolled device, never SMS, and rebind the session on any device change.
- Notify on a channel the attacker has not taken over. An email change alerts the OLD address, and a beneficiary change alerts every channel on file. The point is not the notification, it is that the 72-hour cool-off gives the real customer a window to see it and call.
- Cover the support channel, which is the vector people forget. Calling the broker to change the bank account is a leading real attack. Agents need the same cool-off, agents must not be able to override it, and a high-value change made through support needs a second approver.
- Say the proportionality out loud: the manipulation work above is regulatory obligation, this is where the money goes. If I had one more engineer they would be here.
Four more dives, briefly
- The insider, which this design currently does nothing about and should.
- Look at the API again: freeze, resolve a case, promote a rule. The dangerous direction is not freeze, it is UNFREEZE and RELEASE, and one analyst with the console can release every held withdrawal in the queue.
- So: a second approver for any release above a notional threshold, rule promotion to enforcing approved by someone other than its author, and no standing production access.
- Analyst actions land in the same WORM log as the decisions, and an analyst who releases an unusual share of holds is itself a monitored signal. The fraud system needs a fraud system.
- Adversarial drift, which is what makes this different from ordinary ML.
- The adversary reads your decisions. Every block teaches them where the line is, and a stable model is a solved puzzle.
- So: retrain often, and never expose the reason for a block precisely enough to probe. Keep any randomness in WHICH ambiguous cases get stepped up, never in whether a mandatory control fires, because non-deterministic enforcement is indefensible in an examination.
- Watch for score distribution drift as an alarm in itself. A sudden change in the shape of the distribution usually means either a bug or a new attack, and you want to know which within the hour.
- Regulatory obligations, which shape the system more than people expect.
- SAR filing has deadlines and the customer must not be told, so "tipping off" is a product constraint, not only a legal one.
- Decisions are immutable and retained 7 years, which is why the decision log is WORM storage rather than a table someone can update. Since the 2022 amendments 17a-4(f) also allows an audit-trail alternative, so WORM is one compliant answer rather than the only one.
- Be precise with "adverse action": refusing an order is not an ECOA action, but denying or cutting instant buying power is, because that is credit. It needs a stated reason, so a model that cannot explain itself cannot be its sole basis, which is why the rules layer survives alongside the model.
- That squares with the anti-probing rule above: the customer gets a coarse reason, and the full reasoning sits behind the
decisionIdfor support, the analyst and the regulator.
- The vectors I have not spent a dive on, named so the interviewer knows they were chosen against rather than missed.
- Laundering through trading is the typology unique to this system: deposit dirty money, buy and immediately sell something liquid, and withdraw brokerage proceeds, paying the spread as the fee. It is why the egress gate looks at the RELATIONSHIP between deposits and trading, not just at the withdrawal. Structuring below reporting thresholds and sanctions and PEP screening sit here too.
- Other funding rails have different clocks. Card deposits carry chargeback rights out to 120 days under network rules, longer than ACH. Crypto withdrawal is instant and absolutely irreversible, needs chain analytics and Travel Rule handling, and is the sharpest possible illustration of this page's own thesis.
- Bank-account ownership verification is the cheapest deposit control there is: a name match through an aggregator, or micro-deposits. It stops funding from an account the customer does not own before any of the later machinery has to.
- Promo and referral abuse is a top retail loss line and multi-accounting is the whole attack, so the entity-resolution graph already built is the answer with no new machinery.
- Synthetic identities mature inside the platform over months, which is why "IDV is the vendor's problem" is only half true: the graph re-checks identity after onboarding, and "these accounts all opened in the same week and share an address" is where synthetics actually surface.
- Yes for orders, no for withdrawals, and the reason is reversibility rather than anything about the service.
- An order creates bounded market exposure that can be unwound, and it stays visible. Halting the market is the bigger harm and it is also a denial-of-service target.
- A payout is unrecoverable, and 2.3 withdrawals a second means failing closed costs a support queue, not a business.
- Degrade in stages: cached score, then tier default, then deterministic caps only, then orders under a reduced notional limit. Still trading, blast radius bounded.
- There is a manual conservative posture for when you know you are under attack, owned by someone senior and exercised in a drill.
- Almost nothing. One feature-store read for a precomputed account score, about 2 ms.
- Deterministic rules that need no join: velocity counters, watchlist, notional caps, same-account self-trade. About 1 ms.
- An async decision-log write, 1 ms, and roughly 6 ms of headroom, because the budget is a p99 and not a p50.
- No feature computation and no RPC to an inference service. The arithmetic of scoring is cheap, so a compiled model co-located in the decisioner would fit; what does not fit is joining history to build the features or taking a network hop with a tail you do not own.
- The score is minutes stale and that is accepted. The stream covers what it misses, and the egress gate stops what both miss from becoming money.
- Not with a per-order rule. Each account is individually unremarkable, so the signal does not exist in any single order.
- Entity resolution first: link accounts by device, IP, bank account, beneficiary and address, which turns it into a graph.
- Then it is a graph query: a cycle with the same component on both sides, or a cluster whose internal volume dwarfs its external volume.
- Split by where it is answerable: same account both sides is inline; two accounts sharing a device is the stream correlator in seconds; a five-account ring is offline analytics overnight.
- Overnight is acceptable because the egress gate already holds the money, so the cost of being slow is that they keep trading, not that they get paid.
- Output is a case for a human, because behavioural community detection has a real false-positive rate.
- The deposit lands but is not final. Instant buying power is capped by account tier, and it is the platform lending its own balance sheet knowingly.
- They can trade against it. They cannot withdraw against it: funds traceable to an unsettled deposit are not withdrawable, which removes the exit.
- The withdrawal returns 202 HELD with an eligibleAt, not a rejection, because a hold is reversible and a rejection is a complaint.
- If the beneficiary was added recently the 72 hour cool-off holds it regardless, since change-the-bank-then-withdraw is the takeover playbook.
- Settlement is not finality: the ACH can still be returned as unauthorised for up to 60 days, so exposure is capped per account and tracked in aggregate, not merely delayed.
- If it is returned after a payout, the account restricts, the loss is booked, and the pattern feeds the score.
- It cost them the trade they could not make, which is real money, plus a written complaint I have to answer and possibly a regulator asking why.
- The fix is not a threshold. A single threshold trades one harm for the other and oscillates on whichever is loudest.
- Step-up authentication instead of a block for anything ambiguous: inconvenient for a real customer, fatal for an attacker without the second factor.
- Every new rule runs in shadow mode against real traffic for a week, so its false-positive rate is known before it can refuse anyone.
- Hold out a small random control group from automated blocks to measure the fraud you would otherwise never see. It costs money and it is the only honest recall estimate.
- Track both errors in the same unit, money, so the operating point is a business decision somebody actually signed.
Final design + what is expected at each level
wrapFinal Design

- The order gateway binds a session to a device, asks the risk decisioner for a verdict, and gets allow, step-up or block within 10 ms from a precomputed score plus deterministic rules.
- Every order, login, device change and transfer publishes to one event stream, which is what makes cross-signal patterns expressible.
- Stream rules keep rolling counts the inline path has no time for, and a correlator joins accounts that share a device, an IP or a beneficiary.
- Detection produces two levers of deliberately different strength: a case for an analyst, and an automatic freeze that is safe only because it is reversible.
- The egress gate holds withdrawals on the settlement clock, the beneficiary cool-off, step-up, and the current score, and it fails closed.
- Offline, entity resolution and graph analytics find the rings, closed cases become training labels, and the retrained score is published back to the store the 10 ms path reads.
- The invariant worth closing on: the discretionary part of the order path fails open and the egress path fails closed, because an order's loss is bounded and a payout's is total. Every other decision on this page follows from that one.
What is Expected at Each Level
- Mid
- Builds a risk service on the order path with rules and a score, plus a queue for analyst review.
- Names the common fraud types and writes sensible rules for them.
- Usually tries to do the detection inline and does not notice the latency budget is already spent.
- Usually treats fraud prevention as blocking bad orders, and never gets to the money leaving.
- Senior
- Separates the tiers by latency and puts the expensive work off the request path deliberately.
- Identifies withdrawal as the irreversible step and gates it harder than trading.
- Handles the deposit settlement clock and the beneficiary cool-off without being prompted.
- Talks about false positives as a cost rather than a tuning parameter, and knows what shadow mode is for.
- Staff
- Leads with the two-rate comparison and the reversibility asymmetry, and uses them as the reason for at least three separate decisions.
- Answers "fail open or closed" by rejecting the question and splitting it per action, then gives the staged degradation.
- Knows that cross-account fraud is a graph problem and that no amount of per-order cleverness substitutes, and can say which tier each variant belongs in.
- Treats the analyst loop as a data pipeline: closed cases are the only clean labels, so queue ranking and analyst throughput are modelling constraints.
- Raises the adversarial problem unprompted: the attacker reads your decisions, so a static model is a solved puzzle.
- Knows the regulatory shape, including that you may not tell the customer, and that a model which cannot explain itself cannot be the sole basis for an adverse action.
- Names what the design does not do: novel fast attacks get their first orders through, sophisticated rings that share nothing are found late if at all, and instant buying power is a priced loss.
Say the opening comparison out loud without notes: 50M orders against 200K withdrawals, 250 to 1, so the expensive checks live where the money leaves.
Then write the 10 ms budget from memory: 2 read, 1 rules, 1 async log, 6 headroom, 0 model.
Then say the failure policy in one sentence each: the discretionary score on orders fails open because the loss is bounded, the mandatory pre-trade controls never fail open, and egress fails closed because that loss is total.