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

By Cyprian AaronsUpdated 2026-04-21
kyc-verificationautogentypescriptbanking

A KYC verification agent automates the first pass of customer due diligence: it collects identity data, checks it against policy, flags missing evidence, and routes risky cases to a human reviewer. In banking, that matters because onboarding speed is tied directly to conversion, but every decision still needs to be auditable, policy-driven, and compliant with AML/KYC controls.

Architecture

A production KYC agent in AutoGen needs these components:

  • Customer intake layer

    • Accepts structured inputs like name, DOB, address, document IDs, and source-of-funds notes.
    • Normalizes messy user input before it reaches the agent.
  • Policy engine

    • Encodes bank-specific KYC rules: required fields, country restrictions, PEP/sanctions escalation thresholds, and document freshness.
    • Keeps compliance logic out of prompt text.
  • AutoGen agent orchestration

    • Uses AssistantAgent for reasoning and UserProxyAgent for tool execution or human handoff.
    • Handles multi-step verification without hardcoding every branch.
  • Evidence retrieval tools

    • Pulls from OCR output, document stores, sanctions screening APIs, and internal customer records.
    • Returns structured results the agent can cite in its decision.
  • Audit trail store

    • Persists prompts, tool calls, model outputs, final decisions, and reviewer overrides.
    • Required for regulatory review and internal model governance.
  • Human review queue

    • Escalates incomplete or high-risk cases to operations or compliance analysts.
    • Prevents the agent from making unsupported approvals.

Implementation

1) Install AutoGen and define the KYC input shape

Use the TypeScript AutoGen package that exposes AssistantAgent, UserProxyAgent, and OpenAIChatCompletionClient. Keep your schema strict so downstream tools can validate every field.

npm install @autogen-ai/autogen openai zod
import { z } from "zod";

export const KycApplicationSchema = z.object({
  fullName: z.string().min(1),
  dateOfBirth: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
  nationality: z.string().min(2),
  residentialAddress: z.string().min(5),
  governmentIdNumber: z.string().min(5),
  documentType: z.enum(["passport", "national_id", "drivers_license"]),
});

export type KycApplication = z.infer<typeof KycApplicationSchema>;

2) Build a bank-safe tool for policy checks

Do not let the model invent compliance outcomes. Put deterministic checks behind a tool and return structured results. The agent should summarize those results, not replace them.

import { z } from "zod";

const PolicyResultSchema = z.object({
  status: z.enum(["pass", "review", "fail"]),
  reasons: z.array(z.string()),
});

export async function checkKycPolicy(app: unknown) {
  const parsed = KycApplicationSchema.safeParse(app);
  if (!parsed.success) {
    return PolicyResultSchema.parse({
      status: "fail",
      reasons: ["Invalid or incomplete customer data"],
    });
  }

  const reasons: string[] = [];
  const highRiskCountries = new Set(["IR", "KP", "SY"]);
  if (highRiskCountries.has(parsed.data.nationality.toUpperCase())) {
    reasons.push("High-risk jurisdiction requires enhanced due diligence");
  }

  if (parsed.data.documentType === "passport" && parsed.data.governmentIdNumber.length < 8) {
    reasons.push("Passport number format appears invalid");
  }

  return PolicyResultSchema.parse({
    status: reasons.length ? "review" : "pass",
    reasons,
  });
}

3) Wire AutoGen agents with a controlled tool call pattern

The key pattern is: the assistant reasons over the application, calls your policy tool through the user proxy boundary, then produces an auditable recommendation. Use AssistantAgent for the analysis role and UserProxyAgent to execute tools or stop for human review.

import { AssistantAgent, UserProxyAgent } from "@autogen-ai/autogen";
import OpenAI from "openai";
import { checkKycPolicy } from "./policy";

const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

const kycAnalyst = new AssistantAgent({
  name: "kyc_analyst",
  systemMessage:
    "You are a banking KYC analyst. Use only provided facts. Never approve when data is missing. If risk is unclear or policy returns review/fail, escalate to human review.",
});

const opsProxy = new UserProxyAgent({
  name: "ops_proxy",
  humanInputMode: "NEVER",
});

async function runKycReview(application: unknown) {
  const policyResult = await checkKycPolicy(application);

  const prompt = `
Customer application:
${JSON.stringify(application, null, 2)}

Policy result:
${JSON.stringify(policyResult, null, 2)}

Return one of:
- APPROVE
- REVIEW
- REJECT

Include a short reason and cite only the provided policy result.
`;

  const response = await kycAnalyst.generateReply([
    { role: "user", content: prompt },
  ]);

  return {
    decisionText: response,
    policyResult,
    auditTimestamp: new Date().toISOString(),
    model: "gpt-4o-mini",
    reviewerRequired:
      policyResult.status !== "pass" || String(response).includes("REVIEW"),
  };
}

4) Persist an audit record before any downstream action

For banking workflows, approval is not enough. You need a durable record with input versioning, policy outcome, model output, and who reviewed it. Store this in your regulated data region and keep retention aligned to your AML/KYC policy.

type AuditRecord = {
  caseId: string;
  applicationHash: string;
};

async function saveAuditRecord(record: AuditRecord & Record<string, unknown>) {
  // Replace with your region-bound datastore write.
}

export async function handleKycCase(caseId: string, application: unknown) {
  const result = await runKycReview(application);

  await saveAuditRecord({
    caseId,
    applicationHash: JSON.stringify(application).length.toString(),
    ...result,
    decisionSource: "autogen-assistant",
    requiresHumanReview:
      result.reviewerRequired ? true : false,
    createdAtUtc: new Date().toISOString(),
    dataResidencyRegion: process.env.DATA_REGION ?? "eu-west-1",
   });

   return result;
}

Production Considerations

  • Keep PII inside your controlled boundary

    Redact unnecessary fields before sending context to the model. For banking workloads, minimize what leaves your VPC and avoid placing raw document numbers in logs.

  • Separate policy from prompting

    Compliance rules should live in code or a rules service. Prompts should explain behavior; they should not encode approval logic that auditors will later need to reconstruct.

  • Add deterministic escalation thresholds

    Route cases to humans when sanctions hits are partial matches, documents are expired, addresses conflict across sources, or residency rules require enhanced due diligence.

  • Pin data residency and retention

    Store audit logs in the same jurisdiction as the customer data when required. Define retention windows for onboarding evidence based on local banking regulation and internal policy.

Common Pitfalls

  • Using the LLM as the source of truth

    Don’t ask the model to decide whether a passport is valid by inspection alone. Run OCR validation and document checks through deterministic services first.

  • Skipping auditability

    If you cannot reconstruct why a case was approved or escalated six months later, you do not have a banking-grade workflow. Persist prompts, tool outputs, model version IDs, timestamps, and reviewer actions.

  • Letting prompts drift into compliance logic

    Teams often bury rules like “high-risk jurisdiction means manual review” inside system messages. Move those rules into code so legal/compliance can review them without reading prompt text.


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