How to Build a fraud detection Agent Using CrewAI in TypeScript for payments

By Cyprian AaronsUpdated 2026-04-21
fraud-detectioncrewaitypescriptpayments

A fraud detection agent for payments is not a replacement for your rules engine or risk models. It sits on top of transaction streams, customer context, and device signals to triage suspicious activity, explain why something looks risky, and route the case to the right action: approve, step-up auth, hold, or escalate.

For payments teams, that matters because false positives cost revenue and support time, while false negatives cost chargebacks and compliance exposure. A CrewAI-based agent gives you a structured way to combine multiple specialist checks without turning your fraud workflow into one giant prompt.

Architecture

  • Transaction intake service

    • Receives payment events from your gateway or event bus.
    • Normalizes fields like amount, merchant category, country, device fingerprint, velocity counters, and account age.
  • Risk enrichment layer

    • Pulls customer history, prior disputes, BIN metadata, IP geolocation, and sanctions/watchlist flags.
    • Keeps PII handling explicit so you can enforce residency and retention rules.
  • CrewAI agents

    • A pattern analyst agent looks for anomalies in transaction behavior.
    • A policy/compliance agent checks payment rules, KYC/KYB constraints, and regional requirements.
    • A case summarizer agent turns the output into an audit-friendly decision.
  • Tools

    • Deterministic tools for fetching transaction history, merchant risk scores, and velocity metrics.
    • No free-form guessing where exact data is available.
  • Decision service

    • Converts agent output into APPROVE, STEP_UP, HOLD, or ESCALATE.
    • Writes an immutable audit record with inputs, outputs, model version, and timestamp.

Implementation

1) Install the CrewAI TypeScript package and define your domain types

You want a narrow contract for payment decisions. Keep the agent away from raw database objects and expose only the fields it needs.

npm install @crewai/core zod
export type PaymentTransaction = {
  id: string;
  amount: number;
  currency: string;
  merchantId: string;
  merchantCategory: string;
  country: string;
  customerId: string;
  deviceFingerprint?: string;
  ipAddress?: string;
  createdAt: string;
};

export type FraudDecision = {
  decision: "APPROVE" | "STEP_UP" | "HOLD" | "ESCALATE";
  riskScore: number;
  reasons: string[];
};

2) Create tools for deterministic enrichment

CrewAI works best when agents can call tools instead of inventing data. For payments, that means velocity checks, customer history lookup, and merchant risk lookup should be tool-backed.

import { z } from "zod";
import { Tool } from "@crewai/core";

const getCustomerHistory = new Tool({
  name: "get_customer_history",
  description: "Fetch recent payment behavior for a customer",
  schema: z.object({ customerId: z.string() }),
  execute: async ({ customerId }) => {
    return {
      chargebacks30d: 1,
      declinedAttempts24h: 4,
      avgTicketSize30d: 82.5,
      accountAgeDays: 190,
    };
  },
});

const getMerchantRisk = new Tool({
  name: "get_merchant_risk",
  description: "Fetch merchant-level fraud risk signals",
  schema: z.object({ merchantId: z.string() }),
  execute: async ({ merchantId }) => {
    return {
      merchantRiskScore: 72,
      disputeRate90d: 0.018,
      categoryWatchlistFlag: false,
    };
  },
});

3) Build the CrewAI agents and task flow

Use separate agents for pattern analysis and compliance review. That keeps the reasoning narrow and makes audits easier when a bank asks why a payment was held.

import { Agent, Task, Crew } from "@crewai/core";

const patternAnalyst = new Agent({
  role: "Fraud Pattern Analyst",
  goal:
    "Detect anomalous payment behavior using transaction context and enrichment data.",
  backstory:
    "You analyze payment fraud signals such as velocity spikes, geo mismatch, device anomalies, and unusual ticket sizes.",
});

const complianceAgent = new Agent({
  role: "Payments Compliance Reviewer",
  goal:
    "Check whether the transaction violates payments policy or requires escalation.",
});

const analyzeTask = (tx: PaymentTransaction) =>
  new Task({
    description:
      `Review this payment transaction for fraud risk.\n` +
      `Transaction JSON:\n${JSON.stringify(tx)}\n` +
      `Use available tools to fetch customer history and merchant risk.\n` +
      `Return a concise risk assessment with reasons.`,
    agent: patternAnalyst,
    expectedOutput:
      "A JSON-like assessment with risk score from 0-100 and reasons.",
    tools: [getCustomerHistory, getMerchantRisk],
    asyncExecution: false,
});

Actually run the crew and normalize the result

This is the part you wire into your API handler or stream processor. The crew returns text; you should parse it into a strict decision object before storing or acting on it.

export async function assessFraud(tx: PaymentTransaction): Promise<FraudDecision> {
  const crew = new Crew({
    agents: [patternAnalyst, complianceAgent],
    tasks: [analyzeTask(tx)],
    verbose: true,
    processFlowType: "sequential",
    memoryEnabled: false,
  });

  

const result = await crew.kickoff();

const text = String(result);
const riskScoreMatch = text.match(/risk score[:\s]+(\d{1,3})/i);
const riskScore = riskScoreMatch ? Number(riskScoreMatch[1]) : 50;

if (riskScore >= thresholdHold(tx)) {
    return {
      decision:
        tx.amount >= thresholdEscalate(tx) ? "ESCALATE" : "HOLD",
      riskScore,
      reasons:[text.slice(0,500)],
    };
}

if (riskScore >= thresholdStepUp(tx)) {
    return { decision:"STEP_UP", riskScore,reasons:[text.slice(0,500)] };
}

return { decision:"APPROVE", riskScore,reasons:[text.slice(0,500)] };
}

function thresholdStepUp(tx:any){ return tx.amount >1000 ?45 :60; }
function thresholdHold(tx:any){ return tx.amount >5000 ?65 :75; }
function thresholdEscalate(tx:any){ return tx.amount >10000 ?80 :90; }

Production Considerations

  • Keep PII out of prompts where possible

  • Pass tokenized identifiers instead of raw PANs or full names.

  • Enforce regional data residency by running enrichment in-region and storing only hashed audit references.

  • Make every decision auditable

  • Persist transaction input hashes, tool outputs, model versioning, prompt template version, final decision, and reviewer overrides.

  • In regulated payments environments you need replayable evidence for disputes and regulator requests.

  • Add hard guardrails outside the LLM

  • The agent should never directly approve high-value transfers above policy thresholds.

  • Use deterministic controls for sanctions screening, AML triggers, MCC blacklists, and velocity caps before any LLM call.

  • Monitor drift by segment

  • Track false positives by card type, geography, merchant category code, device family, and channel.

  • Fraud patterns shift fast; if one segment spikes after a model update or prompt change that is an incident.

Common Pitfalls

  • Letting the agent decide without structured inputs

  • If you pass raw logs or entire customer profiles into CrewAI tasks you will get noisy output.

  • Normalize fields first and expose only what is needed for the specific payment decision.

  • Using one generic agent for all fraud work

  • Fraud analysis and compliance review are different jobs.

  • Split them into separate agents so each has a narrow goal and clearer audit trail.

  • Treating LLM output as final truth

  • Never execute APPROVE directly from text output without validation.

  • Parse the response into a typed object then apply deterministic thresholds and policy checks before releasing funds.


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