<?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: ARVIND PAL R</title>
    <description>The latest articles on DEV Community by ARVIND PAL R (@arvind555).</description>
    <link>https://dev.to/arvind555</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%2F4140775%2F792b4982-f669-456f-9b76-981d0a0ae7eb.png</url>
      <title>DEV Community: ARVIND PAL R</title>
      <link>https://dev.to/arvind555</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/arvind555"/>
    <language>en</language>
    <item>
      <title>From Fraud Alert to Defensible Action: Building an Agentic Fraud Investigation System with TigerGraph</title>
      <dc:creator>ARVIND PAL R</dc:creator>
      <pubDate>Thu, 24 Sep 2026 08:41:09 +0000</pubDate>
      <link>https://dev.to/arvind555/from-fraud-alert-to-defensible-action-building-an-agentic-fraud-investigation-system-with-4hcp</link>
      <guid>https://dev.to/arvind555/from-fraud-alert-to-defensible-action-building-an-agentic-fraud-investigation-system-with-4hcp</guid>
      <description>&lt;p&gt;&lt;em&gt;How we combined a temporal knowledge graph, GraphRAG, agentic investigation, and deterministic policy controls to turn suspicious transactions into auditable next-best actions.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Every fraud team already has a way to generate alerts — a risk model scores a transaction, a rule fires, a customer calls in. That part is not the hard problem anymore.&lt;/p&gt;

&lt;p&gt;The hard problem starts one step later: given an alert, &lt;strong&gt;what do you actually look at, when do you have enough to act, what do you do if you don't, and what are you allowed to do once you decide?&lt;/strong&gt; Those four questions are what our system for the TigerGraph × Hacker House Goa fraud investigation challenge is built around.&lt;/p&gt;

&lt;p&gt;This post covers the architecture, temporal safety, GraphRAG, the deterministic decision authority, a real benchmark case (HHG-014), and what an actual LLM-orchestration experiment told us about where a model does and doesn't belong here. Every number below comes from the repository, not from memory.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://hhgoa-fraud-investigation.onrender.com" rel="noopener noreferrer"&gt;https://hhgoa-fraud-investigation.onrender.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/arvind-555/hhgoa-fraud-investigation" rel="noopener noreferrer"&gt;https://github.com/arvind-555/hhgoa-fraud-investigation&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Fraud investigation is not classification
&lt;/h2&gt;

&lt;p&gt;The challenge dataset (IEEE-CIS, anonymized, 590,742 transactions with a risk score in place of a label) makes the temptation obvious: train a classifier, threshold the score, done — not what the challenge asks for, and not what a fraud analyst does.&lt;/p&gt;

&lt;p&gt;An analyst who opens a flagged transaction doesn't output &lt;code&gt;fraud: 0.83&lt;/code&gt;. They ask: &lt;em&gt;is this card connected to anything else I should worry about? Do I have enough to act, or do I need to check with the customer first?&lt;/em&gt; The output isn't a probability — it's a decision about what happens next, and who has to sign off on it.&lt;/p&gt;

&lt;p&gt;Our system answers that question, not the classification one: it investigates the transaction's neighborhood, gathers evidence, decides whether that evidence is sufficient, and — only under fixed policy — recommends an action with an approval route.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Why relationships matter
&lt;/h2&gt;

&lt;p&gt;A single transaction row tells you an amount, a channel, a merchant category. It does not tell you that the device behind it was used by eleven other customers last month, or that its device profile matches one that funded a confirmed-fraud case three months ago.&lt;/p&gt;

&lt;p&gt;Those facts only exist as &lt;em&gt;relationships&lt;/em&gt;: Customer owns Card, Card made Transaction, Transaction came from Device, Device seen on other Cards, other Cards belong to other Customers. Fraud rings are almost never visible in one row — they're visible in the &lt;em&gt;shape&lt;/em&gt; of the graph around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Why TigerGraph is central
&lt;/h2&gt;

&lt;p&gt;We built one graph, &lt;code&gt;FraudInvestigation&lt;/code&gt;, on TigerGraph 4.2.5 (Savanna), separate from the pre-existing &lt;code&gt;Transaction_Fraud&lt;/code&gt; graph — the code refuses to run against anything else by construction (the graph name is checked and hard-fails otherwise).&lt;/p&gt;

