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

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

A fraud detection agent for investment banking watches transaction streams, client activity, and case history, then decides whether to flag, enrich, escalate, or auto-close an alert. It matters because banks need fast triage without losing control: every decision must be explainable, auditable, and aligned with compliance and data residency rules.

Architecture

  • Input normalizer
    • Converts raw payment events, SWIFT messages, trade instructions, and account metadata into a consistent state shape.
  • Risk scoring node
    • Applies deterministic checks first: velocity spikes, counterparty risk, unusual geography, sanction hits, and threshold breaches.
  • Evidence enrichment node
    • Pulls supporting context from internal systems: KYC profile, prior alerts, client segment, relationship manager notes, and case history.
  • Decision router
    • Uses LangGraph conditional edges to route the case to block, review, escalate, or close.
  • Audit logger
    • Persists every intermediate state transition for model governance and regulatory review.
  • Human review handoff
    • Packages the alert into a format that investigators can act on without rehydrating the entire graph state.

Implementation

1) Define the graph state and risk signals

For investment banking, keep the state explicit. Do not hide compliance-critical fields inside opaque objects; investigators and auditors need to see what influenced the decision.

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

type FraudSeverity = "low" | "medium" | "high" | "critical";
type FraudAction = "close" | "review" | "escalate" | "block";

const FraudState = Annotation.Root({
  transactionId: Annotation<string>(),
  clientId: Annotation<string>(),
  amount: Annotation<number>(),
  currency: Annotation<string>(),
  jurisdiction: Annotation<string>(),
  counterpartyCountry: Annotation<string>(),
  sanctionsHit: Annotation<boolean>(),
  velocityScore: Annotation<number>(),
  kycRiskRating: Annotation<"low" | "medium" | "high">(),
  evidence: Annotation<string[]>(),
  severity: Annotation<FraudSeverity>(),
  action: Annotation<FraudAction>(),
});

2) Add deterministic enrichment and scoring nodes

Use pure functions where possible. That makes the graph easier to test and easier to defend in an audit.

const enrichEvidence = async (state: typeof FraudState.State) => {
  const evidence = [...(state.evidence ?? [])];

  if (state.sanctionsHit) evidence.push("Sanctions screening hit");
  if (state.velocityScore > 80) evidence.push("High transaction velocity");
  if (state.amount > 500000) evidence.push("Large-value transfer");
  if (state.counterpartyCountry !== state.jurisdiction) {
    evidence.push("Cross-border movement");
  }

  return { evidence };
};

const scoreSeverity = async (state: typeof FraudState.State) => {
  let severity: FraudSeverity = "low";

  if (state.sanctionsHit) severity = "critical";
  else if (state.velocityScore > 90 || state.kycRiskRating === "high") severity = "high";
  else if (state.velocityScore > 70 || state.amount > 250000) severity = "medium";

  return { severity };
};

const decideAction = async (state: typeof FraudState.State) => {
  let action: FraudAction = "close";

  if (state.severity === "critical") action = "block";
  else if (state.severity === "high") action = "escalate";
  else if (state.severity === "medium") action = "review";

  return { action };
};

3) Build the LangGraph workflow with conditional routing

This is the core pattern. The graph computes risk first, then routes based on the resulting severity. That keeps policy logic separate from detection logic.

import { StateGraph } from "@langchain/langgraph";

function routeByAction(state: typeof FraudState.State): string {
  switch (state.action) {
    case "block":
      return "block_case";
    case "escalate":
      return "escalate_case";
    case "review":
      return "send_to_analyst";
    default:
      return END;
    }
}

const blockCase = async (state: typeof FraudState.State) => {
  // Integrate with your payment controls / case management system here.
  console.log(`Blocking transaction ${state.transactionId}`);
};

const escalateCase = async (state: typeof FraudState.State) => {
  console.log(`Escalating transaction ${state.transactionId} to compliance`);
};

const sendToAnalyst = async (state: typeof FraudState.State) => {
   console.log(`Sending transaction ${state.transactionId} for manual review`);
};

const graph = new StateGraph(FraudState)
   .addNode("enrich_evidence", enrichEvidence)
   .addNode("score_severity", scoreSeverity)
   .addNode("decide_action", decideAction)
   .addNode("block_case", blockCase)
   .addNode("escalate_case", escalateCase)
   .addNode("send_to_analyst", sendToAnalyst)
   .addEdge(START, "enrich_evidence")
   .addEdge("enrich_evidence", "score_severity")
   .addEdge("score_severity", "decide_action")
   .addConditionalEdges("decide_action", routeByAction)
   .compile();

4) Run it with a real transaction payload

Keep execution stateless at the edge. Persist results in your own system of record so you can meet retention and audit requirements.

async function main() {
  const result = await graph.invoke({
    transactionId: "TXN-88421",
    clientId: "C-10291",
    amount: 750000,
    currency: "USD",
    jurisdiction: "GB",
    counterpartyCountry: "AE",
    sanctionsHit: false,
    velocityScore: 92,
    kycRiskRating: "high",
    evidence: [],
    severity: "low",
    action: "close",
  });

console.log(result);
}

main().catch(console.error);

Production Considerations

  • Auditability
    • Store every input field, node output, and final decision with a correlation ID. In investment banking, you need reconstructable decisions for internal audit and regulators.
  • Data residency
    • Keep customer data inside approved regions. If your graph calls external services or LLMs for enrichment summaries, ensure those calls do not move regulated data across borders.
  • Human-in-the-loop controls
    • Never auto-block high-value transactions without policy approval. Use LangGraph routing to force critical cases into compliance review queues.
  • Monitoring
    • Track false positives by desk, region, product type, and client segment. A fraud agent that over-flags prime brokerage flows will get turned off by operations fast.

Common Pitfalls

  • Using an LLM as the first decision-maker

    Start with deterministic rules for sanctions hits, thresholds, and velocity anomalies. Use language models only for summarization or investigator assistance.

  • Not versioning policy logic

    If a threshold changes from 250000 to 100000, treat that as a versioned policy release. Otherwise you cannot explain why two identical cases produced different outcomes.

  • Ignoring downstream operational constraints

    A perfect fraud score is useless if it cannot integrate with case management, payment holds, or analyst workflows. Design the graph around actual controls already used by the bank.


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