How to Build a transaction monitoring Agent Using CrewAI in TypeScript for investment banking
A transaction monitoring agent watches trades, payments, and internal transfers for patterns that could indicate market abuse, sanctions exposure, fraud, or AML risk. In investment banking, that matters because the difference between catching a suspicious sequence early and missing it is measured in regulatory exposure, client impact, and audit findings.
Architecture
- •
Transaction ingestion layer
- •Pulls trade events, wire transfers, booking entries, and reference data from Kafka, S3, a database, or an API.
- •Normalizes everything into one schema before analysis.
- •
Risk scoring toolset
- •Computes deterministic signals like threshold breaches, velocity checks, counterparty concentration, and jurisdiction risk.
- •Keeps the model grounded in explainable features.
- •
CrewAI agent
- •Uses
Agentto reason over the transaction context. - •Produces a structured alert decision: clear, review, or escalate.
- •Uses
- •
Task orchestration
- •Uses
Taskobjects to separate triage, enrichment, and escalation. - •Makes the workflow auditable and easier to test.
- •Uses
- •
Compliance evidence store
- •Persists inputs, outputs, prompts, tool calls, and timestamps.
- •Required for model risk management and regulator review.
- •
Case management integration
- •Pushes suspicious activity into an internal queue like ServiceNow, Actimize-style case systems, or a custom AML platform.
Implementation
1) Install the runtime and define your event shape
For TypeScript projects, keep the agent boundary small. The agent should not talk directly to raw feeds; it should receive normalized transactions with enough context to make a decision.
npm install @crew-ai/crewai zod
// src/types.ts
import { z } from "zod";
export const TransactionSchema = z.object({
transactionId: z.string(),
accountId: z.string(),
counterpartyId: z.string(),
amount: z.number().positive(),
currency: z.string().length(3),
country: z.string().length(2),
channel: z.enum(["wire", "trade", "internal_transfer", "cash"]),
timestamp: z.string().datetime(),
notes: z.string().optional(),
});
export type Transaction = z.infer<typeof TransactionSchema>;
2) Create deterministic tools for compliance-friendly signals
Do not ask the model to invent risk signals. Give it tools that calculate facts you can defend in front of compliance and audit.
// src/tools.ts
import { Transaction } from "./types";
export function calculateRiskScore(tx: Transaction): {
score: number;
reasons: string[];
} {
const reasons: string[] = [];
let score = 0;
if (tx.amount >= 1000000) {
score += 30;
reasons.push("High-value transaction");
}
if (["IR", "KP", "RU", "SY"].includes(tx.country)) {
score += 40;
reasons.push("Higher-risk jurisdiction");
}
if (tx.channel === "cash") {
score += 20;
reasons.push("Cash movement");
}
if (tx.notes?.toLowerCase().includes("urgent")) {
score += 10;
reasons.push("Urgency language detected");
}
return { score: Math.min(score, 100), reasons };
}
3) Wire up the CrewAI agent with explicit instructions
Use one agent for triage. Keep the output narrow so downstream systems can consume it without parsing free text.
// src/monitoringAgent.ts
import { Agent } from "@crew-ai/crewai";
import { calculateRiskScore } from "./tools";
import { Transaction } from "./types";
export function buildMonitoringAgent() {
return new Agent({
role: "Transaction Monitoring Analyst",
goal:
"Assess banking transactions for AML, sanctions, fraud, and market abuse indicators using deterministic evidence.",
backstory:
"You work in an investment bank's financial crime team. You must be conservative, explain every escalation clearly, and never speculate beyond provided facts.",
verbose: true,
allowDelegation: false,
tools: [
{
name: "calculateRiskScore",
description:
"Return a deterministic risk score and reasons for a transaction.",
func: calculateRiskScore,
},
],
maxIter: 3,
memory: false,
llm:
process.env.CREWAI_MODEL ?? undefined,
systemPrompt:
"Only use provided transaction data and tool outputs. Return JSON with decision, riskScore, reasons, and recommendedAction.",
});
}
4) Run a task and persist an audit trail
The important pattern is to store the exact input and output alongside metadata. That is what makes this usable in investment banking instead of just being a demo.
// src/index.ts
import { CrewAI } from "@crew-ai/crewai";
import { Task } from "@crew-ai/crewai";
import { buildMonitoringAgent } from "./monitoringAgent";
import { TransactionSchema } from "./types";
import fs from "node:fs/promises";
async function main() {
const raw = {
transactionId: "TX-88421",
accountId: "ACC-10091",
counterpartyId: "CP-4412",
amount: 2500000,
currency: "USD",
country: "AE",
channel: "wire",
timestamp: new Date().toISOString(),
notes: "urgent settlement requested by client desk",
};
const tx = TransactionSchema.parse(raw);
const agent = buildMonitoringAgent();
const task = new Task({
description:
`Review this transaction for suspicious activity:\n${JSON.stringify(tx)}`,
expectedOutput:
'Valid JSON with keys decision, riskScore, reasons, recommendedAction',
agent,
});
const crew = new CrewAI({
agents: [agent],
tasks: [task],
verbose: true,
});
const result = await crew.kickoff();
await fs.writeFile(
`./audit/${tx.transactionId}.json`,
JSON.stringify(
{
input: tx,
output: result,
reviewedAt: new Date().toISOString(),
modelVersion:
process.env.CREWAI_MODEL ?? "default",
},
null,
2
)
);
console.log(result);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
Production Considerations
- •
Deploy close to your data boundary
Keep the agent inside your approved cloud region or on-prem network segment. Investment banking teams often have strict data residency requirements for client data and trade records.
- •
Log everything needed for audit
Store prompt inputs, tool outputs, model version, timestamps, user identity that triggered the run, and final disposition. If compliance asks why a case was escalated six months later, you need evidence without reconstructing history from logs alone.
- •
Put hard guardrails around outputs
Require structured JSON only. Reject any response that does not match your schema before it reaches case management or alerting systems.
- •
Monitor drift by desk and corridor
Risk patterns differ across equities trading desks, prime brokerage flows, FX payments, and cross-border treasury movements. Track false positives by business line so you do not tune one desk at the expense of another.
Common Pitfalls
- •
Letting the model make unsupported claims
If you do not provide deterministic tools and strict instructions, the agent will infer intent from weak signals. Avoid this by forcing every escalation reason to map back to a known field or tool output.
- •
Skipping schema validation
Free-form text breaks downstream automation fast. Use
zodor equivalent validation before writing alerts into your case system. - •
Ignoring human review thresholds
Not every anomaly should become a SAR or escalation. Define clear thresholds for low-risk logging versus high-risk referral so analysts are not flooded with noise.
- •
Treating audit as an afterthought
In investment banking you need reproducibility first. Persist inputs, outputs, versioning metadata, and rule snapshots at the time of execution; otherwise your monitoring stack will fail governance review even if it works technically.
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