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

By Cyprian AaronsUpdated 2026-04-21
transaction-monitoringcrewaitypescriptbanking

A transaction monitoring agent watches payment activity, scores suspicious patterns, and escalates cases that need human review. In banking, that matters because you need faster detection of fraud and AML signals without turning every alert into an analyst bottleneck.

Architecture

  • Transaction ingestion layer

    • Pulls card, ACH, wire, and internal transfer events from a queue or stream.
    • Normalizes raw payloads into a stable schema the agent can reason over.
  • Risk scoring toolset

    • Exposes deterministic functions for velocity checks, sanctions screening lookups, country risk flags, and customer profile context.
    • Keeps the LLM from inventing facts it should retrieve from systems of record.
  • CrewAI agent layer

    • Uses a focused agent to classify the transaction, explain the reasoning, and decide whether to escalate.
    • Uses tasks with explicit outputs so analysts get structured case notes.
  • Case management integration

    • Writes alerts into your case system with evidence, score, and recommendation.
    • Preserves an audit trail for model output, tool calls, and final disposition.
  • Compliance and policy guardrails

    • Enforces PII minimization, retention rules, and jurisdiction-specific data residency constraints.
    • Blocks unsupported recommendations like account closure or SAR filing without human approval.

Implementation

1) Install CrewAI for TypeScript and define your data model

Start with a narrow transaction shape. Banking agents fail when they accept loose JSON from upstream systems and then try to reason over inconsistent fields.

npm install @crew-ai/crewai zod dotenv
// src/types.ts
export type Transaction = {
  transactionId: string;
  customerId: string;
  amount: number;
  currency: string;
  country: string;
  channel: "card" | "ach" | "wire" | "internal";
  timestamp: string;
  merchantCategory?: string;
};

export type RiskResult = {
  score: number;
  decision: "clear" | "review";
  reasons: string[];
};

2) Create deterministic tools for banking signals

Do not ask the model to “figure out” velocity or sanctions by itself. Put those checks behind tools so the output is reproducible and auditable.

// src/tools.ts
import { z } from "zod";

export const velocityCheckInput = z.object({
  customerId: z.string(),
});

export async function velocityCheck({ customerId }: { customerId: string }) {
  // Replace with real query to feature store / risk engine
  const txnsLastHour = customerId === "cust-123" ? 14 : 2;

  return {
    customerId,
    txnsLastHour,
    thresholdBreached: txnsLastHour > 10,
  };
}

export async function sanctionsScreening({ country }: { country: string }) {
  const highRiskCountries = ["IR", "KP", "SY"];
  return {
    country,
    flagged: highRiskCountries.includes(country),
    source: "internal-policy-list",
  };
}

3) Build the CrewAI agent and task

Use one agent for triage and one task for structured output. The key pattern is to make the model produce a decision plus reasons, not free-form prose.

// src/monitoring-agent.ts
import "dotenv/config";
import { Agent, Task } from "@crew-ai/crewai";
import { velocityCheck, sanctionsScreening } from "./tools";
import type { Transaction, RiskResult } from "./types";

const transactionMonitor = new Agent({
  name: "Transaction Monitor",
  role: "AML and fraud triage analyst",
  goal:
    "Assess transactions for suspicious activity using bank policy, tool outputs, and transaction context.",
});

const reviewTask = new Task({
  description:
    "Review the transaction and return JSON with score, decision, and reasons. Escalate if velocity or sanctions checks are flagged.",
});

export async function assessTransaction(txn: Transaction): Promise<RiskResult> {
  const velocity = await velocityCheck({ customerId: txn.customerId });
  const sanctions = await sanctionsScreening({ country: txn.country });

  const prompt = `
Transaction:
${JSON.stringify(txn)}

Velocity check:
${JSON.stringify(velocity)}

Sanctions screening:
${JSON.stringify(sanctions)}

Return only JSON:
{
  "score": number,
  "decision": "clear" | "review",
  "reasons": string[]
}
`;

const result = await transactionMonitor.execute({
    task: reviewTask,
    input: prompt,
    responseFormat: "json",
});

return result as RiskResult;
}

4) Wire it into an API handler or worker

In production you usually want this running in a worker consuming Kafka/SQS messages. The same pattern works in an HTTP endpoint if your bank’s architecture is request-driven.

// src/index.ts
import { assessTransaction } from "./monitoring-agent";
import type { Transaction } from "./types";

async function main() {
  const txn: Transaction = {
    transactionId: "txn-9001",
    customerId: "cust-123",
    amount: 12500,
    currency: "USD",
    country: "US",
    channel: "wire",
    timestamp: new Date().toISOString(),
    merchantCategory: "money_services",
  };

console.log(await assessTransaction(txn));
}

main().catch((err) => {
console.error("transaction monitoring failed", err);
process.exit(1);
});

Production Considerations

  • Deployment

    • Run the agent in a private VPC with outbound access only to approved model endpoints.
    • Keep transaction data in-region if your policy requires data residency by jurisdiction.
  • Monitoring

    • Log every tool call, input hash, output hash, score, and final decision.
    • Track false positives by segment so you can tune thresholds without retraining blind.
  • Guardrails

    • Redact PANs, account numbers, and free-text PII before sending context to the model.
    • Force JSON-only outputs and reject any response that does not validate against your schema.
    • Require human approval for SAR/STR recommendations; the agent should only recommend escalation.
  • Compliance

    • Store prompts and outputs according to retention policy.
    • Make sure analysts can reconstruct why a case was opened months later during audit or regulator review.

Common Pitfalls

  • Letting the LLM do deterministic banking logic

    • Mistake: asking the model to infer velocity breaches or sanctions hits from raw text.
    • Fix: keep those checks in tools or policy engines and pass results into the agent.
  • Sending too much sensitive data

    • Mistake: dumping full account history or unmasked identifiers into prompts.
    • Fix: minimize fields to what is needed for triage; tokenize or redact where possible.
  • Producing unstructured alerts

    • Mistake: returning long explanations that analysts cannot ingest into case management.
    • Fix: require strict JSON output with score, decision, and reasons, then map that into your alert schema.

If you build it this way, CrewAI becomes the orchestration layer around your controls instead of replacing them. That is the right shape for banking automation.


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