DEV Community

Shakti Raj
Shakti Raj

Posted on

Financial Crime Agent

One Phone, Five "Customers": Catching a Money Mule Ring with FalkorDB in 8.4 ms

Tags: #GraphHacks #WeMakeDevs #FalkorDB #AML #openCypher #AIAgents #GraphDatabase

Built for Graph Hacks 2026 by @WeMakeDevs and @FalkorDB. Track: Agents That Act on Connected Data.

Live demo: https://monumental-marigold-feca47.netlify.app/
GitHub: https://github.com/ShaktiEighteen/Financial_Crime_Agent

Here's a transaction: $48,000 moves from one account to another at 2 AM.

Is it fraud? If you're looking at it in a table, you can't tell. It's a big number, sure, but big numbers move around banks all day. A rules engine checking one row at a time will probably shrug and let it through.

That's the whole problem with how most anti-money laundering (AML) checks work, and it's what I wanted to fix for this hackathon. The project is called Financial Crime Agent. It's an AI agent that investigates a transaction network the way a real investigator would, by following the connections instead of staring at rows. In the demo it finds five accounts (Rivera, Lee, Kim, Patel and Chen) that all turn out to be logging in from the same iPhone 15 Pro, then writes up a draft Suspicious Activity Report for a human to review.

The part that still makes me grin: the traversal that took 1,420 ms with SQL runs in 8.4 ms on FalkorDB. About 169x faster, and no joins at all.

Criminals don't think in rows

Money launderers aren't dumb about this. They know compliance systems look at transactions one by one, so they build structures that only show up when you zoom out. A few classics:

  • Wash rings. Money goes A to B to C to D and back to A. Each transfer looks like ordinary business. The loop is the crime.
  • Layering through shell companies. Funds get split and pushed through chains of empty entities, so each hop puts more distance between the money and where it came from.
  • Device collisions. A bunch of "different" customers who all use the exact same physical phone. That's usually one person running many identities.

Notice that none of these live inside a single row. They live in the relationships between rows. And relationships are exactly what relational databases are worst at.

What SQL does when you ask it a graph question

Say you want every path of up to five transfers that ends up back at the account it started from. In SQL you'd write something like this:

WITH RECURSIVE money_trail AS (
    SELECT from_account, to_account, 1 AS depth,
           ARRAY[from_account] AS path
    FROM transactions
    WHERE from_account = 'ACC-4471'

    UNION ALL

    SELECT t.from_account, t.to_account, mt.depth + 1,
           mt.path || t.from_account
    FROM transactions t
    JOIN money_trail mt ON t.from_account = mt.to_account
    WHERE mt.depth < 5
      AND t.from_account <> ALL(mt.path)
)
SELECT * FROM money_trail
WHERE to_account = 'ACC-4471';
Enter fullscreen mode Exit fullscreen mode

It works. But every extra hop is another self-join on the same giant table, and the intermediate results multiply as you go deeper. Five hops means six or more joins' worth of work, a lot of temp data, and long-running queries that fight with the normal transactional load for locks. In my benchmark this took 1,420 ms, and that was on a small dataset. Imagine a real bank with billions of rows.

The frustrating thing is that "who is connected to whom, through how many layers, and what do they share" is the exact question an investigator wants to ask. It's also the one SQL handles worst.

The same question in openCypher

With FalkorDB, finding a loop of 2 to 5 transfers looks like this:

MATCH path = (a:Account)-[:TRANSFERRED_TO*2..5]->(a)
RETURN path
Enter fullscreen mode Exit fullscreen mode

That's it. One line. And if you squint, it looks like what you'd sketch on a whiteboard: an account with an arrow going out and coming back to itself.

Finding the device ring is just as direct:

MATCH (a:Account)-[:USES_DEVICE]->(d:Device)<-[:USES_DEVICE]-(b:Account)
WHERE a <> b
WITH d, collect(DISTINCT a) AS ring, count(DISTINCT a) AS ring_size
WHERE ring_size >= 5
RETURN d.hardware_id AS device, ring, ring_size
Enter fullscreen mode Exit fullscreen mode

Here's how the two stack up on my 5-hop test:

Latency Joins
Recursive SQL 1,420 ms 6+
FalkorDB (openCypher) 8.4 ms 0

How the agent works

