How to Build a KYC verification Agent Using CrewAI in TypeScript for investment banking

By Cyprian AaronsUpdated 2026-04-21
kyc-verificationcrewaitypescriptinvestment-banking

A KYC verification agent for investment banking does one thing well: it gathers client identity data, checks it against policy and external sources, flags mismatches, and produces an auditable decision trail. That matters because onboarding delays kill revenue, but weak KYC controls create regulatory exposure, failed audits, and reputational damage.

Architecture

Build the agent as a small workflow with hard boundaries:

  • Input normalization layer

    • Parses onboarding packets from CRM, PDFs, or JSON payloads.
    • Extracts core fields: legal name, entity type, UBOs, jurisdiction, tax ID, directors.
  • Document verification tool

    • Validates passports, incorporation docs, proof of address, and beneficial ownership declarations.
    • Returns structured findings instead of free-text summaries.
  • Sanctions / PEP / adverse media checker

    • Calls approved internal or vendor APIs.
    • Produces risk signals with source references and timestamps.
  • Policy reasoning agent

    • Uses CrewAI to compare evidence against bank-specific KYC rules.
    • Decides whether the case is clear, needs escalation, or is blocked.
  • Audit logger

    • Persists every input, tool result, and agent decision.
    • Required for model risk review and regulator inspection.
  • Case output formatter

    • Generates a final KYC memo for compliance ops.
    • Includes rationale, missing items, and next actions.

Implementation

1) Install CrewAI for TypeScript and define your data model

Use a strict schema first. In investment banking, weak typing becomes weak controls.

npm install @crewai/crewai zod dotenv
import { z } from "zod";

export const KycCaseSchema = z.object({
  clientId: z.string(),
  legalName: z.string(),
  entityType: z.enum(["individual", "corporation", "fund", "trust"]),
  jurisdiction: z.string(),
  taxId: z.string().optional(),
  beneficialOwners: z.array(
    z.object({
      name: z.string(),
      ownershipPct: z.number().min(0).max(100),
      country: z.string(),
    })
  ),
  documents: z.array(z.string()),
});

export type KycCase = z.infer<typeof KycCaseSchema>;

2) Create tools for document checks and screening

CrewAI agents work best when tools return deterministic outputs. Keep the LLM out of raw compliance logic.

import { Tool } from "@crewai/crewai";

export const verifyDocumentsTool = new Tool({
  name: "verify_documents",
  description: "Validate KYC documents against bank policy",
  func: async (input: string) => {
    const payload = JSON.parse(input) as { documents: string[]; jurisdiction: string };

    const missingRequiredDocs =
      payload.jurisdiction === "US"
        ? ["certificate_of_incorporation"]
        : ["proof_of_address"];

    return JSON.stringify({
      passed: missingRequiredDocs.length === 0,
      missingRequiredDocs,
      checkedAt: new Date().toISOString(),
    });
  },
});

export const sanctionsScreenTool = new Tool({
  name: "sanctions_screen",
  description: "Screen names against sanctioned parties list",
  func: async (input: string) => {
    const payload = JSON.parse(input) as { names: string[] };

    return JSON.stringify({
      hits: payload.names.filter((n) => n.toLowerCase().includes("test")),
      source: "approved-vendor-api",
      screenedAt: new Date().toISOString(),
    });
  },
});

3) Wire up a KYC crew with roles and task flow

This is the core pattern. One agent gathers evidence; another applies policy; the final task writes the audit-ready conclusion.

import "dotenv/config";
import { Agent, Crew, Task } from "@crewai/crewai";
import { KycCaseSchema } from "./schema";
import { verifyDocumentsTool, sanctionsScreenTool } from "./tools";

const intakeAgent = new Agent({
  role: "KYC Intake Analyst",
  goal: "Validate client onboarding data and extract compliance-relevant facts",
  backstory:
    "You work in investment banking onboarding and only produce structured findings.",
  tools: [verifyDocumentsTool],
});

const screeningAgent = new Agent({
  role: "KYC Screening Analyst",
  goal: "Check names against sanctions and identify escalation triggers",
  backstory:
    "You apply bank policy conservatively and never invent evidence.",
  tools: [sanctionsScreenTool],
});

const reviewerAgent = new Agent({
  role: "KYC Compliance Reviewer",
  goal: "Issue a final decision based on evidence and bank policy",
  backstory:
    "You write audit-ready decisions for regulated financial institutions.",
});

export async function runKycCase(input: unknown) {
  const kycCase = KycCaseSchema.parse(input);

   const intakeTask = new Task({
    description:
      `Verify documents for ${kycCase.legalName} in ${kycCase.jurisdiction}. Return missing docs and pass/fail.`,
    expectedOutput:
      "JSON with passed boolean, missingRequiredDocs array, checkedAt timestamp",
    agent: intakeAgent,
    context:
      `Client ID=${kycCase.clientId}; Entity=${kycCase.entityType}; Docs=${kycCase.documents.join(",")}`,
   });

   const screeningTask = new Task({
     description:
       `Screen ${kycCase.legalName} plus all beneficial owners for sanctions or obvious risk flags.`,
     expectedOutput:
       "JSON with hits array, source string, screenedAt timestamp",
     agent: screeningAgent,
   });

   const reviewTask = new Task({
     description:
       `Decide whether this case is APPROVE, ESCALATE, or REJECT using prior task outputs.`,
     expectedOutput:
       "A concise compliance memo with decision and rationale.",
     agent: reviewerAgent,
   });

   const crew = new Crew({
     agents: [intakeAgent, screeningAgent, reviewerAgent],
     tasks: [intakeTask, screeningTask, reviewTask],
   });

   return crew.kickoff();
}

4) Add an audit trail around every run

Do not rely on the model output alone. Persist inputs and decisions so compliance can reconstruct the case later.

import fs from "node:fs/promises";

async function writeAuditRecord(recordPath: string, payload: unknown) {
  await fs.writeFile(recordPath, JSON.stringify(payload, null, 2), "utf8");
}

export async function processAndAudit(caseInput: unknown) {
  const startedAt = new Date().toISOString();
  
   await writeAuditRecord("./audit/input.json", {
     startedAt,
     caseInput,
   });

   const result = await runKycCase(caseInput);

   await writeAuditRecord("./audit/output.json", {
     finishedAt: new Date().toISOString(),
     result,
   });

   return result;
}

Production Considerations

  • Data residency

    • Keep onboarding data in-region if your booking entity requires it.
    • Route LLM calls through approved infrastructure only; do not send client PII to random SaaS endpoints.
  • Monitoring

    • Track approval rate, escalation rate, false positives on sanctions hits, and average time to disposition.
    • Alert when the agent starts escalating normal cases or approving cases with missing required documents.
  • Guardrails

    • Force structured outputs for decisions like APPROVE, ESCALATE, REJECT.
  • Human-in-the-loop

Common Pitfalls

  1. Letting the LLM make policy

    • Bad pattern: asking the model to “decide if this is compliant” without fixed rules.
    • Fix it by moving policy checks into deterministic code or approved tools.
  2. Skipping audit logging

    • If you cannot replay the case later, you do not have a production KYC system.
    • Log inputs, tool outputs, prompts where allowed by policy, timestamps, model version, and final decision.
  3. Using unstructured outputs

    • Free-text answers are hard to validate downstream.
    • Require JSON-shaped outputs for findings so compliance ops can route cases automatically.
  4. Ignoring beneficial owners

    • Screening only the legal entity misses the actual risk in many investment banking cases.
    • Always screen UBOs, directors where relevant, and any controlling persons under your jurisdictional policy.

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