<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ishika29-tech</title>
    <description>The latest articles on DEV Community by ishika29-tech (@ishika29tech).</description>
    <link>https://dev.to/ishika29tech</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4191568%2F3e1cf02c-bb09-4e61-89a5-8932025e9ab2.png</url>
      <title>DEV Community: ishika29-tech</title>
      <link>https://dev.to/ishika29tech</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ishika29tech"/>
    <language>en</language>
    <item>
      <title>We gave an AI agent one ₹1,85,000 UPI payment. It walked the graph and found the mule ring.</title>
      <dc:creator>ishika29-tech</dc:creator>
      <pubDate>Sun, 18 Oct 2026 06:17:52 +0000</pubDate>
      <link>https://dev.to/ishika29tech/we-gave-an-ai-agent-one-185000-upi-payment-it-walked-the-graph-and-found-the-mule-ring-2dpl</link>
      <guid>https://dev.to/ishika29tech/we-gave-an-ai-agent-one-185000-upi-payment-it-walked-the-graph-and-found-the-mule-ring-2dpl</guid>
      <description>&lt;p&gt;&lt;em&gt;Sherlock Pay (repo name: UPI Fraud Ring Detective) is our Graph Hacks project: an AI agent that takes **one&lt;/em&gt;* 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.*&lt;/p&gt;

&lt;h2&gt;
  
  
  A video call one afternoon
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Anil pays. One UPI payment, a few taps.&lt;/p&gt;