&lt;p&gt;The schema has 9 core vertex types and 18 relationship types (each with an automatic reverse edge):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;VERTEX&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Card&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Transaction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DeviceProfile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="n"&gt;EmailDomain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BillingRegion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ClosedCase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="n"&gt;FI_Case&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TextChunk&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EDGE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;OWNS&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;OWNED_BY&lt;/span&gt;            &lt;span class="n"&gt;Customer&lt;/span&gt;  &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Card&lt;/span&gt;
               &lt;span class="n"&gt;MADE&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;MADE_BY&lt;/span&gt;             &lt;span class="n"&gt;Card&lt;/span&gt;      &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Transaction&lt;/span&gt;
               &lt;span class="k"&gt;NEXT&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;PREV&lt;/span&gt;                &lt;span class="n"&gt;Transaction&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Transaction&lt;/span&gt;
               &lt;span class="n"&gt;FROM_DEVICE&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;DEVICE_OF&lt;/span&gt;    &lt;span class="n"&gt;Transaction&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;DeviceProfile&lt;/span&gt;
               &lt;span class="n"&gt;SEEN_ON&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;SEEN_BY&lt;/span&gt;          &lt;span class="n"&gt;DeviceProfile&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Card&lt;/span&gt;
               &lt;span class="n"&gt;PURCHASER_EMAIL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RECIPIENT_EMAIL&lt;/span&gt;       &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;EmailDomain&lt;/span&gt;
               &lt;span class="n"&gt;BILLED_IN&lt;/span&gt;                 &lt;span class="n"&gt;Transaction&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;BillingRegion&lt;/span&gt;
               &lt;span class="n"&gt;CLOSED_ON_CARD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CLOSED_ON_CUSTOMER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="n"&gt;CLOSED_INVOLVES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CLOSED_CONNECTED_TO&lt;/span&gt;   &lt;span class="n"&gt;ClosedCase&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
               &lt;span class="n"&gt;CASE_ON_CARD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CASE_TXN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CASE_CONNECTED_TO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="n"&gt;CASE_CITES_DEVICE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SIMILAR_CASE&lt;/span&gt;        &lt;span class="n"&gt;FI_Case&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
               &lt;span class="n"&gt;DESCRIBES&lt;/span&gt;                 &lt;span class="n"&gt;TextChunk&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ClosedCase&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;FI_Case&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To be precise about the division of labor: &lt;strong&gt;TigerGraph stores the connected facts and answers traversal queries. It does not reason about fraud.&lt;/strong&gt; Every query (&lt;code&gt;fi_shared_devices&lt;/code&gt;, &lt;code&gt;fi_connected_entities&lt;/code&gt;, &lt;code&gt;fi_card_history&lt;/code&gt;, &lt;code&gt;fi_prior_cases&lt;/code&gt;, &lt;code&gt;fi_txn_context&lt;/code&gt;, &lt;code&gt;fi_customer_history&lt;/code&gt;, &lt;code&gt;fi_text_chunks&lt;/code&gt;) returns raw, time-bounded neighborhoods and counts. Interpreting them happens entirely in application code (&lt;code&gt;src/fraud_tools/&lt;/code&gt;, &lt;code&gt;src/agent/&lt;/code&gt;). The graph answers "what's connected to what, as of when"; it doesn't decide anything.&lt;/p&gt;

