Payment Fraud Detection Engine
Explainable ALLOW / REVIEW / DENY decisions on payments in real time. Ingestion is built; detection is next.
Where it stands: Milestone 1 of 7. The engine ingests and stores payment events today. Detection starts in Milestone 2, so it doesn't flag fraud yet.
Why I'm building it
I investigate payment fraud by hand at work; this is the system I'd want behind that work. It should catch card testing, cloned cards and account takeovers, and explain why it flagged something.
The target: an ALLOW / REVIEW / DENY decision on every payment in under 100 ms, from explainable rules plus an ML risk score.
Architecture
- (built)Clientpayment & card-added events
- (built)FastAPI ingeststrict Pydantic contracts
- (built)PostgreSQLsource of truthRedis Streamevents:incoming
- (planned)Feature enginevelocity · amount z-scores · travel
- (planned)Rules engineexplainable rulesML scorerXGBoost + SHAP
- (planned)DecisionALLOW / REVIEW / DENY
- (planned)Audit log
- (planned)Analyst dashboard
analyst labels feed back into the ML scorer (planned)
What's built (Milestone 1)
- FastAPI endpoints for payment events, card-added events, ordered batches of up to 1,000, lookup and health.
- Every event is saved to PostgreSQL (JSONB) and published to a Redis Stream.
- Strict Pydantic contracts: unknown fields rejected, timezone-aware timestamps, future timestamps refused.
Results
| Check | Result |
|---|---|
| 10,000-event replay | 0 validation errors, ~4,100 events/s batched |
| Single-event latency | 3.9 ms p50 · 5.5 ms p95 |
| Duplicate re-send | All detected, nothing processed twice |
| Tests | 58 automated tests in CI |
Design decisions
The security angle: what the engine refuses to do matters as much as what it does.
No real card numbers, ever.
Events carry tokens. A Luhn check rejects anything that looks like a card number, even with spaces, dashes or hidden in a token. The error never echoes it back.
Idempotent ingestion.
Retries are safe, so duplicates can't inflate velocity signals.
One ordered stream.
Card testing (“3 cards added, then ₹1 probes”) depends on true event order.
Persist first, roll back on stream failure.
The API returns 503 so the client retries cleanly.
Labels never travel with events.
No label leakage into the model.
Synthetic data
A generator simulates 500 users over 30 days (~36,000 events, 5% fraud), with realistic innocent noise and five injected attacks:
- Card testingSeveral cards added in quick succession, then tiny ₹1 probe payments to find out which ones work.
- CVV brute forceRepeated attempts on the same card, cycling through CVV values until one is accepted.
- Impossible travelThe same account paying from two places too far apart for the time between the payments.
- Amount spikeA payment far above what the account normally spends.
- Account takeoverSomeone else gets into an account and starts moving money out of it.
Roadmap
- 1.Ingestion✅
- 2.Feature engine (velocity counters, amount z-scores, impossible travel)
- 3.Rules engine
- 4.ML risk scorer (XGBoost + SHAP)
- 5.Decision service & audit log
- 6.Analyst dashboard
- 7.Write-up & demo
Links
View the repository on GitHubMore Projects