How to Build a transaction monitoring Agent Using LangGraph in TypeScript for fintech
A transaction monitoring agent watches payment events, scores them against policy and risk signals, and decides whether to auto-clear, enrich, escalate, or file for review. For fintech, this matters because you need fast decisions without losing auditability, compliance traceability, or control over false positives.
Architecture
- •
Event intake layer
- •Receives card payments, ACH transfers, wallet top-ups, and internal ledger movements.
- •Normalizes raw payloads into a common
TransactionEventshape.
- •
Feature enrichment node
- •Pulls customer profile data, merchant category codes, device fingerprints, velocity counters, and sanctions/PEP flags.
- •Keeps enrichment deterministic and logged for audit.
- •
Risk scoring node
- •Applies rules plus model outputs to produce a risk score and reason codes.
- •Outputs structured decisions like
APPROVE,REVIEW,BLOCK, orESCALATE.
- •
Policy/controls node
- •Enforces compliance rules: KYC status, jurisdiction constraints, threshold limits, and suspicious activity triggers.
- •Separates model judgment from hard controls.
- •
Case management node
- •Creates an alert or case record when the transaction needs human review.
- •Stores evidence for investigators and regulators.
- •
Audit trail store
- •Persists every node input/output, decision path, and timestamps.
- •Required for explainability and regulatory defensibility.
Implementation
1. Define the state and decision contract
Use a typed state object so each node only reads and writes what it owns. In fintech systems, the state should carry enough context for audit without leaking unnecessary PII.
import { Annotation } from "@langchain/langgraph";
export type TransactionEvent = {
transactionId: string;
customerId: string;
amount: number;
currency: string;
country: string;
merchantCategory: string;
};
export type RiskDecision = "APPROVE" | "REVIEW" | "BLOCK" | "ESCALATE";
export const MonitoringState = Annotation.Root({
event: Annotation<TransactionEvent>(),
enrichment: Annotation<{
kycStatus: "PASS" | "FAIL";
velocity24h: number;
sanctionsHit: boolean;
pepHit: boolean;
}>(),
score: Annotation<number>(),
decision: Annotation<RiskDecision>(),
reasons: Annotation<string[]>(),
});
2. Build the graph with explicit nodes
LangGraph in TypeScript gives you a clean way to model this as a graph of deterministic steps. Keep your nodes small and side-effect aware; anything that writes to case management or audit storage should be isolated.
import { StateGraph, START, END } from "@langchain/langgraph";
const enrichTransaction = async (state: typeof MonitoringState.State) => {
const { event } = state;
// Replace with real lookups to KYC/AML services
return {
enrichment: {
kycStatus: event.customerId.startsWith("cust_") ? "PASS" : "FAIL",
velocity24h: event.amount > 5000 ? 7 : 1,
sanctionsHit: event.country === "IR",
pepHit: false,
},
reasons: [],
};
};
const scoreRisk = async (state: typeof MonitoringState.State) => {
const { event, enrichment } = state;
let score = 0;
const reasons: string[] = [];
if (event.amount >= 10000) {
score += 40;
reasons.push("high_amount");
}
if (enrichment.sanctionsHit) {
score += 100;
reasons.push("sanctions_match");
}
if (enrichment.velocity24h >= 5) {
score += 25;
reasons.push("high_velocity");
}
if (enrichment.kycStatus === "FAIL") {
score += 50;
reasons.push("kyc_failed");
}
return { score, reasons };
};
const decide = async (state: typeof MonitoringState.State) => {
const { score } = state;
if (score >= 100) return { decision: "BLOCK" as const };
if (score >=80) return { decision:"ESCALATE" as const };
if (score >=40) return { decision:"REVIEW" as const };
return { decision:"APPROVE" as const };
};
const graph = new StateGraph(MonitoringState)
.addNode("enrichTransaction", enrichTransaction)
.addNode("scoreRisk", scoreRisk)
.addNode("decide", decide)
.addEdge(START,"enrichTransaction")
.addEdge("enrichTransaction","scoreRisk")
.addEdge("scoreRisk","decide")
.addEdge("decide",END)
.compile();
3. Run the agent and persist the outcome
The compiled graph returns the final state after all nodes run. In production, wrap this in a service that stores the full trace in your audit log and emits an alert only when needed.
const result = await graph.invoke({
event: {
transactionId: "txn_123",
customerId: "cust_456",
amount:10_250,
currency:"USD",
country:"US",
merchantCategory:"6012",
},
});
console.log({
transactionId: result.event.transactionId,
score: result.score,
decision:key=>result.decision,
reasons:user=>result.reasons,
});
What this pattern gives you
- •A deterministic flow from intake to decision.
- •A typed contract that is easy to test.
- •A place to insert human review before any irreversible action.
- •An audit-friendly chain of evidence for regulators.
Production Considerations
- •
Keep data residency boundaries explicit
- •If your customers are in the EU or a specific banking region, run enrichment and storage in-region.
- •Don’t send raw PII or transaction payloads to external model endpoints unless your legal/compliance team has approved it.
- •
Log every decision path
- •Store input features, rule hits, scores, final decisions, and timestamps.
- •Investigators need to answer “why was this blocked?” without reconstructing it from application logs.
- •
Separate hard controls from soft scoring
- •Sanctions hits, blocked geographies, and KYC failures should override model scores.
- •
Add guardrails around human escalation
Common Pitfalls
- •Using an LLM as the primary risk engine
Keep learning
- •The complete AI Agents Roadmap — my full 8-step breakdown
- •Free: The AI Agent Starter Kit — PDF checklist + starter code
- •Work with me — I build AI for banks and insurance companies
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