How to Build a fraud detection Agent Using LangGraph in Python for banking

By Cyprian AaronsUpdated 2026-04-21
fraud-detectionlanggraphpythonbanking

A fraud detection agent in banking watches transaction streams, scores risk, decides when to escalate, and produces an audit trail for every decision. It matters because false negatives cost money and trust, while false positives create customer friction, ops load, and regulatory noise.

Architecture

  • Transaction intake node
    Receives a payment event, card authorization, ACH transfer, or account action with normalized fields like amount, merchant, device fingerprint, geolocation, and customer history.

  • Risk enrichment node
    Pulls internal signals such as velocity checks, account age, prior chargebacks, KYC tier, sanctions flags, and recent login anomalies.

  • Decision node
    Applies deterministic policy thresholds first, then optionally uses an LLM only for explanation or case routing. For banking, the actual block/allow/escalate decision should be explicit and auditable.

  • Case creation node
    Opens a fraud case when the score crosses a threshold. This is where you attach evidence for analysts and compliance review.

  • Audit and logging node
    Writes the full state transition: input features, score breakdown, policy version, model version, and final action. This is non-negotiable in regulated environments.

  • Human-in-the-loop checkpoint
    Routes ambiguous cases to an analyst queue instead of auto-declining high-value customers or suspicious-but-unconfirmed events.

Implementation

1) Define the graph state

Use a typed state object so each step has a clear contract. Keep the state small and deterministic; don’t pass raw payloads around if you can normalize early.

from typing import TypedDict, Literal
from langgraph.graph import StateGraph, START, END

class FraudState(TypedDict):
    transaction_id: str
    amount: float
    merchant: str
    country: str
    velocity_1h: int
    kyc_risk: int
    chargeback_rate: float
    risk_score: float
    decision: Literal["allow", "review", "block"]
    audit_log: list[str]

2) Build deterministic fraud nodes

This pattern works well in banking because it is explainable. The score is a weighted function of business signals; the decision is based on thresholds you can version-control and audit.

def enrich_risk(state: FraudState) -> FraudState:
    score = 0.0

    if state["amount"] > 5000:
        score += 25
    if state["velocity_1h"] > 5:
        score += 20
    if state["country"] not in {"US", "GB", "DE", "CA"}:
        score += 15
    if state["kyc_risk"] >= 7:
        score += 20
    if state["chargeback_rate"] > 0.03:
        score += 20

    return {
        **state,
        "risk_score": min(score, 100.0),
        "audit_log": state.get("audit_log", []) + [f"enriched risk_score={min(score, 100.0)}"]
    }

def decide_action(state: FraudState) -> FraudState:
    score = state["risk_score"]

    if score >= 70:
        decision = "block"
    elif score >= 40:
        decision = "review"
    else:
        decision = "allow"

    return {
        **state,
        "decision": decision,
        "audit_log": state["audit_log"] + [f"decision={decision}"]
    }

3) Add conditional routing with LangGraph

LangGraph’s StateGraph lets you branch based on computed risk. Use add_conditional_edges() so the flow stays readable and easy to test.

def route_by_decision(state: FraudState):
    return state["decision"]

def create_case(state: FraudState) -> FraudState:
    return {
        **state,
        "audit_log": state["audit_log"] + [f"case_created for {state['transaction_id']}"]
    }

def finalize(state: FraudState) -> FraudState:
    return {
        **state,
        "audit_log": state["audit_log"] + ["finalized"]
    }

graph = StateGraph(FraudState)
graph.add_node("enrich_risk", enrich_risk)
graph.add_node("decide_action", decide_action)
graph.add_node("create_case", create_case)
graph.add_node("finalize", finalize)

graph.add_edge(START, "enrich_risk")
graph.add_edge("enrich_risk", "decide_action")
graph.add_conditional_edges(
    "decide_action",
    route_by_decision,
    {
        "allow": "finalize",
        "review": "create_case",
        "block": "create_case",
    },
)
graph.add_edge("create_case", END)
graph.add_edge("finalize", END)

app = graph.compile()

4) Run the agent on a transaction event

This is the part you wire into your payment service or stream processor. The output should be persisted with the same trace ID used by your transaction ledger.

initial_state: FraudState = {
    "transaction_id": "txn_12345",
    "amount": 8200.0,
    "merchant": "electronics_store_91",
    "country": "NG",
    "velocity_1h": 8,
    "kyc_risk": 6,
    "chargeback_rate": 0.05,
    "risk_score": 0.0,
    "decision": "allow",
    "audit_log": [],
}

result = app.invoke(initial_state)

print(result["decision"])
print(result["risk_score"])
print(result["audit_log"])

Production Considerations

  • Keep PII inside your boundary
    Don’t send raw customer data to external LLM APIs unless your legal team has approved it and residency requirements are satisfied. In many banks, the agent should run inside VPC/private cloud with encrypted storage and strict egress controls.

  • Version every policy and threshold
    Store model version, rules version, and graph version with each decision. When compliance asks why a transfer was blocked, you need to reproduce the exact path that produced the result.

  • Add human review for borderline scores
    Auto-blocking everything above a threshold will burn good customers. Route medium-risk cases to analysts and keep analyst decisions as training data for future tuning.

  • Instrument for drift and false positives
    Track approval rate by segment, fraud capture rate, analyst overturn rate, latency per node, and queue depth. A fraud agent that is accurate but slow will still hurt authorization rates.

Common Pitfalls

  • Using an LLM as the primary decision engine
    Don’t let a prompt decide whether to block a card transaction. Use deterministic rules or trained classifiers for the actual action; reserve LLMs for summarization or analyst notes.

  • Skipping auditability
    If your graph doesn’t record inputs, thresholds applied, branch taken, and final output, it won’t survive bank review. Always persist structured audit logs alongside the event ID.

  • Ignoring residency and access control
    A fraud workflow often touches highly sensitive customer data. Keep execution local to approved regions, encrypt at rest/in transit, and restrict who can inspect traces or replay states.


Keep learning

By Cyprian Aarons, AI Consultant at Topiax.

Want the complete 8-step roadmap?

Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.

Get the Starter Kit

Related Guides