Stripe OA: Payment Events Aggregation (State Machine + Dedup + Refunds + Merchant Blocking)
A full walkthrough of all 4 parts of Stripe's Payment Events OA: dedupe by event_id, filter out-of-order events with a payment state machine, refund only successful payments, and permanently block authorizations for merchants with risk ≥ 80. Tested Python reference code included.
A classic Stripe OA: no algorithmic tricks — the test is implementing a long list of business rules without missing a single one. The problem has 4 parts; work through them in order, since each part adds rules on top of the previous one.
Problem
Stripe processes a huge volume of payments and relies heavily on asynchronous events. You implement:
def process_events(events: list[str]) -> list[str]:
eventsare given in the order they were received. Each one is a CSV line with 7 fixed columns:event_id,event_time,event_type,payment_id,payment_event_type,merchant_id,amountevent_type:payment/refund/merchant_updatepayment_event_type:create/authorize/capture- Empty values are written as
-.merchant_idis only present oncreate;amountonly onauthorize/capture(formerchant_update,amountis the risk score) - An
event_idmust never be processed more than once
Output: one line per payment, no header, ordered by when each payment was successfully created:
payment_id,merchant_id,state,amount_authorized,amount_captured,last_event_at
Numeric columns default to 0, e.g. payment_1,merchant_1,created,0,0,0. The output must not contain -; leave missing fields empty.
Part 1: Aggregating payment states
A payment has two stages: Authorization (the buyer authorizes the card; no money moves yet) and Capture (the merchant collects the authorized money). There can be multiple authorize and capture events, and amounts accumulate.
- After a capture, if
amount_captured < amount_authorized→partially_captured - If
amount_captured == amount_authorized→successful - Captured never exceeds authorized
| Current state | Valid events |
|---|---|
| Non-existent | create |
| created | authorize |
| authorized | authorize, capture |
| partially_captured | capture |
| successful | none |
Example (with a duplicate event_id):
evt_1,0,payment,payment_1,create,merchant_1,-
evt_2,1,payment,payment_2,create,merchant_2,-
evt_3,2,payment,payment_1,authorize,-,10
evt_4,3,payment,payment_1,authorize,-,10
evt_5,4,payment,payment_2,authorize,-,15
evt_6,5,payment,payment_1,capture,-,10
evt_6,5,payment,payment_1,capture,-,10
evt_7,6,payment,payment_2,capture,-,15
Output:
payment_1,merchant_1,partially_captured,20,10,5
payment_2,merchant_2,successful,15,15,6
The duplicate evt_6 is processed only once, so payment_1's captured amount is 10, not 20.
Part 2: Ignoring out-of-order events
Events may now arrive out of order. Any event that isn't valid for the current state is ignored: it changes neither the state nor the amounts, and it does not update last_event_at. Capturing before authorizing, or authorizing a payment that is already partially_captured, are both invalid.
evt_1,0,payment,payment_1,create,merchant_1,-
evt_2,1,payment,payment_2,capture,-,20
evt_3,2,payment,payment_1,capture,-,10
evt_4,3,payment,payment_2,create,merchant_2,-
evt_5,4,payment,payment_1,authorize,-,10
evt_6,5,payment,payment_2,authorize,-,20
evt_7,6,payment,payment_1,capture,-,10
evt_8,7,payment,payment_1,authorize,-,5
evt_9,8,payment,payment_2,capture,-,10
evt_10,9,payment,payment_2,capture,-,10
Output:
payment_1,merchant_1,successful,10,10,6
payment_2,merchant_2,successful,20,20,9
In the original problem, the explanation under this example uses event numbers that don't match the input above — most likely a typo in the problem statement. Go by the state transition table.
Part 3: Refunds
A new event_type = refund carries only the payment_id, e.g. evt_7,6,refund,payment_1,-,-,-.
- Only a
successfulpayment can be refunded, becomingrefunded; arefundedpayment accepts no further events - A valid refund changes only
stateandlast_event_at; amounts and merchant stay the same
Example: payment_1 and payment_2 both reach successful and are each refunded once (times 6 and 7), then payment_1 gets another refund at time 8. The second refund is ignored, so payment_1's last_event_at is 6, not 8:
payment_1,merchant_1,refunded,10,10,6
payment_2,merchant_2,refunded,20,20,7
Part 4: Merchant risk blocking
A new merchant_update event: merchant_id is the merchant and amount is its risk score (0–100), e.g. evt_1,0,merchant_update,-,-,merchant_1,95.
- A score ≥ 80 blocks the merchant immediately and permanently — a later lower score does not unblock it
- Once blocked, every authorize for that merchant is ignored, whether the payment was created before or after the block
- create, capture and refund are still processed, so already-authorized payments can finish their lifecycle
- A
merchant_updatenever changes any payment's state, amounts orlast_event_atby itself
evt_1,0,payment,payment_1,create,merchant_1,-
evt_2,1,payment,payment_1,authorize,-,10
evt_3,2,merchant_update,-,-,merchant_1,85
evt_4,3,payment,payment_2,create,merchant_1,-
evt_5,4,payment,payment_2,authorize,-,20
evt_6,5,payment,payment_1,capture,-,10
evt_7,6,merchant_update,-,-,merchant_1,10
Output:
payment_1,merchant_1,successful,10,10,5
payment_2,merchant_1,created,0,0,3
Clarify
Worth confirming before you start coding:
- What counts as a duplicate event_id? Once seen, it counts as processed: even if the first occurrence was ignored because the state was wrong, later events with the same id are skipped.
- A create for a payment that already exists? Only "non-existent" accepts create, so it's ignored.
- A merchant_update with a risk score outside 0–100? The problem says only a valid merchant_update blocks, so out-of-range scores are skipped as invalid.
- What goes in last_event_at? The
event_timeof the last accepted event; ignored events don't count.
Approach
The whole problem is a state machine plus two global rules:
non-existent --create--> created --authorize--> authorized (more authorizes allowed)
authorized --capture--> partially_captured / successful
partially_captured --capture--> partially_captured / successful
successful --refund--> refunded
- Global rule 1: dedupe
event_idwith a set - Global rule 2: merchants with risk ≥ 80 go into a blocked set, and all their later authorizes are ignored
Write the state table directly as a "state → allowed events" dict. For each event, look it up first and continue if it isn't allowed. Parts 2 and 3 then only require editing the table.
Store payments in a dict. Since Python 3.7, dicts preserve insertion order — exactly the create order — so the output needs no extra sorting.
Reference code (added, tested)
This code was added by this site. It passes all 5 examples from the problem and matched an independently written brute-force implementation on 20,000 randomized inputs.
def process_events(events: list[str]) -> list[str]:
# State machine: current state -> allowed events
VALID = {
None: {"create"},
"created": {"authorize"},
"authorized": {"authorize", "capture"},
"partially_captured": {"capture"},
"successful": {"refund"},
"refunded": set(),
}
seen = set() # event_ids already processed
payments = {} # payment_id -> dict (Python 3.7+ keeps insertion order = creation order)
blocked = set() # blocked merchants
for line in events:
cols = [None if c == "-" else c for c in line.strip().split(",")]
if len(cols) != 7:
continue
event_id, t, event_type, pid, pe_type, merchant, amount = cols
if event_id is None or event_id in seen:
continue
seen.add(event_id)
t = int(t)
if event_type == "merchant_update":
# Part 4: risk score >= 80 blocks permanently
if merchant is not None and amount is not None and 0 <= int(amount) <= 100:
if int(amount) >= 80:
blocked.add(merchant)
continue
if event_type == "refund":
action = "refund"
elif event_type == "payment":
action = pe_type
else:
continue
p = payments.get(pid)
state = p["state"] if p else None
if action not in VALID[state]:
continue # Invalid events are simply ignored
if action == "create":
if merchant is None:
continue
payments[pid] = {"merchant": merchant, "state": "created", "auth": 0, "cap": 0, "last": t}
continue
if action in ("authorize", "capture") and amount is None:
continue
if action == "authorize":
if p["merchant"] in blocked:
continue # Part 4: every authorize after blocking is ignored
p["auth"] += int(amount)
p["state"] = "authorized"
elif action == "capture":
p["cap"] += int(amount)
p["state"] = "successful" if p["cap"] == p["auth"] else "partially_captured"
elif action == "refund":
p["state"] = "refunded"
p["last"] = t
return [
f'{pid},{p["merchant"]},{p["state"]},{p["auth"]},{p["cap"]},{p["last"]}'
for pid, p in payments.items()
]
Time O(n), space O(n).
Common pitfalls
- Ignored events must not update
last_event_at. The Part 3 example exists specifically to test this. - Blocking is checked against the payment's merchant. An authorize event carries no
merchant_id, so look up the merchant recorded at create time. - create is still valid after blocking. In the Part 4 example, payment_2 is created after the block; it still appears in the output, stuck at
created. - Re-evaluate the state after every capture: equal to authorized means
successful, otherwisepartially_captured. - Catch duplicate event_ids at the very top of the loop, or later accumulations get counted twice.
Follow-up
- At very high volume with distributed processing, where does the
seendedup set live? Talk about idempotency keys and partitioning bypayment_idso each payment's events stay ordered. - Out-of-order events are currently dropped. How would you design "buffer them and replay once the prerequisite event arrives"?
- If the blocking rule changed to "only block payments created after the block", what would you change? Keeping the state table separate from the business rules makes that quick.
Found this helpful? Let's talk.
Happy to swap interview notes, do mock interviews, or share referral info.

Scan to add me on WeChat
More Stripe notes
View all ›- Stripe SDE VO | How the Integration Round WorksStripe · 2026-10-04›
- Stripe VO (2026–2027): Payment Transaction Risk Linkage System in 3 Progressive PartsStripe · 2026-10-02›
- TikTok OA, All 4 Passed: Min in a Range, Sorting by Vowel Gap, Bouncing Diagonals, Subarrays with at Least k Fruit PairsTikTok · 2026-10-03›
- Four Classic TikTok OA Questions: Adjacent Character Changes, Closest Earlier Timestamp, Placing Shapes, Fewest Operations to an Arithmetic SequenceTikTok · 2026-10-03›