<?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: Pranav S Salian</title>
    <description>The latest articles on DEV Community by Pranav S Salian (@pranav_ssalian_be1687de7).</description>
    <link>https://dev.to/pranav_ssalian_be1687de7</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%2F4117737%2Fe1babf71-71c0-4b5e-abd5-86120f6cc88b.png</url>
      <title>DEV Community: Pranav S Salian</title>
      <link>https://dev.to/pranav_ssalian_be1687de7</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pranav_ssalian_be1687de7"/>
    <language>en</language>
    <item>
      <title>Don't Trust the Score: A Fraud Investigator That Argues Both Sides on TigerGraph</title>
      <dc:creator>Pranav S Salian</dc:creator>
      <pubDate>Thu, 24 Sep 2026 17:06:44 +0000</pubDate>
      <link>https://dev.to/pranav_ssalian_be1687de7/dont-trust-the-score-a-fraud-investigator-that-argues-both-sides-on-tigergraph-299n</link>
      <guid>https://dev.to/pranav_ssalian_be1687de7/dont-trust-the-score-a-fraud-investigator-that-argues-both-sides-on-tigergraph-299n</guid>
      <description>&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%2Fu4sbmd13oia0iiy51fx2.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%2Fu4sbmd13oia0iiy51fx2.png" alt="Tech stack" width="797" height="27"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🎥 Demo video:&lt;/strong&gt; &lt;a href="https://www.youtube.com/watch?v=kXDzmKNrhIE" rel="noopener noreferrer"&gt;Watch the Fraud Investigator in action&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The finding that changed the whole design
&lt;/h2&gt;

&lt;p&gt;Most fraud-agent demos work the same way: a model produces a risk score, then an LLM writes a convincing story about why the score is right.&lt;/p&gt;

&lt;p&gt;Before writing any code, We profiled all five provided files with DuckDB. The data showed that approach would &lt;strong&gt;fail in both directions&lt;/strong&gt;: it blocks innocent people and misses real fraud.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What I measured&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Design consequence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cleared (false-alarm) cases&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;All 900&lt;/strong&gt; scored &lt;strong&gt;0.81 or higher&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;A high score is where innocence lives, so the agent must be able to &lt;em&gt;clear a 0.9&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Confirmed fraud&lt;/td&gt;
&lt;td&gt;Mean score &lt;strong&gt;0.47&lt;/strong&gt;, and &lt;strong&gt;31% scored under 0.3&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;The agent must be able to &lt;em&gt;convict a 0.05&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A hidden fraud ring&lt;/td&gt;
&lt;td&gt;60 transactions, 28 customers, every score &lt;strong&gt;≤ 0.44&lt;/strong&gt;, in no closed case&lt;/td&gt;
&lt;td&gt;A &lt;strong&gt;score-blind&lt;/strong&gt; sweep is needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Graph features vs bank score, among alerts ≥ 0.8&lt;/td&gt;
&lt;td&gt;AUC &lt;strong&gt;0.91&lt;/strong&gt; vs &lt;strong&gt;0.62&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;The &lt;strong&gt;graph&lt;/strong&gt; decides where the score is confused&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Customers"&lt;/td&gt;
&lt;td&gt;104 of them hold &lt;strong&gt;48%&lt;/strong&gt; of all transactions&lt;/td&gt;
&lt;td&gt;A customer is not a person, so baselines must be &lt;strong&gt;per card&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here is the same story as one picture. Fraud appears at every score. Innocence appears in only one zone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bank score bin : 0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9
confirmed fraud: ███ ███ ███ ███ ███ ███ ███ ███ ███ ███
cleared (safe) : · · · · · · · · ███ ███
                                                            ▲
                              the "decoy zone": the score cannot separate them here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Chart shows where each outcome appears; it is not a count.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The score tells you where to look. The graph tells you what happened.&lt;/strong&gt;&lt;br&gt;
So I built an agent that treats the score as one weak witness, not the judge.&lt;/p&gt;
&lt;/blockquote&gt;




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