&lt;p&gt;One detail worth calling out: &lt;code&gt;fi_shared_devices&lt;/code&gt; &lt;strong&gt;hub-gates&lt;/strong&gt; by design. A device profile shared by an implausible number of customers (a null/default fingerprint, say) is never expanded — it's reported separately as a &lt;code&gt;skipped_hub&lt;/code&gt;, so one ubiquitous device can't pull half the customer base into every investigation.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. System architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                         FRAUD SIGNAL
                              │
                              ▼
                    ┌───────────────────┐
                    │   Agent Trigger   │
                    └─────────┬─────────┘
                              │
                              ▼
                 ┌────────────────────────┐
                 │      TigerGraph        │
                 │                        │
                 │ Customer ↔ Card        │
                 │ Card ↔ Transaction     │
                 │ Transaction ↔ Device   │
                 │ Identity / Email /     │
                 │ Billing Region / Cases │
                 └───────────┬────────────┘
                             │
                             ▼
                 ┌────────────────────────┐
                 │       GraphRAG         │
                 │                        │
                 │ Prior Cases             │
                 │ Fraud Patterns          │
                 │ Policies &amp;amp; Rules        │
                 └───────────┬────────────┘
                             │
                             ▼
                 ┌────────────────────────┐
                 │ Investigation Agent    │
                 │                        │
                 │ Evidence gathering      │
                 │ Evidence synthesis      │
                 │ Uncertainty assessment  │
                 │ Tool selection          │
                 └───────────┬────────────┘
                             │
                    ┌────────┴────────┐
                    │                 │
              More evidence?     Sufficient?
                    │                 │
                    ▼                 ▼
          Controlled evidence     Next-Best
               request             Action
                                      │
                                      ▼
                           ┌──────────────────┐
                           │ Policy Authority │
                           │                  │
                           │ Actions          │
                           │ Approval routes  │
                           │ Safety gates     │
                           └────────┬─────────┘
                                    │
                                    ▼
                             Case Record
                         Evidence + Findings
                         Decision + Actions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TigerGraph&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Connected entity storage; answers whitelisted, time-bounded traversal queries. No fraud reasoning.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GraphRAG&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Retrieves prior closed cases, fraud-pattern text, and policy rules as context — text retrieval, not traversal.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Investigation Agent&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Orchestrates tool calls, synthesizes evidence, assesses whether it's sufficient.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Policy Authority&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A fixed decision matrix — evidence in, verdict/action/route out. No model in this layer.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Case Record&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The auditable output: an &lt;code&gt;FI_Case&lt;/code&gt; graph vertex plus the full decision trail.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  5. Temporal safety: the &lt;code&gt;as_of&lt;/code&gt; design
&lt;/h2&gt;

&lt;p&gt;A benchmark case is scored on what an investigator could have known &lt;em&gt;at the moment it opened&lt;/em&gt; — not what happened afterward, and not which closed cases were labeled after the fact. If the agent could see the future, the evaluation would be meaningless.&lt;/p&gt;

&lt;p&gt;Every graph query in this system takes a required &lt;code&gt;INT as_of&lt;/code&gt; parameter with &lt;strong&gt;no default value&lt;/strong&gt;, and filters on edge &lt;code&gt;epoch&lt;/code&gt;, never on a stored "current" flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CREATE OR REPLACE QUERY fi_shared_devices(
    VERTEX&amp;lt;Card&amp;gt; card, INT as_of, INT days, INT max_customers
) FOR GRAPH FraudInvestigation { ... }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three rules enforce this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Transactions and edges&lt;/strong&gt; are only traversed if their &lt;code&gt;epoch &amp;lt;= as_of&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Closed cases&lt;/strong&gt; only become visible once &lt;code&gt;close_epoch &amp;lt;= as_of&lt;/code&gt; — a case that closed after the investigation started doesn't exist yet, as far as the agent is concerned.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GraphRAG text chunks&lt;/strong&gt; carry a &lt;code&gt;valid_from_epoch&lt;/code&gt;; a closed-case narrative chunk isn't retrievable until its case has actually closed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;as_of&lt;/code&gt; is fixed by the trusted runner from the case's own &lt;code&gt;opened_at&lt;/code&gt; timestamp before the agent starts, and never appears in the agent's own request surface — the tool-call schema simply has no &lt;code&gt;as_of&lt;/code&gt; parameter, and it's one of 24 argument names the permission layer refuses by name if anything tries to pass it. This isn't a policy the agent is asked to respect; it's a parameter it cannot supply.&lt;/p&gt;

&lt;p&gt;The result the repository actually measures: &lt;strong&gt;0 temporal-leakage events&lt;/strong&gt; in the LLM-orchestrated evaluation runs, explicitly counted every run, plus a backend suite (434 tests) including multiple tests asserting the temporal guard actually raises when future data is attempted — not "we believe leakage doesn't happen," but a check that runs and is enforced.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. GraphRAG and case memory
&lt;/h2&gt;

