How to Build a transaction monitoring Agent Using AutoGen in TypeScript for banking

By Cyprian AaronsUpdated 2026-04-21
transaction-monitoringautogentypescriptbanking

A transaction monitoring agent watches payment events, scores them against policy and behavioral rules, and escalates suspicious cases for review. In banking, that matters because you need to catch fraud, AML signals, and policy breaches early without flooding analysts with noise or breaking audit requirements.

Architecture

  • Event intake layer

    • Consumes transactions from Kafka, SQS, a webhook, or a batch replay job.
    • Normalizes fields like accountId, counterparty, amount, currency, country, and channel.
  • Policy/rule engine

    • Applies deterministic checks before any LLM call.
    • Handles hard stops like sanctioned geographies, velocity limits, structuring patterns, and threshold breaches.
  • AutoGen agent

    • Uses AssistantAgent to interpret the case context and produce a structured risk assessment.
    • Should never be the first line of defense; it augments rules with narrative reasoning.
  • Case store / audit log

    • Persists every input, decision, model output, and analyst override.
    • Needed for compliance reviews, model governance, and regulator questions.
  • Escalation workflow

    • Creates alerts in your case management system when risk exceeds a threshold.
    • Routes high-risk items to human analysts with the evidence attached.

Implementation

1) Install AutoGen and define the transaction schema

Use the TypeScript AutoGen package and keep your transaction payload explicit. Banking systems break when you let untyped JSON drift across services.

npm install @autogenai/autogen zod
import { z } from "zod";

export const TransactionSchema = z.object({
  transactionId: z.string(),
  accountId: z.string(),
  amount: z.number().positive(),
  currency: z.string().length(3),
  country: z.string().length(2),
  counterpartyName: z.string(),
  channel: z.enum(["card", "ach", "wire", "cash", "mobile"]),
  timestamp: z.string(),
});

export type Transaction = z.infer<typeof TransactionSchema>;

2) Build deterministic pre-checks before the agent runs

This is where you enforce banking controls. If a transaction is clearly blocked by policy, do not ask the model to “reason” about it.

type RuleResult = {
  riskScore: number;
  flags: string[];
};

export function applyRules(tx: Transaction): RuleResult {
  const flags: string[] = [];
  let riskScore = 0;

  if (tx.amount >= 10000) {
    flags.push("HIGH_VALUE");
    riskScore += 30;
  }

  if (["IR", "KP", "SY"].includes(tx.country)) {
    flags.push("SANCTIONED_COUNTRY");
    riskScore += 100;
  }

  if (tx.channel === "wire" && tx.amount > 50000) {
    flags.push("LARGE_WIRE");
    riskScore += 20;
  }

  return { riskScore, flags };
}

3) Create an AutoGen assistant that produces structured case notes

Use AssistantAgent for analysis and keep the prompt narrow. You want a decision support agent, not a chatty generalist.

import { AssistantAgent } from "@autogenai/autogen";
import { TransactionSchema } from "./transaction-schema";
import { applyRules } from "./rules";

const assistant = new AssistantAgent({
  name: "transaction_monitor",
});

async function analyzeTransaction(rawTx: unknown) {
  const tx = TransactionSchema.parse(rawTx);
  const ruleResult = applyRules(tx);

  const prompt = `
You are a banking transaction monitoring analyst.

Transaction:
${JSON.stringify(tx, null, 2)}

Rule results:
${JSON.stringify(ruleResult, null, 2)}

Task:
1. Assess whether this should be escalated.
2. Return strict JSON with keys:
   - decision: "escalate" | "monitor" | "clear"
   - riskLevel: "low" | "medium" | "high"
   - rationale: string[]
   - recommendedAction: string
3. Do not mention policy text outside the JSON.
`;

  const result = await assistant.run(prompt);

  return result;
}

The key pattern here is that the LLM sees both transaction context and deterministic flags. That gives you better recall on suspicious patterns while keeping hard controls outside the model.

4) Add a human-in-the-loop escalation path

Banking workflows need reviewable decisions. The agent should create an alert only after combining rule output with model output.

type MonitoringDecision = {
  decision: "escalate" | "monitor" | "clear";
};

async function processTransaction(rawTx: unknown) {
  const analysis = await analyzeTransaction(rawTx);

const parsedAnalysis = JSON.parse(analysis.content as string) as {
    decision: MonitoringDecision["decision"];
    riskLevel: "low" | "medium" | "high";
    rationale: string[];
    recommendedAction: string;
};

if (parsedAnalysis.decision === "escalate") {
    await saveCase({
      rawTx,
      analysis: parsedAnalysis,
      status: "open",
      assignedQueue: parsedAnalysis.riskLevel === "high" ? "aml-high-priority" : "fraud-review",
    });
}
}

In production, replace saveCase() with your case management integration and persist the full prompt/output pair for auditability.

Production Considerations

  • Data residency

    • Keep PII and transaction data inside approved regions.
    • If your bank requires on-prem or VPC-only processing, run AutoGen against an internal model endpoint and block external egress.
  • Auditability

    • Log every rule hit, prompt version, model response, analyst override, and final disposition.
    • Regulators care about why a case was escalated or cleared, not just the score.
  • Guardrails

    • Use deterministic rules for sanctions screening, threshold checks, and mandatory reporting triggers.
  • Monitoring

  • Track false positives by segment: channel, geography, customer tier, product type.

  • Alert on prompt drift, latency spikes, malformed JSON outputs, and missing fields in upstream events.

  • Add rate limits so one bad feed does not trigger thousands of unnecessary agent calls.

Common Pitfalls

  1. Letting the LLM make final compliance decisions

    • Avoid this by keeping sanctions blocks and mandatory escalation rules outside the model.
    • The agent should recommend; policy should decide on hard failures.
  2. Sending raw PII into prompts without controls

    • Mask account numbers, national IDs, and free-text memo fields before calling AutoGen.
    • Only pass what the task needs for analysis.
  3. Skipping structured outputs

    • Free-form text is hard to parse in case management pipelines.
    • Force strict JSON responses and reject anything that does not validate against your schema.
  4. No replay strategy for investigations

    • You need to rerun historical transactions against old prompt versions during audits or tuning.
    • Store immutable inputs plus prompt/model version metadata so you can reproduce decisions later.

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