<?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: Ayush Kumar</title>
    <description>The latest articles on DEV Community by Ayush Kumar (@ayush_kumar_9fe94e74fb82d).</description>
    <link>https://dev.to/ayush_kumar_9fe94e74fb82d</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%2F4142362%2Fa2498350-f216-443d-9968-b7056f403257.png</url>
      <title>DEV Community: Ayush Kumar</title>
      <link>https://dev.to/ayush_kumar_9fe94e74fb82d</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ayush_kumar_9fe94e74fb82d"/>
    <language>en</language>
    <item>
      <title>A risk score is a reason to look: building a fraud investigator on a graph</title>
      <dc:creator>Ayush Kumar</dc:creator>
      <pubDate>Fri, 25 Sep 2026 06:19:27 +0000</pubDate>
      <link>https://dev.to/ayush_kumar_9fe94e74fb82d/a-risk-score-is-a-reason-to-look-building-a-fraud-investigator-on-a-graph-cc7</link>
      <guid>https://dev.to/ayush_kumar_9fe94e74fb82d/a-risk-score-is-a-reason-to-look-building-a-fraud-investigator-on-a-graph-cc7</guid>
      <description>&lt;p&gt;&lt;em&gt;HHGOA 2026 · TigerGraph Partner Challenge. Repo: github.com/Cometbuster4969/hacker-house ·&lt;br&gt;
Demo: &lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The task handed us 20 alerts and a warning buried in the README: &lt;strong&gt;most transactions with a risk&lt;br&gt;
score above 0.7 are legitimate.&lt;/strong&gt; Our first agent took the obvious route — gather graph evidence,&lt;br&gt;
ask an LLM to reason, let rules fill the gaps. It called 17 of 20 alerts fraud. Half were&lt;br&gt;
legitimate. So we threw the verdict layer away and started from one question:&lt;br&gt;
&lt;strong&gt;what does the bank actually know?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What we built
&lt;/h2&gt;

&lt;p&gt;A deterministic investigation agent. For each alert it forms hypotheses, calls 7 tools (episode&lt;br&gt;
reconstruction, device-ring fan-out, closed-case support, exposure, policy, memory), and emits one&lt;br&gt;
JSON answer file: investigation record, evidence, findings, decision, actions, and the &lt;em&gt;next best&lt;br&gt;
action before and after&lt;/em&gt; more evidence arrives. &lt;code&gt;policy.decide()&lt;/code&gt; is a pure function over the case&lt;br&gt;
state — it is the only thing that writes actions or flags a SAR.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;20/20 answers: &lt;strong&gt;8 fraud, 9 legitimate, 3 left uncertain&lt;/strong&gt;, 3 SARs, &lt;strong&gt;181 tool calls&lt;/strong&gt;,
&lt;strong&gt;0 LLM tokens&lt;/strong&gt; in the decision path.&lt;/li&gt;
&lt;li&gt;58 mechanical contract checks, &lt;code&gt;20 files / 0 violations&lt;/code&gt; via &lt;code&gt;python main.py validate&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;87 passed / 2 skipped unit+integration tests on the minimal install, no dataset or key needed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;alert -&amp;gt; hypothesis loop -&amp;gt; tools (episode | device ring | closed-case support | exposure)&lt;br&gt;
-&amp;gt; policy.decide() # pure function, R1-R10 + routing thresholds&lt;br&gt;
-&amp;gt; answer JSON -&amp;gt; TigerGraph loader -&amp;gt; FastAPI dashboard&lt;br&gt;
-&amp;gt; case memory (retrieved as priors on the next run)&lt;/p&gt;

&lt;p&gt;text&lt;/p&gt;

&lt;p&gt;The LLM is optional and only ever narrates; verdicts and actions come from the engine, so an&lt;br&gt;
explanation can never contradict an action.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we used TigerGraph
&lt;/h2&gt;

