How to Build a KYC verification Agent Using LangChain in TypeScript for lending
A KYC verification agent for lending collects borrower identity data, checks it against policy and external evidence, and returns a decision package your underwriting flow can trust. It matters because lending decisions need traceable compliance, not just a yes/no from a model.
Architecture
- •
Input layer
- •Accepts applicant data: name, DOB, address, ID number, business details, and uploaded documents.
- •Normalizes the payload before any model call.
- •
KYC policy engine
- •Encodes lender rules: required fields, jurisdiction-specific checks, risk thresholds, and escalation rules.
- •Keeps policy deterministic so the LLM is not making compliance decisions from scratch.
- •
LangChain agent
- •Orchestrates the workflow using
ChatOpenAI, tools, and structured output. - •Extracts missing fields, classifies document completeness, and drafts an evidence summary.
- •Orchestrates the workflow using
- •
Verification tools
- •Calls services for ID validation, sanctions/PEP screening, address verification, and document OCR.
- •Returns machine-readable results to the agent.
- •
Audit store
- •Persists prompts, tool calls, outputs, timestamps, model version, and final decision.
- •Needed for lending audits and dispute handling.
- •
Decision API
- •Exposes
approved,needs_review, orrejectedplus reasons and evidence references. - •Feeds the underwriting or onboarding system.
- •Exposes
Implementation
1) Define a strict KYC result schema
Use structured output so the agent cannot drift into free-form answers. For lending workflows, you want a stable contract that downstream systems can validate.
import { z } from "zod";
export const KycDecisionSchema = z.object({
applicantId: z.string(),
status: z.enum(["approved", "needs_review", "rejected"]),
riskLevel: z.enum(["low", "medium", "high"]),
missingFields: z.array(z.string()),
checks: z.array(
z.object({
name: z.string(),
result: z.enum(["pass", "fail", "manual_review"]),
evidence: z.string(),
})
),
rationale: z.string(),
});
export type KycDecision = z.infer<typeof KycDecisionSchema>;
2) Build tools for deterministic verification
Keep external checks outside the model. The agent should call tools that return structured data from your KYC vendors or internal services.
import { tool } from "@langchain/core/tools";
import { z } from "zod";
const idCheckTool = tool(
async ({ idNumber }: { idNumber: string }) => {
// Replace with real vendor call
return {
validFormat: true,
watchlistHit: false,
countryMatch: true,
};
},
{
name: "id_check",
description: "Validate government ID format and basic screening results",
schema: z.object({
idNumber: z.string(),
}),
}
);
const addressCheckTool = tool(
async ({ address }: { address: string }) => {
// Replace with real address verification service
return {
deliverable: true,
residencyCountry: "KE",
};
},
{
name: "address_check",
description: "Verify residential address deliverability and residency country",
schema: z.object({
address: z.string(),
}),
}
);
3) Create the LangChain agent with structured output
For this pattern, use ChatOpenAI plus withStructuredOutput. The LLM should interpret evidence and produce the final decision object, not invent facts.
import { ChatOpenAI } from "@langchain/openai";
import { HumanMessage } from "@langchain/core/messages";
import { KycDecisionSchema } from "./kyc-schema";
import { idCheckTool, addressCheckTool } from "./kyc-tools";
const llm = new ChatOpenAI({
modelName: "gpt-4o-mini",
temperature: 0,
});
const kycAgent = llm.bindTools([idCheckTool, addressCheckTool]).withStructuredOutput(
KycDecisionSchema
);
export async function verifyKyc(applicant: {
applicantId: string;
fullName: string;
dateOfBirth?: string;
idNumber?: string;
address?: string;
}) {
const messages = [
new HumanMessage(
JSON.stringify({
instruction:
"Assess KYC completeness for a lending application. Use tools for verification. Do not approve if required fields are missing.",
applicant,
requiredFieldsForLending: ["fullName", "dateOfBirth", "idNumber", "address"],
complianceRule:
"If identity or residency cannot be verified, mark needs_review or rejected.",
})
),
];
const result = await kycAgent.invoke(messages);
return result;
}
4) Add audit logging around every decision
In lending, you need to reconstruct what happened later. Store raw inputs, tool outputs, model version, and final decision in an immutable log.
type AuditRecord = {
requestId: string;
applicantId: string;
}
export async function runKycWithAudit(applicant: any) {
const requestId = crypto.randomUUID();
const startedAt = new Date().toISOString();
const decision = await verifyKyc(applicant);
const auditRecord = {
requestId,
applicantId: applicant.applicantId,
startedAt,
modelName: "gpt-4o-mini",
decision,
};
console.log(JSON.stringify(auditRecord));
return decision;
}
Production Considerations
- •
Deploy in-region
- •If your lender operates under data residency rules, keep PII processing in the required jurisdiction.
- •Pin vendor endpoints and storage to approved regions.
- •
Log everything needed for audit
- •Persist prompts, tool responses, model version, timestamps, and final disposition.
- •Make logs tamper-evident; lenders get asked to justify adverse decisions later.
- •
Use hard guardrails before model calls
Wait let's craft bullets properly? Need no extra weird. Let's finalize carefully.
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