How to Build a transaction monitoring Agent Using LlamaIndex in TypeScript for payments

By Cyprian AaronsUpdated 2026-04-21
transaction-monitoringllamaindextypescriptpayments

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
    • VectorStoreIndex for semantic retrieval over policies and cases.
    • ContextChatEngine or QueryEngine for 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

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