A rule-based AML compliance tool built for Nigerian microfinance banks. Ingests transaction data, evaluates it against a library of typology rules, and surfaces alerts for analyst review via the BexAML dashboard.
| Layer | Technology |
|---|---|
| Frontend | HTML / Bootstrap 5 / Chart.js |
| Backend DB | Supabase (PostgreSQL) |
| Rule Engine | Python (pandas) |
| Auth | Supabase Auth + MFA |
| Edge Functions | Supabase Edge Functions (Deno) |
aml_engine.py — Core rule evaluation engine (runs batch scans)
ingest_data.py — One-time CSV → Supabase ingestion script
generate_transactions.py — Synthetic transaction data generator
seed_master.py — Seeds minimal test data for local dev
seed_typologies.py — Injects known AML typologies for testing
trigger_smurfing.py — Dev tool: injects smurfing patterns into test DB
dashboard.html — Main analyst triage workspace
compliance-audit.html — Head of Compliance view
admin-dashboard.html — Admin tools (typology injection, system config)
audit-logs.html — Full audit trail
user-management.html — User and role management
role-management.html — Role permission configuration
Requires a .env file in the project root:
SUPABASE_URL=https://<your-ref>.supabase.co
SUPABASE_SERVICE_ROLE_KEY=<your-service-role-key>
Then run:
pip install -r requirements.txt
python aml_engine.pyThe engine will fetch all transactions from Supabase, evaluate them against the active rule library, write generated alerts directly to the alerts table, and save a local CSV backup.
| Rule ID | Name | Typology | Severity |
|---|---|---|---|
| R-001A | High Value Single Transaction | NFIU reporting threshold (₦5M) | HIGH |
| R-001B | High Value Cumulative Daily | Aggregated daily volume ≥ ₦10M | CRITICAL |
| R-002 | Structuring / Smurfing (TAML-52) | Multiple transactions just under ₦5M within 24h | CRITICAL |
| R-003 | High Velocity Account (TAML-50) | ≥15 transactions within a 24h rolling window | MEDIUM |
| R-004 | Round-Tripping (TAML-53) | Large debit returned as credit (90–105%) within 24h | CRITICAL |
| R-005 | KYC Gap — Unverified Customer | Customer KYC status is not VERIFIED; transacting above ₦50k | HIGH |
| R-006 | OFAC SDN Watchlist Hit | Counterparty name fuzzy-matches the OFAC SDN list (≥85% similarity) | CRITICAL |
| R-007 | OpenSanctions Watchlist Hit | Customer entity name matches OpenSanctions (OFAC + UN + EU + PEPs, score ≥85%) | CRITICAL |
| R-008 | Cash Concentration | ≥3 POS/ATM/Branch cash deposits ≥₦500k within a 72h rolling window | HIGH |
| R-009 | Dormant Account Reactivation | Account inactive 180+ days then transacts above ₦100k | HIGH |
Alerts written to Supabase use this shape:
| Column | Description |
|---|---|
alert_ref |
Unique human-readable reference (e.g. ALT-A1B2C3) |
customer_id |
FK → customers |
transaction_id |
FK → transactions (NULL for multi-transaction alerts) |
rule_triggered |
Rule name string |
severity |
MEDIUM / HIGH / CRITICAL |
status |
UNASSIGNED on creation; analysts update to ESCALATED / CLOSED etc. |
details |
Human-readable description of why the alert fired |
aml_engine.py — R-008 Cash Concentration + R-009 Dormant Account Reactivation
R-008: Cash Concentration
Flags accounts with 3 or more cash deposits (POS, ATM, or Branch channels) each at or above ₦500,000 within a rolling 72-hour window. This targets the typology where agents or mules make repeated structured cash deposits on behalf of a principal — a pattern common in Nigerian microfinance fraud. Severity: HIGH. All three thresholds (count, amount, window) are configurable in RULE_CONFIG.
R-009: Dormant Account Reactivation
Flags accounts that were inactive for 180+ days and then transact above ₦100,000. A long silence followed by sudden high-value activity is a strong mule account signal. The rule calculates the gap between consecutive transactions per account and fires once per account on first qualifying reactivation event. Severity: HIGH. Both the inactivity period and transaction threshold are configurable in RULE_CONFIG.
aml_engine.py — R-006 OFAC SDN screening + R-007 OpenSanctions screening
Two new watchlist rules added. Both fire CRITICAL severity alerts.
R-006: OFAC SDN Watchlist (counterparty screening)
At engine startup, load_ofac_sdn() downloads the OFAC Specially Designated Nationals CSV directly from the US Treasury (public, no API key needed). Every unique counterparty_name in the transaction batch is then fuzzy-matched against the ~10,000 SDN entries using rapidfuzz with a token_sort_ratio scorer. Any match at ≥85% similarity triggers an alert per affected transaction. The threshold is configurable in RULE_CONFIG['watchlist_fuzzy_threshold']. If the OFAC download fails (e.g. network issue), R-006 is skipped and the engine continues normally.
R-007: OpenSanctions Watchlist (customer screening)
screen_via_opensanctions() sends all customer entity names to the OpenSanctions matching API, which aggregates OFAC, UN Security Council, EU, UK HMT, and ~100 other lists including global PEP databases. Names are batched in groups of 50 to stay within API rate limits. Any match with a confidence score ≥85% fires a CRITICAL alert, with the matched entity name, score, and risk topics (e.g. sanction, pep, crime) included in the alert details.
Requires OPENSANCTIONS_API_KEY in .env — free for non-commercial use (sign up at opensanctions.org). If the key is absent, R-007 is skipped with a warning. The .env.example file has been updated with the new variable.
New pip dependencies: requests, rapidfuzz (add to your pip install command).
aml_engine.py — alert deduplication
write_alerts_to_db() now queries today's existing alerts before inserting. Any alert with the same (customer_id, rule_triggered) combination already raised today is skipped, so running the engine multiple times in a day no longer produces duplicate alerts in the dashboard. A comment in the function also provides the SQL for an optional DB-level unique index as a belt-and-suspenders measure:
CREATE UNIQUE INDEX IF NOT EXISTS uq_alert_customer_rule_day
ON alerts (customer_id, rule_triggered, (created_at::date));dashboard.html — NFIU SAR PDF generator
Analysts can now download a pre-filled NFIU Suspicious Transaction Report (Form NFIU/STR/001) PDF directly from the SAR filing modal. A "Download SAR PDF" button sits in the modal footer alongside the existing submit button. Clicking it fetches the full alert and customer record from Supabase, and generates a formatted PDF via jsPDF covering all six NFIU STR sections:
- Section A: Reporting Institution
- Section B: Subject of Report (entity name, customer type, BVN/RC, address, risk tier, PEP/KYC status)
- Section C: Suspicious Activity Details (typology, severity, system description)
- Section D: Investigation Findings (officer's notes from the modal)
- Section E: Recommended Action
- Section F: Officer Declaration + signature lines
The PDF downloads automatically as SAR_<alert_ref>_<date>.pdf. The button can be clicked before or after filling in the investigation notes — notes are included in the PDF at the time of download.
aml_engine.py — v1.4
Three changes shipped. No other files modified.
1. Alerts now write directly to Supabase
Previously the engine saved alerts to a local CSV file only, meaning the dashboard never reflected engine output. The engine now calls write_alerts_to_db() after each scan, which:
- Builds an
account_id → customer_idlookup from the accounts table (the alerts table storescustomer_id, notaccount_id) - Builds a
transaction_reference → transaction_idlookup for single-transaction alerts - Transforms each alert to match the
alertstable schema (alert_ref,customer_id,transaction_id,rule_triggered,severity,status,details) - Batch-inserts in chunks of 1,000 rows
- The CSV backup is still written as a local fallback
2. New rule R-005: KYC Gap Detection
Flags any account belonging to a customer whose kyc_status is not VERIFIED, if that account has transactions above the CBN Tier 1 threshold (₦50,000). One alert is raised per account (not per transaction) to avoid alert fatigue. Severity: HIGH.
Requires the kyc_status column to exist on the customers table. Run this once in the Supabase SQL editor before executing the engine:
ALTER TABLE customers
ADD COLUMN IF NOT EXISTS kyc_status TEXT NOT NULL DEFAULT 'PENDING';All existing customers will default to PENDING. Update verified customers to VERIFIED manually or via your onboarding workflow. If the column is missing, R-005 is skipped with a warning rather than crashing the engine.
3. Credentials loaded from environment
The Supabase service role key was previously hardcoded in the source file. It now loads from the .env file via python-dotenv, consistent with how seed_master.py already handled this. The engine exits with a clear error if the env vars are missing.
⚠️ The old key was committed to the repo and should be rotated in the Supabase dashboard — Settings → API → Regenerate service_role key.
Pagination fix
The transaction fetch loop previously had a hardcoded while start < 20000 cap. It has been replaced with a fetch_all_rows() helper that runs until Supabase returns an empty page, so the engine won't silently miss transactions as the dataset grows.