&lt;p&gt;It's worth being precise about what "GraphRAG" means here, since it's easy to conflate with graph traversal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TigerGraph traversal&lt;/strong&gt; answers structural questions: which cards share this device, what did this card do in the last 48 hours, is it connected to a prior case.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GraphRAG&lt;/strong&gt; answers contextual questions: what do similar closed cases look like, what does policy say, what known pattern does this match.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The knowledge base for GraphRAG is exactly four document types — &lt;code&gt;closed_case&lt;/code&gt;, &lt;code&gt;policy&lt;/code&gt;, &lt;code&gt;pattern&lt;/code&gt;, &lt;code&gt;format&lt;/code&gt; — stored as &lt;code&gt;TextChunk&lt;/code&gt; vertices with &lt;code&gt;valid_from_epoch&lt;/code&gt;, retrieved via lexical BM25 (no vector embeddings; retrieval is deterministic lexical matching over the same temporally-filtered chunks). Closed-case chunks are templated (&lt;code&gt;"&amp;lt;pattern&amp;gt; | &amp;lt;outcome&amp;gt; | &amp;lt;channel&amp;gt; | exposure &amp;lt;band&amp;gt;: &amp;lt;analyst notes&amp;gt;"&lt;/code&gt;) — they give wording and base-rate context, never a label for the case under investigation, and a hard assertion (&lt;code&gt;assert_clean&lt;/code&gt;) fails the chunk build if any chunk ever mentions a benchmark case ID.&lt;/p&gt;

&lt;p&gt;So GraphRAG is &lt;em&gt;contextual memory and text retrieval&lt;/em&gt; — it never traverses the entity graph, and it's never the verdict source; that's the policy layer's job.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. The agentic investigation loop
&lt;/h2&gt;

