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

By Cyprian AaronsUpdated 2026-04-21
kyc-verificationlangchaintypescriptfintech

A KYC verification agent collects identity data, checks it against policy, runs the right verification tools, and returns a decision with an audit trail. For fintech, that matters because onboarding speed is only useful if you can prove compliance, reduce manual review load, and keep sensitive customer data inside the right controls.

Architecture

  • Input normalization layer

    • Takes raw user-submitted fields like name, DOB, address, ID number, and country.
    • Validates shape before any model call so you don’t waste tokens on garbage input.
  • Policy prompt + decision engine

    • Uses a structured prompt to classify the case as approve, reject, or manual_review.
    • Keeps the LLM focused on policy interpretation, not free-form reasoning.
  • Verification tools

    • Calls external services for document validation, sanctions screening, PEP checks, and address verification.
    • Each tool should be deterministic and logged.
  • LangChain orchestration layer

    • Coordinates tool calls and model responses using createOpenAIToolsAgent or a similar agent constructor.
    • Wraps the workflow in a single executable chain.
  • Audit and evidence store

    • Persists every input, tool result, final decision, model version, and timestamp.
    • This is non-negotiable for compliance reviews and dispute handling.
  • Human escalation path

    • Routes ambiguous or high-risk cases to manual review.
    • Prevents false positives from blocking legitimate customers.

Implementation

1) Define the KYC schema and verification tools

Use a strict schema so the agent can only reason over approved fields. Then expose your checks as LangChain tools using DynamicStructuredTool.

import { z } from "zod";
import { DynamicStructuredTool } from "@langchain/core/tools";

export const KycInputSchema = z.object({
  fullName: z.string().min(2),
  dateOfBirth: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
  countryCode: z.string().length(2),
  documentNumber: z.string().min(5),
  address: z.string().min(10),
});

export type KycInput = z.infer<typeof KycInputSchema>;

export const sanctionsCheckTool = new DynamicStructuredTool({
  name: "sanctions_check",
  description: "Checks whether a customer matches sanctions or watchlist records.",
  schema: z.object({
    fullName: z.string(),
    countryCode: z.string(),
    dateOfBirth: z.string(),
  }),
  func: async ({ fullName }) => {
    // Replace with real API call
    const hit = fullName.toLowerCase().includes("test");
    return JSON.stringify({
      match: hit,
      source: "mock-sanctions-provider",
      confidence: hit ? 0.97 : 0.02,
    });
  },
});

export const documentCheckTool = new DynamicStructuredTool({
  name: "document_check",
  description: "Validates government ID details against an external verification service.",
  schema: z.object({
    documentNumber: z.string(),
    countryCode: z.string(),
    fullName: z.string(),
  }),
  func: async ({ documentNumber }) => {
    const valid = documentNumber.length >= 8;
    return JSON.stringify({
      valid,
      source: "mock-doc-verifier",
      reason: valid ? "document format accepted" : "invalid document format",
    });
  },
});

2) Create the agent with LangChain’s actual API

For TypeScript, use ChatOpenAI plus createOpenAIToolsAgent. This keeps tool execution explicit and compatible with production logging.

import { ChatOpenAI } from "@langchain/openai";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { AgentExecutor } from "@langchain/core/agents";
import { createOpenAIToolsAgent } from "langchain/agents";
import { sanctionsCheckTool, documentCheckTool, KycInputSchema } from "./kyc-tools";

const llm = new ChatOpenAI({
  model: "gpt-4o-mini",
  temperature: 0,
});

const prompt = ChatPromptTemplate.fromMessages([
  ["system", `
You are a KYC verification agent for a fintech company.
Follow policy strictly.
Return one of: approve, reject, manual_review.
Always cite which checks were used.
Never invent facts.
If sanctions_check returns a match=true, reject immediately.
If data is incomplete or inconsistent, route to manual_review.
`],
]);

export async function verifyKyc(input: unknown) {
  const parsed = KycInputSchema.parse(input);

  const tools = [sanctionsCheckTool, documentCheckTool];
  const agent = await createOpenAIToolsAgent({
    llm,
    tools,
    prompt,
  });

  const executor = new AgentExecutor({
    agent,
    tools,
    verbose: false,
  });

  return executor.invoke({
    fullName: parsed.fullName,
    dateOfBirth: parsed.dateOfBirth,
    countryCode: parsed.countryCode,
    documentNumber: parsed.documentNumber,
    address: parsed.address,
    input: JSON.stringify(parsed),
   });
}

3) Add a structured decision output

Don’t let the agent return prose. Enforce a machine-readable result so downstream systems can route cases reliably.

import { z } from "zod";

export const KycDecisionSchema = z.object({
  decision: z.enum(["approve", "reject", "manual_review"]),
  reasons: z.array(z.string()),
});

export type KycDecision = z.infer<typeof KycDecisionSchema>;

export function parseKycResult(rawText: string): KycDecision {
  const jsonStart = rawText.indexOf("{");
  

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