How to Build a KYC verification Agent Using CrewAI in TypeScript for retail banking
A KYC verification agent automates the first pass of customer due diligence for retail banking. It collects identity data, checks completeness, validates documents against policy, flags mismatches, and routes risky cases to a human reviewer. That matters because onboarding speed and regulatory compliance are usually in tension, and this agent reduces manual work without weakening controls.
Architecture
Build this agent as a small pipeline, not a single “smart” prompt.
- •
Intake layer
- •Accepts customer-submitted data: name, DOB, address, ID document metadata, and source channel.
- •Normalizes fields before the LLM sees anything.
- •
KYC policy checker
- •Encodes bank-specific rules: required fields, document types by country, expiration checks, sanctions escalation thresholds.
- •Returns structured pass/fail signals.
- •
Verification agent
- •Uses CrewAI
Agentto reason over the intake payload and policy output. - •Produces a structured KYC decision draft: approve, reject, or escalate.
- •Uses CrewAI
- •
Task orchestration
- •Uses CrewAI
TaskandCrewto run the workflow in order. - •Keeps the process auditable and deterministic enough for banking review.
- •Uses CrewAI
- •
Audit logger
- •Persists inputs, outputs, timestamps, model version, and decision rationale.
- •Required for traceability and internal model risk review.
- •
Human review handoff
- •Routes exceptions like fuzzy OCR matches, expired IDs, or sanctions hits to compliance ops.
- •The agent should never be the final authority on high-risk cases.
Implementation
1) Install dependencies and define your KYC schema
Use the TypeScript package for CrewAI and keep your payloads typed. In banking systems, typed inputs matter because you want validation before any model call happens.
npm install @crewai/crewai zod dotenv
import { z } from "zod";
export const KycInputSchema = z.object({
customerId: z.string(),
fullName: z.string(),
dateOfBirth: z.string(), // ISO string
countryOfResidence: z.string(),
idDocumentType: z.enum(["passport", "national_id", "drivers_license"]),
idDocumentNumber: z.string(),
idExpiryDate: z.string(), // ISO string
addressLine1: z.string(),
city: z.string(),
postalCode: z.string(),
});
export type KycInput = z.infer<typeof KycInputSchema>;
2) Create a policy checker before the LLM runs
Do not ask the model to invent banking rules. Put hard requirements in code and let CrewAI reason only over validated facts.
type PolicyResult = {
isComplete: boolean;
issues: string[];
};
export function checkKycPolicy(input: KycInput): PolicyResult {
const issues: string[] = [];
const today = new Date();
const expiry = new Date(input.idExpiryDate);
if (!input.fullName.trim()) issues.push("Missing full name");
if (!input.addressLine1.trim()) issues.push("Missing address line1");
if (expiry <= today) issues.push("ID document is expired");
return {
isComplete: issues.length === 0,
issues,
};
}
3) Build the CrewAI agent and task flow
This is the core pattern. The agent receives structured context from your policy checker and returns a decision draft that compliance can review.
import "dotenv/config";
import { Agent, Task, Crew } from "@crewai/crewai";
import { KycInputSchema } from "./kyc-schema";
import { checkKycPolicy } from "./kyc-policy";
const kycAgent = new Agent({
role: "KYC Verification Analyst",
goal:
"Review customer onboarding data against retail banking KYC standards and produce an auditable decision draft.",
backstory:
"You work in a retail bank compliance operations team. You must be conservative, flag uncertainty, and never override hard policy failures.",
});
export async function verifyCustomer(inputRaw: unknown) {
const input = KycInputSchema.parse(inputRaw);
const policy = checkKycPolicy(input);
const task = new Task({
description: `
Review this retail banking KYC case.
Customer data:
${JSON.stringify(input, null, 2)}
Policy check:
${JSON.stringify(policy, null, 2)}
Return:
- decision: APPROVE | ESCALATE | REJECT
- reasons: array of short strings
- missing_info: array of fields
- audit_summary: one paragraph suitable for compliance review
`,
expectedOutput:
"A structured KYC decision draft with decision, reasons, missing_info, and audit_summary.",
agent: kycAgent,
outputJson: true,
outputSchema: {
decision: "string",
reasons: ["string"],
missing_info: ["string"],
audit_summary: "string",
},
});
const crew = new Crew({
agents: [kycAgent],
tasks: [task],
verbose: true,
process: "sequential",
});
const result = await crew.kickoff();
return { inputId: input.customerId, policy, result };
}
4) Add an escalation rule for banking-grade handling
Retail banking needs deterministic escalation. If policy fails or confidence is low, do not auto-approve. Route to human review with the exact reason set attached.
export function shouldEscalate(policyIssues: string[], llmDecision?: string) {
if (policyIssues.length > threshold) return true;
}
The practical version is simpler:
- •Escalate if ID is expired.
- •Escalate if any required field is missing.
- •Escalate if the customer country requires enhanced due diligence.
- •Escalate if OCR or extraction confidence falls below your threshold.
- •Reject only when policy explicitly says so; otherwise prefer escalation over automation.
Production Considerations
- •
Auditability
- •Store every input payload, policy result, task prompt, model response, and final disposition.
- •Keep immutable logs with request IDs so compliance can reconstruct decisions later.
- •
Data residency
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