How to Build a transaction monitoring Agent Using AutoGen in TypeScript for fintech
A transaction monitoring agent watches payment events, scores them for risk, and escalates suspicious cases for human review. In fintech, that matters because you need to catch fraud, AML patterns, and policy breaches without blocking legitimate customers or creating an audit mess.
Architecture
- •
Event ingestion layer
- •Pulls transactions from Kafka, SQS, a webhook, or a database change stream.
- •Normalizes each event into a consistent schema before the agent sees it.
- •
Policy and rules service
- •Encodes hard controls like sanctions hits, velocity limits, country restrictions, and PEP flags.
- •Runs before the LLM so you do not waste tokens on obvious rejects.
- •
AutoGen agent runtime
- •Uses
AssistantAgentto analyze the transaction context. - •Uses
UserProxyAgentas the orchestrator that triggers tools and controls execution.
- •Uses
- •
Tooling layer
- •Exposes deterministic functions like
fetchCustomerProfile,getRecentTransactions,screenSanctions, andcreateCase. - •Keeps the model grounded in system data instead of hallucinating risk reasons.
- •Exposes deterministic functions like
- •
Case management sink
- •Writes alerts to your SIEM, case management system, or queue for analysts.
- •Stores model output, tool results, and decision metadata for auditability.
- •
Audit and observability
- •Logs prompts, tool calls, scores, final decisions, and model version.
- •Needed for compliance review, model governance, and dispute handling.
Implementation
- •Install AutoGen and define your domain types
Use the TypeScript package for AutoGen and keep your transaction schema explicit. Fintech agents fail when they accept loose JSON with missing fields.
npm install @autogen/agentchat openai zod
import { AssistantAgent, UserProxyAgent } from "@autogen/agentchat";
import { z } from "zod";
const TransactionSchema = z.object({
transactionId: z.string(),
customerId: z.string(),
amount: z.number().positive(),
currency: z.string().length(3),
country: z.string(),
merchantCategory: z.string(),
timestamp: z.string(),
});
type Transaction = z.infer<typeof TransactionSchema>;
- •Create deterministic tools for customer context and policy checks
The model should not invent customer history or sanctions status. Wrap your internal services as tools and let the agent call them explicitly.
async function fetchCustomerProfile(customerId: string) {
return {
customerId,
kycStatus: "verified",
riskTier: "medium",
pepFlag: false,
residencyCountry: "GB",
};
}
async function getRecentTransactions(customerId: string) {
return [
{ amount: 1200, currency: "USD", country: "GB", timestamp: "2026-04-21T09:10:00Z" },
{ amount: 980, currency: "USD", country: "GB", timestamp: "2026-04-21T09:14:00Z" },
];
}
async function screenSanctions(customerId: string) {
return { hit: false, sourceList: "OFAC", screenedAt: new Date().toISOString() };
}
- •Wire up an AutoGen assistant with a strict decision format
Use AssistantAgent for analysis and UserProxyAgent to run the workflow. The prompt should force structured output so downstream systems can consume it without parsing free text.
const assistant = new AssistantAgent({
name: "transaction_monitor",
systemMessage:
[
"You are a transaction monitoring analyst.",
"Assess fraud/AML risk using only provided data and tool outputs.",
"Return valid JSON with fields:",
'decision ("approve" | "review" | "reject"),',
'riskScore (0-100),',
'reasons (string[]),',
'requiredActions (string[])',
"Do not mention unsupported facts.",
].join(" "),
});
const orchestrator = new UserProxyAgent({
name: "monitoring_orchestrator",
});
- •Run the analysis flow and persist the decision
This pattern keeps hard controls outside the LLM while still letting the agent explain borderline cases. In production you would replace the inline tool calls with actual service clients.
async function monitorTransaction(txnInput: unknown) {
const txn = TransactionSchema.parse(txnInput);
const [profile, recentTxns, sanctions] = await Promise.all([
fetchCustomerProfile(txn.customerId),
getRecentTransactions(txn.customerId),
screenSanctions(txn.customerId),
]);
const context = {
transaction: txn,
customerProfile: profile,
recentTransactions: recentTxns,
sanctionsCheck: sanctions,
rules: {
highRiskCountries: ["IR", "KP", "SY"],
cashLikeMccs: ["4829", "6011"],
reviewThresholdUsd: 5000,
},
};
const prompt = `
Analyze this transaction for fraud/AML risk.
Context:
${JSON.stringify(context)}
Return JSON only.
`;
const result = await orchestrator.initiateChat(assistant, prompt);
return result;
}
If you want an actual production gate before the LLM runs, add a deterministic precheck:
function hardReject(txn: Transaction) {
const blockedCountries = ["IR", "KP", "SY"];
if (blockedCountries.includes(txn.country)) {
return { decision: "reject", reason: "blocked_country" };
}
return null;
}
Production Considerations
- •
Keep residency boundaries explicit
- •Route EU customer data to EU-hosted inference endpoints.
- •Do not send raw PII across regions unless your legal basis and transfer mechanism are already approved.
- •
Log everything needed for audit
- •Store input hashes, tool outputs, model version, prompt version, final decision, analyst override, and timestamps.
- •Regulators will ask why a case was flagged; you need a traceable path from event to outcome.
- •
Add guardrails before and after the model
- •Pre-filter obvious sanctions hits and policy violations with deterministic rules.
- •Post-filter model output to enforce allowed values only; reject malformed JSON or unsupported recommendations.
- •
Monitor drift and false positives
- •Track alert rate by segment, merchant category, corridor, and risk tier.
- •A spike in reviews usually means rule drift, bad feature quality, or prompt regression.
Common Pitfalls
- •
Letting the LLM make binary compliance decisions alone
- •Bad pattern: asking the model to decide sanctions or AML clearance end-to-end.
- •Fix it by keeping hard compliance checks in code and using AutoGen only for triage and explanation.
- •
Sending raw customer data without minimization
- •Bad pattern: dumping full profiles, addresses, card PANs, and notes into the prompt.
- •Fix it by passing only fields required for the decision and masking sensitive identifiers early.
- •
Skipping structured outputs
- •Bad pattern: consuming free-form prose from the agent in your case pipeline.
- •Fix it by forcing JSON output with fixed keys like
decision,riskScore,reasons, and validating it before storage or escalation.
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