&lt;p&gt;&lt;strong&gt;Fraud Investigator&lt;/strong&gt; takes an alert from any of three triggers (a bank risk flag, a customer saying &lt;em&gt;"I never made this"&lt;/em&gt;, or an analyst asking &lt;em&gt;"look for related activity"&lt;/em&gt;) and investigates it like a small legal team:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Prosecution&lt;/strong&gt; runs fraud signatures: test-then-spend, off-profile burst, threshold hugging, shared rare fingerprint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Defence&lt;/strong&gt; runs innocence signatures: trip continuity, device succession, recurring cadence, baseline-consistent spend.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;calibrated judge&lt;/strong&gt; weighs both sides into a probability.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;policy engine&lt;/strong&gt; converts it into actions, and only it can.&lt;/li&gt;
&lt;li&gt;Plain-language explanations for the customer, and a Suspicious Activity Report narrative when policy requires one.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;Five layers. &lt;strong&gt;Everything that reads goes through TigerGraph's MCP tool surface. Everything that can change the world goes through the policy engine.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TB
  subgraph ENTRY["ENTRY"]
    direction LR
    E1["Risk-score alert"]
    E2["Customer report"]
    E3["Analyst request"]
    E4["Analyst console"]
  end

  subgraph ORCH["ORCHESTRATOR - bounded state machine, typed evidence ledger"]
    direction LR
    O1["Intake"] --&amp;gt; O2["Recall"] --&amp;gt; O3["Advocates x3"] --&amp;gt; O4["Judge"] --&amp;gt; O5["Plan"] --&amp;gt; O6["Gate"] --&amp;gt; O7["Act"] --&amp;gt; O8["Learn"]
  end

  subgraph ENG["DETERMINISTIC ENGINES - no LLM inside"]
    direction LR
    G1["Signature library&amp;lt;br/&amp;gt;prosecution + defence"]
    G2["Contagion + rarity&amp;lt;br/&amp;gt;personalised PageRank"]
    G3["Calibrated judge&amp;lt;br/&amp;gt;log-odds ledger"]
    G4["Policy engine&amp;lt;br/&amp;gt;rules R1 to R10"]
    G5["Response simulator&amp;lt;br/&amp;gt;denies / confirms / silent"]
  end

  subgraph TG["TIGERGRAPH via MCP server"]
    direction LR
    T1[("Transaction graph")]
    T2[("Case memory")]
    T3[("Vector indexes")]
    T4[("Installed GSQL&amp;lt;br/&amp;gt;every query takes as_of")]
  end

  subgraph OFF["OFFLINE - build time"]
    direction LR
    F1["Loader&amp;lt;br/&amp;gt;708 MB CSV to typed graph"]
    F2["Time-machine backtest"]
    F3["Calibration tables&amp;lt;br/&amp;gt;JSON, no ML at runtime"]
  end

  LLM["LLM - 3 schema-checked calls per case&amp;lt;br/&amp;gt;plan evidence, explain, write SAR"]

  ENTRY --&amp;gt; ORCH
  ORCH --&amp;gt; ENG
  ENG --&amp;gt; TG
  OFF -. "loads + calibrates".-&amp;gt; TG
  F3 -. "coefficients".-&amp;gt; G3
  ORCH -. "phrasing only".-&amp;gt; LLM

  classDef entry fill:#F3F4F6,stroke:#6B7280,color:#111827;
  classDef agent fill:#EEF0FF,stroke:#5B5BD6,color:#1B1B4B;
  classDef tgc fill:#E6FAF6,stroke:#0F9D8A,color:#063D36;
  classDef control fill:#FFF4DC,stroke:#D98E04,color:#4A3000;
  class E1,E2,E3,E4 entry;
  class O1,O2,O3,O4,O5,O6,O7,O8,LLM agent;
  class T1,T2,T3,T4,F1,F2,F3,G1,G2 tgc;
  class G3,G4,G5 control;&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;indigo = agent and judge, teal = graph and graph-derived work, amber = control and side effects.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Five principles behind it
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Principle&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;The LLM only phrases&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;It cannot add an action, change a route, set exposure, or invent an ID. That is what makes a mid-sized open model safe here.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Read through MCP, act through policy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One loggable tool contract, and no side effect without a rule citation.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Time is enforced in the data layer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The alert's &lt;code&gt;opened_at&lt;/code&gt; is 1 to 6 hours &lt;em&gt;after&lt;/em&gt; the flagged transaction, and the file runs to Dec 31. Every query takes &lt;code&gt;as_of&lt;/code&gt;, and a leakage test fails if any row is later than the trigger.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Graceful degradation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Every LLM reply is validated, retried twice with the validator's error fed back, then replaced by a deterministic template. A valid case file is always written.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;The graph is the source of truth&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The answer JSON is rendered from the graph and checked: IDs exist, exposure equals the sum of affected transactions, &lt;code&gt;sar.file&lt;/code&gt; matches &lt;code&gt;FILE_REPORT&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




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

