How to Build a KYC verification Agent Using LangGraph in TypeScript for lending
A KYC verification agent for lending collects applicant data, checks it against policy, validates identity documents, screens for sanctions/PEP hits, and decides whether the case can be auto-approved, routed to manual review, or rejected. For lending, this matters because onboarding speed affects conversion, but weak KYC creates compliance exposure, bad audit trails, and downstream credit risk.
Architecture
- •
Input intake node
- •Accepts applicant payloads: name, DOB, address, ID document metadata, consent flags.
- •Normalizes fields before any checks run.
- •
Document verification node
- •Validates document presence and basic integrity.
- •Calls OCR or document extraction service if needed.
- •
Screening node
- •Checks sanctions, PEP, adverse media, and internal watchlists.
- •Returns structured matches with confidence scores.
- •
Policy decision node
- •Applies lending-specific rules:
- •required fields present
- •jurisdiction constraints
- •risk thresholds
- •manual review triggers
- •Applies lending-specific rules:
- •
Audit/logging node
- •Persists every decision and evidence reference.
- •Stores immutable trace for compliance review.
- •
Human review handoff
- •Routes borderline cases to an ops queue with a reason code.
- •Prevents the agent from making unsupported approvals.
Implementation
1) Define state and build the graph
Use LangGraph’s StateGraph with a typed state object. Keep the state explicit; lending workflows need a clean audit trail of inputs, outputs, and reasons for every transition.
import { StateGraph, START, END } from "@langchain/langgraph";
type KycStatus = "pass" | "review" | "fail";
type KycState = {
applicantId: string;
fullName: string;
dateOfBirth: string;
country: string;
idDocumentUrl?: string;
consentGiven: boolean;
ocrText?: string;
sanctionsHit?: boolean;
pepHit?: boolean;
riskScore?: number;
status?: KycStatus;
reasonCodes?: string[];
};
const graph = new StateGraph<KycState>()
.addNode("validateInput", async (state) => {
const reasonCodes: string[] = [];
if (!state.consentGiven) reasonCodes.push("MISSING_CONSENT");
if (!state.fullName || !state.dateOfBirth || !state.country) {
reasonCodes.push("MISSING_REQUIRED_FIELDS");
}
return {
...state,
reasonCodes,
status: reasonCodes.length ? "review" : state.status,
};
})
.addNode("screening", async (state) => {
// Replace with real screening service calls.
const sanctionsHit = state.fullName.toLowerCase().includes("test");
const pepHit = state.country === "IR";
const riskScore = (sanctionsHit ? 80 : 10) + (pepHit ? 20 : 0);
return { ...state, sanctionsHit, pepHit, riskScore };
})
2) Add policy logic and routing
This is where lending rules live. Keep the decision function deterministic so compliance can explain why a borrower was approved or escalated.
function routeDecision(state: KycState): "autoApprove" | "manualReview" | "reject" {
if (state.reasonCodes?.includes("MISSING_CONSENT")) return "manualReview";
if (state.sanctionsHit) return "reject";
if ((state.riskScore ?? 0) >= 50) return "manualReview";
return "autoApprove";
}
const app = graph
.addNode("policyDecision", async (state) => {
const route = routeDecision(state);
if (route === "reject") {
return {
...state,
status: "fail",
reasonCodes: [...(state.reasonCodes ?? []), "SANCTIONS_OR_HIGH_RISK"],
};
}
if (route === "manualReview") {
return {
...state,
status: "review",
reasonCodes: [...(state.reasonCodes ?? []), "ESCALATED_FOR_REVIEW"],
};
}
return { ...state, status: "pass" };
})
3) Add audit logging and compile the workflow
Persist the final state plus evidence references. In production, write this to an append-only store so auditors can reconstruct the path later.
const saveAuditRecord = async (state: KycState) => {
console.log(JSON.stringify({
applicantId: state.applicantId,
status: state.status,
reasonCodes: state.reasonCodes ?? [],
sanctionsHit: state.sanctionsHit ?? false,
pepHit: state.pepHit ?? false,
riskScore: state.riskScore ?? null,
timestamp: new Date().toISOString(),
}));
};
const workflow = app
.addNode("audit", async (state) => {
await saveAuditRecord(state);
return state;
})
4) Wire edges and run it
Use addEdge for linear flow. If you need branching later, move to conditional edges; the core pattern stays the same.
workflow
.addEdge(START, "validateInput")
workflow.addEdge("validateInput", "screening")
workflow.addEdge("screening", "policyDecision")
workflow.addEdge("policyDecision", "audit")
workflow.addEdge("audit", END)
const compiled = workflow.compile();
const result = await compiled.invoke({
applicantId: "app_123",
fullName: "Jane Doe",
dateOfBirth: "1991-03-11",
country: "GB",
consentGiven: true,
});
console.log(result.status);
Production Considerations
- •
Deploy in-region
- •
Keep applicant PII inside approved data residency boundaries. If your lending book is EU-based, don’t ship identity data to a US-hosted model endpoint unless your legal team has signed off on transfer controls.
- •
Log decisions with evidence IDs
- •
Store which screening provider responded, which rule fired, and which document version was used. Regulators care about reproducibility more than model elegance.
- •
Add hard guardrails before any approval
- •
The agent should never auto-approve when consent is missing, sanctions screening fails open, or required identity fields are incomplete.
- •
Monitor false positives by jurisdiction
- •
Sanctions/PEP hit rates vary by geography. Track manual-review rates per country so you can tune thresholds without weakening compliance controls.
Common Pitfalls
- •
Using free-form LLM output for final decisions
- •Don’t let the model emit “approve” or “reject” directly.
- •Use structured state plus deterministic policy code for the final call.
- •
Skipping audit-grade reasons
- •“The agent decided to review” is not enough.
- •Emit stable reason codes like
MISSING_CONSENT,SANCTIONS_HIT, andESCALATED_FOR_REVIEW.
- •
Treating all jurisdictions the same
- •Lending compliance changes by country and product type.
- •Encode residency rules, document requirements, and screening thresholds per market instead of hardcoding one global policy.
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