DEV Community

Cover image for RingBreaker: An AI Agent That Breaks UPI Mule Rings on FalkorDB
Jeevesh Chaurasiya
Jeevesh Chaurasiya

Posted on

RingBreaker: An AI Agent That Breaks UPI Mule Rings on FalkorDB

RingBreaker is an AI agent that investigates UPI mule rings inside a FalkorDB graph, freezes the ring only after a human approves, and then proves with one more graph query that no money path is left.

  • Live demo: ringbreaker-5o88.onrender.com
  • Code: github.com/Jeevesh2605/ringbreaker
  • Built for: Graph Hacks: Context for AI Agents (FalkorDB), Track 01: Agents That Act on Connected Data. Solo, built Oct 15 to 18, 2026.
  • The loop in one line: investigate → verdict → human approval → freeze → verify ("13 open routes, then 0").

The problem

A scam victim pays a "KYC update" fee over UPI. Within hours the money has hopped through four accounts and is gone.

  • Each hop looks harmless. A Rs 8,000 transfer between two accounts is normal on its own.
  • The pattern lives in the connections. Brand-new accounts, minimal KYC, shared phones and devices, money fanning in and out within minutes.
  • Tables hide it. Following money four hops deep in SQL means four self-joins, and the query has to know the shape in advance.
  • Most fraud demos stop at "flagged". Nobody acts on the alert, and nobody checks that the action worked.

In simple words

Think of a bucket brigade passing stolen water down a line of people. You could question each person on their own, and each one would only say "I passed a bucket."

  • RingBreaker looks at the whole line at once: who passed to whom, who stands on the same spot (a shared phone or device), and where the water ends up.
  • It then asks a person: "Remove these five from the line?"
  • After they say yes, it checks again that no bucket can still reach the end. That last check is the point of the project.

The stack

  • FalkorDB: the only datastore. Accounts, transactions, devices, alerts, verdicts and the audit trail all live in one graph. Docker locally, FalkorDB Cloud in production.
  • Node.js + Express: the API, the agent loop and the tools, using the official falkordb client.
  • Groq (groq-sdk) with function calling: openai/gpt-oss-120b as the main model, qwen/qwen3.8-27b as a fallback when the daily quota runs out.
  • React + Vite + react-force-graph-2d: the three-pane dashboard and the live graph canvas.
  • Server-Sent Events: each tool call streams to the browser as it happens.
  • Render: one Docker image serves the API and the built UI on a single URL.

The agent reaches FalkorDB only through read-only tools; the single write path runs from the API after a person approves.

How it actually works, step by step

  1. You click an alert, for example a victim complaint: "paid ~25,000 for a fake KYC update; money moved on within hours."
  2. The API starts the agent and streams every step over SSE. The agent picks graph tools (get_alert, money_trail, fan_stats, shared_identity, expand_ring and others). Each one runs a parameterized Cypher query.
  3. The canvas lights up the exact nodes and edges each query touched. A "Show Cypher" toggle reveals the query behind every step.
  4. The agent submits a structured JSON verdict: ring_confirmed, suspicious or benign, ring members with roles (victim, layer, collector, cashout), evidence citing graph facts, and recommended freezes.
  5. The server validates the verdict and saves it as a Case node in the graph.
  6. You review the evidence, untick anything you disagree with, type your name and press Approve freeze.
  7. The server freezes the approved accounts, writes an audit edge for each, re-runs the verify query and shows the result, for example 13 open routes → 0. The alert then moves to Closed.

The agent proposes; a person decides; the graph proves it.

The dashboard: alert queue with live status on the left, the graph canvas in the middle, the agent trace on the right, and the four-stage tracker in the header.

Mid-investigation on ALT002 (Ring B, the device farm). Every account, device and phone the agent touched is on the canvas, with its latest step glowing. The trace shows each tool call, its arguments and timing, and the switch to the fallback model when the daily quota ran out.

The graph and the agent's tools

One FalkorDB graph, ring, with 7,847 nodes and 25,962 edges, all synthetic and generated from a seeded random generator so every demo is reproducible.

  • Nodes: Account (1,911), VPA, Device, Phone, Alert (7), plus Case nodes written at run time.
  • Edges: SENT (20,007, one per transaction, with amount and ts), USES_DEVICE, LINKED_PHONE, HAS_VPA, ABOUT, and the audit edges INCLUDES {role} and ACTION {type, ts, approved_by}.
  • Transactions are edges, not nodes. A trail of N transfers is an N-hop path, so -[:SENT*1..4]-> answers "where did the money go?" in one query.
  • Planted scenarios:
    • Ring A, a layering chain: 4 victims → 3 first-layer mules → 2 second-layer mules → 1 collector → 1 cash-out, with amounts split into chunks minutes apart.
    • Ring B, a device farm: 6 accounts on 2 devices and 1 phone, each collecting small payments from many senders, then sweeping to one account.
    • Ring C, a decoy: 3 accounts on a shared device but with no money link. The right answer is "suspicious, monitor", not a freeze.
    • Benign look-alikes: a busy merchant with high fan-in, and a family sharing one phone. Neither may be flagged.
  • Tools the model can call (read-only):
