AI-Powered-KYC
- Why I Spent 1 Month Building an AI That Does in 8 Seconds What Banks Take 3 Days to Do
- What Is KYC and Why Does It Still Feel Like 2003?
- What I Actually Built
- The System Architecture
- Part 1: Why I Chose LangGraph for the AI Pipeline
- Part 2: The OCR Problem Nobody Talks About
- Part 3: The FastAPI Backend — Designed for Scale from Day One
- Part 4: The Admin Workflow — Where AI Meets Human Judgment
- Part 5: The React Frontend — Making Complex Simple
- The Infrastructure: One Command, Full Stack
- What This Solves in the Real World
- The Hard Lessons (The Real Ones)
- What’s Next
- The Full Stack, for Reference
- Explore the Code
- Let’s Talk
Why I Spent 1 Month Building an AI That Does in 8 Seconds What Banks Take 3 Days to Do
A deep-dive into building a production-grade KYC verification platform with LangGraph, FastAPI, EasyOCR, and React — and the frustrating real-world problem that made me start.
Let me tell you something that happened to a colleague of mine last year.
He was trying to open a business account for a client engagement. Simple stuff. He had a passport, utility bill, and company registration documents ready to go. The bank’s online portal accepted all three uploads. He got a confirmation email. Then — nothing. Four days later, a generic email arrived: “Your documents could not be verified. Please resubmit.” No reason. No specifics. Just resubmit.
He resubmitted the same documents. Five days later — approved.
Nothing had changed. Same documents. Same person. Somewhere in that bank, a human being manually looked at the same scanned PDFs twice, and the second time, they decided it was fine. That’s not a compliance process. That’s a coin flip wrapped in bureaucracy.
I’ve been working in enterprise operations and digital transformation long enough to know that this isn’t an isolated story. It’s the norm. Across financial services, fintech, crypto, lending platforms, and insurance — KYC is still largely a manual, error-prone, days-long process that costs companies billions and frustrates customers at exactly the moment you want to earn their trust: onboarding.
So I decided to build something better.
What Is KYC and Why Does It Still Feel Like 2003?
For those unfamiliar — KYC (Know Your Customer) is a regulatory requirement. Any company in financial services, fintech, crypto, or insurance is legally obligated to verify the identity of their customers before doing business with them. The goal is to prevent money laundering, terrorism financing, fraud, and a dozen other financial crimes.
The process typically involves:
- Collecting identity documents (passport, national ID, driver’s license)
- Verifying proof of address (utility bills, bank statements)
- Running sanctions and PEP (Politically Exposed Persons) screenings
- Making a compliance decision: approve, reject, or escalate for manual review
Sounds straightforward. In practice, it looks like this:
That’s the reality in most mid-to-large institutions today. Every single box in that diagram is a human being doing something that a well-designed system should do automatically — and doing it slower, less consistently, and with no audit trail that anyone trusts.
The cost? According to Thomson Reuters, financial institutions spend an average of $60 million per year on KYC compliance. Some spend over $500 million. And the average customer onboarding time is 24 days for corporate clients.
Twenty-four days. In 2026.
That’s why I built this.
What I Actually Built
I built a full-stack KYC verification platform that automates the entire process from document upload to compliance decision — using an 8-node AI agent pipeline, real OCR, fraud detection, and a structured admin review workflow.
Here’s what the new process looks like:
flowchart TD
A[Customer uploads documents via portal] --> B[FastAPI receives files]
B --> C[Celery dispatches to AI pipeline]
C --> D[LangGraph 8-Node Pipeline starts]
D --> D1[Node 1: Ingestion & Validation]
D1 --> D2[Node 2: OCR & Image Processing]
D2 --> D3[Node 3: Document Analysis & Fraud Scoring]
D3 --> D4[Node 4: Compliance & Sanctions Check]
D4 --> D5[Node 5: Government ID Validation]
D5 --> D6[Node 6: Cross-Reference Identity]
D6 --> D7[Node 7: Generate Final Report]
D7 --> D8[Node 8: Decision Engine]
D8 --> E{Decision}
E -->|High confidence| F[APPROVED ✓]
E -->|Fraud indicators| G[REJECTED ✗]
E -->|Edge case| H[MANUAL REVIEW ⚠]
F --> I[Customer notified in real-time]
G --> I
H --> J[Admin reviews flagged case]
J --> I
style D fill:#1a1a2e,color:#fff
style F fill:#155724,color:#fff
style G fill:#721c24,color:#fff
style H fill:#856404,color:#fff
The entire automated path — from upload to decision — takes under 15 seconds. For edge cases that require human review, an admin can process the case in under 2 minutes with full AI-assisted context, instead of starting from scratch.
Let me walk you through how every piece of this actually works.
The System Architecture
Before diving into code, here’s the full picture of how every component talks to each other:
graph TB
subgraph FRONTEND["Frontend Layer (React 18 + Vite)"]
CP[Customer Portal]
AD[Admin Dashboard]
RD[Reviewer Dashboard]
end
subgraph API["API Layer (FastAPI + Python 3.11)"]
AUTH[Auth Service\nJWT + RBAC]
KYC_EP[KYC Endpoints]
DOC_EP[Document Endpoints]
ADMIN_EP[Admin Endpoints]
end
subgraph PIPELINE["AI Pipeline (LangGraph)"]
ING[Ingestion Agent]
OCR_NODE[Document Processing\nEasyOCR + OpenCV]
ANALYSIS[Document Analysis\nFraud Detection]
COMP[Compliance Review\nSanctions + PEP]
GOV[Government Validation]
IDENT[Identity Verification]
REPORT[Final Report Generator]
DECISION[Decision Engine]
ERR[Error Handler\nRetry Logic]
end
subgraph ASYNC["Async Layer"]
CELERY[Celery Workers]
REDIS[(Redis\nBroker + Cache)]
FLOWER[Flower\nMonitoring]
end
subgraph DATA["Data Layer"]
PG[(PostgreSQL\nAsyncpg)]
FILES[File Storage\nUploads]
PGADMIN[pgAdmin UI]
end
subgraph INFRA["Infrastructure"]
NGINX[Nginx\nReverse Proxy]
DOCKER[Docker Compose\n10 Services]
end
CP & AD & RD --> NGINX
NGINX --> API
AUTH --> PG
KYC_EP --> CELERY
DOC_EP --> FILES
ADMIN_EP --> PIPELINE
CELERY --> REDIS
CELERY --> PIPELINE
ING --> OCR_NODE --> ANALYSIS --> COMP --> GOV --> IDENT --> REPORT --> DECISION
ANALYSIS -.-> ERR
COMP -.-> ERR
PIPELINE --> PG
FLOWER --> CELERY
Five concerns, clearly separated. The frontend never talks to the database. The pipeline never talks to the frontend directly. Every async job goes through Redis. Let me explain why each of these decisions matters.
Part 1: Why I Chose LangGraph for the AI Pipeline
When I started planning this, I had a choice to make. I could write a simple sequential Python script:
# The naive approach
def process_kyc(submission):
text = extract_ocr(submission.documents)
fraud = check_fraud(text)
compliance = check_compliance(text)
decision = make_decision(fraud, compliance)
return decision
That works for a demo. It falls apart in production. There’s no state tracking, no error recovery, no retry logic, no ability to resume from a specific step if something fails, and no visibility into what stage a submission is currently in.
LangGraph solves all of this. It’s a graph-based orchestration framework built on top of LangChain that lets you define stateful, multi-agent workflows as directed graphs. Each node in the graph is a Python function that reads from and writes to a shared state object.
Here’s how the pipeline state is defined:
from langgraph.graph import StateGraph, END
from typing import TypedDict, Optional, List, Dict, Any
class KYCAgentState(TypedDict):
# Submission context
submission_id: str
user_id: str
documents: List[Dict[str, Any]]
# OCR results
extracted_text: Optional[str]
ocr_confidence: Optional[float]
extracted_fields: Optional[Dict[str, str]] # name, dob, id_number, address
# Fraud analysis
name_match_score: Optional[float]
dob_match: Optional[bool]
id_format_valid: Optional[bool]
fraud_indicators: List[str]
fraud_risk_level: Optional[str] # LOW / MEDIUM / HIGH / CRITICAL
# Compliance
sanctions_clear: Optional[bool]
pep_check_clear: Optional[bool]
high_risk_country: Optional[bool]
compliance_result: Optional[Dict[str, Any]]
# Government validation
gov_validation_result: Optional[Dict[str, Any]]
gov_validation_passed: Optional[bool]
# Final output
identity_verified: Optional[bool]
final_report: Optional[Dict[str, Any]]
final_decision: Optional[str] # APPROVED | REJECTED | MANUAL_REVIEW
decision_reason: Optional[str]
confidence_score: Optional[float]
# Error handling
error_count: int
last_error: Optional[str]
retry_node: Optional[str]
Forty-plus fields, typed, documented, and flowing through every node. This is what gives you full auditability — at any point in time, you can inspect exactly what the pipeline has learned about a submission.
The graph itself is wired up like this:
def build_kyc_pipeline() -> StateGraph:
workflow = StateGraph(KYCAgentState)
# Register all nodes
workflow.add_node("ingestion", ingestion_agent)
workflow.add_node("document_processing", document_processing_agent)
workflow.add_node("document_analysis", document_analysis_agent)
workflow.add_node("compliance_review", compliance_review_agent)
workflow.add_node("government_validation", government_validation_agent)
workflow.add_node("identity_verification", identity_verification_agent)
workflow.add_node("final_report", final_report_agent)
workflow.add_node("decision", decision_agent)
workflow.add_node("error_handler", error_handler_agent)
# Define the happy path
workflow.set_entry_point("ingestion")
workflow.add_edge("ingestion", "document_processing")
workflow.add_edge("document_processing", "document_analysis")
workflow.add_edge("document_analysis", "compliance_review")
workflow.add_edge("compliance_review", "government_validation")
workflow.add_edge("government_validation", "identity_verification")
workflow.add_edge("identity_verification", "final_report")
workflow.add_edge("final_report", "decision")
workflow.add_edge("decision", END)
# Conditional routing: errors go to the error handler
workflow.add_conditional_edges(
"document_processing",
route_on_error,
{"continue": "document_analysis", "error": "error_handler"}
)
workflow.add_edge("error_handler", "ingestion") # retry from start
return workflow.compile()
Here’s the complete pipeline flow as a Mermaid diagram:
Part 2: The OCR Problem Nobody Talks About
Everyone says “just use OCR.” Nobody talks about what OCR actually looks like when real humans photograph real documents.
I’ve seen:
- Passports photographed at a 30-degree angle under fluorescent lighting
- National IDs where the laminate reflects and washes out half the text
- Utility bills that were scanned at 72 DPI and then JPEG-compressed twice
- PDFs that are technically text-based but the font encoding is garbage
EasyOCR handles all of these — but you have to help it. Here’s the preprocessing pipeline I built:
import easyocr
import cv2
import numpy as np
from PIL import Image
import pdf2image
import fitz # PyMuPDF
# The model takes 3-4 seconds to load.
# Cache it at module level — never initialize per-request.
_reader: easyocr.Reader | None = None
def get_ocr_reader() -> easyocr.Reader:
global _reader
if _reader is None:
_reader = easyocr.Reader(['en'], gpu=False, verbose=False)
return _reader
def preprocess_for_ocr(image: Image.Image) -> np.ndarray:
"""
Real documents are messy. This pipeline makes them OCR-friendly.
"""
img = np.array(image.convert('RGB'))
# Step 1: Grayscale — colour information hurts OCR
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
# Step 2: Adaptive thresholding — handles uneven lighting
# (regular threshold fails when one corner is shadowed)
thresh = cv2.adaptiveThreshold(
gray, 255,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 11, 2
)
# Step 3: 2x upscale — dramatically improves accuracy on small print
# Cubic interpolation preserves edge sharpness
resized = cv2.resize(
thresh, None, fx=2, fy=2,
interpolation=cv2.INTER_CUBIC
)
return resized
def extract_text_from_document(file_path: str) -> tuple[str, float]:
"""
Returns extracted text and a confidence score (0-1).
Handles images (JPG/PNG) and PDFs.
"""
reader = get_ocr_reader()
if file_path.lower().endswith('.pdf'):
# Convert each PDF page to image, process separately
images = pdf2image.convert_from_path(file_path, dpi=200)
all_results = []
for img in images:
processed = preprocess_for_ocr(img)
results = reader.readtext(processed)
all_results.extend(results)
else:
img = Image.open(file_path)
processed = preprocess_for_ocr(img)
all_results = reader.readtext(processed)
# Extract text and compute average confidence
texts = [r[1] for r in all_results if r[2] > 0.3] # filter low-confidence tokens
confidences = [r[2] for r in all_results if r[2] > 0.3]
full_text = ' '.join(texts)
avg_confidence = sum(confidences) / len(confidences) if confidences else 0.0
return full_text, avg_confidence
Once I have text, I need to extract structured fields. Each document type has a different layout — a passport looks nothing like a utility bill. I use regex patterns per document type:
import re
from rapidfuzz import fuzz
FIELD_PATTERNS = {
"passport": {
"name": r"surname[:\s]+([A-Z\s]+)\n?given[:\s]+names?[:\s]+([A-Z\s]+)",
"dob": r"date\s+of\s+birth[:\s]+(\d{2}\s+\w+\s+\d{4}|\d{2}/\d{2}/\d{4})",
"id_number": r"passport\s+n[o°][:\s]*([A-Z0-9]{6,9})",
},
"national_id": {
"name": r"(?:name|nom)[:\s]+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)",
"dob": r"(?:dob|born|birth)[:\s]+(\d{2}[-/]\d{2}[-/]\d{4})",
"id_number": r"(?:id\s*no?|number)[:\s]*([A-Z0-9\-]{8,15})",
},
# ... patterns for driver's license, utility bill, etc.
}
def match_name_against_claimed(extracted: str, claimed: str) -> float:
"""
'John O\'Brien' and 'JOHN OBRIEN' should match.
'John Smith' and 'Jane Smith' should not.
Average of two fuzzy algorithms for robustness.
"""
ratio = fuzz.ratio(extracted.lower(), claimed.lower())
token_sort = fuzz.token_sort_ratio(extracted.lower(), claimed.lower())
return (ratio + token_sort) / 2.0
The fraud detection logic accumulates multiple signals into a composite risk score:
One thing I learned the hard way: the threshold for name matching matters enormously. My first pass used 80 as the cutoff. Legitimate users with hyphenated surnames, diacritical marks (é, ñ, ü), or names printed differently across documents (middle name included on one, omitted on another) kept triggering false positives. I spent two days testing against a diverse name dataset and landed on 75 as the right balance. Not everything about ML systems is glamorous.
Part 3: The FastAPI Backend — Designed for Scale from Day One
I’ll be honest: I’ve worked on enough Python backends to know that sync Django or Flask with a SQLite database will get you to demo day. It will not survive your first real traffic spike.
This backend is built async from the ground up.
from fastapi import FastAPI, Depends, UploadFile, File, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.ext.asyncio import AsyncSession
from typing import List
import aiofiles
app = FastAPI(
title="KYC Verification Platform",
version="1.0.0",
docs_url="/api/docs",
)
@app.post("/api/v1/kyc/submit", response_model=KYCSubmissionResponse)
async def submit_kyc_application(
files: List[UploadFile] = File(...),
submission_data: KYCSubmissionCreate = Depends(),
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_async_db),
):
# Validate file types before touching the filesystem
for file in files:
if not is_allowed_file_type(file.content_type):
raise HTTPException(400, f"File type {file.content_type} not allowed")
# Save files asynchronously — never block the event loop on I/O
saved_paths = []
for file in files:
path = await save_upload_file(file)
saved_paths.append(path)
# Create the submission record
submission = await kyc_service.create_submission(
db=db,
user_id=current_user.id,
data=submission_data,
file_paths=saved_paths,
)
# Dispatch to Celery — this returns immediately
# The pipeline runs in the background, results stored in DB
process_kyc_submission.delay(str(submission.id))
return submission
The key decisions here:
Why asyncpg over psycopg2? asyncpg is a pure-async PostgreSQL driver. psycopg2 is synchronous — even with threading, it blocks. In a high-concurrency FastAPI app, a single slow DB query with psycopg2 can stall the entire event loop. asyncpg doesn’t have this problem.
Why Celery + Redis instead of FastAPI BackgroundTasks? FastAPI’s built-in BackgroundTasks are tied to the request lifecycle. If the server restarts mid-processing, the job is lost. Celery jobs are persisted in Redis — they survive restarts, can be retried, monitored via Flower, and distributed across multiple workers. For anything involving real document processing, you want Celery.
Here’s the authentication setup — I deliberately kept it simple and removed unnecessary abstractions:
from datetime import datetime, timedelta
import jwt
import bcrypt
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer
SECRET_KEY = settings.JWT_SECRET_KEY
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
REFRESH_TOKEN_EXPIRE_DAYS = 7
def create_access_token(user_id: str, role: str) -> str:
payload = {
"sub": user_id,
"role": role,
"exp": datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES),
"type": "access",
}
return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
def hash_password(password: str) -> str:
return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
def verify_password(plain: str, hashed: str) -> bool:
return bcrypt.checkpw(plain.encode(), hashed.encode())
# Role-based guard — dead simple, composable via Depends()
async def require_admin(current_user: User = Depends(get_current_active_user)) -> User:
if current_user.role != "admin":
raise HTTPException(status.HTTP_403_FORBIDDEN, "Admin access required")
return current_user
I originally used passlib and python-jose for this. Both have had maintenance issues and dependency conflicts. PyJWT + bcrypt directly is cleaner, faster, and has no baggage. Sometimes the boring decision is the right one.
Part 4: The Admin Workflow — Where AI Meets Human Judgment
Here’s something most automated KYC systems get wrong: they treat “MANUAL_REVIEW” as a dead end. The case goes into a queue, a human picks it up, opens the original documents in one window and the compliance checklist in another, and does everything from scratch.
I designed the admin workflow differently. When a case is flagged for manual review, the AI has already done most of the work. The admin sees:
- All extracted document fields (name, DOB, ID number, address)
- OCR confidence scores
- Fraud indicator breakdown with specific reasons
- Compliance check results (sanctions, PEP, high-risk country)
- Government validation outcome
- A recommended decision with confidence score
The admin’s job is to review and override — not to restart from zero.
The five admin review stages run sequentially:
- Document Analysis Review — validate OCR extraction, correct any misread fields
- Compliance Review — sanctions screening, PEP check, high-risk country flag
- Government Validation — ID number format check, issuing authority verification
- Identity Verification — cross-reference all data points against each other
- Final Report — structured decision summary with all supporting evidence
Every action generates an audit log entry. Every state change creates a timeline event. If anyone ever asks “who approved this, when, and based on what?” — the answer is in the database, timestamped, immutable.
Part 5: The React Frontend — Making Complex Simple
I’ve seen compliance dashboards that look like they were designed by someone who really loves spreadsheets. I wanted something different — clean, fast, and actually informative.
The frontend is React 18 with TypeScript, Vite for builds (it’s meaningfully faster than webpack for this project size), TanStack Query for server state, and Tailwind for styling.
The trickiest part was showing real-time pipeline progress without WebSockets. I used smart polling with TanStack Query:
import { useQuery } from '@tanstack/react-query';
import { kycService } from '@/services/kycService';
const TERMINAL_STATES = ['APPROVED', 'REJECTED', 'MANUAL_REVIEW'] as const;
type TerminalState = typeof TERMINAL_STATES[number];
function useSubmissionProgress(submissionId: string) {
return useQuery({
queryKey: ['submission', submissionId],
queryFn: () => kycService.getSubmission(submissionId),
// The magic: poll every 2s until we reach a terminal state
refetchInterval: (query) => {
const status = query.state.data?.status;
if (status && TERMINAL_STATES.includes(status as TerminalState)) {
return false; // Stop polling — we have our answer
}
return 2000; // Keep polling every 2 seconds
},
// Stale immediately — we always want fresh pipeline state
staleTime: 0,
});
}
The progress bar component maps each pipeline stage to a visual step:
const PIPELINE_STAGES = [
{ key: 'ingestion', label: 'Validating Documents' },
{ key: 'document_processing', label: 'Extracting Text' },
{ key: 'document_analysis', label: 'Analysing Fraud Risk' },
{ key: 'compliance_review', label: 'Compliance Screening' },
{ key: 'government_validation', label: 'Government Validation' },
{ key: 'identity_verification', label: 'Identity Cross-Check' },
{ key: 'final_report', label: 'Generating Report' },
{ key: 'decision', label: 'Final Decision' },
];
Every 2 seconds, the UI updates. The progress bar advances. When the decision arrives, the status card flips from a spinner to either a green “Approved” badge, a red “Rejected” badge with reason, or an amber “Under Review” badge with an estimated wait time.
From the user’s perspective, this feels instantaneous.
The Infrastructure: One Command, Full Stack
The entire system — 10 services — runs with a single command:
docker compose up --build
Here’s the service topology:
Health checks and restart policies are configured for every service. The database waits for healthy before the backend starts. The backend waits for healthy before workers start. No race conditions on startup.
What This Solves in the Real World
Let me be specific about the impact, because “AI automation” is an overused phrase.
Before this system, a typical KYC process in a mid-size fintech:
- Average time to decision: 2-5 business days
- Manual review cost: ~$15-50 per application (analyst time)
- Error rate from manual inspection: 5-8% (missed fraud flags, misread document fields)
- Audit trail: inconsistent, often just email chains
- Customer experience: “We’ll get back to you” — then silence
After this system, for the same applications:
- Automated path (80-85% of cases): under 15 seconds
- Manual review path (15-20% of cases): under 5 minutes with AI-assisted context
- Error rate: Near zero on the automated path — the pipeline either extracts data or flags it as low-confidence
- Audit trail: Complete, timestamped, queryable — every state change, every decision, every override
- Customer experience: Real-time progress bar, instant notification
For a fintech processing 10,000 KYC applications per month, this represents:
- $100,000-$400,000/month in analyst time saved (based on industry averages)
- Faster onboarding → lower drop-off rates → direct revenue impact
- Consistent fraud detection → reduced financial crime exposure
- Full audit compliance → no more scrambling to reconstruct decision chains for regulators
The real-time impact isn’t just speed. It’s that compliance decisions are now explainable. Every “REJECTED” comes with a reason. Every “APPROVED” has a full evidence trail. Every “MANUAL_REVIEW” gives the human reviewer exactly the context they need to make an informed decision — not just a stack of PDFs and a gut feeling.
The Hard Lessons (The Real Ones)
I’m not going to pretend this was smooth. Here are the things that actually cost me time:
EasyOCR model loading. The first request after a cold start took 6 seconds — all of it was the model loading from disk. I spent half a day debugging what I thought was a database issue before realising the OCR reader was being instantiated per request. One module-level singleton fixed it completely.
Async SQLAlchemy session scoping. This one was subtle. I had a bug where the same AsyncSession was being shared across two concurrent requests (because of how I’d set up a module-level session factory). It caused intermittent race conditions that only appeared under load testing. The fix — strictly scoping sessions per request via FastAPI’s Depends() — was one line of code. Finding it was three days.
LangGraph state serialization. I initially passed PIL Image objects through the pipeline state. This worked fine until I tried to add checkpointing (so failed pipelines could resume). PIL images aren’t JSON-serializable. I had to refactor every node to pass file paths instead of image objects. Always design your state for serialization from day one.
Fuzzy matching thresholds. I set the name matching threshold at 80 and watched it reject users named “María José García-López” whose scanned ID rendered as “MARIA JOSE GARCIA LOPEZ”. Real names are messy. International names even more so. Test your thresholds with diverse data, not just English names.
PDF rendering. pdf2image needs poppler installed as a system dependency. This is fine on Linux. On Windows it requires a manual PATH setup. The Docker container handles it automatically, but it caught me off guard during local development and took an hour to diagnose.
What’s Next
This is version 1. There are several things I want to build next:
- Real compliance provider integration — replace the mock sanctions/PEP provider with Mastercard MATCH, WorldCheck, or Refinitiv. The provider abstraction pattern is already in place — swapping implementations is a one-file change.
- WebSocket-based updates — polling works, but WebSockets would eliminate the 2-second lag and reduce unnecessary API calls by 90%.
- ML-based document classification — currently users select their document type. A classification model would detect it automatically from the image.
- Liveness detection — for selfie verification, add a step that checks the photo is of a live person, not a photo of a photo.
- Multi-language OCR — EasyOCR supports 80+ languages. Adding Arabic, Chinese, and Hindi document support is mainly a configuration change.
The Full Stack, for Reference
| Layer | Technology | Why |
|---|---|---|
| Frontend | React 18, TypeScript, Vite | Fast builds, type safety, modern DX |
| State Management | TanStack Query v5 | Server state, smart polling, caching |
| Styling | Tailwind CSS | Utility-first, no CSS files to maintain |
| Backend | FastAPI, Python 3.11+ | Async-native, auto-docs, fast |
| Validation | Pydantic v2 | 5-10x faster than v1, strict types |
| AI Pipeline | LangGraph + LangChain | Stateful multi-agent orchestration |
| LLM | OpenAI / Google Gemini | Provider-abstracted, swappable |
| OCR | EasyOCR + OpenCV + PyMuPDF | Handles real-world document quality |
| Fuzzy Match | RapidFuzz | Fast, accurate name normalization |
| Database | PostgreSQL 15+ + asyncpg | Async-native, production-grade |
| ORM | SQLAlchemy 2.0 + Alembic | Async sessions, schema migrations |
| Task Queue | Celery 5.4 + Redis 7 | Reliable background processing |
| Auth | PyJWT + bcrypt | Simple, maintained, no baggage |
| Infrastructure | Docker Compose + Nginx | One-command full stack |
| Monitoring | Flower + pgAdmin | Celery jobs + DB visibility |
| Distribution | PyInstaller + Electron | Desktop app packaging |
Explore the Code
Everything discussed here is in the GitHub repository — the LangGraph pipeline, OCR utilities, fraud detection, admin workflow, Docker setup, and the React frontend.
GitHub:
github.com/sairajboddula/TrustLens-AI
If something here helped you, a star on the repo goes a long way. If something is broken or you see a better approach, open an issue — I read every one.
Let’s Talk
I’m genuinely interested in connecting with people working on similar problems — identity verification, compliance automation, document AI, or production LangGraph systems. These are hard problems and I’m still figuring a lot of it out.
Find me on LinkedIn:
linkedin.com/in/sairaj-boddula
If you’re building in fintech or regtech and dealing with KYC at scale, drop me a message. Especially if you’ve found a better approach to any of the OCR or fuzzy matching challenges — I’d genuinely love to compare notes.
Thanks for making it this far. If this post saved you a week of architecture decisions, clap a few times — it helps other engineers find it. If you have questions, drop them in the comments and I’ll answer every one.
Tags: Python FastAPI LangGraph AI KYC React TypeScript Fintech Document AI LangChain Compliance OCR Async Python Open Source
Comments
Post a Comment