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

By Cyprian AaronsUpdated 2026-04-21
kyc-verificationcrewaitypescriptlending

A KYC verification agent for lending collects borrower identity data, checks it against policy and external signals, and produces a decision-ready summary for the loan workflow. It matters because lending teams need fast onboarding without losing control over compliance, auditability, and fraud risk.

Architecture

  • Input normalization layer

    • Takes applicant data from the loan origination system.
    • Validates fields like name, DOB, address, ID number, and consent flags.
  • KYC policy engine

    • Encodes lending rules: required documents, jurisdiction-specific checks, sanctions screening thresholds, and escalation rules.
    • Separates hard blocks from soft exceptions.
  • CrewAI orchestration

    • Uses Agent, Task, and Crew to split work between extraction, verification, and compliance review.
    • Keeps each step observable and auditable.
  • External verification tools

    • Calls document OCR, sanctions/PEP screening, address validation, and device/IP risk services.
    • Wraps each integration in a typed CrewAI tool.
  • Decision formatter

    • Produces a structured output for downstream lending systems.
    • Includes pass/fail status, reasons, evidence references, and reviewer notes.
  • Audit log store

    • Persists prompts, tool calls, outputs, timestamps, and versioned policy IDs.
    • Needed for regulatory review and internal dispute handling.

Implementation

1) Set up typed inputs and a strict output shape

For lending, don’t let the agent free-write a decision. Force a schema that your underwriting or onboarding service can consume.

// src/kyc/types.ts
export type KycInput = {
  applicantId: string;
  fullName: string;
  dateOfBirth: string;
  countryOfResidence: string;
  governmentIdNumber: string;
  addressLine1: string;
  consentToCheck: boolean;
};

export type KycDecision = {
  applicantId: string;
  status: "approved" | "review" | "rejected";
  reasons: string[];
  checks: {
    identityMatch: boolean;
    sanctionsClear: boolean;
    addressVerified: boolean;
    consentPresent: boolean;
  };
};

2) Create tools for the real checks

CrewAI works best when the agent can call deterministic tools. In production you’d wrap your OCR vendor, sanctions API, and internal policy service here.

// src/kyc/tools.ts
import { tool } from "@crewai/core";
import { z } from "zod";

export const sanctionsScreenTool = tool({
  name: "sanctions_screen",
  description: "Screen an applicant against sanctions and PEP lists",
  schema: z.object({
    fullName: z.string(),
    dateOfBirth: z.string(),
    countryOfResidence: z.string(),
  }),
  execute: async ({ fullName }) => {
    const hit = fullName.toLowerCase().includes("test");
    return {
      clear: !hit,
      matchReason: hit ? "Potential watchlist match" : null,
      source: "mock-sanctions-service",
    };
  },
});

export const addressVerifyTool = tool({
  name: "address_verify",
  description: "Verify residential address against a trusted provider",
  schema: z.object({
    addressLine1: z.string(),
    countryOfResidence: z.string(),
  }),
  execute: async ({ addressLine1 }) => {
    return {
      verified: addressLine1.length > 10,
      source: "mock-address-service",
    };
  },
});

3) Wire the agents and tasks with CrewAI

Use one agent to gather facts and another to apply lending policy. The compliance reviewer should be constrained to the evidence returned by tools.

// src/kyc/crew.ts
import { Agent, Crew, Task } from "@crewai/core";
import { sanctionsScreenTool, addressVerifyTool } from "./tools";
import type { KycInput } from "./types";

export function buildKycCrew(input: KycInput) {
  const verifier = new Agent({
    role: "KYC Verifier",
    goal:
      "Collect identity evidence and run required KYC checks for a lending application.",
    backstory:
      "You verify borrower identity using only approved tools and return structured findings.",
    tools: [sanctionsScreenTool, addressVerifyTool],
    verbose: true,
    allowDelegation: false,
  });

  const complianceReviewer = new Agent({
    role: "Compliance Reviewer",
    goal:
      "Apply lending KYC policy to the collected evidence and decide approve/review/reject.",
    backstory:
      "You enforce policy strictly. If any mandatory check is missing or failed, escalate.",
    verbose: true,
    allowDelegation: false,
  });

   const verifyTask = new Task({
    description:
      `Run KYC checks for applicant ${input.applicantId}. Validate consent first. Then screen sanctions and verify address.`,
    expectedOutput:
      "A JSON object with evidence for consentPresent, sanctionsClear, identityMatch, and addressVerified.",
    agent: verifier,
   });

   const reviewTask = new Task({
     description:
       "Review the evidence against lending KYC policy. Return only JSON matching the decision schema.",
     expectedOutput:
       'JSON with status approved|review|rejected plus reasons array.',
     agent: complianceReviewer,
     context:[verifyTask],
   });

   return new Crew({
     agents:[verifier, complianceReviewer],
     tasks:[verifyTask, reviewTask],
     verbose:true,
   });
}

4) Execute the crew inside your API handler

Keep execution behind your backend so you can enforce residency controls and write audit logs before returning a result.

// src/server/kyc-handler.ts
import { buildKycCrew } from "../kyc/crew";
import type { KycInput } from "../kyc/types";

export async function runKycVerification(inputJson: unknown) {
  const input = inputJson as KycInput;

  if (!input.consentToCheck) {
    return {
      applicantId: input.applicantId,
      status: "rejected",
      reasons:["Missing borrower consent for KYC processing"],
      checks:{
        identityMatch:false,
        sanctionsClear:false,
        addressVerified:false,
        consentPresent:false,
      },
    };
  }

  const crew = buildKycCrew(input);
  const result = await crew.kickoff();

  
}

In practice you should parse result into your own KycDecision shape before saving it. Also persist the raw crew output with a policy version so auditors can reproduce the decision path later.

Production Considerations

  • Deployment boundaries

    • Run the agent in your private backend VPC or region-bound environment.
    • For lending data residency requirements, keep PII in-region and avoid sending raw documents to third-party models unless your legal basis covers it.
  • Monitoring

    • Track task latency, tool failure rate, sanction-hit rate, manual-review rate, and rejection reasons.
  • Guardrails

  • Enforce JSON-only outputs with schema validation before any downstream action.

  • Add hard stops for missing consent, failed ID match, or positive sanctions hits.

  • Version every policy rule so credit ops can explain why an applicant was reviewed or rejected.

Common Pitfalls

  • Letting the model make unbounded decisions

  • Don’t ask for “approve or reject” without a schema and explicit criteria.

  • Use structured outputs plus deterministic policy checks outside the model.

  • Skipping audit artifacts

  • If you don’t store prompts, tool responses, timestamps, and policy versions you won’t survive lender audits or customer disputes.

  • Log enough to reconstruct the decision without exposing unnecessary PII.

  • Mixing jurisdictions in one runtime

  • A single global deployment can break residency rules when applicants come from regulated markets.

  • Route EU/UK/US borrowers through region-specific workers with separate storage and model endpoints.


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