Rule-Driven Transaction Fraud Detection
Reported by candidates from Stripe's online assessment. Pattern, common pitfall, and the honest play if you blank under the timer.
The Stripe OA reported in September 2026 looks like a toy until you hit 200000 operations. Rule-Driven Transaction Fraud Detection is a simulation with a hash map, and the mistake that sinks a first attempt is scanning every active rule on each EVAL. That's O(n^2) in the worst case and it times out. You have an invite and not much time, so here's the shape: keep rules by ID, but index them by what they match. If you blank on the indexing, StealthCoder is the invisible safety net that reads the problem and hands you the structure live.
The problem
Process a finite ordered batch of rule-configuration and transaction-evaluation operations. Maintain active fraud rules by unique rule ID. Each operation is a string array in one of these forms: ["UPSERT", ruleId, "AMOUNT_AT_LEAST", value]: create or replace a rule that matches when a transaction amount is at least value. ["UPSERT", ruleId, "COUNTRY_IS", value]: create or replace a rule that matches when the country equals value. ["UPSERT", ruleId, "MERCHANT_IS", value]: create or replace a rule that matches when the merchant equals value. ["DELETE", ruleId]: remove that rule. Deleting an unknown ID has no effect. ["EVAL", amount, country, merchant]: evaluate one transaction against the rules active at this point. An UPSERT with an existing ID replaces the complete old rule before later operations are processed. Country and merchant comparisons are case-sensitive exact string comparisons. A transaction is fraudulent if at least one active rule matches it. If no active rule matches, it is allowed. Return one boolean for each EVAL operation, in evaluation order: true for fraudulent and false for allowed. Function classifyTransactions(operations: String[][]) → boolean[] Examples Example 1 operations = [["UPSERT","r1","AMOUNT_AT_LEAST","1000"],["UPSERT","r2","COUNTRY_IS","RU"],["EVAL","900","US","books"],["EVAL","1200","US","books"],["EVAL","20","RU","coffee"]] return = [false,true,true] The first transaction matches neither rule. The second reaches the amount threshold, and the third matches the country rule. Example 2 operations = [["UPSERT","risk","AMOUNT_AT_LEAST","100"],["EVAL","100","US","safe"],["UPSERT","risk","MERCHANT_IS","risky"],["EVAL","100","US","safe"],["EVAL","1","US","risky"],["DELETE","risk"],["EVAL","1","US","risky"]] return = [true,false,true,false] The first evaluation matches the amount rule. Reusing ID risk replaces that rule with a merchant rule. Deleting it leaves no active rules for the final evaluation. Example 3 operations = [["EVAL","0","US","outlet"],["UPSERT","c","COUNTRY_IS","CA"],["UPSERT","m","MERCHANT_IS","outlet"],["EVAL","0","US","outlet"],["EVAL","0","CA","shop"],["DELETE","missing"],["DELETE","m"],["EVAL","0","US","outlet"]] return = [false,true,true,false] An evaluation before any rule is allowed. The next two evaluations each match one active rule. Deleting an unknown ID changes nothing; deleting rule m makes the final transaction allowed. Constraints 1 <= operations.length <= 200000 There is at least one EVAL operation. Every operation has one of the valid forms described above. ruleId, country, and merchant are non-empty ASCII strings of length at most 40. Every amount and every AMOUNT_AT_LEAST value is a base-10 integer from 0 through 10^9. At most 200000 rules are active at once. String comparisons are case-sensitive.
Reported by candidates. Source: FastPrep
Pattern and pitfall
The trick is that a transaction is fraudulent if any rule matches, so you never need the rules themselves at EVAL time. You need counts. Keep a map from ruleId to its type and value. Keep a count map for countries, a count map for merchants, and a multiset of amount thresholds. Only the minimum threshold matters, so use a min-heap with lazy deletion or a sorted multiset. EVAL is true if amount >= min threshold, or the country count is above zero, or the merchant count is above zero. The pitfall is UPSERT on an existing ID. You must remove the old rule's contribution before adding the new one, or stale counts stay behind. DELETE of an unknown ID is a no-op. Parse amounts as integers and compare strings exactly. If the heap bookkeeping slips under pressure, StealthCoder is the hedge on the live OA.
If this hits your live OA and you blank, StealthCoder solves it in seconds, invisible to the proctor.
You can drill Rule-Driven Transaction Fraud Detection cold, or you can hedge it. StealthCoder runs invisibly during screen share and surfaces a working solution in under 2 seconds. The proctor sees the IDE. They don't see what's behind it. Built by an Amazon engineer who would have shipped this the night before his JPMorgan OA if he'd had it.
Get StealthCoderRelated leaked OAs
You've seen the question.
Make sure you actually pass Stripe's OA.
Stripe reuses patterns across OAs. Built by an Amazon engineer who would have shipped this the night before his JPMorgan OA if he'd had it. Works on HackerRank, CodeSignal, CoderPad, and Karat.
Rule-Driven Transaction Fraud Detection FAQ
What's the trick in the Stripe fraud detection OA?+
Don't loop over all rules per EVAL. Store rules by ID, then keep count maps for countries and merchants plus a structure for the smallest amount threshold. EVAL becomes a few lookups instead of a full scan.
How hard is this problem really?+
Logic is easy, the constraints are the catch. With 200000 operations and up to 200000 active rules, a naive scan per EVAL is too slow. It's medium difficulty mostly because of the replace-then-index bookkeeping.
How do I handle UPSERT on an existing ID?+
Look up the old rule by ID first and undo its effect: decrement its country or merchant count, or remove its threshold. Then apply the new rule and store it. Skipping the undo step is the most common bug.
How do I track the minimum amount threshold with deletions?+
Use a sorted multiset or a min-heap with lazy deletion. With a heap, keep a count map of thresholds and pop the top while its count is zero. Only the smallest active threshold decides AMOUNT_AT_LEAST matches.
How do I prepare for this in 48 hours?+
Write it once from scratch with the three indexes and test against the three examples, especially the replace and unknown-delete cases. Also check an EVAL before any rule exists. That covers nearly every edge in the statement.