Small e‑commerce companies lose revenue to fraud but lack the data‑science teams to build multi‑signal detection. A single LLM call misses patterns that require analyzing orders, payments, and customer behavior together.
A complete, working implementation of this recipe — downloadable as a zip or browsable file by file. Generated by our build pipeline; tested with full coverage before publishing.
This recipe builds a multi-agent fraud detection system for e-commerce using Anthropic’s Claude and the REAA Agent Mesh framework. Three specialist agents — a Transaction Anomaly Detector, an Account Takeover Detector, and a Chargeback Risk Predictor — collaborate through the mesh to evaluate each order. A confidence gate then decides whether to approve the order, flag it for manual review, or block it outright. A human-review escalation agent steps in when the specialists disagree or their confidence is low. Everything is logged to Supabase for an audit trail.
This tutorial is for TypeScript developers familiar with Next.js who want to wire up a real multi-agent orchestration with session management, confidence-based routing, and observability.
Prerequisites
Node.js >= 22 and pnpm installed (packageManager field pins pnpm@10.0.0)
An Anthropic API key (set as ANTHROPIC_API_KEY in .env)
A Supabase project (set SUPABASE_URL and SUPABASE_SECRET_KEY in .env)
A Langfuse account (optional — set LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY for observability)
Familiarity with Next.js App Router, Zod schemas, and basic agent concepts
Step 1: Create the agent registry YAML files
The @reaatech/agent-mesh-registry package loads agent configurations from YAML files in a directory. Each file describes one agent: its ID, display name, endpoint, confidence threshold, and behavioral flags. Create the agents/ directory and four YAML files.
Expected output: A file at agents/transaction-anomaly.yaml. The ${VAR:-default} syntax lets you override endpoints via environment variables while falling back to the default port.
Next, the account takeover detector:
yaml
agent_id: "account-takeover"display_name: "Account Takeover Detector"description: "Identifies indicators of account takeover in e-commerce orders"endpoint: "${ACCOUNT_TAKEOVER_ENDPOINT:-http://localhost:8082}"type: mcpis_default: falseconfidence_threshold: 0.75clarification_required: falseexamples: - "New shipping address for existing account" - "Unusual login location"
The chargeback risk predictor:
yaml
agent_id: "chargeback-risk"display_name: "Chargeback Risk Predictor"description: "Predicts likelihood of chargeback based on order characteristics"endpoint: "${CHARGEBACK_RISK_ENDPOINT:-http://localhost:8083}"type: mcpis_default: falseconfidence_threshold: 0.8clarification_required: falseexamples: - "High-value first purchase" - "Order with digital goods"
And the human-review escalation agent — note is_default: true and confidence_threshold: 0:
yaml
agent_id: "human-review"display_name: "Human Review Escalation"description: "Escalates suspicious orders for human review when confidence is low"endpoint: "${HUMAN_REVIEW_ENDPOINT:-http://localhost:8084}"type: mcpis_default: trueconfidence_threshold: 0clarification_required: trueclarification_context: "This order requires human review"examples: - "Flagged by multiple agents" - "High-value suspicious order"
Expected output: Four YAML files in agents/. The registry requires exactly one agent with is_default: true — that’s human-review. The default agent has confidence_threshold: 0 so the confidence gate routes to it directly without threshold checks.
Step 2: Define domain types and Zod schemas
Create src/lib/types.ts. This file holds every domain type in the system, defined as Zod schemas with inferred TypeScript types. This gives you runtime validation at the API boundary and compile-time safety everywhere else.
Expected output: A file with 6 Zod schemas and 7 inferred types. FraudOrderSchema uses z.email() (Zod 4 built-in) and z.number().positive() to validate email format and require positive amounts. The response schema uses z.enum([...]) to restrict verdicts to exactly "approve", "review", or "block".
Now create the constants file at src/lib/constants.ts:
Expected output: The constants file re-exports service metadata from @reaatech/agent-mesh and defines the weighted scoring formula. The weights sum to 1.0 (0.33 + 0.33 + 0.34). An order with an overallFraudScore below 0.3 is approved, below 0.7 goes to review, and at or above 0.7 is blocked.
Step 3: Create the Anthropic LLM service
Create src/services/anthropic-service.ts. This service wraps the Anthropic SDK to call Claude with role-specific prompts. Each agent type gets a different system prompt that instructs the model to return a specific JSON shape.
terminal
mkdir -p src/services
ts
import Anthropic from "@anthropic-ai/sdk";import { createChildLogger } from "@reaatech/agent-mesh-observability";import { Langfuse } from "langfuse";import { TransactionAnomalyVerdictSchema, AccountTakeoverVerdictSchema, ChargebackRiskVerdictSchema, type FraudOrder, type AgentVerdict,} from "../lib/types.js";const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });const langfuse = new Langfuse({ publicKey: process.env.LANGFUSE_PUBLIC_KEY, secretKey: process.env.LANGFUSE_SECRET_KEY,});function buildSystemPrompt(agentType:
Expected output: A service that calls anthropic.messages.create() with model: "claude-sonnet-4-6". It validates the LLM response against the appropriate Zod schema, logs through the agent-mesh observability system, and traces via Langfuse. Errors are prefixed with [analyzeFraud:<agentType>] for easy filtering.
Step 4: Create the Supabase fraud log service
Create src/services/supabase-fraud-log-service.ts. This service persists every evaluation result and can retrieve past evaluations for audit.
ts
import { createClient } from "@supabase/supabase-js";import { createChildLogger } from "@reaatech/agent-mesh-observability";import { EvaluateOrderResponseSchema, type EvaluateOrderResponse,} from "../lib/types.js";const supabase = createClient( process.env.SUPABASE_URL ?? "", process.env.SUPABASE_SECRET_KEY ?? "",);const log = createChildLogger({ component: "supabase-fraud-log" });interface FraudEvaluationRow { order_id: string; verdict: string;
Expected output: Three exported functions. logFraudEvaluation inserts a new row into the fraud_evaluations table. getFraudEvaluationByOrder fetches a single evaluation by order ID. getRecentEvaluations returns the most recent N evaluations. Every row read from Supabase is validated against EvaluateOrderResponseSchema before being returned.
Step 5: Create the agent registry configuration
Create src/mesh/config.ts. This module wraps the @reaatech/agent-mesh-registry API to initialize the registry at startup and provide lookup helpers.
terminal
mkdir -p src/mesh
ts
import { initRegistry, registryState, setupSighupHandler } from "@reaatech/agent-mesh-registry";import type { AgentConfig } from "@reaatech/agent-mesh-registry";export async function initializeFraudMesh(): Promise<void> { await initRegistry(); setupSighupHandler();}export function getAgentForVerdict(agentId: string): AgentConfig | null { const agent = registryState.getAgent(agentId); if (!agent) { return null; } return agent;}export function getDefaultAgent(): AgentConfig | null { const agent = registryState.defaultAgent; if (!agent) { return null; } return agent;}
Expected output: Three functions. initializeFraudMesh calls initRegistry() (which loads YAML files from the ./agents/ directory by default) then registers a SIGHUP handler for hot-reload. getAgentForVerdict looks up a specific agent by ID. getDefaultAgent returns the agent with is_default: true — in this recipe the human-review agent.
Step 6: Create the fraud mesh orchestrator
This is the core of the system — the orchestrator that ties everything together. Create src/mesh/fraud-mesh.ts:
ts
import { dispatchToAgent, buildTurnEntry, formatAgentResponse, shouldCloseSession } from "@reaatech/agent-mesh-router";import { createSession, getActiveSession, appendTurn, closeSession,} from "@reaatech/agent-mesh-session";import { evaluateConfidenceGate } from "@reaatech/agent-mesh-confidence";import { createChildLogger, recordAgentDispatchDuration, recordAgentDispatchError, recordClarification, logAgentRouted,} from "@reaatech/agent-mesh-observability";import { registryState } from "@reaatech/agent-mesh-registry";import type { AgentResponse } from "@reaatech/agent-mesh";import { getAgentForVerdict, getDefaultAgent } from "./config.js"
Expected output: The evaluateOrder function orchestrates the full fraud detection pipeline:
Resolves or creates an agent-mesh session for the customer
Dispatches to all three specialist agents in sequence via dispatchToAgent
Computes a weighted overallFraudScore and maps it to a verdict
Runs the confidence gate via evaluateConfidenceGate
If the gate says “clarify” and ENABLE_CLARIFICATION is on, records clarification
If the gate routes to human-review, dispatches a fourth evaluation step
Persists the result to Supabase and closes the session
Step 7: Set up Next.js instrumentation
The agent registry needs to be loaded before any requests arrive. Create src/instrumentation.ts:
The scaffold generates a minimal next.config.ts. You need to add the experimental.instrumentationHook flag so Next.js calls register() at startup. Update next.config.ts to:
ts
import type { NextConfig } from "next";const nextConfig: NextConfig = { experimental: { instrumentationHook: true, },};export default nextConfig;
Expected output: The register() function fires once at server startup. It only runs in the Node.js runtime (guarded by NEXT_RUNTIME === "nodejs"). It initializes OpenTelemetry via initOtel() and then loads the agent registry via initializeFraudMesh(). The experimental.instrumentationHook flag — note the exact spelling — is required for Next.js to call register() at all.
Step 8: Create the API route handler
Create app/api/evaluate-order/route.ts. This is the public endpoint that receives fraud order data.
Expected output: A Next.js App Router route handler that accepts POST requests. It validates the body against FraudOrderSchema — returning 400 with issue details on validation failure — then dynamically imports the evaluateOrder orchestrator at call time. On success it returns { result: EvaluateOrderResponse } with status 200; on failure it returns { error: "Internal server error" } with status 500.
Step 9: Update the app shell and entry point
Update the layout metadata in app/layout.tsx:
tsx
import type { Metadata } from "next";import { Geist, Geist_Mono } from "next/font/google";import "./globals.css";const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"],});const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"],});export const metadata: Metadata = { title: "Anthropic Agent Mesh for E-commerce Fraud Detection", description: "A mesh of specialized Anthropic-powered agents that collaborate to detect and block e-commerce fraud in real time for small businesses.",};export default function RootLayout({ children,}: Readonly<{ children: React.ReactNode;}>) { return ( <html lang="en" className={`${geistSans.variable} ${geistMono.variable}`}> <body>{children}</body> </html> );}
Replace app/page.tsx with a minimal landing page:
tsx
import styles from "./page.module.css";export default function Home() { return ( <div className={styles.page}> <main className={styles.main}> <div className={styles.intro}> <h1>Anthropic Agent Mesh for E-commerce Fraud Detection</h1> <p> A mesh of specialized Anthropic-powered agents that collaborate to detect and block e-commerce fraud in real time for small businesses. </p> </div> <div className={styles.ctas}> <code>POST /api/evaluate-order</code> </div> </main> </div> );}
Create src/index.ts to re-export the public API:
ts
export { evaluateOrder } from "./mesh/fraud-mesh.js";export { initializeFraudMesh } from "./mesh/config.js";
Expected output: The app now has a proper landing page, correct metadata, and a clean public entry point. The app/page.tsx displays the recipe name and the API endpoint. The src/index.ts re-exports the two key functions for programmatic use.
Step 10: Configure environment variables
Create .env.example with every environment variable the application reads:
env
# Env vars used by anthropic-agent-mesh-for-e-commerce-fraud-detection.# Keep placeholders only — never commit real values.ANTHROPIC_API_KEY=<your-anthropic-key>SUPABASE_URL=<your-supabase-url>SUPABASE_SECRET_KEY=<your-supabase-secret>LANGFUSE_PUBLIC_KEY=<your-langfuse-public-key>LANGFUSE_SECRET_KEY=<your-langfuse-secret-key>TRANSACTION_ANOMALY_ENDPOINT=http://localhost:8081ACCOUNT_TAKEOVER_ENDPOINT=http://localhost:8082CHARGEBACK_RISK_ENDPOINT=http://localhost:8083HUMAN_REVIEW_ENDPOINT=http://localhost:8084MCP_REQUEST_TIMEOUT_MS=30000MCP_MAX_RETRIES=3ENABLE_CIRCUIT_BREAKER=trueENABLE_CLARIFICATION=trueSESSION_TTL_MINUTES=30SESSION_MAX_TURNS=100AGENTS_DIR=./agentsNODE_ENV=development
Copy it to .env and fill in your real API keys before running:
terminal
cp .env.example .env
Expected output: Every process.env.X read in the source code has a corresponding entry. The AGENTS_DIR tells the registry where to find YAML configs. Set ENABLE_CLARIFICATION=true to turn on the confidence-gate clarification flow.
Step 11: Run the tests
The test suite covers every module. Run it with:
terminal
pnpm test
This executes vitest run --coverage --reporter=json --outputFile=vitest-report.json. The tests are organized by module:
tests/lib/types.test.ts — validates FraudOrderSchema, EvaluationStepSchema, and EvaluateOrderResponseSchema with happy-path, error, and boundary cases
tests/services/anthropic-service.test.ts — mocks the Anthropic HTTP API and tests analyzeFraud for all three agent types, error handling (non-JSON, Zod validation failure), and non-text content blocks
tests/services/supabase-fraud-log-service.test.ts — mocks @supabase/supabase-js and tests logFraudEvaluation, getFraudEvaluationByOrder, and getRecentEvaluations with error and boundary cases
tests/mesh/config.test.ts — mocks @reaatech/agent-mesh-registry and tests registry initialization and agent lookups
tests/mesh/fraud-mesh.test.ts — the largest test file, covering the full orchestrator pipeline: happy paths (3-step dispatch, weighted scoring, all three verdicts), error handling (per-agent failures, fallback to default), escalation (human-review 4th step, clarification flow), session management, and metrics
tests/api/evaluate-order.test.ts — tests the route handler with valid/invalid orders, malformed JSON, and internal errors
tests/instrumentation.test.ts — tests the register() function with both nodejs and edge runtime values
tests/index.test.ts — verifies the public exports from src/index.ts
Expected output: All tests pass. The resolver imports use .js extensions as required by the module resolution configured in tsconfig.json.
When the confidence gate detects low confidence or conflicting signals, the response includes a fourth step from the human-review agent. The result is also persisted to your Supabase fraud_evaluations table automatically.
Next steps
Add a web dashboard that visualizes fraud evaluation results from the Supabase fraud_evaluations table in real time
Integrate a payment gateway webhook to trigger evaluateOrder automatically when a new order is placed
Add more specialist agents — for example a device fingerprinting agent or a loyalty-program abuse detector — by creating new YAML files in the agents/ directory
Extend the confidence gate logic with custom routing rules that handle specific combinations of flags across agents
Replace the in-process MCP agent endpoints with remote MCP servers running in their own containers for production isolation
"transaction-anomaly"
|
"account-takeover"
|
"chargeback-risk"
)
:
string
{
switch (agentType) {
case "transaction-anomaly":
return `You are a transaction anomaly detection specialist for e-commerce fraud prevention.
Analyze the order for anomalous patterns including:
- Unusual order amounts or item combinations
- Suspicious payment method usage
- Geographic anomalies in IP, billing, and shipping addresses
- Velocity patterns in order timing
- Known fraud indicators in payment details
Return a JSON object with:
- anomalyScore: number (0-1, higher = more anomalous)
- suspiciousFlags: string[] (list of detected flags)
- reasoning: string (detailed explanation)
- confidence: number (0-1, your confidence in this assessment)`;
case "account-takeover":
return `You are an account takeover detection specialist for e-commerce fraud prevention.
Analyze the order for indicators of account takeover including:
- Unusual shipping addresses for the customer
- Changes in purchasing patterns
- Suspicious email or contact information
- IP address mismatch with account history indicators
- Card fingerprint inconsistencies
Return a JSON object with:
- takeoverScore: number (0-1, higher = more likely takeover)
- indicators: string[] (list of detected indicators)
- reasoning: string (detailed explanation)
- confidence: number (0-1, your confidence in this assessment)`;
case "chargeback-risk":
return `You are a chargeback risk prediction specialist for e-commerce fraud prevention.
Analyze the order for chargeback risk factors including:
- High-value orders from new accounts
- Digital goods or high-risk item categories
- Shipping address inconsistencies
- Previous chargeback patterns
- Payment method risk factors
- Unusual email domains or phone numbers
Return a JSON object with:
- riskScore: number (0-1, higher = more chargeback risk)
- riskFactors: string[] (list of identified risk factors)
- reasoning: string (detailed explanation)
- confidence: number (0-1, your confidence in this assessment)`;
}
}
function getValidationSchema(agentType: "transaction-anomaly" | "account-takeover" | "chargeback-risk") {