How to Build a transaction monitoring Agent Using LlamaIndex in TypeScript for payments
A transaction monitoring agent for payments watches payment events, enriches them with policy and case context, and flags activity that needs review. In practice, it helps you catch suspicious patterns early, reduce false positives, and produce an audit trail that compliance teams can defend.
Architecture
A production setup needs a few concrete pieces:
- •Transaction event source
- •Kafka, Kinesis, or a webhook consumer that receives card, ACH, RTP, or wallet events.
- •Policy and controls corpus
- •AML rules, sanctions handling notes, internal escalation playbooks, and prior investigator decisions.
- •LlamaIndex query layer
- •
VectorStoreIndexfor semantic retrieval over policies and cases. - •
ContextChatEngineorQueryEnginefor investigator-facing questions.
- •
- •Risk scoring and decisioning
- •A deterministic risk engine that combines rules with LLM output.
- •Never let the model be the only decision-maker for blocking payments.
- •Audit log store
- •Immutable storage for prompts, retrieved context IDs, model outputs, scores, and human actions.
- •Case management integration
- •Push alerts into your case system with evidence snippets and reason codes.
Implementation
1) Install the packages and define your document model
You want LlamaIndex to retrieve from policy docs and past cases. Keep the source material clean: one document per policy section or case note chunk.
npm install llamaindex zod
import { Document } from "llamaindex";
type PaymentEvent = {
id: string;
amount: number;
currency: string;
country: string;
merchantCategory: string;
customerId: string;
timestamp: string;
};
export function paymentEventToDocument(event: PaymentEvent): Document {
return new Document({
id_: event.id,
text: [
`Payment ID: ${event.id}`,
`Amount: ${event.amount} ${event.currency}`,
`Country: ${event.country}`,
`Merchant category: ${event.merchantCategory}`,
`Customer ID: ${event.customerId}`,
`Timestamp: ${event.timestamp}`,
].join("\n"),
metadata: {
type: "payment_event",
country: event.country,
currency: event.currency,
merchantCategory: event.merchantCategory,
},
});
}
2) Build a retrieval index over policies and historical cases
Use VectorStoreIndex.fromDocuments() to index compliance docs. This gives you semantic lookup when the agent needs to explain why a payment is risky.
import {
Settings,
VectorStoreIndex,
Document,
} from "llamaindex";
import { OpenAI } from "llamaindex";
Settings.llm = new OpenAI({
model: "gpt-4o-mini",
});
const policyDocs = [
new Document({
text:
"Escalate any payment above $10,000 when combined with high-risk geographies or unusual velocity.",
metadata: { source: "aml_policy_v3", section: "thresholds" },
}),
new Document({
text:
"Do not auto-decline solely on model output. All holds require a human review path and reason code.",
metadata: { source: "payments_controls", section: "decisioning" },
}),
];
const index = await VectorStoreIndex.fromDocuments(policyDocs);
const queryEngine = index.asQueryEngine();
const result = await queryEngine.query({
query:
"What policy applies to a $25,000 transfer to a high-risk geography?",
});
console.log(String(result));
3) Add a transaction monitoring function with deterministic guardrails
The pattern here is simple: compute rule-based signals first, then ask LlamaIndex for supporting context. Use the model for explanation and triage support, not final enforcement.
import { QueryEngineTool } from "llamaindex";
type MonitoringResult = {
riskScore: number;
action: "allow" | "review" | "hold";
reasons: string[];
};
function ruleBasedScore(eventAmountUSD: number, countryRiskHigh: boolean): number {
let score = 0;
if (eventAmountUSD > 10000) score += 40;
if (countryRiskHigh) score += 35;
if (eventAmountUSD > 50000) score += 20;
return Math.min(score, 100);
}
export async function monitorPayment(
eventText: string,
): Promise<MonitoringResult> {
const response = await queryEngine.query({
query:
`Assess whether this payment should be reviewed. Payment details:\n${eventText}\n` +
`Return only a short explanation of relevant policy.`,
});
const baseScore = ruleBasedScore(25000, true);
const reasons = [String(response)];
const action =
baseScore >= 70 ? "hold" : baseScore >=55 ? "review" : "allow";
return {
riskScore: baseScore,
action,
reasons,
};
}
4) Expose the agent behind an API endpoint and persist audit data
For payments, every decision needs traceability. Store the input payload hash, retrieved policy IDs, output text, score inputs, and reviewer overrides.
import express from "express";
import crypto from "crypto";
const app = express();
app.use(express.json());
app.post("/monitor", async (req, res) => {
const eventText = JSON.stringify(req.body);
const hash = crypto.createHash("sha256").update(eventText).digest("hex");
const decision = await monitorPayment(eventText);
await saveAuditRecord({
requestHash: hash,
payloadType: "payment_event",
riskScore: decision.riskScore,
actionTakenBySystem: decision.action,
reasonsJson: JSON.stringify(decision.reasons),
timestampUtc: new Date().toISOString(),
});
res.json({
requestHash: hash,
...decision,
});
});
async function saveAuditRecord(record: Record<string, unknown>) {
console.log("AUDIT", record);
}
app.listen(3000);
Production Considerations
- •Keep the model out of the final control plane
- •Use it to summarize evidence and suggest review paths.
- •Log everything needed for audit
- •Input hashes, retrieved document IDs, prompt version, model version, score thresholds, reviewer actions.
- •Enforce data residency
- •Keep payment PII and case notes in-region if your regulatory footprint requires it.
- •Add hard guardrails
- •Sanitize PANs/account numbers before retrieval.
- •Block free-form generation from changing hold/release decisions without rule-engine approval.
Common Pitfalls
- •Letting the LLM decide block vs allow
How to avoid it:
- •
Make rules deterministic.
- •
Let LlamaIndex explain why something matched policy.
- •
Indexing raw PII and sensitive payment data
How to avoid it:
- •
Redact PANs, bank account numbers, names where possible.
- •
Index normalized summaries instead of raw payloads.
- •
Skipping auditability
How to avoid it:
- •
Store prompt versions and retrieval sources.
- •
Persist every manual override with reviewer identity and timestamp.
- •
Treating all regions the same
How to avoid it:
- •Separate policies by jurisdiction.
- •Apply residency-aware storage and region-specific compliance rules before retrieval.
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