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

By Cyprian AaronsUpdated 2026-04-21
kyc-verificationautogentypescriptfintech

A KYC verification agent automates the boring but risky part of customer onboarding: collecting identity data, checking it against policy, and deciding whether to approve, reject, or escalate for manual review. For fintech, this matters because KYC failures become compliance incidents, onboarding delays, fraud exposure, and audit problems.

Architecture

Build this agent as a small workflow, not a single prompt.

  • Orchestrator agent
    • Owns the conversation flow.
    • Decides which checks run next and when to stop.
  • Document extraction tool
    • Pulls structured fields from passports, national IDs, utility bills, and bank statements.
    • Normalizes names, dates of birth, addresses, and document numbers.
  • Policy/rules engine
    • Applies deterministic checks like age thresholds, country restrictions, document expiry, and required fields.
    • Keeps compliance logic out of the LLM.
  • Risk reviewer agent
    • Flags mismatches, suspicious patterns, or missing evidence.
    • Produces a reasoned escalation summary for human ops.
  • Audit logger
    • Stores every decision, tool call, model output, and timestamp.
    • Required for regulators and internal audit.
  • Case store
    • Persists the KYC case state in your database with residency controls.

Implementation

1) Install AutoGen for TypeScript and define your case model

Use AutoGen’s TypeScript package and keep your KYC state explicit. Do not let the agent “remember” compliance state only in chat history.

npm install @autogenai/autogen openai zod
// kyc-types.ts
export type KycStatus = "pass" | "fail" | "review";

export interface KycCase {
  caseId: string;
  fullName: string;
  dateOfBirth: string;
  country: string;
  documentType: "passport" | "id_card" | "drivers_license";
  documentNumber: string;
  documentExpiry: string;
  address?: string;
}

export interface KycDecision {
  status: KycStatus;
  reasons: string[];
  missingFields: string[];
}

2) Create an assistant agent that performs structured review

AutoGen’s AssistantAgent is the right place for policy reasoning. Keep the system message narrow: it should classify evidence and explain decisions, not invent facts.

// kyc-agent.ts
import { AssistantAgent } from "@autogenai/autogen";
import { z } from "zod";
import type { KycCase } from "./kyc-types";

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

export const kycAgent = new AssistantAgent({
  name: "kyc_verifier",
  systemMessage:
    "You are a KYC verification assistant for a fintech. " +
    "Use only provided case data and policy rules. " +
    "Do not infer missing facts. Return concise compliance decisions."
});

export async function reviewKycCase(input: KycCase) {
  const prompt = `
Review this KYC case against standard onboarding rules.

Case:
${JSON.stringify(input, null, 2)}

Return JSON with:
- status: pass | fail | review
- reasons: array of strings
- missingFields: array of strings
`;

  const result = await kycAgent.run([{ role: "user", content: prompt }]);
  const text = result.messages[result.messages.length - 1].content as string;

  return decisionSchema.parse(JSON.parse(text));
}

3) Add deterministic policy checks before the model decides

This is where fintech teams usually get it wrong. The LLM should not decide whether a passport is expired or whether a sanctioned country is allowed; that belongs in code.

// kyc-policy.ts
import type { KycCase } from "./kyc-types";

const restrictedCountries = new Set(["IR", "KP", "SY"]);

export function applyPolicyRules(input: KycCase) {
  const reasons: string[] = [];
  const missingFields: string[] = [];

  if (!input.fullName) missingFields.push("fullName");
  if (!input.dateOfBirth) missingFields.push("dateOfBirth");
  
  if (restrictedCountries.has(input.country)) {
    reasons.push(`Country ${input.country} is restricted by policy`);
    return { status: "fail" as const, reasons, missingFields };
    }

  const expiry = new Date(input.documentExpiry);
  if (Number.isNaN(expiry.getTime())) {
    reasons.push("Document expiry date is invalid");
    return { status: "review" as const, reasons, missingFields };
    }

  if (expiry < new Date()) {
    reasons.push("Identity document is expired");
    return { status: "fail" as const, reasons, missingFields };
    }

  return { status: "review" as const, reasons, missingFields };
}

4) Orchestrate the workflow and persist an auditable decision

Use a service layer that combines rule-based checks with agent review. In production you would write auditEvent to your database or event bus in-region.

// kyc-service.ts
import { applyPolicyRules } from "./kyc-policy";
import { reviewKycCase } from "./kyc-agent";
import type { KycCase } from "./kyc-types";

async function auditEvent(event: unknown) {
  console.log(JSON.stringify(event));
}

export async function verifyKyc(caseData: KycCase) {
  await auditEvent({
    type: "kyc_case_received",
    caseId: caseData.caseId,
    timestamp: new Date().toISOString()
  });

  
const policyResult = applyPolicyRules(caseData);

if (policyResult.status === "fail") {
    await auditEvent({
      type: "kyc_rejected_by_policy",
      caseId: caseData.caseId,
      reasons: policyResult.reasons
    });
    return policyResult;
}

const agentDecision = await reviewKycCase(caseData);

await auditEvent({
    type: "kyc_review_completed",
    caseId: caseData.caseId,
    decision: agentDecision
});

return agentDecision;
}

Production Considerations

  • Deployment

    • Keep customer data in-region to satisfy data residency requirements.
    • Run the orchestration service inside your regulated VPC or private cluster.
    • Do not send raw PII to third-party logging systems.
  • Monitoring

    • Track approval rate, manual review rate, false reject rate, and average review latency.
    • Alert on spikes in “review” outcomes; that often means upstream OCR or identity capture degraded.
    • Log every model input/output pair with immutable timestamps for audit trails.
  • Guardrails

    • Separate deterministic compliance rules from LLM reasoning.
    • Redact sensitive fields before sending them to non-essential tools.
    • Require human approval for edge cases like politically exposed persons (PEPs), sanctions matches, or conflicting addresses.
  • Model governance

    • Version prompts like code.
    • Keep a rollback path when a prompt change increases false positives.
    • Test against historical onboarding cases before shipping updates.

Common Pitfalls

  1. Letting the model make hard compliance decisions

    • Don’t ask the LLM to decide sanctions eligibility or document validity on its own.
    • Put those checks in code so they are explainable and reproducible.
  2. Skipping audit logging

    • If you can’t reconstruct why a customer was rejected, you have a regulatory problem.
    • Store inputs, outputs, timestamps, rule hits, and operator overrides.
  3. Mixing PII handling with general app telemetry

    • Never dump raw identity data into generic logs or analytics tools.
    • Use field-level redaction and separate storage policies for sensitive records.
  4. Treating “review” as failure

    • “Review” is a valid outcome in fintech onboarding.
    • Route it into an ops queue with clear reason codes instead of dropping the application.

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