Problem#Link to this section
Kenyan banks, SACCOs and payment providers must spot money laundering in fast, high-volume mobile-money traffic, then report it to the Financial Reporting Centre within strict deadlines. Off-the-shelf tools are built for card and wire data. They don't model M-Pesa patterns such as agent cash-outs or split transfers, and they don't follow Kenyan law, POCAMLA, out of the box.
Constraints#Link to this section
- The law sets the deadlines. A suspicious-activity report is due within 3 days of a hold.
- Rules must stay in charge. FATF guidance says a machine-learning score can't be the only reason to report someone.
- Personal data is regulated. The Kenya Data Protection Act 2019 limits who can see national ID numbers and phone numbers.
- Every decision must be auditable, by an examiner who reads the trail months later.
Approach#Link to this section
Payments arrive from M-Pesa, CSV files or core banking. A normaliser turns each into one canonical shape, and Kafka carries it to detection. There, 35 typology rules check for known laundering patterns, such as structuring a large sum into many small transfers.
Behaviour and velocity profiles in Redis add context, and an Isolation Forest plus a River online model add an anomaly score. A composite scorer fuses the engine scores with reviewed weights. Some outcomes can't be tuned away: a strong sanctions match always escalates, and a politically exposed person is always treated as high risk.
Above the hold threshold, the system creates a hold, opens a case and drafts the report. An analyst investigates, and a second person must approve before anything is filed.
Evaluation#Link to this section
There's no real-world accuracy figure, and I don't claim one. No institution's transaction data has gone through the system. The rules and scoring are tested against a synthetic M-Pesa-style dataset with all 35 typologies planted in it, which shows the logic works as designed, not how often it would catch real laundering.
| Metric | Value | Measured on | Caveat |
|---|---|---|---|
| Typology rules | 35 | Versioned JSON registry, exercised by a synthetic M-Pesa-style generator (seed 42) | Synthetic data only. The rules have never scored a real institution's transactions. |
| Sanctions names screened against | 285,503 | Live OFAC SDN, UN Consolidated and OpenSanctions downloads on 2026-10-20, after deduplication | One full download, checked against a SHA-256 manifest. There's no incremental update feed yet. |
| PEP names screened against | 835,168 | OpenSanctions PEP list downloaded on 2026-10-20, after deduplication | OpenSanctions data is free for non-commercial use only; a commercial pilot needs a licence. |
| Test files | about 240 | Unit and integration suites, with a 90% coverage gate in CI | Tests run in-process and on SQLite. Several database paths have never run on real infrastructure. |
Results#Link to this section
The full analyst journey runs end to end in a one-command demo: a payment comes in, raises an alert and a hold, becomes a case, gets a drafted report and passes four-eyes approval. The sanctions and PEP screening lists were synced from live public sources on 2026-10-20.
Security#Link to this section
Personal identifiers go into a reversible vault encrypted with AES-GCM. Revealing one needs a stated purpose and leaves an audit record, and keys can be rotated or erased. Logs scrub national IDs, phone numbers and names automatically.
Decisions are written to an append-only audit ledger in which each entry carries the hash of the one before, so a changed record breaks the chain. An API gateway checks a signed token and a role on every call, and rate-limits each client. Production settings fail closed when a required secret is missing.
Limits#Link to this section
- It's a pre-pilot build, not a production deployment. It has never run on real infrastructure or real customer data.
- The Postgres audit triggers and the last three database migrations have never run on a live Postgres.
- National ID and credit bureau checks run against mocks; real access needs agreements with those bodies.
- Live filing with the Financial Reporting Centre has never been tried with institution credentials.
- The graph engine isn't live, so its score is missing and its weight is spread across the other engines.
What I'd do next#Link to this section
- Bring the stack up on a real Postgres and Docker host, and prove the audit triggers refuse updates and deletes.
- Run a shadow pilot on one institution's historical data, to measure real alert rates and false positives.
- Replace full watchlist downloads with incremental updates.
- Bring the graph engine live, so related accounts count towards risk.
Evidence
- Typology registry and synthetic data generator (Private work)
- Watchlist sync manifest (2026-10-20) (Private work)
- Test suite and coverage gate (Private work)