&lt;p&gt;Schema in &lt;code&gt;tigergraph/schema.gsql&lt;/code&gt;: 8 vertex types — &lt;code&gt;Customer&lt;/code&gt;, &lt;code&gt;Card&lt;/code&gt;, &lt;code&gt;Transaction&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;DeviceProfile&lt;/code&gt;, &lt;code&gt;EmailDomain&lt;/code&gt;, &lt;code&gt;BillingRegion&lt;/code&gt;, &lt;code&gt;ClosedCase&lt;/code&gt;, &lt;code&gt;InvestigationCase&lt;/code&gt; — with the&lt;br&gt;
evidence traversals in &lt;code&gt;tigergraph/queries/engine_queries.gsql&lt;/code&gt; (2-hop device fan-out via&lt;br&gt;
&lt;code&gt;FROM_DEVICE&lt;/code&gt;, burst aggregation, closed-case support). &lt;code&gt;scripts/deploy_schema.py&lt;/code&gt; installs to&lt;br&gt;
Savanna; &lt;code&gt;scripts/load_to_tigergraph.py&lt;/code&gt; writes all 20 cases as &lt;code&gt;InvestigationCase&lt;/code&gt; vertices&lt;br&gt;
(&lt;code&gt;written_to_graph: 20&lt;/code&gt;, snapshot in &lt;code&gt;evidence/tg_live_check.json&lt;/code&gt;). The same graph doubles as the&lt;br&gt;
agent's memory: the next investigation starts from vertices, not a cold re-read of CSVs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the graph found that no label did
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sub-$500 structuring.&lt;/strong&gt; Four online purchases in 30 minutes, each a &lt;em&gt;different&lt;/em&gt; amount between
$456 and $499. Organic bursts repeat one price ($499.95 × 4); these don't. 22 such bursts exist
in the corpus, 5 are confirmed closed cases — HHG-006 is another. A population sweep found the
same shape on 3 other customers' cards the previous month, so the agent files it as undocumented
under R9.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An anonymous-proxy device ring.&lt;/strong&gt; One Samsung SM-G935F profile, marked &lt;em&gt;New&lt;/em&gt; on every account,
behind an anonymous proxy, on &lt;strong&gt;20 cards of 20 customers in 30 days&lt;/strong&gt;. HHG-014 sits on it: the
agent puts the other 19 cards under monitoring and escalates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We were as careful about &lt;em&gt;false&lt;/em&gt; rings: "Windows / Chrome / 1920×1080" is shared by thousands of&lt;br&gt;
honest people, so cards link only through hardware-specific devices or anomalous shared use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results, measured honestly
&lt;/h2&gt;

&lt;p&gt;We replayed &lt;strong&gt;544 October closed cases&lt;/strong&gt; (400 confirmed, 144 cleared) through the whole agent,&lt;br&gt;
models trained only on Jul–Aug, the calibrator on Sep, and every alert presented as a plain&lt;br&gt;
risk-score trigger on a &lt;em&gt;random&lt;/em&gt; transaction of the episode — harder than a real alert.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measure&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Transaction model, Sep–Oct holdout (193,323 txns, 3.65% fraud)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;ROC-AUC 0.947&lt;/strong&gt; vs 0.861 for the bank's risk score; Brier 0.018&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verdict accuracy on decided cases&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;0.82&lt;/strong&gt; (0.73 if every uncertain counts as wrong), 10.5% uncertain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Episode reconstruction vs analyst lists&lt;/td&gt;
&lt;td&gt;mean Jaccard &lt;strong&gt;0.82&lt;/strong&gt;, exact 63%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pattern accuracy on fraud verdicts&lt;/td&gt;
&lt;td&gt;0.77&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blocking on cases the bank cleared&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;8.3%&lt;/strong&gt; (vs 64.5% of confirmed cases blocked)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SAR agreement with analysts&lt;/td&gt;
&lt;td&gt;0.91&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;8.1 tool calls and 0.17 s per case; 95.6 s for all 544&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Ground truth is the bank's own 5,565 closed cases (4,665 confirmed fraud) over 590,742&lt;br&gt;
transactions and 144,432 identities — the only confirmed outcomes in the data. Features are&lt;br&gt;
strictly causal: a closed case counts only once it was &lt;em&gt;closed&lt;/em&gt; at alert time. The dataset's&lt;br&gt;
"customer" is an issuer-level bucket pooling many real people, so identity uses the card +&lt;br&gt;
billing-region + account-open-day key.&lt;/p&gt;

&lt;p&gt;Then the same engine swept Nov–Dec with no case pack and opened &lt;strong&gt;15 alerts of its own&lt;/strong&gt;: 12&lt;br&gt;
structuring bursts, the full SM-G935F ring (28 customers, 60 transactions), one thin candidate ring&lt;br&gt;
left for a human, one card-testing sequence.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A score is a reason to look, not a verdict.&lt;/strong&gt; Our first run's 17/20 fraud rate was the model
being rational about a misleading feature.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separating policy from reasoning is the whole product.&lt;/strong&gt; Once rules are code, the explanation
can't drift from the action, and a validator can grade all 20 files mechanically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uncertainty has to be an output.&lt;/strong&gt; 3 cases stayed open; forcing them to a label costs
precision you can't see in aggregate accuracy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build for re-derivation.&lt;/strong&gt; Every number above is regenerated from checked-in artifacts;
CI runs the suite on a clean clone with no graph and no key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The graph earns its place on multi-hop questions&lt;/strong&gt; — device fan-out and shared-merchant bursts
— not on the tabular ones.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Next
&lt;/h2&gt;

&lt;p&gt;Deploy the case vertices for the whole corpus and let recall improve via TigerVector on&lt;br&gt;
&lt;code&gt;InvestigationCase&lt;/code&gt;, lift fraud recall (0.65) with a sequential model, and run the agent on live&lt;br&gt;
alerts behind the dashboard.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Data note: a public column extract of the official HHGOA_IEEE files; never the public IEEE-CIS&lt;br&gt;
labels. MIT-licensed.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tigergraph</category>
      <category>hackathon</category>
      <category>fraud</category>
      <category>python</category>
    </item>
  </channel>
</rss>
