How to Build a transaction monitoring Agent Using LlamaIndex in TypeScript for banking
A transaction monitoring agent watches payment events, account behavior, and customer context to flag suspicious activity before it turns into fraud, AML exposure, or regulatory pain. In banking, the value is not just detection — it is consistent triage, explainable decisions, and an audit trail that compliance teams can defend.
Architecture
Build this agent as a pipeline, not a single prompt.
- •
Transaction ingestion layer
- •Pulls events from Kafka, SQS, webhook handlers, or batch files.
- •Normalizes fields like
accountId,amount,merchant,country,timestamp, andchannel.
- •
Risk feature builder
- •Computes deterministic signals before the LLM sees anything.
- •Examples: velocity checks, threshold breaches, geo anomalies, new payee usage, structuring patterns.
- •
LlamaIndex retrieval layer
- •Uses
VectorStoreIndexto retrieve relevant policy docs, typology notes, prior cases, and internal controls. - •Keeps the agent grounded in bank-specific rules instead of generic reasoning.
- •Uses
- •
Decision agent
- •Uses
OpenAIAgentor a chat engine over tools to classify the transaction into review buckets. - •Produces a structured output:
clear,review, orescalate.
- •Uses
- •
Case management sink
- •Writes the decision, rationale, retrieved evidence, and feature snapshot to your case system.
- •This is what makes the output auditable.
- •
Controls and observability
- •Logs prompts, retrieved chunks, model version, latency, and final disposition.
- •Needed for compliance review, model governance, and incident response.
Implementation
1) Install dependencies and define your types
Use LlamaIndex TS with a real OpenAI-backed setup. Keep the model input small by precomputing risk signals outside the LLM.
npm install llamaindex zod
import {
Document,
OpenAI,
OpenAIAgent,
QueryEngineTool,
VectorStoreIndex,
} from "llamaindex";
import { z } from "zod";
const TransactionSchema = z.object({
transactionId: z.string(),
accountId: z.string(),
amount: z.number(),
currency: z.string(),
merchant: z.string(),
country: z.string(),
timestamp: z.string(),
});
type Transaction = z.infer<typeof TransactionSchema>;
type RiskFeatures = {
velocity24h: number;
isNewMerchant: boolean;
highRiskCountry: boolean;
amountZScore: number;
};
2) Build your policy corpus index
This is where LlamaIndex earns its keep. Put your AML policy excerpts, fraud playbooks, SAR guidance summaries, and internal escalation rules into documents and index them.
const policyDocs = [
new Document({
text:
"Escalate transactions above $10,000 when combined with unusual velocity or new beneficiary activity.",
metadata: { source: "AML-Policy", section: "Thresholds" },
}),
new Document({
text:
"Transactions involving sanctioned jurisdictions must be blocked and escalated immediately.",
metadata: { source: "Sanctions-Policy", section: "Jurisdictions" },
}),
];
const policyIndex = await VectorStoreIndex.fromDocuments(policyDocs);
const policyQueryEngine = policyIndex.asQueryEngine();
const policyTool = QueryEngineTool.fromDefaults({
queryEngine: policyQueryEngine,
name: "policy_lookup",
});
3) Create the monitoring agent with structured decisioning
Use an agent that can inspect policy guidance before deciding. The pattern below keeps the output consistent enough for downstream case systems.
const llm = new OpenAI({
model: "gpt-4o-mini",
});
const agent = new OpenAIAgent({
tools: [policyTool],
llm,
});
Now wire in deterministic features plus retrieval. The LLM should explain a decision using both the transaction facts and retrieved policy text.
function buildRiskFeatures(txn: Transaction): RiskFeatures {
return {
velocity24h: txn.amount > 5000 ? 4 : 1,
isNewMerchant: txn.merchant.toLowerCase().includes("cash"),
highRiskCountry: ["IR", "KP", "SY"].includes(txn.country),
amountZScore: txn.amount > 10000 ? 3.2 : txn.amount > 5000 ? 2.1 : 0.4,
};
}
async function monitorTransaction(txnInput: unknown) {
const txn = TransactionSchema.parse(txnInput);
const features = buildRiskFeatures(txn);
const prompt = `
You are a banking transaction monitoring analyst.
Classify this transaction as clear, review, or escalate.
Return concise reasoning and cite policy if relevant.
Transaction:
${JSON.stringify(txn)}
Risk features:
${JSON.stringify(features)}
Rules:
- Prefer escalate for sanctioned countries or severe anomaly combinations.
- Prefer review when patterns are suspicious but not definitive.
- Keep rationale suitable for audit logs.
`;
const response = await agent.chat({
message: prompt,
});
return {
transactionId: txn.transactionId,
decisionText: response.response,
features,
};
}
4) Use it in a service boundary
In production you do not call this directly from random app code. Wrap it behind an API handler or stream consumer so every event gets normalized and logged consistently.
async function handleIncomingTransaction(rawEvent: unknown) {
const result = await monitorTransaction(rawEvent);
console.log(JSON.stringify({
eventType: "transaction_monitoring_result",
...result,
model: "gpt-4o-mini",
timestamp: new Date().toISOString(),
}));
return result;
}
Production Considerations
- •
Deploy close to data residency boundaries
- •If your bank requires EU-only processing or regional isolation, keep both embeddings and inference inside that region.
- •Do not ship raw customer data across borders just because the model endpoint is convenient.
- •
Log everything needed for audit
Wait no need? Actually need continue correctly without markdown issue? Let's continue cleanly.
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