Skip to content

About

Money mule card detection prototype: finds transit chains, fan-in funnels and fast cash-outs in card transfer data. Works with synthetic data or real CSV bank exports.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

drop_finder

Open in Streamlit Tests Python 3.11 Ruff License: MIT

Prototype anti-fraud tool for detecting money mule ("drop") cards from card transfer history.

A money mule card is used to receive stolen money and pass it on: to other cards or out through an ATM. drop_finder looks for the patterns such cards leave in transaction data.

What is inside: a synthetic data generator with hidden mule schemes, a loader for real bank exports, explainable rule-based detection with accuracy metrics, money tracing from a victim to the ATM, and a Streamlit dashboard with an interactive money flow graph.

Live demo: drop-finder.streamlit.app (synthetic data only). The free hosting puts the app to sleep when nobody has opened it for a while. If you see a sleeping page, press "Yes, get this app back up!" and wait about 30 seconds.

Suspicious cards with risk scores and reasons

Quick start

Requires Python 3.11.

python -m venv .venv
.venv\Scripts\activate
pip install -r requirements.txt
python generator.py --seed 42
python detectors.py data/transactions.csv --truth data/ground_truth.csv
streamlit run app.py

On Linux / macOS activate the environment with source .venv/bin/activate.

Project structure

File Purpose
generator.py Synthetic transactions with hidden mule schemes
loader.py Loads and validates any CSV (synthetic or real) into the common format
detectors.py Three detection rules, one row per suspicious card with a reason
metrics.py Precision / recall against the ground truth, benchmark over many seeds
flow_graph.py Money flow graph: networkx for the structure, pyvis for the picture
tracing.py Money tracing: where the money from one operation went, Sankey diagram
app.py Streamlit dashboard
tests/ pytest suite
data/ CSV files, not tracked by git

Architecture

flowchart LR
    G["generator.py<br>synthetic data"] --> T(["transactions<br>in the common format"])
    R["real bank export<br>CSV"] --> L["loader.py<br>validation, card masking"] --> T
    T --> D["detectors.py<br>3 explainable rules"]
    D --> M["metrics.py<br>precision / recall"]
    GT["ground truth"] --> M
    D --> A["app.py<br>Streamlit dashboard"]
    F["flow_graph.py<br>networkx + pyvis"] --> A
    TR["tracing.py<br>money tracing, Sankey"] --> A
Loading

Every component after the loader works with one data format, so the same detection code runs on synthetic data and on real exports.

Data format

Every other part of the project works with this format only, whatever the data source.

Column Meaning
datetime Operation time, 2026-08-01 14:30:00
sender_card Sender card, EXTERNAL for top-ups
receiver_card Receiver card, ATM for cash withdrawals
amount Amount in rubles
op_type transfer, cash_withdrawal, top_up (other if not recognized in real data)

Cards in synthetic data are masked: 2202 **** **** 4821.

Synthetic data

python generator.py --seed 42 --clients 200

The same seed always gives the same files. Output:

  • data/transactions.csv - 30 days of operations: ~200 regular clients (salaries, transfers to friends, ATM withdrawals) with mule schemes hidden among them;
  • data/ground_truth.csv - mule cards and their scheme, to measure detection accuracy.

Hidden schemes:

Scheme Pattern
transit Victim -> mule 1 -> mule 2 -> ATM. Each hop within 1-3 hours, amounts almost equal
funnel 8-15 different cards send money to one card within 1-2 days, then it moves on
fast_cashout A card receives a large amount and withdraws almost all of it within an hour

Regular clients also include legitimate look-alikes, so that a detector has something to get wrong: group money collections (many small transfers to one card) and cash withdrawals right after payday.

Real data

loader.py accepts real bank exports in CSV:

  • UTF-8 or Windows-1251, separated by ,, ; or tab;
  • columns are matched by name, including Russian ones (Дата операции, Сумма, ...); other names can be mapped explicitly;
  • dates like 2026-08-01 14:30:00 or 01.08.2026 14:30, amounts like 1 234,56 руб.;
  • operation types are recognized by keywords (Перевод, Снятие наличных, Пополнение);
  • rows that cannot be parsed are dropped with a warning.

Check a file:

python loader.py path/to/export.csv

Use it from code:

from loader import load_transactions

df, warnings = load_transactions(
    "export.csv",
    column_map={"datetime": "Дата проводки", "amount": "Сумма в руб."},
    datetime_format="%d/%m/%Y %H:%M",  # only if the format is unusual
)

Card numbers

Full card numbers never leave the loader. They are masked and tagged with a keyed hash: 2202 **** **** 4821 #a1b2c3. The tag keeps different cards with the same first and last digits apart, and the number cannot be recovered from it without the key.

