Demo Notice: This project is a DEMO / educational reference implementation. It is NOT production-ready. It reproduces architectural patterns from a real-world AML-FT system but with significant simplifications (see Design Decisions for details). Do not use as-is in production environments.
A monorepo containing two integrated applications for AML-FT (Anti-Money Laundering / Financing of Terrorism):
- banking-app (port 8080) - Core banking data: companies, users, bank accounts, transactions, payment cards
- aml-ft-app (port 8081) - Detection system (rules, transaction investigations, notices, reports)
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ Banking & AML-FT Platform │
│ │
│ ┌────────────────────────────────────────────────────────────────────────────┐ │
│ │ PostgreSQL / SQLite │ │
│ │ │ │
│ │ ┌────────────────────────────────┐ ┌──────────────────────────────┐ │ │
│ │ │ banking schema │ │ aml_ft schema │ │ │
│ │ │ │ │ │ │ │
│ │ │ - companies │ │ - rule_params │ │ │
│ │ │ - users │ │ - rules │ │ │
│ │ │ - bank_accounts │ │ - transaction_investigations │ │ │
│ │ │ - transactions │ │ - notices │ │ │
│ │ │ - payment_cards │ │ - reports │ │ │
│ │ │ - audit_logs │ │ - operators │ │ │
│ │ └────────────────────────────────┘ └──────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────────────────────┘ │
│ ▲ │
│ read/analyze │
│ │ │
│ ┌──────────────────────────────┐ │ ┌──────────────────────────────┐ │
│ │ Banking App │ │ │ AML-FT App │ │
│ │ (port 8080) │ │ │ (port 8081) │ │
│ │ │ │ │ │ │
│ │ ┌───────────────────────┐ │ │ │ ┌───────────────────────┐ │ │
│ │ │ API │ │ │ │ │ API │ │ │
│ │ │ (FastAPI) │ │ │ │ │ (FastAPI) │ │ │
│ │ └───────────────────────┘ │ │ │ └───────────────────────┘ │ │
│ │ │ │ │ ┌───────────────────────┐ │ │
│ │ ┌───────────────────────┐ │◀─────┼─────▶│ │ RabbitMQ │ │ │
│ │ │ RabbitMQ │ │ │ │ │ Consumer │ │ │
│ │ │ Publisher │ │ │ │ └───────────────────────┘ │ │
│ │ └───────────────────────┘ │ │ │ │ │
│ │ │ │ │ ┌───────────────────────┐ │ │
│ │ - CRUD entities │ │ │ │ Detection │ │ │
│ │ - Validations │ │ │ │ Rules │ │ │
│ │ - Event push │ │ │ │ (ThreadPool) │ │ │
│ └──────────────────────────────┘ │ └──────────────────────────────┘ │
│ │ │
└────────────────────────────────────────┼────────────────────────────────────────────┘
▼
Event Flow Diagram:
┌─────────────┐ ┌──────────┐ ┌─────────────┐
│ Banking │ │ RabbitMQ │ │ AML-FT │
│ App │─────▶│ Queue │─────▶│ App │
│ creates │ │ │ │ runs rules │
│ transaction │ │ │ │ creates │
│ │ │ │ │ rules │
└─────────────┘ └──────────┘ └─────────────┘
- Entity Created - User creates transaction via Banking App API
- Stored in DB - Transaction saved to SQLite/PostgreSQL
- Published to Queue - Event published to RabbitMQ (
transaction.created) - Consumed by AML-FT - RabbitMQ consumer receives event
- Rules Executed - DetectionAdapter runs all matching rules in parallel
- Rules Created - For each triggered rule, a Rule record is created
- TransactionInvestigation/Notice - If thresholds exceeded, TransactionInvestigation or Notice created
Core banking data management.
Entities:
Company- Legal entities (SIREN, NAF code, address)User- Persons linked to companies (KYC level, nationality)BankAccount- IBAN/BIC accountsTransaction- Money transfers (PAYIN/PAYOUT)PaymentCard- Credit/debit cards
API Endpoints:
GET/POST /api/companiesGET/POST /api/users?company_id=xxxGET/POST /api/bank-accounts?company_id=xxxGET/POST /api/transactions?bank_account_id=xxxGET/POST /api/payment-cards?user_id=xxxor?bank_account_id=xxx
Port: 8080
Features:
- Hexagonal architecture (ports/adapters)
- RESTful API with FastAPI
- RabbitMQ event publishing
- Pydantic validation (SIREN, IBAN, BIC, country codes)
- Soft delete via status field
Anti-Money Laundering / Financing of Terrorism detection.
Entities:
RuleParam- Detection rule configurationsRule- Individual rule results triggered by detection rulesTransactionInvestigation- Transaction investigation decisionsNotice- Investigation casesReport- Investigation workflowsReportMessage- Messages attached to reportsOperator- Back-office users
API Endpoints:
GET/POST /api/rule-paramsGET/POST /api/rulesGET/POST /api/transaction-investigationsGET/POST /api/noticesGET/POST /api/reportsGET/POST /api/operators
Port: 8081
Features:
- Event-driven detection via RabbitMQ
- Parallel rule execution (ThreadPoolExecutor)
- investigation_score vs notice_score thresholds
- NiceGUI back-office UI (port 8082)
- Sanctions/PEP name screening (rapidfuzz fuzzy matching)
- Hexagonal architecture
shared/
├── shared/
│ ├── __init__.py
│ ├── context_session.py # Async session management
│ ├── adapter_base.py # Common adapter utilities
│ └── dependencies.py # FastAPI dependencies
└── pyproject.toml
Provides:
SessionContext- Manages async DB sessionsapply_deleted_filter,get_deleted_status,paginate_query- Soft delete utilitiesget_session_dependency- FastAPI dependency injection
| Component | Technology |
|---|---|
| Framework | FastAPI |
| ORM | SQLAlchemy 2.0 (async) |
| Database | SQLite (dev), PostgreSQL (prod) |
| Message Queue | RabbitMQ (aio-pika) |
| Validation | Pydantic |
| UI Framework | NiceGUI (aml-ft-app only) |
| Testing | Pytest, pytest-asyncio |
| Linting | Ruff |
| Type Checking | Mypy |
| Package Manager | Poetry |
- Python 3.12+
- Poetry
- RabbitMQ (optional, for event-driven mode)
# Install banking-app
cd banking-app
poetry install
# Install aml-ft-app
cd ../aml-ft-app
poetry installcd banking-app
poetry run python main.py
# API: http://127.0.0.1:8080cd aml-ft-app
poetry run python main.py
# API: http://127.0.0.1:8081cd aml-ft-app
poetry run python ui.py
# UI: http://127.0.0.1:8082# Seed banking data (5 companies, 6 users, 11 accounts, 27 transactions)
cd banking-app
poetry run python scripts/seed.py
# Seed rule params and operators
cd ../aml-ft-app
poetry run python scripts/seed.py# Banking app tests
cd banking-app
poetry run pytest
# AML-FT app tests
cd aml-ft-app
poetry run pytestcurl -X POST http://127.0.0.1:8081/api/detection/evaluate \
-H "Content-Type: application/json" \
-d '{
"entity_type": "TRANSACTION",
"entity_data": {
"id": "tx-123",
"amount": 15000,
"currency": "EUR",
"company_id": "comp-456"
}
}'aml-ft-platform/
├── README.md # This file
│
├── shared/ # Shared utilities
│ ├── pyproject.toml
│ └── shared/
│ ├── __init__.py
│ ├── context_session.py
│ ├── adapter_base.py
│ └── dependencies.py
│
├── banking-app/ # Core banking application
│ ├── main.py # FastAPI entry point
│ ├── config.py # Settings
│ ├── pyproject.toml # Dependencies
│ ├── banking/
│ │ ├── models.py # SQLAlchemy models
│ │ ├── ports/ # Interface definitions
│ │ ├── adapters/ # Implementations
│ │ └── api/ # REST routes
│ └── scripts/
│ └── seed.py # Data seeding
│
└── aml-ft-app/ # AML-FT detection application
├── main.py # FastAPI entry point
├── config.py # Settings
├── ui.py # NiceGUI UI
├── pyproject.toml # Dependencies
├── aml_ft/
│ ├── models.py # SQLAlchemy models
│ ├── enums.py # Status enums
│ ├── adapters/ # Business logic
│ ├── rules/ # Detection rules
│ ├── services/ # Business services
│ ├── ports/ # Interfaces
│ └── api/ # REST routes
├── ui_pages/ # NiceGUI pages
├── scripts/
│ └── seed.py # Seed data
└── tests/ # Test suite
# Environment variables
DATABASE_URL=sqlite:///banking.db
RABBITMQ_URL=amqp://guest:guest@localhost:5672/
API_PREFIX=/api
HOST=0.0.0.0
PORT=8080# Environment variables
DATABASE_URL=sqlite:///./banking.db
BANKING_API_URL=http://127.0.0.1:8080
RABBITMQ_HOST=localhost
RABBITMQ_PORT=5672
RABBITMQ_USER=guest
RABBITMQ_PASSWORD=guest
TX_INVESTIGATION_THRESHOLD=7
NOTICE_THRESHOLD=50
JWT_SECRET=dev-secret-key| Threshold | Default | Description |
|---|---|---|
tx_investigation_threshold |
7 | Create TransactionInvestigation if sum(rule.investigation_score) >= 7 |
notice_threshold |
50 | Create Notice if sum(rule.notice_score) >= 50 |
Rules are organized by entity type. All thresholds are class constants pulled directly from the rule source files — see each class for the exact values. Rules execute in parallel via ThreadPoolExecutor in the detection engine.
rule_name |
Class | rule_group |
Condition | Data Dependency |
|---|---|---|---|---|
TRANSACTION_HIGH_AMOUNT |
HighAmountRule |
HIGH_AMOUNT |
amount >= 9 900 EUR |
None — base event data |
TRANSACTION_CASH_DEPOSIT |
CashDepositRule |
HIGH_AMOUNT |
payment_method == "CASH" AND amount >= 4 900 EUR |
None — base event data |
TRANSACTION_COMPANY_NEW_HIGH_VALUE |
NewCompanyHighValueRule |
HIGH_AMOUNT |
amount >= 4 900 EUR AND company registered ≤30 days ago |
company_registration_date (EntityEnricher) |
TRANSACTION_VELOCITY_HIGH |
VelocityRule |
VELOCITY |
>10 transactions on the same account in 24h window | last_24_hours_bank_account_transactions (Redis) |
TRANSACTION_PATTERN_SIMILAR_AMOUNTS |
AmountPatternRule |
SUSPICIOUS_PATTERN |
≥3 transactions with identical (type, amount) in 24h window |
last_24_hours_bank_account_transactions (Redis) |
TRANSACTION_RAPID_SEQUENCE |
RapidTransactionsRule |
SUSPICIOUS_PATTERN |
≥9 transactions on the same account within 4 minutes | last_60_minutes_bank_account_transactions_count + time window (Redis) |
TRANSACTION_CARD_HIGH_VALUE |
HighValueCardRule |
CARD_FRAUD |
payment_method == "CARD" AND amount >= 4 900 EUR |
None — base event data |
TRANSACTION_CARD_VISHING |
CardVishingRule |
CARD_FRAUD |
≥3 identical-amount CARD moneyout transactions in 60min (30d fallback) | last_60_minutes_bank_account_transactions (Redis) |
TRANSACTION_HIGH_RISK_COUNTRY |
HighRiskCountryRule |
HIGH_RISK_COUNTRY |
Sender/recipient country is in HIGH_RISK_COUNTRIES_FOR_SENDING (19 sanctioned) or TAX_HAVEN_COUNTRIES (10) | sender_country, recipient_country (EntityEnricher) |
rule_name |
Class | rule_group |
Condition | Data Dependency |
|---|---|---|---|---|
USER_RISKY_COUNTRY |
RiskyCountryRule |
RISKY_COUNTRY |
User's country is in the HIGH_RISK_COUNTRIES list (29 sanctioned + tax-haven) | None — base country field |
USER_MULTIPLE_ACCOUNTS |
MultipleAccountsRule |
MULTIPLE_ACCOUNTS |
bank_account_count > 3 |
bank_account_count (EntityEnricher) |
USER_VISHING |
VishingRule |
CARD_FRAUD |
>3 virtual payment cards created in the last 7 days | payment_cards (EntityEnricher) |
rule_name |
Class | rule_group |
Condition | Data Dependency |
|---|---|---|---|---|
COMPANY_NEW |
NewCompanyRule |
COMPANY_AGE |
Company registered ≤30 days ago | company_registration_date (EntityEnricher) |
COMPANY_HIGH_VOLUME |
HighVolumeRule |
HIGH_VOLUME |
Monthly tx count ≥170 OR monthly volume ≥600 000 EUR | monthly_volume (EntityEnricher via Redis) |
Note: Data marked as "(EntityEnricher)" is populated by the EntityEnricher service, which queries Redis projections (TransactionHistory, VolumeAggregation, etc.) with a REST API fallback to banking-app. Data marked as "(Redis)" comes directly from Redis projections without a REST fallback. Rules with "None" operate on raw fields from the event payload.
The AML-FT app screens company and user names against an in-memory sanctions/PEP-style list via two rules:
COMPANY_SANCTIONS_MATCH— company name against the listUSER_SANCTIONS_MATCH— user name against the list
Matching uses rapidfuzz fuzzy matching with a default threshold of 0.87, tunable via rule parameters. Each rule combines the name check with a high-risk country check (either one triggers). The transaction-level high-risk country rule (TRANSACTION_HIGH_RISK_COUNTRY) is unchanged.
CSV import skipped. A CSV import feature for the sanctions list should have been implemented, but for simplicity the list is seeded from a hardcoded dict in aml-ft-app/scripts/seed.py (list source synthetic_demo). A real implementation would add CSV import.
Limitations. This is a demo simplification, not a production-grade control. It has no phonetic awareness (e.g. Catherine vs Katherine), no date-of-birth or address disambiguation, and no alias handling.
Noise risk. Shared-recipient detection (TRANSACTION_SHARED_RECIPIENT) fires when a recipient IBAN is paid by 2+ otherwise-unrelated companies in a 90-day window. Legitimate high-fan-in recipients (utilities, tax authorities, landlords, payment processors) can naturally trigger it; known-good payees can be added to the trusted-recipient allowlist (/api/trusted-recipients, manageable from the back-office UI). Expect noise beyond curated demo data.
Hashing trade-off. Recipient IBANs are stored only as sha256 hashes (data minimization). Status/debug output shows opaque hex keys, which trades debuggability for not holding raw account numbers.
- Python 3.12+, Poetry installed
- Redis running (projections)
- RabbitMQ running (event bus)
- banking-app on http://127.0.0.1:8080
- aml-ft-app on http://127.0.0.1:8081
- NiceGUI UI on http://127.0.0.1:8082 (started automatically by
demo.sh)
# 1. Install & start Redis (one-time)
brew install redis && brew services start redis
# 2. From project root, run the demo
bash demo.shThe demo.sh script handles everything — cleans stale processes and databases, seeds data, starts both apps and the NiceGUI UI in the background, creates entities, runs detection, and prints a summary. The back-office UI is available at http://127.0.0.1:8082 (default login: admin / admin).
If you prefer to run each step manually:
# 1. From project root, clean state
rm -f banking-app/banking.db aml-ft-app/aml_ft.db
redis-cli FLUSHALL
# 2. Seed both apps
(cd banking-app && poetry run python scripts/seed.py)
(cd aml-ft-app && poetry run python scripts/seed.py)
# 3. Start banking-app (terminal 1)
cd banking-app && poetry run python main.py
# 4. Start aml-ft-app (terminal 2)
cd aml-ft-app && NOTICE_THRESHOLD=35 poetry run python main.py
# 5. Run the demo (terminal 3) — creates entities and triggers detection
bash demo.sh
# 6. Open the back-office UI (port 8082) — manage notices, investigations, reports
# Visit http://127.0.0.1:8082 (default login: admin / admin)Note: The
demo.shscript starts the NiceGUI UI automatically. If you run the demo from scratch (bash demo.sh), step 6 is handled for you.
Creates 5 companies, 11 bank accounts, 6 users, and 8 transaction scenarios across all 5 companies. The demo automatically starts three services:
| Service | Port | Description |
|---|---|---|
| Banking App | 8080 | Core banking CRUD API |
| AML-FT App | 8081 | Detection API (rules, investigations, notices) |
| NiceGUI UI | 8082 | Back-office interface for operators |
Detection targets:
| Company | Activity | Notices Created |
|---|---|---|
| C1 - HRT-Corp | High-risk country transactions | RISKY_COUNTRY, HIGH_RISK_COUNTRY |
| C2 - NewCo Ventures | Card activity pattern detection | CARD_ACTIVITY_PATTERN |
| C3 - VolumeCorp | High amounts + batch patterns | HIGH_AMOUNT, SUSPICIOUS_PATTERN |
After the demo, view stored projection data:
curl http://localhost:8081/api/projections/bank_account/data
curl http://localhost:8081/api/projections/transaction_history/data
curl http://localhost:8081/api/projections/volume_aggregation/data
curl http://localhost:8081/api/projections/card_activity/dataAfter running the demo:
- ~5 Notices across 3 companies (mix of solo-trigger and combo-trigger)
- ~7 TransactionInvestigations (most require 2+ rules combining)
- ~25+ Rule records created
View results in the back-office UI at http://127.0.0.1:8082 (login: admin / admin), or via the API:
# Notices
curl http://127.0.0.1:8081/api/notices?limit=20
# Transaction investigations
curl http://127.0.0.1:8081/api/transaction-investigations?limit=20
# Triggered rules
curl http://127.0.0.1:8081/api/rules?limit=50The repo ships a four-job GitHub Actions pipeline (.github/workflows/ci.yml) that runs on every push and pull request to main:
| Job | Purpose |
|---|---|
banking-app-ci |
Lint, format check, tests with coverage floor, and type checking for banking-app/ |
aml-ft-app-ci |
Lint, format check, tests with coverage floor, and type checking for aml-ft-app/ |
docker-build |
Builds both Docker images (repo-root build context, GitHub Actions cache, retry on transient install failures) |
e2e-smoke |
Boots the isolated ci-e2e stack, seeds the AML-FT app, runs the end-to-end smoke script, always tears the stack down |
Each app job runs the same quality gate:
ruff check .— lintruff format . --check— format checkpytest --cov=<app> --cov-fail-under=<floor>— tests with a coverage floor:banking80% andaml_ft65%mypy .— blocking type check (52 files clean in banking-app, 167 in aml-ft-app)
docker-build builds both images with the build context at the repository root, uses GitHub Actions cache (cache-mode: max) to reuse layers across runs, and retries installs to ride over transient PyPI timeouts.
e2e-smoke brings up the isolated -p ci-e2e stack (docker-compose.yml + docker-compose.e2e.yml), seeds the AML-FT app, runs banking-app/scripts/e2e_smoke.py against both apps, and always cleans up (down -v) even on failure.
# Banking app
cd banking-app
poetry run ruff check .
poetry run ruff format . --check
poetry run pytest --cov=banking --cov-fail-under=80
poetry run mypy .
# AML-FT app
cd aml-ft-app
poetry run ruff check .
poetry run ruff format . --check
poetry run pytest --cov=aml_ft --cov-fail-under=65
poetry run mypy .# Install hooks
pre-commit install
# Run hooks manually
pre-commit run --all-filesKey design choices and known gaps are documented in docs/architecture-decisions.md. Topics include:
- Hexagonal (ports/adapters) structure and why
- Single Transaction model vs payment-method-specific entities
- Simplified state machine (PENDING/VALIDATED/CANCELED)
- Unidirectional event flow (RabbitMQ one way, HTTP the other)
- Idempotency via
Idempotency-Keyheader - Rule thresholds configurable via
RuleParam(code constants as fallback defaults) - Thin events + Redis projections
- Company-level notice locking and single-instance limitation
- Observability: structured JSON logging via structlog; metrics/tracing intentionally omitted for demo scope
- Production-readiness gaps (secrets management)