&lt;p&gt;This is what makes the system agentic rather than a fixed pipeline: tool-call count and choice aren't hardcoded per case. The orchestrator (&lt;code&gt;src/agent/orchestrator.py&lt;/code&gt;) moves through a fixed state machine, but &lt;em&gt;what happens inside each state&lt;/em&gt; depends on the evidence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TRIGGER
  → INITIAL_INVESTIGATION        (tool selection, graph traversal)
  → EVIDENCE_SYNTHESIS           (combine everything returned)
  → UNCERTAINTY_ASSESSMENT       (is this evidence sufficient?)
  → [ADDITIONAL_EVIDENCE_REQUEST]  (only if policy says it's needed)
  → NEXT_BEST_ACTION              (deterministic decision matrix)
  → EXPLANATION
  → CASE_WRITE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ten whitelisted, read-only tools are available: &lt;code&gt;get_transaction_context&lt;/code&gt;, &lt;code&gt;get_customer_history&lt;/code&gt;, &lt;code&gt;get_card_history&lt;/code&gt;, &lt;code&gt;find_shared_devices&lt;/code&gt;, &lt;code&gt;find_connected_entities&lt;/code&gt;, &lt;code&gt;find_prior_cases&lt;/code&gt;, &lt;code&gt;detect_fraud_patterns&lt;/code&gt;, &lt;code&gt;get_policy_context&lt;/code&gt;, &lt;code&gt;find_similar_cases&lt;/code&gt;, &lt;code&gt;retrieve_policy&lt;/code&gt;. If a shared-device search turns up connected cards, the agent pulls each one's history — a case with 19 connected cards genuinely runs more tool calls than a case with none. That's the "agentic" part: &lt;strong&gt;the investigation's shape follows the evidence, not a fixed script.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What's deterministic versus LLM-assisted matters for the rest of this post: &lt;strong&gt;tool selection, order, and stopping can optionally be delegated to an LLM investigator.&lt;/strong&gt; Evidence synthesis, uncertainty assessment, the decision matrix, approval routing, and the case write never are — fixed Python logic, the same for every run.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Controlled additional evidence and uncertainty
&lt;/h2&gt;

&lt;p&gt;Uncertainty assessment produces an &lt;code&gt;evidence_strength&lt;/code&gt; (&lt;code&gt;none&lt;/code&gt; / &lt;code&gt;weak&lt;/code&gt; / &lt;code&gt;moderate&lt;/code&gt; / &lt;code&gt;strong&lt;/code&gt;), an independent-source count, and a list of what's still missing — never a guessed answer. If that's insufficient under policy, the agent may request customer verification or step-up authentication.&lt;/p&gt;

&lt;p&gt;That request is deterministically gated. The model — if one is orchestrating — can only &lt;em&gt;suggest&lt;/em&gt; one; a separate, non-LLM check decides &lt;code&gt;NOT_SUGGESTED&lt;/code&gt; / &lt;code&gt;ALLOWED_POLICY_REQUIRED&lt;/code&gt; / &lt;code&gt;REJECTED_NOT_REQUIRED&lt;/code&gt; before anything is issued. A response comes from a deterministic simulator, never an actual customer or a model roleplaying one — the benchmark supplies no real responses, so the simulator's one built-in assumption is "no reply within 24 hours," treated as &lt;em&gt;absence of evidence&lt;/em&gt;, never confirmation or denial. Every simulated value is tagged &lt;code&gt;[SIMULATED]&lt;/code&gt; and marked &lt;code&gt;simulated: true&lt;/code&gt;, so it's never confused with graph-derived evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Deterministic policy authority and approval routes
&lt;/h2&gt;

&lt;p&gt;This layer actually decides what happens, and it is intentionally boring: a fixed decision matrix mapping five evidence classes to actions and routes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Class&lt;/th&gt;
&lt;th&gt;Evidence at &lt;code&gt;as_of&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;Typical initial actions&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;A&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Strong graph evidence (device ring, ≥2 independent validated sources)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CREATE_CASE&lt;/code&gt;, &lt;code&gt;ESCALATE_TO_ANALYST&lt;/code&gt;, &lt;code&gt;FILE_REPORT&lt;/code&gt;, &lt;code&gt;MONITOR_CONNECTED_CARDS&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;T+&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Customer report (immutable) + one validated source&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;BLOCK_CARD&lt;/code&gt;, &lt;code&gt;CREATE_CASE&lt;/code&gt;, conditional &lt;code&gt;FILE_REPORT&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;T&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Customer report only, weak/no graph evidence&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;VERIFY_WITH_CUSTOMER&lt;/code&gt;, &lt;code&gt;MONITOR_CARD&lt;/code&gt;, &lt;code&gt;CREATE_CASE&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;B&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One validated independent source&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;STEP_UP_AUTH&lt;/code&gt; / &lt;code&gt;VERIFY_WITH_CUSTOMER&lt;/code&gt;, &lt;code&gt;MONITOR_CARD&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;C&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Weak context only&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;STEP_UP_AUTH&lt;/code&gt; / &lt;code&gt;VERIFY_WITH_CUSTOMER&lt;/code&gt;, or &lt;code&gt;ALLOW_TRANSACTION&lt;/code&gt; if there's genuinely nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every action carries an approval route — &lt;code&gt;auto&lt;/code&gt;, &lt;code&gt;L1&lt;/code&gt; (team lead), or &lt;code&gt;L2&lt;/code&gt; (fraud manager) — decided by fixed thresholds (&lt;code&gt;BLOCK_CARD&lt;/code&gt; routes L1 under a fixed exposure ceiling, L2 above it). &lt;strong&gt;A customer's own denial is immutable&lt;/strong&gt;: once reported, no simulated reply can reverse it. The verdict follows directly from the class and post-verification outcome — strong evidence or a denied report is &lt;code&gt;fraud&lt;/code&gt;; confirmed/passed verification alone is &lt;code&gt;legitimate&lt;/code&gt;; anything short of the bar is &lt;code&gt;uncertain&lt;/code&gt;, never a guess.&lt;/p&gt;

&lt;p&gt;Graph writes are equally constrained: only &lt;code&gt;FI_Case&lt;/code&gt; and its own outgoing edges can be written, content-hashed and idempotent, with source facts — &lt;code&gt;Transaction&lt;/code&gt;, &lt;code&gt;Customer&lt;/code&gt;, &lt;code&gt;Card&lt;/code&gt;, &lt;code&gt;DeviceProfile&lt;/code&gt;, &lt;code&gt;ClosedCase&lt;/code&gt; — structurally unwritable from this path. Nothing recommended is ever executed; every action carries &lt;code&gt;executed: false&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Walkthrough: HHG-014
&lt;/h2&gt;

&lt;p&gt;HHG-014's trigger is an analyst request, not a model score or a customer complaint:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Analyst request: several cards this month show purchases from the same unusual device profile. Review transaction 3478561 on card C13487-K1 and look for related activity."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Initial investigation.&lt;/strong&gt; The agent pulls transaction context, customer history, card history, prior cases, and pattern detection — nine distinct tool types in its first pass. &lt;code&gt;detect_fraud_patterns&lt;/code&gt; fires four signals: &lt;strong&gt;S01&lt;/strong&gt; (shared-origin device ring — a compound anomaly), &lt;strong&gt;S10&lt;/strong&gt; (new device ID), &lt;strong&gt;S11&lt;/strong&gt; (anonymizing proxy), &lt;strong&gt;S12&lt;/strong&gt; (new device &lt;em&gt;and&lt;/em&gt; proxy together).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Graph expansion.&lt;/strong&gt; &lt;code&gt;find_connected_entities&lt;/code&gt; returns 19 other cards sharing the flagged device. The agent then pulls &lt;strong&gt;each of those 19 cards' own history&lt;/strong&gt; — one &lt;code&gt;get_card_history&lt;/code&gt; call per connected card, because the shared-device signal alone isn't evidence about what those cards actually did; the graph has to show it. That expansion finds 24 transactions across those 19 cards inside the same 30-day window. In total: &lt;strong&gt;29 tool calls, 28 graph queries, 10 served from cache&lt;/strong&gt;, 46 seconds end to end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Contextual evidence.&lt;/strong&gt; &lt;code&gt;find_similar_cases&lt;/code&gt; (GraphRAG) returns 10 similar closed cases by text similarity — zero directly linked to this card, customer, or device; precedent, not proof. &lt;code&gt;retrieve_policy&lt;/code&gt; pulls the actual text of R1, R4, R6, and R9 in as citable evidence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Uncertainty.&lt;/strong&gt; Evidence strength comes out &lt;strong&gt;strong&lt;/strong&gt; (the S01 ring plus the pattern-defining signals), but &lt;code&gt;needs_more_evidence&lt;/code&gt; is still &lt;code&gt;True&lt;/code&gt; — verification is missing and the model alert band is only "low." Recorded as missing: no merchant field (so R7 can't be checked), and no verification reply yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Additional evidence.&lt;/strong&gt; The agent requests &lt;code&gt;customer_validation&lt;/code&gt;. No real reply exists for this benchmark, so the simulator returns the one thing it's allowed to: &lt;em&gt;"[SIMULATED] No reply received within 24 hours"&lt;/em&gt; — tagged simulated, not treated as a denial or confirmation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Policy evaluation and final action.&lt;/strong&gt; Under the no-reply rule (R4), the initial action set (&lt;code&gt;CREATE_CASE&lt;/code&gt;, &lt;code&gt;ESCALATE_TO_ANALYST&lt;/code&gt;, &lt;code&gt;FILE_REPORT[L2]&lt;/code&gt;, &lt;code&gt;MONITOR_CONNECTED_CARDS&lt;/code&gt;, &lt;code&gt;VERIFY_WITH_CUSTOMER&lt;/code&gt;) gains two more: &lt;code&gt;MONITOR_CARD&lt;/code&gt; and &lt;code&gt;DECLINE_TRANSACTION[L1]&lt;/code&gt;. &lt;strong&gt;Verdict: fraud. Status: escalated.&lt;/strong&gt; A suspicious-activity report is recommended, citing R9 (coordinated activity across customers fitting no documented pattern) and R6 (shared device profile), generated from case facts, not boilerplate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case record.&lt;/strong&gt; The case writes to the graph as &lt;code&gt;CASE-HHG-014&lt;/code&gt;, revision 3, content-hashed and idempotent — unchanged content writes nothing; changed content bumps the revision. Verified directly: running this same investigation live against TigerGraph today reproduces a content hash matching the stored revision-3 vertex exactly, with no additional write.&lt;/p&gt;

&lt;p&gt;One honest caveat, from our own second-pass audit, not hidden: the S01 ring rests on one seeded device-sharing signal without an independently labeled precedent, and all 26 nearby transactions are treated as one episode on device-sharing alone — roughly 81% of the early burst was never itself part of a closed case. It's the single case in our 20-case adversarial re-review rated a residual &lt;strong&gt;MEDIUM&lt;/strong&gt; concern rather than LOW; the rest of the chain checks out, but the ring evidence's strength is disclosed as imperfect, not overstated.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. LLM orchestration: what we actually measured
&lt;/h2&gt;

&lt;p&gt;We ran a real LLM-orchestrated version of the investigator (&lt;code&gt;HHG_LLM_PROVIDER=openai&lt;/code&gt;, GPT-5 mini), where the model chose which tools to call, in what order, and when to stop — with the deterministic evidence rules, decision matrix, and policy layer left untouched underneath it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First run, 6 of 20 cases, stopped by design.&lt;/strong&gt; The harness halts immediately on any safety violation, and it did: on HHG-006, the model requested customer verification even though the deterministic layer had already determined none was needed, and that request changed the final action set — a real policy-drift violation, caught exactly as intended.&lt;/p&gt;

&lt;p&gt;We fixed the two integration issues this exposed — equivalent tool calls weren't being recognized as duplicates, and the model's evidence &lt;em&gt;suggestions&lt;/em&gt; weren't gated by policy before executing — and re-ran the same six cases. Safety was clean across all three attempts: &lt;strong&gt;0 temporal leakage, 0 forbidden-parameter attempts, 0 benchmark IDs from other cases, 0 executed actions, 0 fabricated evidence.&lt;/strong&gt; The honest finding didn't change, though:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No case showed the LLM discovering evidence the deterministic pipeline missed.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Deterministic and LLM-orchestrated authority (verdict, final actions, SAR) was &lt;strong&gt;identical in every one of the five measured cases&lt;/strong&gt; once policy drift was fixed — HHG-004 was attempted but never measured after three attempts all hit TigerGraph read timeouts.&lt;/li&gt;
&lt;li&gt;The LLM path cost 5,000–17,000 tokens per case and was not faster in any comparable run — about 3× the deterministic pipeline's latency in the initial, clean run; the post-fix latency numbers were not comparable because TigerGraph was unstable during that re-run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the engineering decision isn't "LLMs don't work here." It's: &lt;strong&gt;use the LLM where it can genuinely help, and don't give it authority that a deterministic system already enforces more reliably.&lt;/strong&gt; The deterministic pipeline produced the 20 submitted answer files (&lt;code&gt;tokens: 0&lt;/code&gt; in every one). The LLM investigator stays in the codebase, opt-in, sandboxed, and safety-gated — not the default path.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. Engineering validation and reproducibility
&lt;/h2&gt;

&lt;p&gt;No answer key was ever provided for the 20 benchmark cases, so &lt;strong&gt;we make no accuracy claim&lt;/strong&gt; — no precision, recall, F1, or "N/20 correct." What follows is checkable by re-running the repository's own tests.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&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;Benchmark cases processed end to end&lt;/td&gt;
&lt;td&gt;20 / 20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;FI_Case&lt;/code&gt; graph records written and validated&lt;/td&gt;
&lt;td&gt;20 / 20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SAR records validated for internal consistency&lt;/td&gt;
&lt;td&gt;20 / 20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Temporal-leakage violations&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Policy violations&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Graph-invariant violations&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend test suite&lt;/td&gt;
&lt;td&gt;434 tests passing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontend test suite&lt;/td&gt;
&lt;td&gt;33 tests passing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Independent evidence-strength oracle vs. live agent (40 sampled non-benchmark transactions)&lt;/td&gt;
&lt;td&gt;40 / 40 agreement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adversarial re-audit of all 20 submitted cases&lt;/td&gt;
&lt;td&gt;0 HIGH findings, 1 MEDIUM, 19 LOW&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deterministic reproducibility&lt;/td&gt;
&lt;td&gt;re-running produces byte-identical case content and graph-write hashes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On &lt;code&gt;fraud_probability&lt;/code&gt;: it's &lt;code&gt;null&lt;/code&gt; in every one of the 20 submitted files, deliberately. The only labeled outcomes available are the 5,565 closed investigations, and they're not representative of the alert population — about 84% of closed cases are confirmed fraud, while the specification itself states roughly half the benchmark cases are legitimate. Calibrating from a differently-shaped population would just transfer its base rate onto this one, so we built the calibration gates anyway (&lt;code&gt;src/calibration/&lt;/code&gt;, &lt;code&gt;config/calibration_v1.json&lt;/code&gt;) and let them fail honestly rather than loosen them or substitute a placeholder — also why our own verdicts skew toward &lt;code&gt;uncertain&lt;/code&gt; rather than &lt;code&gt;legitimate&lt;/code&gt;: without a real reply confirming innocence, the system won't guess &lt;code&gt;legitimate&lt;/code&gt; just because a case looks routine. Policy thresholds phrased as probabilities in the specification (0.30 for case-opening, 0.85/0.15 for stopping) are instead applied through evidence-strength classes and independent-source counts, which the data actually supports.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Separating agentic from consequential was the single most load-bearing design decision.&lt;/strong&gt; Variable tool calls, variable depth, an optional LLM — none of it touches the part that carries consequences, and keeping that boundary hard rather than conventional is what makes the rest of this trustworthy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Temporal correctness has to be structural.&lt;/strong&gt; A required &lt;code&gt;as_of&lt;/code&gt; parameter with no default, checked at the query layer, is worth more than any amount of "the agent is instructed not to look at future data."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An honest negative result is still a result.&lt;/strong&gt; The LLM experiment could have been quietly dropped instead of documented down to the safety violation it triggered — "we tested it and it didn't help" is exactly the finding a production fraud team needs, not a footnote.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ring evidence from one signal source is real but thin.&lt;/strong&gt; HHG-014's S01 ring is the most dramatic case in the set, and also the one our own adversarial review flagged as resting on uncorroborated evidence. Better to say that plainly than let the most impressive-looking case be the least scrutinized.&lt;/p&gt;

