How fraud caps stop abuse
A gift card code is a bearer instrument: whoever holds it can spend it. So when a code leaks — photographed, forwarded, scraped from an inbox — the only thing standing between a thief and the full balance is how fast they can spend it. This post walks through the fraud caps that sit on top of the atomic redemption: a ceiling on any single redemption, a daily limit per card, and a velocity check on how many redemptions happen in a short window. Each is enforced in the same indivisible write as the balance, so a card that trips a cap freezes on the spot instead of draining.
Key takeaways
- A leaked code — not a guessed one — is the realistic threat, and the caps are built to blunt exactly that.
- Three caps: a per-redemption ceiling, a daily limit per card, and a velocity limit on how many redemptions fit in a short window.
- Every cap is enforced inside the same atomic conditional write as the balance, so a tripped cap never spends first and asks later.
- The card carries a rolling daily total and a recent-count, both reset by time, so the checks are a single self-contained write.
- A card that trips a cap freezes — it’s flagged for review rather than drained — and every freeze lands in the ledger and an alert.
The threat is a leaked code, not a guessed one
Part 2 made the code space large enough that guessing a live card is hopeless. So the realistic way a card gets abused is not guessing — it is a leak. A photo of the card posted online, a forwarded confirmation email, a code skimmed from a printed slip, a batch of e-gift codes scraped from a compromised inbox. Once a code is loose, whoever holds it can spend the balance, and the only thing that limits the damage is how fast and how freely they can spend before anyone notices.
The caps exist to make a leaked code worth far less than the balance on it. They cannot stop the first small spend — that is indistinguishable from a real customer — but they can stop a $200 card being emptied in three rapid hits from a script, and they can stop a single redemption walking off with the whole balance at once.
Three caps, all on the hot path
- A per-redemption ceiling. No single redemption may take more than a set amount — say $100, or the opening value, whichever the policy sets. This stops one request draining a large balance in a single move and makes a stolen high-value card behave like a small one.
- A daily limit per card. A card may give up at most a set amount per calendar day — say $150. A genuine customer rarely bumps into this; a thief trying to liquidate a card before it’s reported hits it fast.
- A velocity limit. At most a set number of redemptions — say 5 — in a rolling short window such as an hour. Real spending is bursty but small in count; a script hammering a code is not. The velocity cap is the one that catches automated draining.
The important design choice is where these run. It would be easy to check the caps first, in code, and then do the redemption — but that reopens exactly the gap part 3 spent its whole length closing. Two requests could both pass the cap check and then both spend. So the caps do not run before the write; they run inside it.
Caps inside the same write
The redeem step keeps two extra fields on the card row alongside the balance: spent_today, a rolling total of cents redeemed so far today, and recent_count, how many redemptions have landed in the current short window. Both carry the timestamp of their window, so the redeem step knows when to treat them as reset to zero rather than carrying yesterday’s figure forward.
Because all three caps read off fields on the same card row as the balance, every check folds into one condition expression on a single UpdateItem. The write commits only if all of these hold at once: balance >= :amount, :amount <= :ceiling, spent_today + :amount <= :daily, and recent_count < :velocity. When it commits, the same write subtracts the balance, adds to spent_today, and increments recent_count — so the counters can never drift out of step with the spend. There is no separate read of the counters and therefore no gap for two requests to both slip through. The caps inherit the exact guarantee from part 3.
What a tripped cap does
When the condition fails, DynamoDB rejects the write and nothing on the card changes — no balance moves, no counter ticks. The redeem step then has to decide why it failed. A plain “balance too low” is the ordinary part 3 case: tell the till the real balance. But a cap failure — especially the velocity cap — is treated as suspicious. The card’s status is moved to frozen, which makes every future redemption fail closed until a human clears it, an alert goes to the owner with the code and what tripped it, and the attempt is recorded in the ledger as a frozen event. A real customer who genuinely hit a daily limit can be unfrozen with a quick look; a thief gets a card that stops paying out after a few cents rather than after a few hundred dollars.
Freezing rather than silently declining matters. A silent decline tells an attacker probing a code exactly where the limit sits, so they tune just under it. A freeze takes the card out of play entirely on the first trip and puts a person in the loop — which is the right place for a judgement call about someone else’s money.
Design rules for fraud caps
- Assume the threat is a leaked code; size the caps to make a leaked card worth little.
- Three caps: per-redemption ceiling, daily limit, velocity limit — all in policy, not code.
- Every cap is a condition on the same atomic write as the balance. Never check first, spend second.
- Counters live on the card row and update in the same write, so they can’t drift from the spend.
- A tripped cap freezes the card and alerts a human — it fails closed, never silently declines.