&lt;p&gt;TigerGraph is the substrate for the reasoning, not a place to park rows. The schema is shaped around the &lt;em&gt;questions the agent asks&lt;/em&gt;, so each question becomes a one-hop lookup.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  subgraph TXN["Transaction graph"]
    Customer --&amp;gt;|OWNS| Card
    Card --&amp;gt;|MADE| Transaction
    Transaction --&amp;gt;|NEXT| NextTxn["Next txn on card"]
    Transaction --&amp;gt;|FROM_DEVICE| DeviceProfile["DeviceProfile&amp;lt;br/&amp;gt;carries rarity weight"]
    Transaction --&amp;gt;|BILLED_IN| BillingRegion
    Transaction --&amp;gt;|PURCHASER_EMAIL| EmailDomain
    Transaction --&amp;gt;|IN_EPISODE| Episode["Episode&amp;lt;br/&amp;gt;derived burst window"]
    Card --&amp;gt;|MEMBER_OF| Ring["Ring&amp;lt;br/&amp;gt;derived component"]
  end

  subgraph MEM["Case memory"]
    ClosedCase --&amp;gt;|MATCHES| Pattern
    Case --&amp;gt;|MATCHES| Pattern
    Case --&amp;gt;|RETRIEVED| ClosedCase
    Case --&amp;gt;|HAS_EVIDENCE| Evidence
    Case --&amp;gt;|RECOMMENDS| Action
    Action --&amp;gt;|UNDER_RULE| PolicyChunk
    PolicyChunk --&amp;gt;|DESCRIBES| Pattern
  end

  ClosedCase --&amp;gt;|INVOLVES| Transaction
  Case --&amp;gt;|INVOLVES| Transaction
  Case --&amp;gt;|CONNECTED_TO| Card
  Case --&amp;gt;|SUSPECTS| Ring
  Evidence --&amp;gt;|CITES| Transaction

  classDef derived fill:#FFF4DC,stroke:#D98E04,color:#4A3000;
  class Episode,Ring,DeviceProfile derived;&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;Amber vertices are the ones I added beyond the suggested schema. They are built offline so the agent's questions stay local to a card's neighbourhood.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Four ways the graph does the heavy lifting
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Rarity-weighted devices.&lt;/strong&gt; Each &lt;code&gt;DeviceProfile&lt;/code&gt; stores how many customers use it, its share of &lt;em&gt;New&lt;/em&gt; marks, its share behind an anonymous proxy, and an IDF weight. A generic Windows/Chrome profile shared by 1,000 customers gets a near-zero weight. The ring fingerprint gets a high one.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;My first ring rule flagged &lt;strong&gt;212 of 9,706&lt;/strong&gt; device profiles, mostly common iPhones. Requiring &lt;strong&gt;5 to 200 users, &lt;em&gt;New&lt;/em&gt; on 90% of uses, and an anonymous proxy on 80%&lt;/strong&gt; left the real ring and very few others.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;A score-blind contagion sweep.&lt;/strong&gt; A personalised PageRank starts from confirmed-fraud entities and runs over the card, device, region and email graph. It ranks &lt;em&gt;unflagged&lt;/em&gt; cards by proximity to known fraud and ignores the bank score. One query on the ring fingerprint recovers victims the model never flagged.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case memory as GraphRAG.&lt;/strong&gt; Closed cases attach to the same transactions, cards and devices that live traffic touches, and recall works three ways:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Recall mode&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Structural&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Closed cases on the same device or card neighbourhood, ranked by overlap, rarity and recency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Semantic&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Vector search over case notes, policy and regulatory text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Numeric&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Nearest neighbours in a 32-dimension PCA of the Vesta features&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The three results are compiled into one brief for the LLM: &lt;em&gt;what we know, what conflicts, what is missing&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Memory uses &lt;strong&gt;shape matches only&lt;/strong&gt;, never "same customer". Closed history is 84% fraud, so anyone with history would look guilty.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Causal write-back.&lt;/strong&gt; Every new case becomes a &lt;code&gt;Case&lt;/code&gt; vertex linked to its transactions, cards, ring and pattern, with retrieved cases as &lt;code&gt;RETRIEVED&lt;/code&gt; edges. Cases are processed in &lt;code&gt;opened_at&lt;/code&gt; order, so case N may recall earlier cases and &lt;strong&gt;never a later one&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The agentic capabilities
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The investigation loop
&lt;/h3&gt;