&lt;p&gt;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 &lt;strong&gt;12.64 lakh UPI fraud cases worth ₹981 crore in FY 2024-25&lt;/strong&gt; (&lt;a href="https://www.boomlive.in/web-stories/india/upi-fraud-cases-and-losses-rise-over-five-years-finance-ministry-tells-lok-sabha-2755" rel="noopener noreferrer"&gt;BOOM&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Here is the frustrating part. When a bank analyst opens Anil's complaint, they see one row:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;txn&lt;/th&gt;
&lt;th&gt;from&lt;/th&gt;
&lt;th&gt;to&lt;/th&gt;
&lt;th&gt;amount&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TXN502668&lt;/td&gt;
&lt;td&gt;Anil Mehta&lt;/td&gt;
&lt;td&gt;Vikram Yadav&lt;/td&gt;
&lt;td&gt;₹1,85,000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;We are Ishika and Arpan, two beginners from Punjab Engineering College, and this is the problem we wanted to crack in one weekend.&lt;/p&gt;

&lt;h2&gt;
  
  
  The money doesn't sit still. It hops.
&lt;/h2&gt;

&lt;p&gt;Stolen money goes through &lt;strong&gt;mule accounts&lt;/strong&gt;: 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.&lt;/p&gt;

&lt;p&gt;Look at one payment and everything looks fine. Look at the &lt;strong&gt;connections&lt;/strong&gt; and the ring lights up. That's a graph problem, so we put the data in a graph.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we modelled UPI in FalkorDB
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4gmcq8jdzxaz5kaox0n2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4gmcq8jdzxaz5kaox0n2.png" alt="The Sherlock Pay graph model: Account, Phone, Device, IP, UPI and Case nodes" width="800" height="495"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Five kinds of nodes: &lt;code&gt;Account&lt;/code&gt;, &lt;code&gt;UPI&lt;/code&gt;, &lt;code&gt;Phone&lt;/code&gt;, &lt;code&gt;Device&lt;/code&gt;, &lt;code&gt;IP&lt;/code&gt;. Edges: &lt;code&gt;PAID&lt;/code&gt; (with amount and timestamp), &lt;code&gt;HAS_UPI&lt;/code&gt;, &lt;code&gt;HAS_PHONE&lt;/code&gt;, &lt;code&gt;USES_DEVICE&lt;/code&gt;, &lt;code&gt;SEEN_ON&lt;/code&gt;. And a sixth node type the agent writes itself: &lt;code&gt;Case&lt;/code&gt;, linked to accounts with &lt;code&gt;ABOUT&lt;/code&gt; and to the shared phones/devices with &lt;code&gt;EVIDENCE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The data is &lt;strong&gt;synthetic&lt;/strong&gt;: 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 &lt;strong&gt;traps&lt;/strong&gt;: a family sharing one tablet, and a big but normal rent payment. A good investigator must &lt;em&gt;not&lt;/em&gt; freeze those.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tools, not free-form Cypher
&lt;/h2&gt;

&lt;p&gt;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 &lt;strong&gt;11 tools&lt;/strong&gt;, and each tool is one tested Cypher query or a FalkorDB graph algorithm. The LLM's job is the detective work: &lt;em&gt;which tool next, and what does this evidence mean?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Three of them, copied straight from our &lt;code&gt;tools.py&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Who else is on the same IP?&lt;/strong&gt; One pattern, four hops: account → device → IP → another device → another account.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;a:&lt;/span&gt;&lt;span class="n"&gt;Account&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;id:&lt;/span&gt; &lt;span class="n"&gt;$id&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:USES_DEVICE&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;d1:&lt;/span&gt;&lt;span class="n"&gt;Device&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:SEEN_ON&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;ip:&lt;/span&gt;&lt;span class="n"&gt;IP&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:SEEN_ON&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;d2:&lt;/span&gt;&lt;span class="n"&gt;Device&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:USES_DEVICE&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;other:&lt;/span&gt;&lt;span class="n"&gt;Account&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;d1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;d2&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;other.id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;other.holder&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'IP'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;via&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ip.addr&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Follow the money, but only when it moves fast.&lt;/strong&gt; A variable-length path, plus a rule that each next payment happens after the previous one and within &lt;code&gt;$gap&lt;/code&gt; seconds (2 hours by default). Real layering is &lt;em&gt;quick&lt;/em&gt;; this rule killed almost all of our false chains.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;a:&lt;/span&gt;&lt;span class="n"&gt;Account&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;id:&lt;/span&gt; &lt;span class="n"&gt;$id&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:PAID&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;b:&lt;/span&gt;&lt;span class="n"&gt;Account&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;IN&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;length&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt;
          &lt;span class="nf"&gt;relationships&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="ss"&gt;)[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="n"&gt;.ts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nf"&gt;relationships&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="ss"&gt;)[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="n"&gt;.ts&lt;/span&gt; &lt;span class="ow"&gt;AND&lt;/span&gt;
          &lt;span class="nf"&gt;relationships&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="ss"&gt;)[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="n"&gt;.ts&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;relationships&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="ss"&gt;)[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="n"&gt;.ts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;$gap&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;length&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;hops&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;hops&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Fun beginner fact: the &lt;code&gt;4&lt;/code&gt; in &lt;code&gt;*1..4&lt;/code&gt; can't be a &lt;code&gt;$parameter&lt;/code&gt; in Cypher, so Python fills it in and caps it at 6.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Find the whole ring in one call.&lt;/strong&gt; FalkorDB's built-in &lt;code&gt;algo.WCC&lt;/code&gt; (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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;algo.WCC&lt;/span&gt;&lt;span class="ss"&gt;({&lt;/span&gt;&lt;span class="py"&gt;nodeLabels:&lt;/span&gt; &lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Account'&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Phone'&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Device'&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'IP'&lt;/span&gt;&lt;span class="ss"&gt;],&lt;/span&gt;
               &lt;span class="py"&gt;relationshipTypes:&lt;/span&gt; &lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'HAS_PHONE'&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'USES_DEVICE'&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'SEEN_ON'&lt;/span&gt;&lt;span class="ss"&gt;]})&lt;/span&gt;
&lt;span class="k"&gt;YIELD&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;componentId&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;componentId&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;members&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;IN&lt;/span&gt; &lt;span class="n"&gt;members&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="py"&gt;n:&lt;/span&gt;&lt;span class="n"&gt;Account&lt;/span&gt; &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;n.id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;$id&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;IN&lt;/span&gt; &lt;span class="n"&gt;members&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="py"&gt;n:&lt;/span&gt;&lt;span class="n"&gt;Account&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;n.id&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;accounts&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;members&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;component_size&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rest: &lt;code&gt;algo.pageRank&lt;/code&gt; on &lt;code&gt;PAID&lt;/code&gt; edges to find the collector, &lt;code&gt;shortestPath&lt;/code&gt; 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"), &lt;code&gt;check_past_cases&lt;/code&gt; for memory, and &lt;code&gt;take_action&lt;/code&gt;, which writes the decision back into the graph.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watching it investigate, hop by hop
&lt;/h2&gt;

&lt;p&gt;We gave the agent (Gemini by default) Anil's payment and pressed &lt;strong&gt;Investigate&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa1c47ptq9ae85hcjxuqq.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa1c47ptq9ae85hcjxuqq.jpg" alt="Sherlock Pay mid-investigation: steps on the left, the graph growing on the right" width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The receiver is suspicious.&lt;/strong&gt; Vikram's account is only 44 days old, with minimum KYC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;He isn't alone.&lt;/strong&gt; Connected components (&lt;code&gt;algo.WCC&lt;/code&gt;) link him to another account through shared identifiers (phones, devices).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The money keeps moving.&lt;/strong&gt; The money trail shows a 4-hop layering chain, each hop forwarding almost everything it received, within minutes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe2cu54ifny9fq9rvsnwj.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe2cu54ifny9fq9rvsnwj.jpg" alt="Close-up of the graph the agent built" width="800" height="715"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And every step can be opened to show the &lt;strong&gt;exact Cypher&lt;/strong&gt; it ran. No black box.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9sf897wgjrtyztu9jeqd.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9sf897wgjrtyztu9jeqd.jpg" alt="A step expanded to show the Cypher query the agent ran" width="800" height="935"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The verdict: freeze, escalate, explain
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiwonivnfn44tvrjrna93.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiwonivnfn44tvrjrna93.png" alt="What the agent found from one payment: a 4-hop chain, 4 FREEZE, 1 ESCALATE" width="800" height="511"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The agent &lt;strong&gt;froze the four mule accounts&lt;/strong&gt; in the middle of the chain and &lt;strong&gt;escalated the last hop&lt;/strong&gt; 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 &lt;code&gt;Case&lt;/code&gt; 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?".&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fod08kaun2zy7cl5a2ai7.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fod08kaun2zy7cl5a2ai7.jpg" alt="Verdict cards: FREEZE x4 and ESCALATE x1 with the explanation" width="800" height="1083"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap: an innocent family
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The agent checked the graph instead: their money goes to everyday shops, not to fresh mule accounts, and nothing races onward hop after hop. &lt;strong&gt;Risk 15. CLEAR.&lt;/strong&gt; It even wrote that down as a &lt;code&gt;Case&lt;/code&gt;, so nobody re-investigates the family next week.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk8svpd1drj773efebpyq.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk8svpd1drj773efebpyq.jpg" alt="The family sharing a device gets CLEAR with risk 15" width="800" height="482"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How well does it work?
&lt;/h2&gt;

&lt;p&gt;We wrote &lt;code&gt;evaluate.py&lt;/code&gt;, which runs a full investigation for every flagged transaction and compares the result with the answer key we planted:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Run&lt;/th&gt;
&lt;th&gt;Ring accounts caught&lt;/th&gt;
&lt;th&gt;Innocent accounts frozen&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Offline rule-based mode (no LLM)&lt;/td&gt;
&lt;td&gt;27 / 28&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM agent with OpenAI gpt-4.1-mini&lt;/td&gt;
&lt;td&gt;27 / 28&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini (partial run, stopped by free-tier quota)&lt;/td&gt;
&lt;td&gt;device farm 8 / 8, layering chain 5 / 5&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why we built it this way
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Graph instead of SQL joins.&lt;/strong&gt; "Accounts that share a device that was seen on an IP used by another device" is one readable &lt;code&gt;MATCH&lt;/code&gt; in Cypher. In SQL it's a tower of self-joins, and a variable-length money trail is worse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tools instead of free-form queries.&lt;/strong&gt; Every tool is tested, so the LLM can't write a broken or dangerous query. And graph algorithms make amazing tools: one &lt;code&gt;algo.WCC&lt;/code&gt; call answers what would take an LLM ten steps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP.&lt;/strong&gt; All 11 tools also run as an MCP server, so Claude Desktop, Cursor or any MCP client can investigate the same graph. Our &lt;code&gt;mcp_agent.py&lt;/code&gt; discovers the tools with &lt;code&gt;list_tools&lt;/code&gt; instead of importing them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explainability and humans in the loop.&lt;/strong&gt; 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 &lt;code&gt;Case&lt;/code&gt; that the next investigation can find.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fce5t50igsmpf9attixno.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fce5t50igsmpf9attixno.jpg" alt="Cases written back into FalkorDB become the agent's memory" width="800" height="173"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What we learned (the honest version)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Windows setup was our first boss fight.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;College WiFi blocked the FalkorDB Cloud port.&lt;/strong&gt; We got "connection refused" and spent a long time blaming our code before realising it was the network.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free Gemini keys run out.&lt;/strong&gt; 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 &lt;strong&gt;offline mode&lt;/strong&gt; so the demo never dies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cypher felt strange for an hour, then obvious.&lt;/strong&gt; &lt;code&gt;-[:PAID*1..4]-&amp;gt;&lt;/code&gt; reads like a sentence: follow the money up to four hops.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The traps taught us the most.&lt;/strong&gt; A family sharing a tablet looks exactly like a device farm until you look at account age and where the money goes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Test on bigger and messier synthetic data (and, one day, real anonymised data with a bank).&lt;/li&gt;
&lt;li&gt;Stream new payments in, so the agent investigates as soon as something is flagged.&lt;/li&gt;
&lt;li&gt;Let a human analyst's ESCALATE decision feed back into the next investigation.&lt;/li&gt;
&lt;li&gt;Nothing here is India-specific: the same graph works for Pix, Zelle or Faster Payments.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🕵️ Live app: &lt;a href="https://upi-fraud-detective.streamlit.app" rel="noopener noreferrer"&gt;upi-fraud-detective.streamlit.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 Code: &lt;a href="https://github.com/ishika29-tech/upi-fraud-ring-detective" rel="noopener noreferrer"&gt;github.com/ishika29-tech/upi-fraud-ring-detective&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🎥 2-minute demo: &lt;a href="https://youtu.be/_oTZMlVvnbI" rel="noopener noreferrer"&gt;youtu.be/_oTZMlVvnbI&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/_oTZMlVvnbI" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Built by Ishika Bansal and Arpan Goel (Punjab Engineering College) for Graph Hacks by WeMakeDevs × FalkorDB.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>hackathon</category>
      <category>python</category>
      <category>database</category>
    </item>
  </channel>
</rss>
