How to Build a KYC verification Agent Using LangGraph in Python for wealth management
A KYC verification agent for wealth management takes client onboarding data, checks it against policy and external sources, and decides whether the case can auto-approve, needs manual review, or must be rejected. It matters because wealth firms are dealing with high-value clients, stricter AML/KYC obligations, and a paper trail that has to survive compliance review, audits, and regulator scrutiny.
Architecture
- •
Input normalization layer
- •Takes raw onboarding data from CRM, PDFs, scanned IDs, and form submissions.
- •Converts it into a consistent schema before any checks run.
- •
Policy engine
- •Encodes KYC rules specific to wealth management.
- •Handles thresholds for PEPs, sanctions hits, source-of-funds requirements, beneficial ownership depth, and jurisdiction risk.
- •
Verification tools
- •Calls external services for identity validation, sanctions screening, adverse media, address verification, and document authenticity checks.
- •Returns structured results that the graph can reason over.
- •
Decision graph
- •Uses LangGraph to route cases through verify, escalate, or reject paths.
- •Keeps the control flow explicit and auditable.
- •
Audit trail store
- •Persists every decision, tool call, rule hit, and human override.
- •Required for compliance reviews and model governance.
- •
Human review queue
- •Captures ambiguous or high-risk cases.
- •Lets compliance analysts approve or reject with comments.
Implementation
1) Define the state model and decision outputs
For KYC workflows you want a typed state object that carries the customer profile, verification results, risk score, and final disposition. LangGraph works well here because every node reads and updates the same state in a controlled way.
from typing import TypedDict, Literal, List, Optional
from langgraph.graph import StateGraph, START, END
class KYCState(TypedDict):
customer_id: str
name: str
country: str
pep_match: bool
sanctions_match: bool
adverse_media_hits: int
id_verified: bool
risk_score: int
decision: Optional[Literal["approve", "review", "reject"]]
audit_log: List[str]
2) Add deterministic verification nodes
Keep the first version deterministic. In wealth management you do not want an LLM inventing compliance outcomes; use it later only for summarization if needed. The core routing should be based on explicit checks.
def verify_identity(state: KYCState) -> dict:
# Replace with real vendor calls: Onfido, Trulioo, LexisNexis, etc.
id_verified = state["name"].strip() != "" and len(state["customer_id"]) > 5
return {
"id_verified": id_verified,
"audit_log": state["audit_log"] + [f"identity_verified={id_verified}"]
}
def screen_risk(state: KYCState) -> dict:
risk = 0
if state["country"] in {"IR", "KP", "RU"}:
risk += 50
if state["pep_match"]:
risk += 40
if state["sanctions_match"]:
risk += 100
risk += min(state["adverse_media_hits"] * 10, 30)
return {
"risk_score": risk,
"audit_log": state["audit_log"] + [f"risk_score={risk}"]
}
def decide(state: KYCState) -> dict:
if not state["id_verified"] or state["sanctions_match"]:
decision = "reject"
elif state["risk_score"] >= 50:
decision = "review"
else:
decision = "approve"
return {
"decision": decision,
"audit_log": state["audit_log"] + [f"decision={decision}"]
}
3) Build the LangGraph workflow with conditional routing
This is the part that makes the agent useful. StateGraph gives you an explicit workflow; add_conditional_edges lets you route high-risk cases to human review instead of forcing a binary auto-decision.
from langgraph.graph import StateGraph, START, END
def route_after_screening(state: KYCState) -> str:
if state["sanctions_match"]:
return "reject"
if not state["id_verified"]:
return "review"
if state["risk_score"] >= 50:
return "review"
return "decide"
def manual_review(state: KYCState) -> dict:
# In production this would write to a queue like ServiceNow/Jira/Casebook.
return {
"decision": "review",
"audit_log": state["audit_log"] + ["routed_to_manual_review"]
}
graph = StateGraph(KYCState)
graph.add_node("verify_identity", verify_identity)
graph.add_node("screen_risk", screen_risk)
graph.add_node("decide", decide)
graph.add_node("manual_review", manual_review)
graph.add_edge(START, "verify_identity")
graph.add_edge("verify_identity", "screen_risk")
graph.add_conditional_edges(
"screen_risk",
route_after_screening,
{
"reject": END,
"review": "manual_review",
"decide": "decide",
},
)
graph.add_edge("manual_review", END)
graph.add_edge("decide", END)
app = graph.compile()
4) Run a case through the compiled app
Use .invoke() for synchronous onboarding flows. The output should be written to your audit store immediately after execution so compliance can reconstruct what happened later.
initial_state: KYCState = {
"customer_id": "CUST-102938",
"name": "Jane Doe",
"country": "GB",
"pep_match": False,
"sanctions_match": False,
"adverse_media_hits": 1,
"id_verified": False,
"risk_score": 0,
"decision": None,
"audit_log": [],
}
result = app.invoke(initial_state)
print(result["decision"])
print(result["audit_log"])
Production Considerations
- •
Data residency
- •Wealth firms often need client data to stay in-region.
- •Keep vendor calls and graph execution inside approved jurisdictions where possible.
- •If you use hosted LLMs for summaries or analyst notes, make sure they do not receive raw PII unless your legal team has signed off.
- •
Auditability
- •Persist every node input/output pair with timestamps and case IDs.
- •Store rule versions alongside decisions so you can explain why a client was approved six months later.
- •Treat the audit log as immutable evidence.
- •
Guardrails
- •Never let an LLM make the final approve/reject decision without deterministic checks.
- •Use it only for extraction from unstructured documents or drafting analyst summaries.
- •Hard-block sanction hits from auto-approval paths.
- •
Monitoring
- •Track false positives by country segment, advisor desk, and product type.
- •Alert when review volume spikes or when one vendor starts returning unusually high match rates.
- •Measure time-to-decision because onboarding delays directly affect assets gathered.
Common Pitfalls
- •
Using free-form LLM output as the decision source
- •Don’t ask the model “is this customer compliant?” and trust plain text.
- •Convert model output into structured fields first, then apply policy code on top.
- •
Ignoring case provenance
- •If you do not version rules and vendor responses together, audits become painful fast.
- •Log which screening provider returned which result at which time.
- •
Treating all escalations as equal
- •A missing utility bill is not the same as a sanctions match.
- •Separate low-risk manual review from hard-stop compliance events so analysts do not drown in noise.
- •
Skipping regional controls
- •Wealth management is jurisdiction-sensitive.
- •Build country-based routing early so EU clients do not follow the same data path as US or APAC clients when residency rules differ.
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