How to Build a transaction monitoring Agent Using LangGraph in TypeScript for wealth management

By Cyprian AaronsUpdated 2026-04-21
transaction-monitoringlanggraphtypescriptwealth-management

A transaction monitoring agent for wealth management watches client activity, scores risk, and routes suspicious cases to the right next action. It matters because wealth firms need to catch AML patterns, sanctions exposure, unusual transfers, and account behavior changes without drowning analysts in false positives.

Architecture

  • Transaction ingestion layer

    • Pulls events from custodial systems, core banking APIs, or a message bus.
    • Normalizes fields like client ID, counterparty, amount, currency, jurisdiction, and channel.
  • Risk enrichment layer

    • Adds client profile context: KYC status, PEP/sanctions flags, expected activity bands, AUM tier, and geography.
    • This is where wealth-specific logic starts to matter.
  • LangGraph decision graph

    • Orchestrates deterministic checks and LLM-assisted analysis.
    • Routes between low-risk pass-through, enhanced review, or escalation.
  • Case output layer

    • Writes alerts into a case management system with evidence, reason codes, and analyst-ready summaries.
    • Must preserve audit trails.
  • Policy and guardrail layer

    • Enforces compliance rules before any model-generated output is used.
    • Keeps data residency and retention constraints in scope.

Implementation

1) Define the state shape and nodes

Use LangGraph’s StateGraph with a typed state object. Keep the state explicit so every decision is auditable later.

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

type Txn = {
  id: string;
  clientId: string;
  amount: number;
  currency: string;
  country: string;
  counterpartyCountry?: string;
};

type ClientProfile = {
  kycStatus: "verified" | "pending" | "failed";
  pep: boolean;
  sanctionsHit: boolean;
  expectedMonthlyVolume: number;
  aumBand: "low" | "mid" | "high";
};

type MonitoringState = {
  txn: Txn;
  profile?: ClientProfile;
  riskScore?: number;
  decision?: "clear" | "review" | "escalate";
  reasons?: string[];
};

const enrichProfile = async (state: MonitoringState): Promise<Partial<MonitoringState>> => {
  // Replace with CRM/KYC lookup
  return {
    profile: {
      kycStatus: "verified",
      pep: false,
      sanctionsHit: false,
      expectedMonthlyVolume: 250000,
      aumBand: "high",
    },
  };
};

2) Add deterministic risk scoring first

For wealth management, don’t start with an LLM. Use rule-based scoring for obvious triggers like sanctions hits, high-risk jurisdictions, or large deviations from expected activity.

const scoreTransaction = async (state: MonitoringState): Promise<Partial<MonitoringState>> => {
  const reasons: string[] = [];
  let score = 0;

  if (!state.profile) throw new Error("Missing profile");

  if (state.profile.sanctionsHit) {
    score += 100;
    reasons.push("Sanctions screening hit");
  }

  if (state.profile.pep) {
    score += 15;
    reasons.push("PEP flag present");
  }

  if (state.txn.amount > state.profile.expectedMonthlyVolume * 0.5) {
    score += Math.min(30, Math.floor(state.txn.amount / state.profile.expectedMonthlyVolume * 10));
    reasons.push("Amount exceeds expected activity band");
    }
  
   if (["IR", "KP", "RU"].includes(state.txn.country)) {
    score += 40;
    reasons.push("High-risk origin country");
   }

   return { riskScore: score, reasons };
};

3) Route based on thresholds and generate analyst-ready output

Use addConditionalEdges to branch into clear/review/escalate paths. In production this keeps low-risk traffic cheap and makes high-risk cases explicit.

const decideRoute = (state: MonitoringState) => {
  if ((state.riskScore ?? 0) >= 80) return "escalate";
   if ((state.riskScore ??0) >=30) return "review";
   return "clear";
};

const generateSummary = async (state: MonitoringState): Promise<Partial<MonitoringState>> => {
   const summary =
     `Txn ${state.txn.id} for client ${state.txn.clientId} scored ${state.riskScore}. ` +
     `Reasons: ${(state.reasons ?? []).join("; ")}`;

   return { decision: state.decision ?? "review", reasons:[...(state.reasons ?? []), summary] };
};

const graph = new StateGraph<MonitoringState>()
 .addNode("enrichProfile", enrichProfile)
 .addNode("scoreTransaction", scoreTransaction)
 .addNode("generateSummary", generateSummary)
 .addEdge(START,"enrichProfile")
 .addEdge("enrichProfile","scoreTransaction")
 .addConditionalEdges("scoreTransaction", decideRoute,{
   clear: END,
   review:"generateSummary",
   escalate:"generateSummary",
 })
 .addEdge("generateSummary", END)
 .compile();

4) Execute the graph with an audit-friendly input

Keep the invocation boundary clean. Persist input/output pairs with timestamps so compliance can reconstruct every alert decision.

const result = await graph.invoke({
   txn:{
     id:"txn_123",
     clientId:"cli_456",
     amount:175000,
     currency:"USD",
     country:"AE",
     counterpartyCountry:"GB",
   },
});

console.log(result.decision);
console.log(result.riskScore);
console.log(result.reasons);

Production Considerations

  • Data residency

  • Keep customer data in-region. If your wealth platform operates across EU, UK, and APAC books, deploy separate graph runtimes or region-bound workers.

  • Auditability

  • Store the full state transition log for each transaction. Analysts need to see which node produced which reason code.

  • Guardrails

  • Never let an LLM make the final compliance decision alone. Use it for summarization or triage after deterministic checks have run.

  • Operational monitoring

  • Track false-positive rate by segment:

  • private banking

  • HNW

  • UHNW

  • trust structures

  • A good model in retail banking can fail badly once you add offshore entities and complex beneficial ownership.

Common Pitfalls

  1. Putting the LLM before rules

    • If you ask the model to classify raw transactions first, you’ll get inconsistent results and weak defensibility.
    • Run policy checks and hard thresholds before any generative step.
  2. Ignoring client context

    • A $200k transfer means something different for a mass affluent client than for an UHNW family office.
    • Always enrich against expected activity bands, AUM tier, entity type, and jurisdictional exposure.
  3. Not persisting decision traces

    • Wealth management reviews are audit-heavy.
    • Log input state, node outputs, thresholds used, and final route so compliance can replay the exact decision path later.

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