C++ Credit Card Fraud Detection Project
Scan transaction streams and flag the ones that smell wrong — with reasons.
What you will build
Feed the system a stream of card transactions and it raises alerts: impossible travel, velocity spikes, amounts far above the cardholder's norm. There's no machine learning here — just well-chosen rules and statistics, which is how real fraud systems started and largely still work.
What you will learn
- Rule-based anomaly detection
- Rolling averages and standard deviation
- Velocity checks over sliding time windows
- Balancing false positives vs missed fraud
- Explaining alerts with evidence
Feature checklist
- Loads transactions: card, time, place, amount
- Rule: amount > 3× the cardholder's average
- Rule: two cities in an impossible time gap
- Rule: burst of small 'test' transactions
- Each alert explains which rule fired and the evidence
- Summary: alerts per rule, precision test on labelled data
Starter code
#include <string>
#include <vector>
#include <cmath>
struct Txn { std::string card; long timeMin; std::string city; double amount; };
struct Alert { std::string card, rule, detail; };
std::vector<Alert> detect(const std::vector<Txn>& txns) {
std::vector<Alert> alerts;
for (size_t i = 0; i < txns.size(); ++i) {
const Txn& t = txns[i];
// History for this card so far:
double sum = 0; int n = 0;
const Txn* prev = nullptr;
for (size_t j = 0; j < i; ++j)
if (txns[j].card == t.card) { sum += txns[j].amount; ++n; prev = &txns[j]; }
if (n >= 5) {
double avg = sum / n;
if (t.amount > 3 * avg)
alerts.push_back({t.card, "AMOUNT_SPIKE",
"amount is over 3x the card average"});
}
if (prev && prev->city != t.city &&
std::abs(t.timeMin - prev->timeMin) < 60)
alerts.push_back({t.card, "IMPOSSIBLE_TRAVEL",
"two cities within an hour"});
}
return alerts;
}Full walkthrough
Step 1: Load transactions into honest data
struct Txn { cardId, timestamp (as minutes since epoch or a comparable number), city, amount } loaded from a file, SORTED by card then time — every rule below is a pattern over a card's timeline, and timelines only make sense sorted. struct Alert { txn index, rule name, evidence string } is the output type: an alert without evidence is an accusation, so evidence is a required field, not a garnish.
Step 2: Group by card and compute each card's baseline
detect() walks the transactions grouped per card (a std::map<std::string, std::vector<Txn>> or a single sorted pass). Each card gets an average amount — the baseline its own behaviour is judged against. Fraud is deviation from SELF, not from a global average: 500 is pocket money for one card and a siren for another.
Step 3: Rule 1: amount spike — over 3× the card's average
Flag any transaction whose amount exceeds 3 × the cardholder's average, with evidence naming both numbers ('amount 500 vs card average 120 — 4.2×'). Computing the average EXCLUDING the transaction being judged (or including it, deliberately) is a choice that moves borderline cases — make it consciously and document it.
Step 4: Rule 2: impossible travel
Consecutive transactions for the same card in different cities whose time gap is smaller than any plausible journey (say under 60 minutes for inter-city) cannot both be the cardholder. Evidence: both cities, both timestamps, the gap. This rule needs the sorted timeline from step 1 and nothing else — distance tables are an optional refinement.
if (b.city != a.city && b.time - a.time < 60)
alerts.push_back({b, "impossible travel",
a.city + " " + timeStr(a) + " -> " + b.city + " " + timeStr(b)});Step 5: Rule 3: the small-test burst
Card thieves test stolen cards with tiny charges first. Flag 3+ transactions under a small threshold (e.g. 2.00) within a short window (e.g. 10 minutes) on one card. It is a sliding-window count over the card's timeline — two pointers, no nested loops, and a lovely small algorithm hiding inside a business rule.
Step 6: Summarise per rule — and test precision on labelled data
Print alerts grouped by rule with counts, then the honest metric: given a labelled test file (transactions marked fraud/not), precision = true alerts / all alerts. A detector that flags everything has perfect catch rate and zero usefulness; precision on labelled data is the number that tells the truth, and printing it in the summary is the checklist's demand.
Step 7: Test with a scripted story per rule
Plant: a card averaging 120 that suddenly spends 500 (rule 1); the same card in Lagos at 10:02 and London at 10:40 (rule 2 — 38 minutes); four 1.00 charges in 6 minutes (rule 3); and a control card behaving normally that must produce ZERO alerts. The control case matters most — a detector that cries wolf on the innocent card fails the only test that counts in production.
Common mistakes
- Judging amounts against a GLOBAL average, flagging every purchase by your wealthiest cardholder and none by your poorest.
- Alerts without evidence strings — an analyst cannot act on 'rule 1 fired' with no numbers attached.
- Rules run on unsorted transactions, making 'consecutive' and 'within 10 minutes' meaningless order-dependent accidents.
Compile and run
g++ -std=c++17 -o fraud fraud.cppSample run
$ ./fraud
Loaded 1,240 transactions for 86 cards (sorted per card).
Baselines computed per card. Running 3 rules...
ALERT — rule: amount spike
card ****4417: 500.00 at MegaMart — card average is 120.00 (4.2x)
ALERT — rule: impossible travel
card ****4417: Lagos 10:02 -> London 10:40 (38 minutes apart)
ALERT — rule: small-test burst
card ****9023: 4 charges of 1.00 within 6 minutes (test-charge pattern)
Summary: 3 alerts — amount spike 1 · impossible travel 1 · small-test burst 1
On the labelled test set: precision 0.83 (5 of 6 alerts were true fraud)Try it in your browser
No browser demo is included for this project. Build the C++ version below — that is the real program.