SanctionsGraph is a FinTech compliance intelligence tool that combats against sanctions violations.
It screens education agents and international applicants against global sanctions lists, then visualzies hidden risk connections (shared addresses, phone numbers, banking details) as an interactive graph.
- learn more about challenges and solutions in fintech/ed-tech.
- learn how to wire restful apis & database in django, how to make components in vue.js and work with postgresql database and integrate them with one another.
- quench my curiosity ...
In order to better understand the problem that we are solving, one must acquaint himself with the following vocabulary.
Sanctions list: A government register of people and organisations you are legally barred from doing business with. Payments to anyone on one must be blocked, not just flagged.
OFAC: The US Office of Foreign Assets Control, which publishes and enforces the best known lists. Penalties reach roughly $377,700 per violation, or twice the transaction value.
SDN List: Specially Designated Nationals, OFAC's main list of blocked persons and entities.
PEP: Politically Exposed Person. Someone holding a prominent public role, or their close family and associates. Not illegal to deal with, but higher risk of corruption or bribery, so it warrants extra scrutiny rather than an outright block.
Education agent: A recruiter paid commission to place international students with universities. They handle tuition money, which is what brings them inside the scope of sanctions and AML rules. They are the subject being screened here.
KYC / AML: Know Your Customer and Anti-Money Laundering. The regulatory obligation to identify who you are dealing with and where their money came from.
UBO: Ultimate Beneficial Owner. The real human behind a chain of companies. Layers of shells exist precisely to obscure this, which is why ownership is worth modelling as a graph.
False positive: An alert that turns out to be the wrong person. Around 90 to 95% of alerts are false positives, so an analyst's real job is dismissing noise, not finding hits. That is the constraint this project is built around: every match carries a human-readable explanation so it can be dismissed quickly and defensibly.
OpenSanctions: An open dataset consolidating sanctions lists and PEP registers from many jurisdictions into one schema. This project ingests its bulk download.
FollowTheMoney (FtM): The schema OpenSanctions publishes in. Every record has the
same envelope (id, caption, schema, properties, datasets, target) but the
keys inside properties vary by schema, and every value is a list even when there is
only one item.
Target: An FtM record with target: true is sanctioned or wanted in its own right.
target: false means it is a related party, pulled in because it is connected to
someone who is. Stored as SanctionedEntity.is_target and used to weight the risk score.
Screening case: One run of the matcher against one agent. Holds the risk score, the
review status (open or resolved), and a network_snapshot: the graph frozen as it
was at run time, so an old decision stays auditable after the sanctions data refreshes.
Match: One hit inside a case. Carries a match type, a confidence from 0 to 100, an explanation, and the reviewer's verdict.
Evidence tiers: The five match types, ranked strongest to weakest. New matching logic maps onto these rather than inventing a parallel score:
| Tier | Meaning | Confidence |
|---|---|---|
identifier_exact |
Same passport, tax ID, or company registration number | 100 |
name_exact |
Name or alias matches exactly | 95 |
name_fuzzy |
Name is a close but inexact match | similarity score |
address_fuzzy |
Address is a close match | similarity score, penalised if broad |
network_2nd_degree |
Not directly matched, but two hops away through a shared attribute | 50 |
Trigram similarity: How the fuzzy tiers work. Postgres' pg_trgm extension splits
strings into three-character chunks and scores the overlap. similarity('Sergey Lavrov', 'Sergei Lavrov') is 0.75, which clears the 0.6 threshold.
Broad address: A free-text address so generic that matching on it is meaningless. "Hong Kong" hits 128 entities. Confidence on such matches is scaled down, and the screening form warns before submission.
2nd-degree link: An entity reached not by matching the agent directly, but by following a shared address or identifier from something that did match. This is where a graph earns its place: it surfaces relationships a flat list of alerts hides.
- Refactor the current codebase by creating a python package for each service.
- Create CI/CD pipelines for both the backend and frontend via GitHub Action.
- Fix all pylance and pyright issue using mypy static typechecker ...
- Fix all unreachable path errors.
- Synchronize the postgresql db running locally in my docker desktop with the one running in neon to avoid unexpected behavior ...
- Preserving traceability from the original record once an association is detected between an education agent and an entity from the sanction list.
- Building reusable
viewsandcomponentsin Vue.js - Architecting Restful APIs
- Building multi-tenant software
- Using agentic workflows to efficiently argument speed of design (brainstorming), implementation and testing.
Frontend and backend both run on Vercel, with Postgres on Neon. The backend is a serverless function, so there could be a bit of a delay depending on who accesses the webpage first.
The live database holds a snapshot of roughly 50,000 sanctioned entities. It is refreshed by hand rather than on a schedule, so it will drift behind OpenSanctions over time.
| Layer | Built with | Hosted on |
|---|---|---|
| Frontend | Vue 3 (Composition API), TypeScript, Vite, Cytoscape | Vercel |
| Backend | Django 6.1 + Django REST Framework | Vercel (serverless function) |
| Database | PostgreSQL with the pg_trgm extension for fuzzy matching |
Neon |
| Testing | pytest and pytest-django; Vitest and Playwright | run locally |
Fuzzy name and address matching is done in Postgres with trigram similarity()
rather than in Python, and second-degree network links are found with NetworkX.
Postgres is required. The project does not fall back to SQLite.
Prerequisites: Docker, uv, Node 20+
docker run -d --name sg-postgres \
-e POSTGRES_USER=sguser -e POSTGRES_PASSWORD=sgpass \
-e POSTGRES_DB=sanctionsgraph -p 5432:5432 postgres:15If a native postgresql service is already bound to 5432 it will answer instead of the
container, and migrate fails with password authentication failed for user "sguser".
Check with sudo lsof -i :5432 and stop the native service.
2. Backend: http://localhost:8000
cd backend
uv sync
uv run manage.py migrate
uv run manage.py createsuperuser # optional, for /admin/
uv run manage.py runserver3. Frontend: http://localhost:5173
cd frontend
npm install
npm run devAt the moment, as the software was built to be more of a demo, the backend is connected to PostgreSQL which
is being provisioned via Neon. However, it's difficult to not expect misuse or abuse of the /
The screening pipeline works against an empty database, but there is nothing to match.
The source dump is roughly 2.6 GB and is streamed line by line rather than loaded into
memory. Start with --limit so a first run takes minutes rather than an hour.
curl -o backend/data/entities.ftm.json \
https://data.opensanctions.org/datasets/latest/default/entities.ftm.json
cd backend
uv run manage.py ingest_opensanctions --limit 5000Re-running is safe: entities are upserted on the OpenSanctions source_id.
cd backend && uv run pytest # Django: models, matcher, API
cd frontend && npx vitest run # Vue components + composables
cd frontend && npm run test:e2e # Playwright, boots the dev server itself
cd frontend && npm run build # vue-tsc, type errors fail the buildDay-by-day terminal transcripts of the above live in .logbook/.
Let me know if you have issues tangpiseth43@gmail.com
The code in this repository is MIT licensed. See LICENSE.
The OpenSanctions dataset it consumes is not covered by that license. It is published under CC BY-NC 4.0, and OpenSanctions treats compliance screening as commercial use regardless of whether it generates revenue. Non-commercial use (student projects, personal research) needs no permission; use inside a for-profit business requires a data license from them.
- OpenSanctions docs: entity matching, bulk downloads, and the FollowTheMoney data model this project ingests.
- OpenSanctions API: the hosted
/matchendpoint, an alternative to ingesting the bulk file. - OpenSanctions Swagger UI: interactive API docs.
- OFAC official website: the US sanctions authority.
- SDN List Specially Designated Nationals, the best known sanctions list.
- US government sanctions and OFAC primers.
The premise is that screening alerts are overwhelmingly noise, so the useful output is an auditable explanation rather than a score.
- "The Problem of False Positives in AML Screening" 90 to 95% of alerts are false positives. This is the number the whole design responds to.
- "How to Reduce False Positives in Sanctions Screening" institutions spend 5 to 10 minutes per alert and resolve under 10% as true matches.
- "Sanctions Screening Challenges and Best Practices" why regulators now expect holistic matching, not just name comparison.
- "Why False Positives No Longer Matter in AML" the counter-argument: automate the volume instead of tuning thresholds.
- "OFAC Meaning: What Is the Office of Foreign Assets Control?" penalties reach $377,700 per violation, or twice the transaction value.
- OFAC video series and a 13-part playlist covering the 50% rule, false positives, and evasion red flags.
Books on sanctions and financial crime (6)
| Book | Author | Why read it |
|---|---|---|
| Sanctions Screening: A Key Element of AML and Financial Crime Prevention | CA Mayur Joshi & Vedant Sangit | The most directly applicable of these. 144 pages on how screening systems work, evasion techniques, and building a compliance program. |
| The Art of Sanctions: A View from the Field | Richard Nephew | Written by an architect of the Iran sanctions. Covers why sanctions exist and what they are meant to achieve. |
| Backfire | Agathe Demarais | How evasion works in practice, and how overuse of sanctions creates workaround economies. |
| Moneyland | Oliver Bullough | Offshore finance and how wealth is hidden. Useful intuition for why ultimate beneficial ownership matters in a graph model. |
| Billion Dollar Whale | Tom Wright & Bradley Hope | The 1MDB scandal. Shows how intermediaries exploit siloed bank KYC, which is the network problem this tool targets. |
| Mastering Anti-Money Laundering and Counter-Terrorist Financing | Tim Parkman | Operational guide to AML programs, transaction monitoring, and SARs. |
- Kharon API: a commercial equivalent.
- PuppyGraph: compliance graph graph modelling for compliance data.
- Microsoft OpenSanctions connector how enterprises wire OpenSanctions into Power Automate.
- D3.js: considered for the graph view; this project uses Cytoscape.
- GitHub CLI: used for the branch and PR flow.
- Method Draw: used to hand-edit
logo.svg. - rsms/rsm
.logbook: the inspiration for keeping a dated dev journal in the repo. - mypy static python typechecker