Anti-Fraud Engine
Mahad Farooq
Every app with a free tier quietly leaks money to the same thing: one person making a pile of accounts to keep claiming the free credits. On a normal product that costs almost nothing. On an AI product, where every free generation burns real compute, it shows up straight on your margin.
I ran into this on CasperCoach, so I built an engine to catch it. The plain version: it figures out when a bunch of accounts are secretly the same person, and instead of banning anyone, it makes those accounts share one free allowance.
The technical version is the rest of this page.
The real problem is not detection, it is false positives
Catching duplicates sounds like the hard part. It is not. The hard part is not punishing real people who happen to look like duplicates. A family on one card. An office behind one IP. Two students on the same campus computer. A single person who jumps from home wifi to their phone to a coffee shop in one afternoon.
If you get aggressive, you lock out paying customers and they never come back. So the whole system is built around being safe to get wrong.
How it decides two accounts are one person
No single signal is trustworthy on its own, so the engine collects a lot of weak, independent ones and adds them up. The idea is simple: beating one signal is easy, beating all of them at once is expensive, and that expense is the entire point.
The signals I lean on, roughly in order of value:
- Payment fingerprint.Stripe exposes a stable fingerprint for the same card across different accounts. Hard to beat without a fresh, real card every time, which kills the abuser's economics.
- Email normalization. Gmail ignores dots and anything after a plus sign, so
j.o.h.n+trial5@gmail.comandjohn@gmail.comare the same inbox. I collapse those, then flag disposable and throwaway domains. - Device and browser fingerprint. Built from many low-entropy browser attributes hashed together, with fuzzy matching so the same device still matches itself after a browser update.
- Signup and usage behavior. Accounts created minutes apart, each draining almost exactly the free quota and then going idle, is a loud tell. Real users are noisy. Farmers are mechanically uniform.
The matching engine
This part is record linkage, not an LLM. I start deterministic: if two accounts share an exact card fingerprint or a normalized email, they get hard-linked. That is the backbone.
For the fuzzier cases I score similarity across email, device, and behavioral signals, and weigh rare matches more heavily than common ones. Two accounts sharing an unusual device and audio fingerprint means a lot. Two accounts sharing a common screen resolution means almost nothing. Then I cluster accounts that share two or more strong signals into one identity.
On standard email addresses the matcher hits about 85% accuracy, and on disposable and duplicate addresses it hits 100%, by pairing normalization with fuzzy string matching to beat aliasing and plus-address tricks.
Where the machine learning comes in
The first version was a hand-tuned weighted score. Transparent, debuggable, and it ships without any training data. That matters, because when something gets flagged you want to be able to say exactly why.
Now I'm extending it with a machine learning model trained on the signup data I've collected. Instead of me guessing the weights, the model learns them from real confirmed cases, handles the messy interactions between signals that a flat score misses, and outputs a calibrated risk number. The goal is pushing match accuracy past 95% without giving up the ability to explain a decision.
What it does once it is sure
This is the part I'm proud of. A high-confidence cluster does not get banned. It gets pooled: all the linked accounts share one free allowance between them. Three accounts that link together get one free tier, not three.
That kills the abuse completely, and if I'm ever wrong about two real people, the worst case is they share a free quota for a bit. Nobody gets locked out. That is what lets an imperfect engine ship safely, which loops right back to the real problem: false positives.
The action layer ships as drop-in React components that match your brand. Detection happens server-side; you render the fix. Pick one to preview it:
What I took away from it
The instinct is to chase a perfect detector. The better move was designing the response so it does not need to be perfect. Build for how people actually behave, make the failure mode gentle, and you can ship something useful long before the model is flawless.