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

By Cyprian AaronsUpdated 2026-04-21
fraud-detectionlanggraphtypescriptretail-banking

A fraud detection agent in retail banking watches incoming transactions, scores risk, decides whether to approve, step up authentication, or escalate to a human analyst, and writes an audit trail for every decision. It matters because fraud loss is only half the problem; false positives create customer friction, compliance exposure, and operational load.

Architecture

Build this agent as a small graph, not a monolith. For retail banking, the minimum useful components are:

  • Transaction intake node

    • Normalizes card or transfer events into a typed state object.
    • Pulls in customer profile, device metadata, merchant data, and recent transaction history.
  • Risk feature enrichment node

    • Computes features like velocity checks, geo-distance anomalies, first-time payee flags, and amount deviation.
    • Keeps deterministic logic outside the model where possible.
  • Policy/risk scoring node

    • Produces a fraud score and reason codes.
    • Can combine rules with an LLM-generated explanation for analysts, but not for final approval decisions.
  • Decision router

    • Routes to approve, step_up, or manual_review.
    • In banking, this must be deterministic and explainable.
  • Audit and case creation node

    • Persists the full state snapshot, model outputs, decision path, and timestamps.
    • Required for compliance review and dispute handling.
  • Human escalation path

    • Sends high-risk or ambiguous cases to an analyst queue.
    • Prevents the agent from making irreversible decisions on low-confidence signals.

Implementation

1) Define the state and graph shape

Use a typed state so every node has a clear contract. Keep raw inputs separate from derived fraud signals so you can audit what changed at each step.

import { StateGraph, Annotation, START, END } from "@langchain/langgraph";

type Transaction = {
  transactionId: string;
  customerId: string;
  amount: number;
  currency: string;
  merchantCategory: string;
  country: string;
  timestamp: string;
};

type FraudState = {
  transaction: Transaction;
  customerRiskTier?: "low" | "medium" | "high";
  velocityCount?: number;
  geoDistanceKm?: number;
  fraudScore?: number;
  reasonCodes?: string[];
  decision?: "approve" | "step_up" | "manual_review";
};

const FraudAnnotation = Annotation.Root({
  transaction: Annotation<Transaction>(),
  customerRiskTier: Annotation<FraudState["customerRiskTier"]>(),
  velocityCount: Annotation<number>(),
  geoDistanceKm: Annotation<number>(),
  fraudScore: Annotation<number>(),
  reasonCodes: Annotation<string[]>(),
  decision: Annotation<FraudState["decision"]>(),
});

type GraphState = typeof FraudAnnotation.State;

2) Add deterministic enrichment nodes

Do your feature engineering in code. That keeps the graph auditable and avoids asking a model to infer basic banking rules.

const enrichCustomerRisk = async (state: GraphState) => {
  const tier =
    state.transaction.amount > 5000 ? "high" :
    state.transaction.amount > 1000 ? "medium" : "low";

  return { customerRiskTier: tier };
};

const enrichVelocity = async (state: GraphState) => {
  // Replace with real query to your feature store / ledger.
  const countLastHour = state.transaction.amount > 2000 ? 7 : 1;
  return { velocityCount: countLastHour };
};

const enrichGeo = async (state: GraphState) => {
    // Replace with IP/device vs last-known-location calculation.
    const km = state.transaction.country === "NG" ? 1200 : 8;
    return { geoDistanceKm: km };
};

3) Score risk and route by threshold

Use a scoring node that returns both score and reason codes. Then route with addConditionalEdges, which is the right pattern when decisions must stay explicit.

const scoreFraud = async (state: GraphState) => {
    let score = 0;
    const reasons: string[] = [];

    if ((state.velocityCount ?? 0) >= 5) {
      score += 40;
      reasons.push("high_velocity");
    }

    if ((state.geoDistanceKm ?? 0) >   ? ) {}

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