By default the key is random for each run. To get the same tags across runs, set the DROP_FINDER_SECRET environment variable.

Card tokens or internal IDs are the best input: they are kept as is. Already masked numbers (2202 **** **** 4821) are kept too, but different cards with the same visible digits will then be merged into one.

Handling real data

The data/ folder is excluded from git. Do not commit or share real exports, and follow your organization's rules for personal and payment card data.

Detection

python detectors.py data/transactions.csv --truth data/ground_truth.csv

Three explainable rules. Thresholds live in detectors.Rules and are deliberately looser than the generator's parameters, so the detector is not tuned to its own synthetic data.

Rule Fires when
transit A transfer of 10 000+ arrives and 90-100% of it leaves the card within 6 hours. Cards passing money to each other are linked into chains with networkx
funnel 8+ different senders within 48 hours, 50 000+ collected, and 80%+ of it leaves within the next 24 hours
fast_cashout 30 000+ arrives and 80%+ of it is withdrawn in cash within an hour

Every flagged card comes with a human-readable reason (in Russian), for example:

Транзит: пришло 88 000 ₽ от 5536 **** **** 9197, за 1 ч 42 мин ушло 87 399 ₽ (99%) на 4276 **** **** 4734, цепочка из 2 транзитных карт

The risk score orders the list, it is not a probability: transit 70 (90 in a chain of transit cards), fast cash-out 75, funnel 85, plus 5 for each additional rule. Without --truth the command works on real exports and just saves the list to data/suspicious.csv.

Results on synthetic data

python metrics.py --seeds 20

Precision Recall F1
Seed 42 93.3% 100% 96.6%
Mean over 20 seeds 94.6% 100% 97.1%
Worst seed 87.5% 100% 93.3%

All 17 false positives over 20 seeds come from the transit rule: a regular client receives 10-53 thousand rubles and happens to send about the same amount within a few hours. The legitimate look-alikes (group collections, cash after payday) produce none. False positives get the lowest risk score (70), while real transit chains get 90.

Caveat: the rules and the generator were written by the same author, so these numbers are optimistic. Real data is noisier, and the thresholds will need tuning on it.

Performance: about 6 seconds for 1 million operations. Operations are indexed by (card, time) keys and searched with binary search instead of row-by-row loops.

Dashboard

streamlit run app.py
  • data source: synthetic data (seed, number of clients) or your own CSV with an optional ground truth file;
  • rule thresholds in the sidebar, the list and the metrics update immediately;
  • suspicious cards with risk, rules and reasons, CSV download;
  • card drill-down: every rule hit, the card's operations and an interactive money flow graph that follows the chain through other suspicious cards;
  • network view of all suspicious cards and their direct links;
  • money tracing (see below).

Card drill-down: a transit chain from the victim through two mule cards to an ATM

Network view of all suspicious cards

Money tracing

Pick a transfer into a suspicious card, for example a victim's payment, and see where that money went: through which cards, how much was withdrawn in cash and how much is still on cards, and how soon the cash-out started.

The tracer follows the receiving card's outgoing operations in time order and assumes that the traced money leaves first, but never more than arrived (a common conservative assumption in investigations). It stops at an ATM, after a time horizon (72 hours per card by default) or after a number of steps, never counts the same operation twice and keeps the total exactly equal to the traced amount.

With seed 42, the share of a victim's money that ends up withdrawn in cash is 95.8-98.5% in transit chains, 100% in funnels and 91.3-94.9% in fast cash-outs.

Money tracing: Sankey diagram from the victim through two mule cards to cash

Demo mode. Uploading files is only available when the app is opened on localhost. A public deployment, such as Streamlit Community Cloud, works on synthetic data only, so that nobody uploads real customer data to a public server. To allow uploads on your own server, set DROP_FINDER_ALLOW_UPLOAD=1.

Tests

pip install -r requirements-dev.txt
pytest --cov
ruff check .

GitHub Actions runs the same on every push: lint, tests and coverage (the build fails below 90%).

The tests check that every hidden scheme matches its definition, that the legitimate look-alikes stay below detection thresholds, that the loader handles messy real exports (Windows-1251, ;, Russian headers, mixed date formats, currency in amounts, full card numbers), that each detection rule fires on its pattern and stays silent on hand-made near misses, that money tracing never loses or double-counts money (splits, cycles, horizons, step limits), and that the dashboard builds without errors and reacts to its settings (Streamlit AppTest).

License

MIT, see LICENSE.

About

Money mule card detection prototype: finds transit chains, fan-in funnels and fast cash-outs in card transfer data. Works with synthetic data or real CSV bank exports.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages