How we combined TigerGraph, Jev, Bayesian scoring, deterministic rules, and human approval to investigate fraud without letting an LLM make the final call.
Jev decides when to look. TigerGraph decides what is true.
That was the basic idea behind what we built.
We wanted to build a fraud investigation agent that doesn't just look at a transaction, generate a risk score, and immediately decide whether it's fraud. Instead, an alert comes in, the agent gathers the surrounding evidence from a graph, updates its belief about what might be happening, checks the known fraud patterns, and then decides what it is actually allowed to do.
The important part is that the LLM isn't in charge of the whole thing.
TigerGraph is the source of truth for the investigation. Jev controls a few bounded decisions. The Bayesian layer updates the fraud probability. A deterministic rule engine decides the actions. The language model comes in at the end to explain what the system already decided.
And when the evidence isn't strong enough, the system is allowed to say uncertain.
That's the part we cared about most.
What we built
The starting point is simple: one card alert.
It can come from a customer dispute, a high bank risk score, or an analyst asking the system to investigate something. There is no fraud label attached to the transaction, so the agent has to build the case from the evidence around it.
The first thing it does is collect a fixed set of facts from TigerGraph: the transaction itself, the card history, the device involved, other cards connected to that device, identity information, billing regions, recurring amounts, previous closed cases, and the exposure belonging to the current episode.
That initial graph lookup is deliberately the same for every case.
We didn't want the model deciding which facts were important enough to fetch. If it could choose the initial queries, it could easily skip something like a card-testing sequence or a shared device relationship. Instead, every investigation starts from the same measurement pack.
From there, the system updates a Bayesian fraud chance. The bank's risk score acts as the prior, and each piece of evidence moves the log-odds up or down. A new device can increase the chance, while a long quiet history, a trip, or a recurring monthly payment can pull it back in the other direction. Older fraud cases can help us find relevant parts of the graph, but they don't become proof that the current transaction is fraudulent.
Once we have that evidence and probability, we look at the pattern.
Some patterns can be identified deterministically. If one of those detectors is confident enough, we keep that pattern and don't ask the model to rename it. If the fixed detectors can't identify one, that's where Jev gets involved. Jev can look at the remaining possibilities and return a probability map. If the top two patterns are too close, the system leaves the pattern blank instead of pretending that it knows.
That gives us a pretty important property: the model can express uncertainty instead of being forced to pick something.
Where Jev fits
Jev isn't an unrestricted agent that can keep calling tools until it finds an answer.
We deliberately gave it a very small budget.
It gets three meaningful decisions during an investigation.
First, it can identify the pattern when the deterministic checks haven't already locked one.
Second, it can decide whether one more graph lookup would be useful. That lookup can retrieve the other cards on a device, perform a bounded community walk, retrieve older cases again, or find one relevant policy passage. It can also simply choose none.
Once that one additional lookup is done, the graph walk stops.
Finally, after the language model writes the explanation, Jev checks the sentences. If a sentence is backed by an evidence reference, it stays. If the sentence introduces a fact that wasn't actually present in the case, it gets replaced with the underlying claim.
So Jev isn't deciding whether the card should be blocked.
It's controlling how much additional investigation happens and whether the final explanation stays grounded in the evidence.
That distinction ended up being really useful.
If Jev times out or doesn't answer, we record the fallback. The deterministic policy engine still runs, and the missing model response doesn't suddenly get to decide the action.
The whole investigation is basically one controlled pipeline
The architecture looks complicated when you draw every component, but the actual flow is pretty straightforward:
The policy engine is also intentionally deterministic.
It runs twice.
The first pass produces the initial action plan. If that plan needs a customer response, the system assumes one answer from the evidence ledger and runs the rules again. The second pass becomes the final action plan.
We keep both plans.
That means we can see not only what the final recommendation was, but also how the recommendation changed when the additional evidence was introduced.
For example, one case can start with a recommendation to verify the customer. If the assumed response confirms that the transaction came from a new phone belonging to the customer, the second policy pass can close the case as legitimate.
The first plan doesn't disappear. It remains on the case as part of the investigation trace.
TigerGraph is the evidence layer
The graph we're using is HHGOA.
The agent doesn't generate GSQL at runtime. We installed the queries beforehand and expose them through the official TigerGraph MCP.
The graph contains vertices such as Transaction, BankCard, DeviceProfile, ClosedCase, IdentityFlag, PolicyNote, and ExamCase, connected through relationships such as TXN_ON_CARD, FROM_DEVICE, and CASE_ON_CARD.
The initial pack always runs the same queries:
| Query | What we're looking for |
|---|---|
txn_and_card, card_window
|
The flagged transaction and the card's history |
device_profile, device_neighbors, identity_flag
|
The device, connected cards, and identity information |
region_history, recurring_match
|
Previous regions and recurring payment patterns |
prior_cases |
Closed cases on the card and connected cards |
exposure_episode |
Transactions belonging to the current episode and total exposure |
Then Jev can spend its one additional query on component_cards, card_community, prior_cases, or the policy vector search.
The vector search is worth calling out because it isn't being used as a replacement for the graph.
TigerGraph's vectorSearch finds the closest PolicyNote.vec, and that paragraph becomes context for the writer. It isn't treated as a new fact about the transaction.
So, in this system, the two retrieval mechanisms have very different jobs:
The graph tells us what is connected. The vector search tells us which policy text is relevant.
That keeps the context small without throwing the entire transaction dataset into the model.
The decision is intentionally boring
Once all of that is finished, the actual verdict is deliberately simple.
We ask three questions in order.
First: did the rules already close this as a legitimate transaction?
If yes, the case is legitimate and exposure is zero.
If not, we ask whether the pattern is undocumented. If the data shows a shape that the bank's named patterns don't cover, that goes into the fraud bucket.
If that doesn't apply either, we look at the probability. If the chance crosses the fraud threshold, it becomes fraud.
Otherwise, it stays uncertain.
There is no extra LLM reasoning layer that gets to override this.
A named pattern doesn't automatically make something fraud. A high bank score doesn't automatically make something fraud. And a customer dispute doesn't automatically mean every card should be blocked.
The rules determine the action.
One case makes the idea much clearer
Take HHG-011.
The customer says:
"I never made this $131.30 purchase."
The transaction matches a card_not_present_new_device pattern.
The probability starts around 0.39.
The new device pushes it to around 0.56.
That device is also connected to an older confirmed fraud case, which pushes the probability to around 0.83.
Then the long quiet history pulls it back down to around 0.76.
Now we have a problem that looks suspicious, but the final number is still below the fraud threshold.
So the system doesn't force the answer.
The final verdict is uncertain.
The case is opened, the report is filed according to policy, the other cards on the device are monitored, and the case is escalated to an analyst. The card itself remains open.
The interesting thing here is that the system can point to the evidence family that actually changed the action plan.
That's what we call the point of commitment.
If we remove the device evidence, the action list changes. Remove some other unrelated evidence and the action list stays the same.
So instead of producing a paragraph saying "the model thinks this is suspicious because...", we can actually show which evidence mattered to the decision.
And then there is the opposite case
HHG-016 starts differently.
The first plan is to verify the customer and open a case.
The system assumes a response from the evidence: the customer confirms that the transaction came from their new phone.
That additional information changes the probability to around 0.12.
The policy engine runs again.
This time the final plan is to close the case as legitimate.
Nothing is waiting for a signature.
Both plans remain on the file. The alternative responses that weren't assumed are stored as counterfactuals rather than being presented as something that actually happened.
This is one of the reasons we wanted the policy engine to run twice.
The investigation isn't just:
input → final answer
It's:
input → initial plan → new evidence → final plan
Human approval is still part of the system
Not every action requires someone to click a button.
Opening a case, monitoring something, warning a customer, or closing a legitimate transaction can proceed automatically.
A DECLINE_TRANSACTION, FILE_REPORT, or BLOCK_CARD action is different. Those actions require the appropriate approval route.
The system also refuses an approval request for an action that isn't present in the final action list.
And this benchmark doesn't actually send a live SMS, freeze a real card, or write to a production CRM. The case file is the boundary of the simulation.
That boundary matters because it keeps the demonstration honest about what the agent actually did.
Memory is also part of the graph
When an investigation finishes, we create an ExamCase vertex.
It stores the verdict, pattern, exposure, and the relationships back to the card, transactions, device, and connected cards.
Then we read it back.
If the read-back doesn't return the same verdict and exposure, the case isn't considered successfully stored.
It's a small detail, but it changes how we think about memory.
A write that we never read back is just a log.
Once the ExamCase is verified, a later investigation can retrieve it from the graph just like it can retrieve one of the earlier closed cases.
So the agent's memory isn't a chat transcript sitting somewhere outside the investigation. It's another piece of structured graph state.
What we learned
The biggest lesson for us was that putting a model in the middle of everything doesn't necessarily make the system more agentic.
In fact, we found the opposite.
The more important question was where the model should stop.
The graph should run first because it owns the facts.
The initial query set should stay fixed because we don't want the model deciding which evidence exists.
Jev should have a small budget because we want its decisions to be inspectable.
The policy engine should be deterministic because actions such as blocking or reporting shouldn't depend on how a language model happens to phrase its reasoning.
And the language model should come after the decision so that its job is to explain the case rather than invent it.
We also learned that GraphRAG doesn't have to mean throwing a massive graph into a prompt.
A much more useful pattern for this kind of system is:
structured graph evidence + bounded graph traversal + one relevant policy passage.
That gives the model enough context to explain the investigation without turning the model into the system of record.
What we'd improve with more time
There are a few things we'd change.
The current probability model uses the bank risk_score as the prior and fixed log-odds weights for the evidence. With more time, we'd fit that mapping on earlier closed cases, freeze the resulting configuration, and keep the scored alerts outside the fit.
We'd also run a proper community detection pass offline and store a community ID on the cards. The current card_community query is a bounded walk through shared devices. It works for finding connected cards, but a stored community would give us a more explicit representation of larger unnamed clusters.
We'd also make Jev mandatory for the final scored run whenever its pattern gate is unlocked, so every such case carries the model's probability map rather than occasionally recording a fallback.
And finally, we'd let the graph continue growing beyond the benchmark. The twenty scored cases would stay fixed, while future investigations could create additional ExamCase vertices that later investigations could retrieve.
Final thought
The interesting part of this project wasn't really "we put an LLM on top of TigerGraph."
It was deciding how much control the LLM should actually have.
TigerGraph tells us what is connected.
The Bayesian layer tells us how the evidence changes the probability.
The deterministic detectors identify known patterns.
Jev decides when another look is worth taking.
The rule engine decides what actions are allowed.
The language model explains what happened.
And a human still signs the actions that matter.
The result is not an agent that pretends to know everything.
It's an agent that can investigate, explain its reasoning, take a controlled next step, and — probably most importantly — stop and say "I'm not sure" when the evidence doesn't justify anything stronger.

Top comments (0)