&lt;h2&gt;
  
  
  14. What remains imperfect
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;fraud_probability&lt;/code&gt; is null everywhere; if the scoring path strictly requires a numeric value, this field won't score, and we chose that over inventing one.&lt;/li&gt;
&lt;li&gt;Ring/shared-device evidence (S01) relies on a single traversal signal without a second, independent confirming source for the connected cards it implicates.&lt;/li&gt;
&lt;li&gt;Customer, step-up, and analyst responses are simulated for every case (there's no real customer to answer), which caps how many cases reach a confident &lt;code&gt;legitimate&lt;/code&gt; verdict — disclosed honestly rather than fabricated.&lt;/li&gt;
&lt;li&gt;The LLM-orchestrated path is measured on 6 of 20 cases (5 fully, per §11); we didn't spend further budget once deterministic and LLM authority were established as identical.&lt;/li&gt;
&lt;li&gt;No official accuracy figure exists, and none is claimed.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;An agent that, given a fraud alert, investigates a connected graph of customers, cards, transactions, and devices at a fixed point in time; retrieves prior cases and policy text through GraphRAG; assesses whether its evidence is sufficient and requests more only under policy control; and produces a next-best action with an approval route — through a decision layer a language model is never allowed to override. A read-only live-investigation mode on the deployed demo runs this same pipeline against live TigerGraph on request, with no graph write, so it's inspectable in real time, not only as a replay.&lt;/p&gt;

&lt;p&gt;Built for the &lt;strong&gt;TigerGraph × Hacker House Goa&lt;/strong&gt; challenge.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live demo (including the live investigation preview):&lt;/strong&gt; &lt;a href="https://hhgoa-fraud-investigation.onrender.com" rel="noopener noreferrer"&gt;https://hhgoa-fraud-investigation.onrender.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub repository:&lt;/strong&gt; &lt;a href="https://github.com/arvind-555/hhgoa-fraud-investigation" rel="noopener noreferrer"&gt;https://github.com/arvind-555/hhgoa-fraud-investigation&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>database</category>
      <category>rag</category>
    </item>
  </channel>
</rss>
