Financescr is a FastAPI decisioning service for:
- FinCrime screening + triage (sanctions / PEP / adverse media), and
- Credit decisioning (personal loan underwriting),
with an agentic front-door.
Design principle (strict):
- Deterministic engines decide (scoring + explicit policy).
- The LLM assists (tool-calling orchestration, one follow‑up question, grounded explanation).
- Everything is auditable, replayable, and evaluable.
GET /health(validates mounted data pack and exposes policy/data versions)POST /agent/chat(rule-based agent turn, persistent transcript, always returns tool trace)GET /agent/conversations/{conversation_id}(replay transcript + trace)- Postgres persistence:
conversationsconversation_messages
- SQLAlchemy + Alembic migrations
- Docker Compose (API + Postgres)
- Provider layer (file-backed “data APIs”):
- customers, watchlist, credit applications (JSONL under
/data/...)
- customers, watchlist, credit applications (JSONL under
- Synthetic data generator:
scripts/generate_data_v1.py(seeded, deterministic)
- FinCrime decision engine
POST /screen(deterministic screening)GET /cases,GET /cases/{id}(review workflow)- immutable audit log + case creation flow
- offline fincrime eval harness + threshold tuning
- Credit decision engine
POST /credit/apply(deterministic PD + affordability + policy)- credit case flow (REFER queue)
- offline credit eval harness + threshold tuning
- LLM integration
- OpenAI tool-calling + grounded explanations (LLM never decides)
- Tiny UI
- static HTML at
/ui+ scripted demo mode (curl + UI)
- static HTML at
- Python 3.11
- FastAPI
- SQLAlchemy 2.x
- Alembic
- Postgres (psycopg)
- Docker Compose
app/
main.py
api/
routes.py
schemas.py
agent/
intent.py
trace.py
orchestrator.py
db/
session.py
models.py
repo.py
providers/
jsonl.py
customers.py
watchlist.py
credit.py
alembic/
scripts/
generate_data_v1.py
tests/
docker-compose.yml
Dockerfile
python3.11 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtFinancescr reads versioned data packs from a host directory mounted into the container as /data.
This keeps demo data out of the repo and makes runs reproducible.
Create the pack root (example):
mkdir -p ~/financescr_data/fincrime/v1
mkdir -p ~/financescr_data/credit/v1
mkdir -p ~/financescr_data/golden/v1Generate synthetic v1 packs (seeded, deterministic):
python scripts/generate_data_v1.py --out_root ~/financescr_data --seed 42 \
--n_customers 3000 --n_watchlist 3000 --n_credit_apps 3000 --n_fincrime_scenarios 1000Create a local .env (not committed) with:
FINANCESCR_DATA_DIR=/Users/solua1/financescr_dataDocker Compose mounts:
${FINANCESCR_DATA_DIR}:/data:ro
docker compose up --build -d
curl -s localhost:8000/health | python -m json.toolFinancescr uses versioned data packs under /data. A “version” is simply a folder label you control (e.g. v1).
/data/
fincrime/v1/
policy.json
customers.jsonl
watchlist.jsonl
labels.jsonl
credit/v1/
policy.json
applications.jsonl
labels.jsonl
golden/v1/
fincrime_golden.jsonl
credit_golden.jsonl
JSON Lines (.jsonl) means:
- streaming-friendly
- easy batch processing
- stable line-by-line records (good for regression sets and eval harnesses)
- supports large packs without loading everything into memory
Purpose: Mimics what a bank already knows about a customer (KYC/CRM).
Used by: FinCrime screening (by customer_id), eval scenarios, agent follow-ups.
Each line is one customer profile.
Schema (v1)
customer_id(string, required): stable ID (e.g.C_002445)name(string, required): full name as stored by the bankdob(string YYYY-MM-DD, nullable): date of birthnationality(string ISO2, nullable)residence_country(string ISO2, nullable)id_hash(string, nullable): hashed identifier (never raw ID number)region(enum string, required):EUorASIA(for slice metrics)
Example
{"customer_id":"C_002445","name":"Mariam Khan","dob":"1995-10-29","nationality":"SG","residence_country":"CZ","id_hash":null,"region":"ASIA"}Privacy rule (strict)
- Never store raw ID numbers.
- Only
id_hashis stored. Hashing is deterministic with a salt version.
Purpose: The watchlist is the runtime candidate set (sanctions/PEP/adverse).
Used by: retrieval + feature computation + evidence output.
Each line is one watchlist entity.
Schema (v1)
entity_id(string, required): stable unique ID (e.g.WL_000762)list_type(enum string, required):SANCTIONS|PEP|ADVERSE_MEDIAprimary_name(string, required): canonical watchlist namealiases(list[string], required but can be empty): alternate names/spellings/orderings/transliteration variantsdob(list[string], optional): known DOB(s) (often missing in real lists)nationalities(list[string ISO2], optional)residence_countries(list[string ISO2], optional)id_hashes(list[string], optional): hashed identifiers if present (rare)source(string, optional): e.g.OFAC,EU,HMT,UN,DEMOactive(bool, required): active/inactive record
Example
{"entity_id":"WL_000762","list_type":"PEP","primary_name":"Walker Sarah","aliases":["Sarah Walker"],"dob":[],"nationalities":["HU"],"residence_countries":[],"id_hashes":[],"source":"DEMO","active":true}What list types mean
- SANCTIONS: legally restricted parties (highest severity, typically P0 when confidence is MED/HIGH)
- PEP: politically exposed person (higher AML risk, triggers enhanced due diligence)
- ADVERSE_MEDIA: negative news / reputational risk (review policy varies)
Purpose: Defines “what should have happened” for an end-to-end screening scenario.
This is your labelled ground truth for evaluation/training.
Each line is one scenario (a screening event for a customer).
Schema (v1)
scenario_id(string, required): stable scenario ID (e.g.SCN_000603)customer_id(string, required): which customer was screenedlabel(enum, required):MATCH|NO_MATCHtrue_entity_id(string, nullable): the correctentity_idif label isMATCHnotes(string, optional): human note (synthetic / golden / edge case tags)
Example
{"scenario_id":"SCN_000603","customer_id":"C_001469","label":"MATCH","true_entity_id":"WL_000338","notes":"synthetic_match"}How it’s used
- Retrieval evaluation:
recall@k= % of MATCH scenarios wheretrue_entity_idappears in top-K retrieved candidates. - Decision evaluation: precision/recall of REVIEW decision at a threshold.
- Training (later): expand each scenario into pairwise rows
(subject, candidate)with targety=1fortrue_entity_id, elsey=0.
Purpose: Policy = operational decision rules (thresholds, severity/priority mapping).
Policy is separate from the model so you can tune thresholds without retraining.
Keys (v1)
retrieval_top_k(int): number of candidates retrieved for scoringreturn_top_n(int): number of matches returned as evidencethreshold(float 0–1): cutoff for REVIEW decisionuncertainty_band(float): ± band around threshold to trigger a single follow-up questioncost_fp(float): false review cost (analyst load)cost_fn(float): missed match cost (high)severity_by_list_type(map): sanctions=HIGH etc.priority_rules(map): P0/P1/P2 routing based on severity + confidence band
Purpose: Application-level signals (as if pulled from origination + internal risk systems).
Used by: credit decisioning engine (PD + affordability + policy).
Each line is one application.
application_id(string): e.g.A_002445customer_id(string): links to fincrime customer recordproduct(string): currentlyPERSONAL_LOANamount_gbp(number)term_months(int)income_monthly_gbp(number)outgoings_monthly_gbp(number)employment_status(enum):EMPLOYED|SELF_EMPLOYED|UNEMPLOYEDemployment_months(int)credit_utilisation(float 0–1): derived if raw fields exist; otherwise treated as providedmissed_payments_12m(int)bank_balance_volatility(float 0–1): derived if raw fields exist; otherwise treated as provided
These allow derived fields to be explained and recomputed deterministically.
revolving_balance_gbp(number): total outstanding revolving balance (cards)revolving_limit_gbp(number): total revolving credit limitbalance_mean_gbp(number): mean daily balance over lookback window (e.g. 90d)balance_std_gbp(number): std dev of daily balance over lookback windowdays_overdraft_90d(int): count of overdraft days in last 90 days
Derivation formulas (if raw fields present)
Credit utilisation
credit_utilisation = clip(
revolving_balance_gbp / max(revolving_limit_gbp, epsilon),
0,
1
)
where epsilon is a small constant (for example 1.0) to avoid division by zero.
Bank balance volatility
bank_balance_volatility = clip(
balance_std_gbp / max(balance_mean_gbp, epsilon),
0,
1
)
Optionally, you can incorporate overdraft days as a separate feature or as a policy guardrail.
Precedence rule
- If raw fields exist and are valid, Financescr derives the feature deterministically.
- Otherwise it uses the provided
credit_utilisation/bank_balance_volatility.
Example (v1 minimal)
{"application_id":"A_002445","customer_id":"C_002445","product":"PERSONAL_LOAN","amount_gbp":300,"term_months":12,"income_monthly_gbp":600,"outgoings_monthly_gbp":325.5,"employment_status":"EMPLOYED","employment_months":23,"credit_utilisation":0.3094,"missed_payments_12m":0,"bank_balance_volatility":0.7115}Purpose: supervised learning/evaluation truth for PD modelling.
Each line is one outcome label.
Schema (v1)
application_id(string, required)default_12m(int 0/1, required): whether the borrower defaulted within 12 monthspd_12m(float 0–1, optional/debug): synthetic underlying probability used during generation
Example
{"application_id":"A_002445","pd_12m":0.2055,"default_12m":0}What is default_12m
- Binary target: 1 if default event within 12 months, else 0.
- In real lending, “default” is defined by delinquency/charge-off rules (e.g., 90+ DPD). Here it is a consistent synthetic label.
Purpose: converts PD + affordability into APPROVE/REFER/DECLINE and (optionally) APR pricing.
Key behaviours
- Compute features (including derived utilisation/volatility if raw fields exist)
- Score PD (heuristic logistic → trained LR later)
- Apply affordability stress test and PD thresholds
- Emit reason codes and a decision bundle
Golden sets are regression fixtures: small curated subsets used to detect unintended changes in feature compute, scoring, or policy.
golden_id(string)application_id(string)notes(string)
Example:
{"golden_id":"GCRED_026","application_id":"A_002945","notes":"golden_credit_regression"}Same idea, but for fincrime scenarios.
Example:
{"scenario_id":"GSCN_012","customer_id":"C_000049","label":"MATCH","true_entity_id":"WL_000265","notes":"golden_match"}- Required v1: the current fields in generator (amount/term/income/outgoings/employment/utilisation/missed/volatility)
- Optional v1 (recommended): raw balance/limit/stats fields to derive utilisation/volatility
- Planned v2: more realistic stability fields (months_at_address, housing_status, dependents) and richer bureau-like signals
- Required v1: customers + watchlist + scenario labels + policy
- Planned v2: list subtypes (e.g. terrorism/narcotics) and richer alias/transliteration coverage
- Data (
*.jsonl): “world state” (customers, watchlist, applications) - Labels (
labels.jsonl): truth used for evaluation/training - Policy (
policy.json): operational decision thresholds + routing rules - Model (later
model.json): weights/coefficients used to score deterministically
curl -s http://127.0.0.1:8000/health | python -m json.toolCID=$(python -c "import uuid; print(uuid.uuid4())")
curl -X POST http://127.0.0.1:8000/agent/chat \
-H "Content-Type: application/json" \
-d "{\"conversation_id\":\"$CID\",\"message\":\"help\"}" | python -m json.toolcurl -s http://127.0.0.1:8000/agent/conversations/$CID | python -m json.tool- Fincrime: implement
POST /screen(retrieval → features → heuristic logistic → policy) - Credit: implement
POST /credit/apply(PD → affordability → policy) - Persistence + audit + cases for both domains
- Offline eval harness + threshold tuning + slice metrics
- OpenAI tool-calling + grounded explanation generation
- Tiny static UI + scripted demo mode (curl + UI)
- Deterministic inference: same input + same data pack + same policy = same output.
- No raw secrets/PII in audit logs (no raw ID numbers).
- Model score is separate from policy decision.
- Traces and transcripts are persisted for replay.