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

By Cyprian AaronsUpdated 2026-04-21
transaction-monitoringcrewaitypescriptretail-banking

A transaction monitoring agent watches payment activity, scores it against fraud and AML rules, and routes suspicious cases for review. In retail banking, that matters because you need to catch mule accounts, structuring, card fraud, and unusual cash movement without flooding analysts with false positives.

Architecture

  • Transaction ingestion layer

    • Pulls events from core banking, card rails, ACH/SEPA feeds, or a Kafka topic.
    • Normalizes fields like customerId, amount, merchantCategory, channel, country, and timestamp.
  • Risk enrichment service

    • Adds customer profile data: KYC tier, account age, historical velocity, expected spend bands.
    • Pulls sanctions/PEP flags and device or IP metadata when available.
  • CrewAI agent

    • Evaluates each transaction batch using a structured prompt and tool calls.
    • Produces a risk score, reason codes, and a disposition like clear, review, or escalate.
  • Rules engine

    • Handles deterministic checks: threshold breaches, rapid transfers, geographic mismatch, round-dollar patterns.
    • Keeps compliance logic auditable and easy to tune outside the model.
  • Case management sink

    • Writes suspicious activity to your case platform or SIEM.
    • Stores the full decision trace for audit and model governance.
  • Audit and telemetry

    • Logs prompts, tool outputs, model version, policy version, and final decision.
    • Required for internal audit, regulator review, and post-incident analysis.

Implementation

  1. Install dependencies and define the transaction shape

Use the TypeScript CrewAI package plus a schema validator so your agent only sees structured data.

npm install @crewaiinc/crewai zod dotenv
import "dotenv/config";
import { z } from "zod";
import { Agent, Task, Crew } from "@crewaiinc/crewai";

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

type Transaction = z.infer<typeof TransactionSchema>;
  1. Create tools for enrichment and policy checks

CrewAI agents work best when you keep hard rules outside the model. Expose only the minimum banking context needed for a decision.

const getCustomerProfile = async (customerId: string) => {
  return {
    kycTier: "standard",
    accountAgeDays: 420,
    avgMonthlySpend: 1800,
    pepFlag: false,
    sanctionsHit: false,
    homeCountry: "GB",
  };
};

const getVelocityStats = async (customerId: string) => {
  return {
    txCount24h: 14,
    totalVolume24h: 9200,
    highRiskCounterpartyCount7d: 1,
  };
};

const applyRules = (tx: Transaction, profile: any, velocity: any) => {
  const reasons: string[] = [];
  let score = 0;

  if (tx.amount > profile.avgMonthlySpend * 3) {
    score += 30;
    reasons.push("Amount exceeds normal monthly spend band");
  }
  if (velocity.txCount24h > 10) {
    score += 25;
    reasons.push("High transaction velocity in last 24h");
  }
  if (profile.sanctionsHit || profile.pepFlag) {
    score += 50;
    reasons.push("Compliance flag present");
  }
};
  1. Build the CrewAI agent and task

This pattern uses one analyst agent with explicit instructions to produce a bank-friendly output. Keep the response format strict so downstream systems can parse it.

const transactionMonitoringAgent = new Agent({
role="Transaction Monitoring Analyst",
goal="Assess retail banking transactions for fraud and AML risk using provided facts only.",
backstory="You are a bank analyst who writes concise decisions with reason codes.",
verbose: true,
});

const monitorTask = new Task({
description:
`Review the transaction JSON plus enrichment data.
Return JSON with fields:
riskLevel ("low" | "medium" | "high"),
score (0-100),
decision ("clear" | "review" | "escalate"),
reasons (string[]),
auditNotes (string).`,
agent: transactionMonitoringAgent,
expectedOutput:
"Strict JSON object suitable for case management and audit storage.",
});
  1. Run the crew per transaction batch

In production you usually process micro-batches rather than single events so you can include velocity context. The key is to pass only sanitized data into the agent.

async function assessTransaction(txInput: unknown) {
const tx = TransactionSchema.parse(txInput);
const [profile, velocity] = await Promise.all([
getCustomerProfile(tx.customerId),
getVelocityStats(tx.customerId),
]);

const ruleResult = applyRules(tx as Transaction, profile, velocity);

const crew = new Crew({
agents: [transactionMonitoringAgent],
tasks: [monitorTask],
verbose: true,
});

const result = await crew.kickoff({
transaction: tx,
profile,
velocity,
ruleResult,
});

return result;
}

Production Considerations

  • Keep PII out of prompts where possible

    • Mask names, account numbers, and full addresses before sending data to the model.
    • Use internal IDs plus derived features like spend bands and velocity metrics.
  • Enforce data residency

    • Retail banking often requires regional processing boundaries.
    • Pin model execution to approved regions and keep prompt logs in the same jurisdiction as the customer record.
  • Store full audit traces

    • Persist input snapshot, rule outputs, agent output, model version, prompt template version, and reviewer override.
  • Add human-in-the-loop thresholds

score <40 -> auto-clear
40-69 -> analyst review
70+ -> immediate escalation
  • Tune thresholds by product line; card payments need different sensitivity than wire transfers or cash deposits.

Common Pitfalls

  1. Letting the LLM make deterministic compliance decisions

    • Don’t ask the agent to “decide if this violates AML policy” without rules support.
    • Put sanctions hits, threshold breaches, and jurisdiction checks in code first.
  2. Sending raw customer records into the prompt

    • That creates privacy exposure and noisy context.
    • Send only normalized features needed for assessment.
  3. Skipping explanation fields

    • A score without reason codes is useless for analysts and auditors.
    • Always return structured reasons tied to observable facts like velocity spikes or country mismatch.

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