How to Build a transaction monitoring Agent Using LangGraph in TypeScript for payments
A transaction monitoring agent watches payment events, scores them for risk, and decides whether to approve, flag, or escalate them for review. For payments teams, this matters because you need fast decisions without losing control over fraud, AML, auditability, and regulatory obligations.
Architecture
- •
Event intake
- •Receives card, ACH, wallet, or transfer events from your payment rail or message bus.
- •Normalizes raw payloads into a single internal transaction shape.
- •
Risk enrichment
- •Pulls customer profile data, merchant metadata, device signals, geo/IP context, and historical behavior.
- •Keeps enrichment deterministic so the same event produces the same inputs for review.
- •
Policy engine
- •Applies hard rules for sanctions hits, velocity thresholds, amount limits, and jurisdiction-specific controls.
- •Produces explicit reasons for every decision.
- •
LangGraph orchestration
- •Routes the transaction through scoring, rule checks, escalation, and final decision nodes.
- •Keeps the workflow stateful and auditable.
- •
Case management output
- •Writes flagged transactions to a queue or case system for analyst review.
- •Stores the full decision trace for compliance and dispute handling.
Implementation
1) Define the transaction state and dependencies
Use a typed state object so every node in the graph knows exactly what it can read and write. For payments work, keep raw inputs separate from derived risk fields so audit logs stay clean.
import { Annotation } from "@langchain/langgraph";
export type Transaction = {
id: string;
amount: number;
currency: string;
customerId: string;
merchantId: string;
country: string;
};
export type RiskDecision = "approve" | "review" | "decline";
const TransactionState = Annotation.Root({
tx: Annotation<Transaction>(),
score: Annotation<number>(),
reasons: Annotation<string[]>(),
decision: Annotation<RiskDecision>(),
enriched: Annotation<Record<string, unknown>>(),
});
2) Build nodes for enrichment, scoring, and policy checks
This pattern keeps each step small and testable. In production payments systems, that matters because fraud logic changes often and you do not want to redeploy a monolith every time a threshold changes.
import { StateGraph, START, END } from "@langchain/langgraph";
async function enrichTx(state: typeof TransactionState.State) {
const tx = state.tx;
// Replace with real calls to customer profile / device / merchant services.
const enriched = {
customerTenureDays: 420,
priorChargebacks: 1,
merchantRiskBand: "medium",
highRiskCountry: tx.country === "NG" ? true : false,
};
return { enriched };
}
async function scoreTx(state: typeof TransactionState.State) {
const e = state.enriched;
let score = 10;
if ((e.customerTenureDays as number) < 30) score += 25;
if ((e.priorChargebacks as number) > 0) score += 20;
if (e.merchantRiskBand === "high") score += 20;
if (e.highRiskCountry) score += 30;
return {
score,
reasons: [
...(state.reasons ?? []),
`score=${score}`,
`chargebacks=${e.priorChargebacks}`,
`merchantRiskBand=${e.merchantRiskBand}`,
],
};
}
async function policyCheck(state: typeof TransactionState.State) {
const reasons = [...(state.reasons ?? [])];
const { tx } = state;
if (tx.amount >= 10000) reasons.push("amount_above_manual_review_threshold");
if (tx.currency !== "USD") reasons.push("non_usd_transaction");
const decision =
state.score >= 60 ? "decline" :
state.score >= 30 ? "review" :
"approve";
return { decision, reasons };
}
3) Add routing for escalations and build the graph
LangGraph gives you explicit control over branching. That is useful in payments because some paths must be deterministic under regulation while others can trigger analyst review.
function routeDecision(state: typeof TransactionState.State) {
return state.decision === "review" ? "caseReview" : "finish";
}
async function caseReview(state: typeof TransactionState.State) {
// Send to case management / queue here.
console.log("Flagged transaction for review:", state.tx.id);
return {};
}
const graph = new StateGraph(TransactionState)
.addNode("enrich", enrichTx)
.addNode("score", scoreTx)
.addNode("policy", policyCheck)
.addNode("caseReview", caseReview)
.addEdge(START, "enrich")
.addEdge("enrich", "score")
.addEdge("score", "policy")
.addConditionalEdges("policy", routeDecision, {
caseReview: "caseReview",
finish: END,
})
.addEdge("caseReview", END);
const app = graph.compile();
4) Run the agent on a payment event
Keep the input payload minimal and normalized. If you need PCI-sensitive fields like PAN or CVV in upstream systems, tokenize them before they reach this workflow.
const result = await app.invoke({
tx: {
id: "tx_123",
amount: 12500,
currency: "USD",
customerId: "cus_456",
merchantId: "m_789",
country: "NG",
},
});
console.log(result.decision); // approve | review | decline
console.log(result.reasons);
Production Considerations
- •
Keep decisions explainable
- •
Store every node output with timestamps so compliance teams can reconstruct why a payment was approved or blocked.
- •
Persist reasons in plain language; auditors do not want opaque model outputs without rule references.
- •
Respect data residency
- •
Keep transaction data inside the region required by your processor or regulator.
- •
If you use external LLM calls for summaries or analyst notes, strip PII first and avoid exporting regulated data across borders.
- •
Add hard guardrails before any model call
- •
Sanctions screening, velocity limits, amount caps, and jurisdiction rules should run before any probabilistic step.
- •
In payments, an LLM should assist with explanation or triage; it should not be the only control deciding release of funds.
- •
Instrument everything
- •
Emit metrics for approval rate, manual review rate, false positives, latency per node, and queue backlog.
- •
Alert on drift in risk scores by merchant category or corridor because fraud patterns move fast.
Common Pitfalls
- •
Letting the model make the final decision
- •Do not use an LLM as the sole approver/decliner for regulated transactions.
- •Put deterministic policies first and use LangGraph to orchestrate them.
- •
Mixing raw payment data with derived signals
- •Keep PANs, account numbers, and other sensitive fields out of downstream nodes unless absolutely required.
- •Use tokenization and store only masked values in graph state where possible.
- •
Ignoring audit traceability
- •If you cannot explain why a transaction was flagged six months later, your design is incomplete.
- •Log node inputs/outputs with immutable storage keyed by transaction ID and version your policy logic.
- •
Deploying without regional controls
- •Payments workloads often have residency constraints tied to country or processor contracts.
- •Pin workloads to approved regions and make external enrichment services region-aware.
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