Sherlock Pay (repo name: UPI Fraud Ring Detective) is our Graph Hacks project: an AI agent that takes **one* flagged UPI payment, walks a FalkorDB graph, and comes back with the whole mule ring, a decision for every account, and the exact Cypher behind each step.*
A video call one afternoon
Anil Mehta's phone rings. A man in a police uniform on a video call says Anil's Aadhaar was used to send drugs abroad. He is under "digital arrest". He must not hang up, must not tell his family, and must move ₹1,85,000 to a "verification account" to prove his innocence.
Anil pays. One UPI payment, a few taps.
Anil isn't real. He lives in the synthetic dataset we built for this hackathon. But the scam is, and so are the numbers: the Ministry of Finance told the Lok Sabha that India saw 12.64 lakh UPI fraud cases worth ₹981 crore in FY 2024-25 (BOOM).
Here is the frustrating part. When a bank analyst opens Anil's complaint, they see one row:
| txn | from | to | amount |
|---|---|---|---|
| TXN502668 | Anil Mehta | Vikram Yadav | ₹1,85,000 |
Nothing about that row screams "gang". Vikram's account exists, has a UPI ID, received a payment. By the time someone has checked his statement by hand, the money is four accounts away.
We are Ishika and Arpan, two beginners from Punjab Engineering College, and this is the problem we wanted to crack in one weekend.
The money doesn't sit still. It hops.
Stolen money goes through mule accounts: fresh accounts, often minimum KYC, run by the same people from the same phones and laptops. Each mule receives, keeps a little, and forwards the rest within minutes.
Look at one payment and everything looks fine. Look at the connections and the ring lights up. That's a graph problem, so we put the data in a graph.
How we modelled UPI in FalkorDB
Five kinds of nodes: Account, UPI, Phone, Device, IP. Edges: PAID (with amount and timestamp), HAS_UPI, HAS_PHONE, USES_DEVICE, SEEN_ON. And a sixth node type the agent writes itself: Case, linked to accounts with ABOUT and to the shared phones/devices with EVIDENCE.
The data is synthetic: we generated 431 accounts and 2,766 payments with Indian names, banks and UPI handles, then hid five fraud patterns inside it (a device farm, a layering chain, structuring just under ₹10,000, one phone with many UPI IDs, a fake merchant). We also planted traps: a family sharing one tablet, and a big but normal rent payment. A good investigator must not freeze those.
Tools, not free-form Cypher
Our first instinct was "let the LLM write Cypher". We didn't. A model that invents queries on live bank data is scary and hard to debug. Instead, the agent gets 11 tools, and each tool is one tested Cypher query or a FalkorDB graph algorithm. The LLM's job is the detective work: which tool next, and what does this evidence mean?
Three of them, copied straight from our tools.py:
1. Who else is on the same IP? One pattern, four hops: account → device → IP → another device → another account.
MATCH p = (a:Account {id: $id})-[:USES_DEVICE]->(d1:Device)-[:SEEN_ON]->(ip:IP)<-[:SEEN_ON]-(d2:Device)<-[:USES_DEVICE]-(other:Account)
WHERE other <> a AND d1 <> d2
RETURN p, other.id AS other, other.holder AS name, 'IP' AS via, ip.addr AS value
2. Follow the money, but only when it moves fast. A variable-length path, plus a rule that each next payment happens after the previous one and within $gap seconds (2 hours by default). Real layering is quick; this rule killed almost all of our false chains.
MATCH p = (a:Account {id: $id})-[:PAID*1..4]->(b:Account)
WHERE all(i IN range(0, length(p) - 2) WHERE
relationships(p)[i + 1].ts >= relationships(p)[i].ts AND
relationships(p)[i + 1].ts - relationships(p)[i].ts <= $gap)
RETURN p, length(p) AS hops
ORDER BY hops DESC LIMIT 150
(Fun beginner fact: the 4 in *1..4 can't be a $parameter in Cypher, so Python fills it in and caps it at 6.)
3. Find the whole ring in one call. FalkorDB's built-in algo.WCC (weakly connected components) over accounts, phones, devices and IPs. Everything linked to our suspect through any chain of shared identifiers lands in the same component.
CALL algo.WCC({nodeLabels: ['Account', 'Phone', 'Device', 'IP'],
relationshipTypes: ['HAS_PHONE', 'USES_DEVICE', 'SEEN_ON']})
YIELD node, componentId
WITH componentId, collect(node) AS members
WHERE any(n IN members WHERE n:Account AND n.id = $id)
RETURN [n IN members WHERE n:Account | n.id] AS accounts, size(members) AS component_size
The rest: algo.pageRank on PAID edges to find the collector, shortestPath to explain how two suspects connect, a 0–100 risk score built from 14 small feature queries (each reason written out, like "+25 device shared with 5 other accounts"), check_past_cases for memory, and take_action, which writes the decision back into the graph.
Watching it investigate, hop by hop
We gave the agent (Gemini by default) Anil's payment and pressed Investigate.
The left side is the agent thinking out loud, one tool per step. The right side is the graph it has touched so far, growing with every call. What it worked out:
- The receiver is suspicious. Vikram's account is only 44 days old, with minimum KYC.
-
He isn't alone. Connected components (
algo.WCC) link him to another account through shared identifiers (phones, devices). - The money keeps moving. The money trail shows a 4-hop layering chain, each hop forwarding almost everything it received, within minutes.
And every step can be opened to show the exact Cypher it ran. No black box.
The verdict: freeze, escalate, explain
The agent froze the four mule accounts in the middle of the chain and escalated the last hop to a human analyst. In its own words, that's the "final destination account in layering chain holding the fraud proceeds", and it wanted a person to review it before acting. Both decisions were written into FalkorDB as Case nodes (FREEZE with risk 100, ESCALATE with risk 48), linked to the accounts and to the shared identifiers as evidence. The whole run took 12 Cypher queries, all listed under "Why?".
The trap: an innocent family
This was the test we cared about most. In another flagged payment, three members of one family, the Aroras, all use the same device. A naive "shared device = fraud" rule would freeze all three.
The agent checked the graph instead: their money goes to everyday shops, not to fresh mule accounts, and nothing races onward hop after hop. Risk 15. CLEAR. It even wrote that down as a Case, so nobody re-investigates the family next week.
How well does it work?
We wrote evaluate.py, which runs a full investigation for every flagged transaction and compares the result with the answer key we planted:
| Run | Ring accounts caught | Innocent accounts frozen |
|---|---|---|
| Offline rule-based mode (no LLM) | 27 / 28 | 0 |
| LLM agent with OpenAI gpt-4.1-mini | 27 / 28 | 0 |
| Gemini (partial run, stopped by free-tier quota) | device farm 8 / 8, layering chain 5 / 5 | — |
These are our own test runs on synthetic data, not a real bank. The one miss was the "boss" of the one-phone ring: the agent put it on watch (FLAG) instead of freezing it, which honestly isn't a terrible call.
Why we built it this way
-
Graph instead of SQL joins. "Accounts that share a device that was seen on an IP used by another device" is one readable
MATCHin Cypher. In SQL it's a tower of self-joins, and a variable-length money trail is worse. -
Tools instead of free-form queries. Every tool is tested, so the LLM can't write a broken or dangerous query. And graph algorithms make amazing tools: one
algo.WCCcall answers what would take an LLM ten steps. -
MCP. All 11 tools also run as an MCP server, so Claude Desktop, Cursor or any MCP client can investigate the same graph. Our
mcp_agent.pydiscovers the tools withlist_toolsinstead of importing them. -
Explainability and humans in the loop. A bank can't freeze someone's salary account because "the AI said so". Every verdict ships with its path and its Cypher, unclear cases go to a human (ESCALATE), and every decision is saved as a
Casethat the next investigation can find.
What we learned (the honest version)
- Windows setup was our first boss fight. Getting Python, FalkorDB and our API keys to talk to each other took longer than writing the first tool. We now have a lot of respect for a good README.
- College WiFi blocked the FalkorDB Cloud port. We got "connection refused" and spent a long time blaming our code before realising it was the network.
- Free Gemini keys run out. Daily limits per model kept hitting us while testing. So the agent keeps a list of Gemini models: on a 503 it waits and retries, on a quota error it switches to the next model and tells you in the UI. It did exactly that during our final live test. We also added an offline mode so the demo never dies.
-
Cypher felt strange for an hour, then obvious.
-[:PAID*1..4]->reads like a sentence: follow the money up to four hops. - The traps taught us the most. A family sharing a tablet looks exactly like a device farm until you look at account age and where the money goes.
What's next
- Test on bigger and messier synthetic data (and, one day, real anonymised data with a bank).
- Stream new payments in, so the agent investigates as soon as something is flagged.
- Let a human analyst's ESCALATE decision feed back into the next investigation.
- Nothing here is India-specific: the same graph works for Pix, Zelle or Faster Payments.
Try it
- 🕵️ Live app: upi-fraud-detective.streamlit.app
- 💻 Code: github.com/ishika29-tech/upi-fraud-ring-detective
- 🎥 2-minute demo: youtu.be/_oTZMlVvnbI
Pick a flagged payment, press Investigate, and watch the ring appear. If you have ideas, or you work on fraud at a bank and think we got something wrong, tell us in the comments. We're beginners and we'd love to learn.
Built by Ishika Bansal and Arpan Goel (Punjab Engineering College) for Graph Hacks by WeMakeDevs × FalkorDB.








Top comments (0)