Tool What it asks the graph
money_trail Follow money forward or backward up to N hops, with all(r IN relationships(p) WHERE r.ts >= $since) on every edge
shared_identity Who else uses this device or phone: `(a)-[:USES_DEVICE\
{% raw %}fan_stats In/out degree, totals and pass-through ratio
expand_ring Grow a candidate ring along money and identity links at once (UNION ALL)
account_profile Age, KYC level, and how many accounts share each device or phone
get_alert The alert and its target account
run_readonly_cypher A guarded free-form fallback for anything else
  • Every tool starts from an indexed lookup (Account.id, Device.id, Phone.number, VPA.handle, Alert.id, Case.id). Queries take 5 to 30 ms locally and 230 to 470 ms from Render to FalkorDB Cloud.
  • Every tool call emits a trace event: tool, arguments, the exact Cypher, a one-line summary, and the node and edge IDs it touched. The canvas highlights from those IDs.

Act, then prove it

Flagging is half the job. RingBreaker acts on the verdict and then checks its own work.

  • The model cannot freeze anything. Action tools (create_case, freeze_account, verify_cutoff, close_case, generate_report) are never offered to the LLM. Only the approval API calls them.
  • The approval gate is one Cypher statement. It freezes an account only if the case proposed it and it is still active, and writes the audit edge in the same query:
MATCH (c:Case {id: $case_id}), (a:Account {id: $account_id})
WHERE $account_id IN c.proposed_freezes AND a.status = 'active'
SET a.status = 'frozen'
CREATE (c)-[:ACTION {type: 'freeze', ts: $now, approved_by: $approved_by, reason: $reason}]->(a)
Enter fullscreen mode Exit fullscreen mode
  • Verify counts open money routes into the ring's end account that avoid every frozen account:
MATCH p = (src:Account)-[:SENT]->(m:Account)-[:SENT*0..5]->(t:Account)
WHERE m.id IN $ring AND t.id IN $terminals AND NOT src.id IN $ring
  AND none(n IN nodes(p) WHERE n.status = 'frozen')
RETURN count(DISTINCT [n IN nodes(p) | n.id]) AS routes
Enter fullscreen mode Exit fullscreen mode
  • It counts distinct routes, not raw paths. Each transaction is its own edge, so split payments multiply paths: Ring A has 1,488 raw paths but 13 distinct routes. After the freeze: 0.
  • A leak check stops the proof from fooling itself. It counts ring money that already reached an active account the case never named. The cut-off counts as verified only when routes and leaks are both 0.
  • Alerts close themselves from graph state. Each alert shows Open, Awaiting approval, Closed, Needs review, Monitoring or Dismissed, derived from its latest case.
  • A report is generated from graph data only: accounts and roles, the agent's evidence, every approved action with approver and time, and the live verify result.

Safety: an agent that writes Cypher on untrusted data

The agent reads text it cannot trust and writes queries, so safety lives in code, not in the prompt.

  • Reads physically cannot write. Every read tool runs as GRAPH.RO_QUERY, so FalkorDB itself refuses any write. A test bypasses our own guard and confirms the database still blocks it.
  • Free-form Cypher passes a guard (guard.js): it rejects CREATE, MERGE, DELETE, SET, REMOVE, DROP, CALL, LOAD, FOREACH and multiple statements, enforces a LIMIT of at most 100, and runs with a server-side timeout.
  • Every query is parameterized. Values arrive as $parameters, never spliced into query text, including the seed script.
  • Verdicts are grounded. The validator rejects ring members no tool returned, freezes on victims or non-members, and any freeze on a verdict weaker than ring_confirmed. An invalid verdict gets one retry.
  • Graph text is data. Holder names, UPI handles and alert text are marked untrusted in the system prompt. Even a successful prompt injection could not freeze anything: the model has no action tools.
  • Hard limits per run: 10 model steps, a 10-minute timeout, a 120K-token cap, and one investigation at a time.
  • Public demo hygiene: investigations and approvals need a demo passcode, resetting the graph needs an admin secret, both compared in constant time. No secrets in the repo.

Built to run on a free tier

The whole project runs on Groq's free tier: about 8K tokens per minute and 200K tokens per day, per model. The agent loop is designed around that.

  • Compact tool results. Tools return summaries and distinct transactions, not hundreds of near-duplicate paths.
  • Context trimming. Once the prompt passes about 5.5K tokens, older tool results shrink to their one-line summaries.
  • Model fallback mid-run. When the main model's daily quota runs out, the loop switches to the fallback model and continues from the same step. The trace shows the switch.
  • Self-adjusting requests. If a model rejects a request as too large (Qwen allows 7K input and 1K output tokens per minute), the loop trims the context or lowers the output cap and redoes the step.
  • Malformed tool calls are retried with a hint to send shorter arguments, so a truncated verdict does not fail the same way twice.
  • The report is a template, not LLM prose. It costs zero tokens and can only contain facts that are in the graph.
  • Cost of one investigation: roughly 18K to 35K tokens and 1 to 5 minutes, almost all of it waiting on rate limits. The graph queries are a rounding error.

What fought back

  • The proof could fool itself. In the final test the agent stopped at the collector and missed the cash-out account, which still held Rs 65,202. Verify still said "11 routes → 0". The route count only looks at paths through the case's own members, so freezing everything proposed always gives 0. The fix: a leak check that asks whether ring money already sits in an active account the case never named. That run now reads "Routes cut, but money leaked" instead of a false pass.
  • One transaction, one edge, hundreds of paths. Split payments multiply paths fast: 1,488 raw paths for 13 real routes. Counting distinct sequences of account IDs (count(DISTINCT [n IN nodes(p) | n.id])) made the number something a reviewer can read.
  • Some things cannot be Cypher parameters. Edge direction, variable-length bounds and labels cannot be $params. money_trail pre-builds one query per (direction, hops) pair from constants, so user input never becomes query text.
  • Epoch milliseconds vs date strings. The agent compared ts to '2026-10-07' in free-form Cypher and got empty results. The tool description now says times are epoch ms.
  • Free-tier walls. A daily quota that runs out mid-investigation, a fallback model with 1K output tokens per minute, and verdicts cut off mid-word (tool_use_failed). Each became a handled case in the loop, with a test that replays the real error.
  • Prompting is not free. Telling the agent to also trace forward from the collector raised Ring A recall from 0.57 to 0.71. A stricter "run both trails" rule made it loop until the step cap, so it was dropped. Every prompt change was measured, not assumed.

What it does now

npm run eval runs the agent on all 7 alerts and scores it against a ground-truth file. 7 of 7 verdicts correct, 0 unsafe freezes: no victim, merchant or family account was ever recommended for freezing.

Alert Scenario Expected Got Freeze precision / recall
ALT001 Ring A complaint ring_confirmed ring_confirmed 1.00 / 0.71
ALT002 Ring B complaint ring_confirmed ring_confirmed 1.00 / 1.00
ALT003 Ring B fan-in ring_confirmed ring_confirmed 1.00 / 1.00
ALT004 Ring C decoy suspicious suspicious, no freeze
ALT005 Merchant benign benign
ALT006 Family benign benign
ALT007 Noise benign benign
  • Live run on the deployed site: Ring A's complaint named all 8 accounts (the victim plus all 7 mules, including the cash-out); after approval, verify went from 13 open routes to 0 with no leak, and the alert closed.
  • 53 automated tests: the Cypher guard, the read-only backstop, the approval gate, verify and the leak check, the passcode, verdict validation, and every free-tier failure mode, replayed with a fake LLM client so they cost no tokens.
  • The dashboard: an alert queue with live status, the graph canvas with role-colored rings and a step-by-step HUD, the agent trace with each step's Cypher, the verdict card with a confidence meter and run stats, and a before/after proof card.

The live demo after approval: Ring A frozen (red) with roles on the canvas, each proposed freeze and its reason, and the proof card: 13 open routes before the freeze, 0 after, no leak.

Who this is for

Fraud and risk teams at banks, payment apps and UPI providers who already get alerts but spend hours tracing each one by hand. It is also a pattern for anyone building agents that act on data: let the model read and recommend, keep writes behind a human and a single audited query, and end every action with a query that proves it worked.

What I learned

  • Graphs make the question short. "Where did this money go, four hops out, after Tuesday?" is one Cypher pattern, not four joins.
  • Put safety in the database, not the prompt. GRAPH.RO_QUERY is a stronger guarantee than any instruction to "only read".
  • Verify the verifier. A proof that only checks the agent's own case can pass while money sits in an account the agent missed.
  • Give the model less, not more. Compact tool results, one-line summaries and a short required checklist beat long instructions on a small token budget.
  • Measure every prompt change. A rule that sounded better made the agent worse; only the eval showed it.

What's next

  • Upstream leak detection: flag a missed mule that feeds money into the ring, not only one that receives from it.
  • Streaming ingestion: real-time transaction feeds into FalkorDB, so investigations start the moment a pattern appears.
  • Real integrations: bank and UPI freeze APIs behind the same approval gate.

Try it

Needs Node.js 22.9+, Docker and a free Groq API key.

git clone https://github.com/Jeevesh2605/ringbreaker && cd ringbreaker
cp .env.example .env               # add GROQ_API_KEY
docker compose up -d               # FalkorDB on :6379, Browser on http://localhost:3000
(cd server && npm install) && (cd web && npm install)
npm run generate && npm run seed   # seeded synthetic graph into FalkorDB
(cd server && npm start)           # API on http://localhost:8080
(cd web && npm run dev)            # UI on http://localhost:5173
Enter fullscreen mode Exit fullscreen mode

Open the UI, click ALT001, approve the freeze, and watch the open routes drop to 0.

Disclosures: all data is synthetic; no real people, accounts or UPI handles. Built with help from an AI coding assistant; I reviewed the code and can explain every part of the architecture and its safety model.

Built for Graph Hacks: Context for AI Agents by FalkorDB, Track 01. Live demo: ringbreaker-5o88.onrender.com · Code: github.com/Jeevesh2605/ringbreaker

Top comments (0)