I think of the agent as a junior investigator who never gets tired. An alert comes in on a suspicious account, and from there it:

  1. Walks outward through the graph, following money flows and shared attributes like devices.
  2. Spots that five accounts are tied to a single hardware ID.
  3. Lights up that ring in the UI and fades every innocent node into the background.
  4. Drafts a FinCEN Suspicious Activity Report (SAR) based on what it found.

The highlight-and-dim thing sounds like a small touch, but it ended up being my favorite part of the demo. Real investigators drown in information. When the five suspicious nodes glow and everything else goes quiet, the answer is just there. You don't need to read a report to get it.

The stack is pretty simple: React + Vite for the front end, TailwindCSS for styling, Cytoscape.js to draw the graph, and FalkorDB underneath. Cytoscape was a good pick because you can style nodes by state, which is exactly what the highlight/dim effect needs.

Why FalkorDB

A few things about it mattered a lot for this project.

It's in-memory. Investigation is interactive. You click a node and expect its neighborhood immediately. With the graph held in memory, queries come back in a few milliseconds, so the agent can fire off lots of exploratory queries during one investigation and nobody notices the wait.

Index-free adjacency. This is the reason for the 169x. In a relational database, "who did this account send money to?" means an index lookup plus a join. In a graph, each node already points at its neighbors, so following an edge is basically a pointer hop. The cost depends on how much of the graph you actually touch, not on how big the whole thing is. A 5-hop walk doesn't pay for the billions of rows it never visits.

openCypher. Readable queries matter more than I expected. In compliance, someone other than the author often has to look at a query and decide whether it's doing the right thing. A line like (a)-[:TRANSFERRED_TO*2..5]->(a) is something a non-engineer can follow. The recursive CTE above is not.

It keeps the agent honest. This is the one I care about most. LLMs have a habit of sounding sure of themselves while making things up, and in financial crime that's dangerous. Wrongly accusing a real person is a serious harm, not a funny bug. So I set the agent up so that every step in its reasoning timeline points at actual nodes and edges in the graph. When it says Rivera, Lee, Kim, Patel and Chen share a device, you can click through to the query result that proves it. The graph is the witness. The language model just tells the story of what the graph returned.

A human still signs the report

I want to be really clear about this part: the agent does not decide anyone is a criminal.

Filing a SAR carries real legal weight. It can lead to frozen accounts and law enforcement attention. That's a call for a trained, accountable person, not a model. So the agent does the slow, tedious work (traversing the graph, pulling together the evidence, drafting the narrative) and hands a compliance officer a case file with the proof attached. The officer reviews it, applies judgment and context, and gives the final legal sign-off.

I actually think this is the only realistic way AI gets into regulated industries. Regulators don't mostly care how clever your model is. They want to know who's accountable and whether you can show your work. The human sign-off answers the first question. The graph-linked timeline answers the second.

What I took away from this

Graphs aren't a nice-to-have for AML. The fraud itself is shaped like a graph. If you force it into rows and columns, you're slower, and worse, you can't see some patterns at all, because the query cost blows up before you reach the depth where the pattern lives.

Cypher is easier to read, not just faster. I went in expecting a performance win. I didn't expect the clarity to matter just as much. A query people can read is a query people can trust and audit.

Being able to see what the agent did is what builds trust. The reasoning timeline ended up more valuable than the speed. A mystery "risk score: 94" makes people nervous. A trail that says here's the device, here are the five accounts, here are the transfers between them, is something a regulator can actually check.

Fix hallucination with architecture, not just prompts. If the agent can only report what the database gave it, there's nowhere for invented evidence to hide.

Design for the person doing the job. The dimmed nodes, the plain-language timeline, the draft SAR with a sign-off step: none of that is fancy, but it's what makes the tool usable for the human carrying the responsibility.

Where I'd take it next

  • Streaming transactions, so rings get flagged while they're forming instead of afterwards
  • Better entity resolution across shared phone numbers, addresses, IPs and beneficial owners
  • Graph algorithms like community detection to rank suspicious clusters automatically
  • Proper case management, with audit logs and multiple reviewers

Try it

Open the demo, kick off the investigation, and watch five unrelated-looking customers collapse into one ring around a single phone.

Criminals already think in networks. Our tools should too. Thanks to @WeMakeDevs and @FalkorDB for putting on Graph Hacks 2026 and for building a database that makes thinking this way feel natural.

If you've ever looked at a spreadsheet and felt like the real story was hiding between the rows, you weren't imagining it. It was in the graph.

GraphHacks #WeMakeDevs #FalkorDB

Top comments (0)