How to Build a KYC verification Agent Using LangChain in TypeScript for fintech
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, ormanual_review. - •Keeps the LLM focused on policy interpretation, not free-form reasoning.
- •Uses a structured prompt to classify the case as
- •
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
createOpenAIToolsAgentor a similar agent constructor. - •Wraps the workflow in a single executable chain.
- •Coordinates tool calls and model responses using
- •
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
- •The complete AI Agents Roadmap — my full 8-step breakdown
- •Free: The AI Agent Starter Kit — PDF checklist + starter code
- •Work with me — I build AI for banks and insurance companies
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