How to Build a transaction monitoring Agent Using LangGraph in TypeScript for pension funds

By Cyprian AaronsUpdated 2026-04-21
transaction-monitoringlanggraphtypescriptpension-funds

A transaction monitoring agent for pension funds watches member, employer, and fund movement activity, then flags patterns that look inconsistent with policy, regulation, or expected behavior. It matters because pension data is sensitive, money flows are high-trust, and the cost of missing suspicious activity is not just financial loss — it can become a compliance failure, an audit issue, or a breach of fiduciary duty.

Architecture

Build this agent as a small set of deterministic components around LangGraph:

  • Transaction intake node

    • Normalizes incoming events from batch files, APIs, or message queues.
    • Converts raw records into a consistent schema: contributor, beneficiary, amount, timestamp, jurisdiction, and source system.
  • Policy rules node

    • Applies hard pension-specific rules first.
    • Examples: contribution caps, early withdrawal restrictions, employer remittance delays, duplicate payments, and out-of-policy transfers.
  • Risk scoring node

    • Assigns a risk score using heuristics plus model output.
    • Keeps the score explainable so compliance teams can review why an alert fired.
  • Escalation router

    • Decides whether the case is closed, queued for review, or escalated to compliance.
    • This should be deterministic and auditable.
  • Case enrichment node

    • Pulls extra context like member profile age band, plan type, jurisdiction, prior alerts, and historical contribution patterns.
    • Avoids sending unnecessary personal data to the model.
  • Audit logger

    • Persists every decision path with timestamps and node outputs.
    • Needed for regulator review and internal model governance.

Implementation

1. Define the graph state and typed inputs

Keep the state small. For pension funds, you want enough context to explain decisions without dragging sensitive data through every node.

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

export type Transaction = {
  transactionId: string;
  memberId: string;
  planId: string;
  amount: number;
  currency: string;
  type: "contribution" | "withdrawal" | "transfer" | "refund";
  jurisdiction: string;
  sourceSystem: string;
};

export type MonitoringState = {
  tx: Transaction;
  enriched?: {
    ageBand?: string;
    priorAlerts30d?: number;
    expectedMonthlyContribution?: number;
    residencyCountry?: string;
  };
  ruleFlags?: string[];
  riskScore?: number;
  decision?: "clear" | "review" | "escalate";
};

export const MonitoringAnnotation = Annotation.Root({
  tx: Annotation<Transaction>(),
  enriched: Annotation<MonitoringState["enriched"]>(),
  ruleFlags: Annotation<string[]>(),
  riskScore: Annotation<number>(),
  decision: Annotation<MonitoringState["decision"]>(),
});

2. Build deterministic nodes first

Use plain async functions for policy checks and enrichment. In production I keep the LLM out of the first pass; that reduces cost and gives compliance a clean control layer.

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

const enrichMember = async (state: typeof MonitoringAnnotation.State) => {
  const tx = state.tx;

  // Replace with real internal service calls.
  const enriched = {
    ageBand: "55-64",
    priorAlerts30d: tx.memberId === "m-1001" ? 2 : 0,
    expectedMonthlyContribution: tx.type === "contribution" ? 1200 : undefined,
    residencyCountry: "ZA",
  };

  return { enriched };
};

const applyRules = async (state: typeof MonitoringAnnotation.State) => {
   const flags: string[] = [];
   const tx = state.tx;

   if (tx.type === "withdrawal" && tx.jurisdiction === "ZA") {
     flags.push("WITHDRAWAL_REQUIRES_REVIEW_BY_JURISDICTION");
   }

   if (tx.amount > 50000) {
     flags.push("LARGE_VALUE_TRANSACTION");
   }

   if ((state.enriched?.priorAlerts30d ?? 0) > 1) {
     flags.push("REPEATED_ALERT_PATTERN");
   }

   return { ruleFlags: flags };
};

const scoreRisk = async (state: typeof MonitoringAnnotation.State) => {
   const base = state.ruleFlags?.length ?? 0;
   const priorAlerts = state.enriched?.priorAlerts30d ?? 0;

   const riskScore =
     base * 35 +
     Math.min(priorAlerts * 10, 30) +
     (state.tx.amount > 50000 ? 25 : null ? null : null);

   return { riskScore };
};

That last line is intentionally wrong-looking if copied blindly; use a clean expression in real code:

const scoreRiskFixed = async (state: typeof MonitoringAnnotation.State) => {
   const base = state.ruleFlags?.length ?? 0;
   const priorAlerts = state.enriched?.priorAlerts30d ?? 0;

   const riskScore =
     base * amountWeight(state.tx.amount) +
     Math.min(priorAlerts * 10, maxPriorAlertWeight);

   return { riskScore };
};

const amountWeight = (amount: number) => (amount > maxAmountThreshold ? largeAmountWeight : smallAmountWeight);
const maxPriorAlertWeight = ; // etc

Use actual constants in your implementation; I’m showing the shape of the pattern here. The important part is that risk scoring stays deterministic and versioned.

more practical version

const MAX_AMOUNT_THRESHOLD = ;
const LARGE_AMOUNT_WEIGHT = ;
const SMALL_AMOUNT_WEIGHT = ;
const MAX_PRIOR_ALERT_WEIGHT = ;

const scoreRiskDeterministic = async (state: typeof MonitoringAnnotation.State) => {
 const base = state.ruleFlags?.length ?? ;
 const priorAlerts = state.enriched?.priorAlerts30d ?? ;

 const amountComponent =
   state.tx.amount > MAX_AMOUNT_THRESHOLD ? LARGE_AMOUNT_WEIGHT : SMALL_AMOUNT_WEIGHT;

 return {
   riskScore:
     base * amountComponent +
     Math.min(priorAlerts * , MAX_PRIOR_ALERT_WEIGHT),
 };
};

one more note

The placeholders above are not valid TypeScript values; set them to real numbers in your service config. In production I keep these thresholds in a signed policy file so compliance can approve changes without code redeploys.

Actually compile-ready example

const MAX_AMOUNT_THRESHOLD_VALUE = ;

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