Fighting Financial Crime with AI Agents: AML, Alert Triage and Production Compliance Workflows
How to build AI agents that accelerate anti-money laundering investigation, reduce alert fatigue in compliance teams, and reach production safely using in-house MCPs, MCP gateways, and rigorous evals.
This lesson is original educational writing based on this video by Anthropic (published May 22, 2026). All credit for the original content goes to the creators.
1. The alert-fatigue problem
Financial crime compliance teams live inside a brutal mismatch. Transaction monitoring systems flag every pattern that could indicate money laundering. Experienced analysts know that ninety percent of those alerts are false positives — structuring patterns that resolve cleanly once you look at two more data points, or transactions that match a rule only in the narrowest technical sense. But the rules can’t be relaxed; regulators require documentation that every alert was reviewed.
The result is alert fatigue: analysts spend most of their day triaging noise, which means less bandwidth for the genuinely suspicious cases that warrant a Suspicious Activity Report (SAR) or a referral for law-enforcement action.
This is an ideal problem for AI agents because the bottleneck is not judgment — it is information retrieval and synthesis. A skilled analyst knows exactly what to look up (counterparty profiles, prior transaction history, sanctions list membership, historical context on the customer) and can form a disposition in minutes. The agent can perform that same retrieval in seconds, every time, at scale, and surface a pre-populated investigation summary for the analyst to review.
2. The three-stage AML lifecycle and where agents fit
Every AML alert moves through three stages before it is closed. Agents can reduce human time at each one without replacing human judgment on the final call.
Stage 1 — Detection (automated, no AI change needed)
Transaction monitoring engines — whether rules-based (structuring, velocity, round-number thresholds) or ML-based — already run fully automatically. Don’t replace them; they encode regulatory and audit requirements. The output is a queue of alerts.
Stage 2 — Triage (where agents deliver the biggest gain)
This is where the agent earns its keep. For each alert, the agent:
- Pulls context from every relevant system — core banking, CRM, prior case notes, watch lists, negative news — through tools exposed via an in-house MCP server.
- Synthesises a plain-language summary: “This alert triggered the structuring rule because the customer made three cash deposits of $9,800 over 72 hours. Their account was opened 14 months ago, average monthly balance $22,000. No prior alerts. No sanctions matches. Counterparties are known household members.”
- Proposes a disposition — close or escalate — with a cited confidence level and a list of the evidence it used.
The analyst receives a pre-populated case, not a blank form. Decision time per alert drops from 20-40 minutes to under five.
Stage 3 — Disposition (human, supported by agent output)
The analyst reads, can interrogate the agent (“what was the cash activity in the prior 12 months?”), adds any context the agent missed, and makes the legally required human judgement. If they file a SAR, the agent’s summary can feed directly into the SAR narrative draft.
3. Building an in-house MCP server for compliance data
The agent needs structured access to internal systems without touching raw database credentials or PII endpoints directly. The right architecture is an in-house MCP server: a thin service you control that translates the agent’s tool calls into authenticated, audited, permission-checked queries against your internal APIs.
A compliance MCP server typically exposes four categories of tools:
| Tool category | Example tools | Notes |
|---|---|---|
| Customer data | get_customer_profile, get_account_history | Returns only fields needed for AML review; no credit card numbers |
| Transaction data | query_transactions, get_counterparty_info | Date-bounded; expensive queries rate-limited |
| Watchlists | check_sanctions, check_pep_list, check_adverse_news | Can call third-party data providers (Refinitiv, LexisNexis) |
| Case management | get_prior_cases, create_case_note, draft_sar_narrative | Write operations gated on analyst confirmation |
Keep the server implementation small and boring. Each tool is a function that:
- Validates the input (date ranges, account IDs)
- Calls the internal API with a service-account credential
- Strips any fields not needed for the agent’s task
- Returns clean JSON
The MCP gateway layer
For enterprise deployments, add an MCP gateway between Claude Cowork and your in-house MCP servers. The gateway is the single security boundary where you enforce:
- Authentication — which agents are allowed to call which tools
- Audit logging — every tool call, with the agent session ID, is written to a compliance log (this itself may be a regulatory requirement)
- Rate limiting and cost controls — prevent runaway agent loops from hammering production systems
- PII masking — strip or hash personal identifiers before they enter model context if your data governance policy requires it
The gateway pattern also lets you swap the underlying model or the underlying data source independently. Your compliance team’s workflows don’t need to change when a new Claude model ships.
4. Evals: the prerequisite for production
Shipping a compliance agent without evals is not an option in a regulated environment. Before you deploy, you need quantitative evidence that the agent makes better (or at least equivalent) decisions to an analyst working unaided — at a reproducible rate, across a representative set of cases.
Building the eval dataset
Start with historical, closed alerts — cases where an analyst already made a disposition that turned out to be correct. Work with your compliance SMEs to label a set of 100-500 alerts with:
- The correct disposition (close / escalate)
- The key evidence that drove the decision
- A brief rationale
This becomes your golden set. Treat it like a test suite: never use it for prompt iteration, only for final evaluation.
Metrics that matter in compliance
Generic accuracy (“did the agent get the right answer?”) is not enough. For AML triage, track at minimum:
| Metric | Target direction | Why it matters |
|---|---|---|
| False-negative rate (missed escalations) | Minimize ruthlessly | Missing a true SAR is a regulatory failure |
| False-positive rate (unnecessary escalations) | Reduce, but never at the cost of false negatives | Saves analyst time |
| Evidence coverage | Maximize | Did the agent retrieve all the data points a human would check? |
| Hallucination rate | Zero tolerance | Any fabricated transaction detail makes the summary legally inadmissible |
The last two require a different eval approach than a binary correct/incorrect label. Use LLM-as-judge with a carefully written rubric, validated against expert human ratings: have the judge model score the agent’s summary on evidence completeness and factual grounding against the source data it retrieved.
Human-expert baseline
Before shipping, compare the agent’s performance on the golden set to a sample of analyst decisions made without AI assistance. This baseline serves two purposes: it gives you a deployment go / no-go criterion, and it gives you the documented evidence regulators may ask for.
5. Transferable patterns: beyond financial crime
The architecture described above — alert queue → agent triage → human disposition — is not specific to AML. The same pattern applies anywhere a rules engine generates more signals than humans can review. Here are three direct analogues:
Fraud operations
Card fraud and account takeover teams face identical alert-fatigue dynamics. The agent tools change (device fingerprinting APIs, velocity checks on card-not-present transactions, merchant risk scores) but the workflow is the same. The key difference: fraud decisions are often time-critical (seconds, not minutes), so latency budgets for tool calls are tighter.
Insider threat and security operations
SIEM systems generate thousands of alerts per day about anomalous employee behaviour or network activity. Tier-1 analysts spend most of their time on alerts that resolve with three lookups (employee tenure, normal working hours, peer group baseline). An agent can handle that retrieval in parallel across hundreds of alerts simultaneously and route only the genuinely ambiguous cases to a human analyst.
Content policy enforcement
Trust and safety teams reviewing reported content follow the same pattern: automated classifier generates a queue, human makes the final call, but the human needs context (user history, prior violations, policy interpretation) to decide efficiently. An agent can assemble that context and pre-classify cases by severity.
The transferable design decisions
Whatever your domain, these decisions carry over:
- Read-only tools by default. Write operations (filing a case, sending a report, taking action against an account) require explicit analyst confirmation. An agent that can take irreversible actions autonomously is an unacceptable risk in any regulated context.
- Cite every claim. The agent’s summary must link every factual assertion to the source data that supports it. This makes verification fast and supports audit trails.
- Explicit uncertainty. When the agent cannot find sufficient evidence to propose a disposition, it should say so explicitly — not guess. “I was unable to retrieve transaction history for this customer from the past 90 days due to an API timeout” is far more useful than a hedged close recommendation.
- Treat the MCP server as the compliance boundary. Do not let the model call internal APIs directly. Every data access goes through the MCP server, which logs, validates, and enforces data governance.
Check your understanding
5 questions · your answers are saved in this browser only
-
1. What is the primary reason AI agents are well-suited to AML alert triage?
-
2. Why should write operations (e.g. filing a SAR, closing a case) require explicit analyst confirmation rather than being executed autonomously by the agent?
-
3. What is the purpose of an MCP gateway in a compliance agent architecture?
-
4. Which metric should be minimised most aggressively when evaluating an AML triage agent?
-
5. Which of the following best describes how to handle the case where an agent cannot retrieve sufficient evidence to propose a disposition?
Build it yourself
Follow these exact steps to reproduce it yourself · estimated time: ~20 min
Prerequisites
- Anthropic API key
- Python 3.10+
- A JSON file of sample alerts (see Step 1)
Step 1 — Create a sample alert dataset
For this exercise, create a file alerts.json with a few mock AML alerts. Each alert should
have an alertId, a customerId, a ruleTriggered, and a transactionAmount:
[
{
"alertId": "ALT-001",
"customerId": "CUST-9921",
"ruleTriggered": "STRUCTURING_CASH",
"transactionAmount": 9800,
"transactionDate": "2026-06-01",
"notes": "Third deposit under $10k in 5 days"
},
{
"alertId": "ALT-002",
"customerId": "CUST-4477",
"ruleTriggered": "HIGH_VELOCITY_WIRES",
"transactionAmount": 250000,
"transactionDate": "2026-06-03",
"notes": "Four outbound wires to new beneficiaries in 48h"
}
]Step 2 — Build stub MCP tools
Create compliance_mcp.py. For now, return realistic-looking mock data so you can iterate on
the agent without a real database:
def get_customer_profile(customer_id: str) -> dict:
"""Return customer KYC profile."""
return {
"customerId": customer_id,
"accountAge": "14 months",
"riskRating": "medium",
"averageMonthlyBalance": 22000,
"priorAlerts": 0,
"sanctionsMatch": False,
}
def query_transactions(customer_id: str, days: int = 90) -> list[dict]:
"""Return recent transactions for a customer."""
# Stub: return two mock transactions
return [
{"date": "2026-05-28", "amount": 9800, "type": "CASH_DEPOSIT"},
{"date": "2026-06-01", "amount": 9800, "type": "CASH_DEPOSIT"},
]
def check_sanctions(customer_id: str) -> dict:
return {"match": False, "listsChecked": ["OFAC_SDN", "EU_CONSOLIDATED"]}Step 3 — Write the triage agent
import anthropic, json
client = anthropic.Anthropic()
TOOLS = [
{
"name": "get_customer_profile",
"description": "Retrieve KYC profile and risk rating for a customer.",
"input_schema": {
"type": "object",
"properties": {"customer_id": {"type": "string"}},
"required": ["customer_id"],
},
},
{
"name": "query_transactions",
"description": "Fetch the customer's recent transactions.",
"input_schema": {
"type": "object",
"properties": {
"customer_id": {"type": "string"},
"days": {"type": "integer", "default": 90},
},
"required": ["customer_id"],
},
},
{
"name": "check_sanctions",
"description": "Check whether the customer appears on any sanctions list.",
"input_schema": {
"type": "object",
"properties": {"customer_id": {"type": "string"}},
"required": ["customer_id"],
},
},
]
SYSTEM = """You are an AML triage assistant. For each alert you receive:
1. Retrieve the customer profile, recent transactions, and sanctions status using the available tools.
2. Synthesise the evidence into a concise summary (3-5 sentences).
3. Propose a disposition: CLOSE or ESCALATE, with a confidence level (low/medium/high).
4. List every data point you used to reach that conclusion.
If any tool call fails, state that explicitly rather than guessing.
Do not take any write actions — your output is for analyst review only."""
def dispatch_tool(name, inputs):
from compliance_mcp import get_customer_profile, query_transactions, check_sanctions
if name == "get_customer_profile":
return get_customer_profile(inputs["customer_id"])
if name == "query_transactions":
return query_transactions(inputs["customer_id"], inputs.get("days", 90))
if name == "check_sanctions":
return check_sanctions(inputs["customer_id"])
def triage_alert(alert: dict) -> str:
messages = [{"role": "user", "content": f"Triage this alert: {json.dumps(alert)}"}]
while True:
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
system=SYSTEM,
tools=TOOLS,
messages=messages,
)
if response.stop_reason == "end_turn":
return next(b.text for b in response.content if hasattr(b, "text"))
# Handle tool calls
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = dispatch_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result),
})
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
if __name__ == "__main__":
with open("alerts.json") as f:
alerts = json.load(f)
for alert in alerts:
print(f"\n=== {alert['alertId']} ===")
print(triage_alert(alert))Step 4 — Run and review
python triage_agent.pyRead each summary critically: does it cite the data it used? Is the confidence level calibrated? Does it acknowledge when data is missing? Refine the system prompt until the summaries are ones you’d actually trust as a starting point for analyst review.
Step 5 — Add a simple eval harness
Create eval.json with the correct dispositions for your mock alerts, then run the agent
against it and report accuracy:
with open("eval.json") as f:
golden = json.load(f) # [{"alertId": "ALT-001", "correctDisposition": "CLOSE"}, ...]
correct = 0
for item in golden:
alert = next(a for a in alerts if a["alertId"] == item["alertId"])
summary = triage_alert(alert)
predicted = "ESCALATE" if "ESCALATE" in summary else "CLOSE"
if predicted == item["correctDisposition"]:
correct += 1
else:
print(f"MISMATCH {item['alertId']}: predicted {predicted}, expected {item['correctDisposition']}")
print(f"\nAccuracy: {correct}/{len(golden)} ({100*correct//len(golden)}%)")