A Client Rebuilt My AML System in a Weekend. Here's What Both Codebases Actually Look Like.
A client rebuilt my AML screening system with AI in a weekend. I audited both codebases. Here's what the rebuild missed — and why it matters.
A client told me he rebuilt my AML screening system in a weekend.
He wasn't lying. He also wasn't entirely right.
He'd found OpenSanctions — an open-source aggregator, 400+ sources, free to download. He wired it into a screening tool with Claude Code in three days. It looked finished. It returned results. It felt like the same thing I'd built for him months earlier, at a fraction of the time.
He was half right, and I want to start there, because the half he got right matters more than the half he got wrong.
He was right that building is cheap now.
Most of what I did for ten years is free. Wiring an API, writing a CRUD layer, gluing a frontend to a backend — an afternoon's work for a tool that didn't exist three years ago. I'm not going to pretend otherwise, and I'm not going to argue that building is still hard. It isn't. That fight is already lost, and arguing it just makes you look like you're protecting a monopoly on typing.
Here's the half he got wrong. Two things, both invisible until they aren't.
The first is licensing.
OpenSanctions is free — but the license says non-commercial use only. Screening your own customers against a sanctions list is commercial use. It doesn't matter that no money changes hands for the data itself. The moment that screening result touches a paying customer, a compliance decision, or a regulatory filing, the free tier no longer applies.
Most people using OpenSanctions in production don't know this, because nothing in the download process tells them. There's no license-check step in a pip install or an API call. You find out you're in breach when someone asks, not before.
The second is the matching engine, and this one takes thirty seconds to show.
Search "Gazprom" in a sanctions database. You'll get a result count. Now search "Газпром" — same company, written in Cyrillic. Same entity, same sanctions status, different alphabet.
I ran both searches. 44 results for the Latin spelling. 327 for the Cyrillic. Same company. Same list. Different number, because the matching engine treats them as different strings unless someone has explicitly built the transliteration layer to connect them.
That gap isn't a bug in OpenSanctions. It's the actual hard part of sanctions screening — the part that isn't in the demo, because the demo doesn't test names in other alphabets. Real sanctioned entities aren't polite enough to only exist in one script.
So what does the system he was replacing actually do?
It pulls from 13 government sanctions lists directly — not through an aggregator, from the source. OFAC. The UN. The EU. The UK. Australia. Canada. The UAE. Singapore. Japan. Switzerland. New Zealand. Hong Kong. Interpol.
Each one has its own collection worker, because each one is a different format. OFAC publishes XML. The EU publishes a structured JSON feed. Some sources still hand you a CSV built for a mainframe in 2004. A single "sanctions API" that wraps all of these still has to solve this problem somewhere — the question is whether it's solved once, upstream, by someone who reads each format, or left as a shared point of failure.
That's the actual argument for pulling direct from source instead of depending on one aggregator: if OpenSanctions changes its terms, gets rate-limited, or goes down, a system depending on it alone goes blind. A system with 13 independent collection workers loses one feed and keeps running on twelve.
Behind that: a job queue, because 13 feeds updating on different schedules need to run without blocking each other. Redundancy, because a screening system that's down during a compliance check isn't a screening system. None of that shows up in a demo. All of it shows up in an audit.
There's a third thing worth mentioning, because it's how this whole conversation started.
The repo he'd seen — an earlier version, before some cleanup — had a .env.example file listing seven API keys. One of them was live. The other six were declared, wired up at some point, then abandoned when we picked a different provider or a different approach. Nobody deleted them.
That file is the one document a non-technical person can actually read. Everything else is application code. So when someone opens a repo looking for what it's "really" using, .env.example is what they see — and if it lists six dead keys next to one live one, it reads like seven paid integrations, or worse, like nobody knows what's actually running.
That's not a security problem. It's a communication problem, and it's an easy one to fix: treat .env.example like a document someone will actually read, because someone will.
Here's where I land.
He didn't build an AML system in a weekend. He built the demo of one — and the demo is genuinely impressive, and genuinely free now, and I mean that without irony.
The distance between the demo and the system that survives an audit is: a license you've actually checked, a matching engine that handles more than one alphabet, thirteen collection workers instead of one dependency, and a config file that doesn't lie to the next person who reads it.
That distance is the entire job now. It always was, really. It's just no longer disguised by how long the demo takes to build.