&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
  T["Trigger"] --&amp;gt; I["1. Intake&amp;lt;br/&amp;gt;pin as_of, resolve card"]
  I --&amp;gt; M["2. Recall&amp;lt;br/&amp;gt;prior cases, 3 ways"]
  M --&amp;gt; PR["Prosecution"]
  M --&amp;gt; DE["Defence"]
  M --&amp;gt; SW["Sweep"]
  PR --&amp;gt; J["Judge&amp;lt;br/&amp;gt;p, confidence, evidence classes"]
  DE --&amp;gt; J
  SW --&amp;gt; J
  J --&amp;gt; S{"Stop rule met?&amp;lt;br/&amp;gt;p &amp;gt;= 0.85 or p &amp;lt;= 0.15&amp;lt;br/&amp;gt;with 2 independent classes"}
  S -- "yes"--&amp;gt; A["Policy engine&amp;lt;br/&amp;gt;choose actions + routes"]
  S -- "no, budget left"--&amp;gt; V["Plan&amp;lt;br/&amp;gt;best value-of-information request"]
  V --&amp;gt; R["Reply&amp;lt;br/&amp;gt;customer / step-up / analyst"]
  R --&amp;gt; J
  S -- "no, nothing left to try"--&amp;gt; E["Escalate to analyst&amp;lt;br/&amp;gt;with gap list"]
  E --&amp;gt; A
  A --&amp;gt; X["Explain, SAR, write case to graph"]

  classDef agent fill:#EEF0FF,stroke:#5B5BD6,color:#1B1B4B;
  classDef tgc fill:#E6FAF6,stroke:#0F9D8A,color:#063D36;
  classDef control fill:#FFF4DC,stroke:#D98E04,color:#4A3000;
  class PR,DE,J,V,R agent;
  class SW,M tgc;
  class A,E,X,S control;&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;Budgets: 3 evidence rounds, about 20 graph calls, 1 request of each type.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Adversarial advocates
&lt;/h3&gt;

&lt;p&gt;Prosecution and Defence run &lt;strong&gt;in parallel over the same as-of graph&lt;/strong&gt;. An unanswered defence test is reported as uncertainty, never ignored. The bank score is not a signature. It enters as one evidence class through a learned, &lt;strong&gt;non-monotonic&lt;/strong&gt; curve.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The meaning of a signal flips by zone.&lt;/strong&gt; At score 0.8+, &lt;strong&gt;98%&lt;/strong&gt; of cleared online alerts show a &lt;em&gt;New&lt;/em&gt; device, against &lt;strong&gt;36%&lt;/strong&gt; of fraud. So "New device" is &lt;strong&gt;defence&lt;/strong&gt; evidence in that zone and &lt;strong&gt;prosecution&lt;/strong&gt; evidence below it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prosecution signatures&lt;/th&gt;
&lt;th&gt;Defence signatures&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Test then spend&lt;/td&gt;
&lt;td&gt;Trip continuity (a clone would keep spending at home)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Off-profile burst&lt;/td&gt;
&lt;td&gt;Device succession (new phone replaces old, same brand and OS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New-device attribution&lt;/td&gt;
&lt;td&gt;Recurring cadence, with a &lt;em&gt;coincidence test&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Out-of-region while home continues&lt;/td&gt;
&lt;td&gt;Baseline-consistent spend&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Threshold hugging (structuring)&lt;/td&gt;
&lt;td&gt;Hub hygiene (down-weight aggregate customers)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shared rare fingerprint&lt;/td&gt;
&lt;td&gt;Novelty detector (flags patterns nothing explains)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  2. A calibrated judge, not a vibe
&lt;/h3&gt;

&lt;p&gt;Fraud probability is graded for calibration and drives every policy threshold, so it is &lt;strong&gt;computed, not guessed&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;prior (reset for 84% fraud history)&lt;/code&gt; → &lt;code&gt;+ each evidence item's log-likelihood ratio&lt;/code&gt; → &lt;code&gt;same class counts once (max, not sum)&lt;/code&gt; → &lt;code&gt;global damping tuned on backtest&lt;/code&gt; → &lt;strong&gt;&lt;code&gt;fraud_probability&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Evidence is tagged by class (bank score, card behaviour, device, geography, network, memory, lookalike, customer reply). Correlated evidence inside a class does not stack, so the policy's &lt;em&gt;"at least two independent pieces"&lt;/em&gt; rule is &lt;strong&gt;computed, not asserted&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Value-of-information planning
&lt;/h3&gt;

&lt;p&gt;If the stop rule isn't met, the agent scores candidate requests (customer validation, step-up auth, analyst info, one more graph hop) by &lt;em&gt;expected shift in p, minus friction&lt;/em&gt;. It must state &lt;strong&gt;which two hypotheses the request is meant to separate&lt;/strong&gt;, and that sentence becomes the recorded "why more evidence was requested".&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Response-conditional planning
&lt;/h3&gt;

&lt;p&gt;Replies aren't provided, so before asking, the agent &lt;strong&gt;precomputes the plan for all three outcomes&lt;/strong&gt;. The simulated reply is drawn from its own pre-reply posterior, and the assumption is recorded.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
  ASK["Ask the customer"] --&amp;gt; D["Denies"]
  ASK --&amp;gt; C["Confirms"]
  ASK --&amp;gt; Z["Silent for 24h"]
  D --&amp;gt; DR["Rule R2&amp;lt;br/&amp;gt;BLOCK_CARD, CREATE_CASE,&amp;lt;br/&amp;gt;FILE_REPORT if threshold met,&amp;lt;br/&amp;gt;MONITOR_CONNECTED_CARDS"]
  C --&amp;gt; CR["Rule R3&amp;lt;br/&amp;gt;CLOSE_NO_FRAUD"]
  Z --&amp;gt; ZR["Rule R4&amp;lt;br/&amp;gt;MONITOR_CARD, DECLINE_TRANSACTION,&amp;lt;br/&amp;gt;escalate if exposure over $500"]
  classDef bad fill:#FDECEC,stroke:#D1242F,color:#4A0A0F;
  classDef good fill:#E7F8EC,stroke:#2EA043,color:#0B3D1A;
  classDef mid fill:#FFF4DC,stroke:#D98E04,color:#4A3000;
  class D,DR bad;
  class C,CR good;
  class Z,ZR mid;&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;All three branches are stored as a &lt;code&gt;contingency&lt;/code&gt; object, so the before-and-after recommendation is a &lt;strong&gt;lookup, not an improvisation&lt;/strong&gt;, and an analyst can override the simulated reply live.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. A policy engine that can't be sweet-talked
&lt;/h3&gt;

&lt;p&gt;Rules R1 to R10 are a decision table in code. Exposure is computed from transaction IDs, never by the LLM. The executor only runs &lt;code&gt;auto&lt;/code&gt; actions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Route&lt;/th&gt;
&lt;th&gt;Actions&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Auto&lt;/strong&gt; (executed)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CREATE_CASE&lt;/code&gt;, &lt;code&gt;MONITOR_CARD&lt;/code&gt;, &lt;code&gt;VERIFY_WITH_CUSTOMER&lt;/code&gt;, &lt;code&gt;WARN_CUSTOMER&lt;/code&gt;, &lt;code&gt;ESCALATE_TO_ANALYST&lt;/code&gt;, &lt;code&gt;CLOSE_NO_FRAUD&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;L1 team lead&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;DECLINE_TRANSACTION&lt;/code&gt;, &lt;code&gt;BLOCK_CARD&lt;/code&gt; if exposure up to $2,500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;L2 fraud manager&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;BLOCK_CARD&lt;/code&gt; above $2,500, &lt;code&gt;BLOCK_ALL_CARDS&lt;/code&gt;, &lt;code&gt;FILE_REPORT&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;L1 and L2 actions appear as approval cards in the console, each showing the rule that required it.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. A time machine
&lt;/h3&gt;

&lt;p&gt;Any closed case can be replayed &lt;em&gt;as if it had just arrived&lt;/em&gt;, with the graph frozen at its &lt;code&gt;as_of&lt;/code&gt;. That is how thresholds and likelihood ratios are calibrated, and how the UI can show a live calibration plot.&lt;/p&gt;




&lt;h2&gt;
  
  
  A live case, step by step (TST-003)
&lt;/h2&gt;

&lt;p&gt;The screenshot at the top is this exact run. An analyst wrote: &lt;em&gt;"several cards this month show purchases from the same unusual device profile."&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
  autonumber
  participant A as Analyst
  participant AG as Agent
  participant TG as TigerGraph (MCP)
  participant PE as Policy engine
  A-&amp;gt;&amp;gt;AG: Review $74.96 online purchase on card T9003-K1
  AG-&amp;gt;&amp;gt;TG: Card history, as_of = alert time
  TG--&amp;gt;&amp;gt;AG: 47 earlier payments
  AG-&amp;gt;&amp;gt;TG: Run fraud signatures (prosecution)
  TG--&amp;gt;&amp;gt;AG: 2 signs: device is New behind anonymous proxy, shared by 12 customers
  AG-&amp;gt;&amp;gt;TG: Sweep from the shared device profile
  TG--&amp;gt;&amp;gt;AG: 5 other cards still live on it, 6 already handled
  AG-&amp;gt;&amp;gt;TG: Run innocence signatures (defence)
  TG--&amp;gt;&amp;gt;AG: 1 reason it could be innocent: amount within card p95 ($97.20)
  AG-&amp;gt;&amp;gt;PE: p = 92%, classes: device, network, geography
  PE--&amp;gt;&amp;gt;AG: Likely fraud, $187.33 at risk across 2 payments
  AG--&amp;gt;&amp;gt;A: Verdict, reasoning with "Why" lines, customer-friendly message&lt;/code&gt;&lt;/pre&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;What the agent did&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;1. Read the alert&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Found 47 earlier payments, used only what was known at alert time&lt;/td&gt;
&lt;td&gt;No look-ahead&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;2. Look for fraud&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2 signs, including a rare &lt;em&gt;New&lt;/em&gt; device behind an anonymous proxy used by 12 customers&lt;/td&gt;
&lt;td&gt;Graph-derived evidence, not the score&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;3. Look for innocence&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Found 1 reason: amount is normal for this card&lt;/td&gt;
&lt;td&gt;The defence gets a real hearing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;4. Verdict&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;92%, very likely fraud&lt;/strong&gt;, &lt;strong&gt;$187.33 at risk across 2 payments&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Computed by the judge and policy engine, not by the LLM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;5. Explain&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Customer-friendly message, next steps&lt;/td&gt;
&lt;td&gt;Plain language, not a risk-model dump&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each step has an italic &lt;strong&gt;"Why"&lt;/strong&gt; line, and a &lt;em&gt;"slow down so I can watch"&lt;/em&gt; toggle lets a human follow the reasoning live.&lt;/p&gt;




&lt;h2&gt;
  
  
  What makes this different
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Typical fraud agent&lt;/th&gt;
&lt;th&gt;Fraud Investigator&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Trusts the bank score&lt;/td&gt;
&lt;td&gt;Treats the score as one witness on a learned, &lt;strong&gt;non-monotonic&lt;/strong&gt; curve&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hunts only for fraud signals&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Prosecution &lt;em&gt;and&lt;/em&gt; defence&lt;/strong&gt; in parallel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Same customer" memory&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Shape-matched&lt;/strong&gt; memory, aware that customers are aggregates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM decides the action&lt;/td&gt;
&lt;td&gt;LLM &lt;strong&gt;only phrases&lt;/strong&gt;; a policy engine decides&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Probability from vibes&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Log-odds ledger&lt;/strong&gt; with independence classes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sees the future by accident&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;as_of&lt;/code&gt; enforced in the &lt;strong&gt;data layer&lt;/strong&gt;, with a leakage test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Only finds flagged cards&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Score-blind sweep&lt;/strong&gt; finds the ring at score 0.05&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Profile before you architect.&lt;/strong&gt; The most valuable hour was measuring the data. Adversarial advocates, a non-monotonic score and a score-blind sweep all came from three surprising charts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Explainable" needs structure, not prose.&lt;/strong&gt; Forcing every claim to cite entity IDs, and rejecting any file whose IDs don't exist in the graph, cut hallucination far more than prompting did.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Constrain the model and it gets safer, not weaker.&lt;/strong&gt; Three schema-checked phrasing calls with template fallbacks meant a smaller model was enough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Label weak evidence honestly.&lt;/strong&gt; Anonymous Vesta features carry real signal (AUC 0.845 alone), but they appear as &lt;em&gt;"engineered features, unnamed"&lt;/em&gt;, never as an explained reason.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simulation can flatter you.&lt;/strong&gt; An assumed customer denial pushed a weak 0.65 to 0.97. A system that lets its own simulated evidence inflate confidence is grading itself, so the fix is to report the pre-reply probability and let only the actions reflect the assumption.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure specificity on the whole population.&lt;/strong&gt; My first ring rule looked great on the target cases and terrible across 9,706 device profiles.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  What I'd improve with more time
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Next step&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Full time-machine backtest&lt;/td&gt;
&lt;td&gt;Harness and replay built&lt;/td&gt;
&lt;td&gt;Publish per-pattern accuracy, per-signature precision and the calibration curve &lt;code&gt;[insert results]&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Account-takeover signature&lt;/td&gt;
&lt;td&gt;Not built&lt;/td&gt;
&lt;td&gt;Add to the prosecution set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Numeric lookalike channel&lt;/td&gt;
&lt;td&gt;Not built&lt;/td&gt;
&lt;td&gt;PCA-32 vectors and kNN restricted to labelled exemplars&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full PageRank sweep&lt;/td&gt;
&lt;td&gt;One-hop version today&lt;/td&gt;
&lt;td&gt;Run personalised PageRank in-database with the GDS library&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vector search in TigerGraph&lt;/td&gt;
&lt;td&gt;Planned&lt;/td&gt;
&lt;td&gt;Move semantic recall in-database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monitor mode&lt;/td&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;td&gt;Let the agent raise alerts on its own from graph contagion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Learn from analysts&lt;/td&gt;
&lt;td&gt;Candidate patterns proposed&lt;/td&gt;
&lt;td&gt;Feed analyst-named patterns back into signatures and calibration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Narration model comparison&lt;/td&gt;
&lt;td&gt;Planned&lt;/td&gt;
&lt;td&gt;Compare models on schema pass rate, citation accuracy and SAR completeness&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The verdict
&lt;/h2&gt;

&lt;p&gt;A fraud agent should not be a persuasive storyteller. It should be a &lt;strong&gt;disciplined investigator&lt;/strong&gt; that argues both sides, states its uncertainty, cites everything, and leaves the final decision to &lt;strong&gt;auditable policy&lt;/strong&gt;. TigerGraph is what makes that possible: it holds the transactions, the memory and the time-bounded queries in one place, and the agent reaches all of it through a single MCP tool surface.&lt;/p&gt;




&lt;h3&gt;
  
  
  Stack
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;TigerGraph&lt;/code&gt; · &lt;code&gt;TigerGraph MCP server&lt;/code&gt; · &lt;code&gt;GSQL&lt;/code&gt; · &lt;code&gt;Ollama (gemma4:31b-cloud, nomic-embed-text)&lt;/code&gt; · &lt;code&gt;Next.js + TypeScript&lt;/code&gt; · &lt;code&gt;Zod&lt;/code&gt; · &lt;code&gt;DuckDB&lt;/code&gt; · &lt;code&gt;Python&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Links
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Demo Video:&lt;/strong&gt; &lt;code&gt;https://www.youtube.com/watch?v=kXDzmKNrhIE&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;code&gt;https://github.com/shreesha345/hhg-fraud-agent&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live Link:&lt;/strong&gt; &lt;code&gt;https://hhg-goa-frontend-hc8k.vercel.app/&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;#TigerGraph @TigerGraphDB&lt;/code&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>database</category>
      <category>llm</category>
    </item>
  </channel>
</rss>
