Exchange KYC/AML System Development
KYC/AML for crypto exchange is not a form "enter your passport". It is a comprehensive system that solves regulatory licensing requirements, manages risks, and does not kill conversion rate. Most exchanges lose 40-60% of users at KYC funnel — this is an architecture and UX problem, not just regulation.
Tiered KYC Structure
Industry standard is multi-level verification:
Tier 0 (no KYC): platform browsing only. No transactions.
Tier 1 (Email + AML check): up to $1,000 USD equivalent per month. Crypto deposit and withdrawal only. Automatic wallet screening via Chainalysis or Elliptic on each deposit. Verification time: < 1 minute.
Tier 2 (Full KYC): up to $50,000/month. Passport + liveness check. Provider: Sumsub, Onfido, Jumio. Time: automatically 2-5 minutes, manual review up to 24 hours for complex cases. Unlocked: fiat deposit/withdrawal, unlimited crypto withdrawal.
Tier 3 (Enhanced Due Diligence): unlimited. Source of Funds + Source of Wealth + extended background check. VIP clients only, manual processing by compliance officer.
KYC Service Architecture
// KYC State Machine
enum KYCStatus {
NOT_STARTED = 'not_started',
PENDING = 'pending',
UNDER_REVIEW = 'under_review',
APPROVED = 'approved',
REJECTED = 'rejected',
RESUBMISSION_REQUIRED = 'resubmission_required',
}
interface UserKYCProfile {
userId: string;
tier: 0 | 1 | 2 | 3;
status: KYCStatus;
applicantId?: string; // Sumsub applicant ID
approvedAt?: Date;
expiresAt?: Date; // re-verification every N years
riskScore: number; // 0-100, calculated on approval
pep: boolean; // Politically Exposed Person
sanctioned: boolean;
walletRiskLevels: Map<string, WalletRisk>; // cached wallet assessments
}
class KYCService {
async initiateKYC(userId: string, targetTier: number): Promise<{ accessToken: string }> {
const accessToken = await this.sumsubClient.createApplicantToken(
userId,
this.getLevelName(targetTier)
);
await this.db.updateUserKYC(userId, {
status: KYCStatus.PENDING,
initiatedAt: new Date()
});
return { accessToken };
}
async processWebhook(payload: SumsubWebhook): Promise<void> {
const { applicantId, reviewResult, type } = payload;
const userId = await this.db.getUserByApplicantId(applicantId);
if (type === 'applicantReviewed' && reviewResult.reviewAnswer === 'GREEN') {
const riskData = await this.calculateRiskScore(applicantId);
await this.db.updateUserKYC(userId, {
status: KYCStatus.APPROVED,
tier: 2,
approvedAt: new Date(),
expiresAt: new Date(Date.now() + 2 * 365 * 24 * 60 * 60 * 1000), // 2 years
riskScore: riskData.score,
pep: riskData.pep,
});
await this.notifyUser(userId, 'kyc_approved');
await this.unlockFeatures(userId, 2);
} else if (reviewResult.reviewAnswer === 'RED') {
const reason = this.mapRejectionReason(reviewResult.reviewRejectType);
await this.handleRejection(userId, reason);
}
}
private async calculateRiskScore(applicantId: string): Promise<RiskData> {
const applicantData = await this.sumsubClient.getApplicant(applicantId);
// PEP check
const pepResult = await this.amlProvider.checkPEP(
applicantData.info.firstName,
applicantData.info.lastName,
applicantData.info.dob,
applicantData.info.country
);
// Sanctions check (OFAC, EU, UN)
const sanctionsResult = await this.amlProvider.checkSanctions(applicantData.info);
// Risk scoring
let score = 0;
if (pepResult.isPEP) score += 40;
if (sanctionsResult.isSanctioned) score = 100; // automatic reject
if (HIGH_RISK_COUNTRIES.includes(applicantData.info.country)) score += 20;
return { score, pep: pepResult.isPEP, sanctioned: sanctionsResult.isSanctioned };
}
}
On-chain AML Screening
Every incoming deposit and every withdrawal is screened via wallet screening:
class WalletScreeningService {
async screenDepositAddress(
walletAddress: string,
asset: string,
amount: number,
userId: string
): Promise<ScreeningResult> {
// Cache: same address not re-screened every time
const cached = await this.cache.get(`wallet:${walletAddress}`);
if (cached && cached.age < 3600) return cached.result; // 1 hour TTL
const [chainalysisResult, ellipticResult] = await Promise.all([
this.chainalysis.getAddressRisk(walletAddress, asset),
this.elliptic.getWalletRisk(walletAddress),
]);
const riskScore = Math.max(chainalysisResult.riskScore, ellipticResult.riskScore);
const categories = [...new Set([
...chainalysisResult.categories,
...ellipticResult.categories,
])];
const result: ScreeningResult = {
allowed: riskScore < 70 && !this.hasBlockedCategory(categories),
riskScore,
categories,
requiresReview: riskScore >= 40 && riskScore < 70,
};
await this.cache.set(`wallet:${walletAddress}`, { result, age: Date.now() });
await this.logScreening(userId, walletAddress, result);
if (!result.allowed) {
await this.alertComplianceTeam(userId, walletAddress, result);
}
return result;
}
private hasBlockedCategory(categories: string[]): boolean {
const BLOCKED = ['darknet_market', 'ransomware', 'stolen_funds', 'sanctions'];
return categories.some(c => BLOCKED.includes(c));
}
}
Transaction Monitoring (TM)
Ongoing monitoring of transactions to identify suspicious patterns after verification:
Structuring detection: multiple transactions slightly below reporting threshold (classic smurfing).
Velocity monitoring: sharp increase in activity — 10x from normal volume per day.
Round-trip detection: funds are withdrawn and returned through several hops.
Mixing/tumbling indicators: transactions through known mixing services (Tornado Cash and similar).
class TransactionMonitor {
async analyzeTransaction(tx: Transaction): Promise<AlertLevel> {
const userHistory = await this.db.getUserTxHistory(tx.userId, 30); // 30 days
const checks = await Promise.all([
this.checkStructuring(tx, userHistory),
this.checkVelocity(tx, userHistory),
this.checkGeographicAnomalies(tx),
this.checkTimePatterns(tx, userHistory),
]);
const maxLevel = Math.max(...checks.map(c => c.level));
if (maxLevel >= AlertLevel.HIGH) {
await this.createSAR(tx, checks.filter(c => c.level >= AlertLevel.MEDIUM));
}
return maxLevel;
}
private async checkStructuring(tx: Transaction, history: Transaction[]): Promise<Check> {
const threshold = await this.getReportingThreshold(tx.currency);
// Multiple transactions within 24 hours totaling above threshold,
// each individually below
const last24h = history.filter(h =>
Date.now() - h.timestamp < 86400000 && h.amount < threshold
);
const total24h = last24h.reduce((sum, h) => sum + h.amount, 0) + tx.amount;
if (total24h >= threshold * 0.9 && last24h.length >= 3) {
return { level: AlertLevel.HIGH, reason: 'structuring_detected' };
}
return { level: AlertLevel.NONE };
}
}
SAR (Suspicious Activity Report) Automation
When alerts trigger — automatic draft SAR generation for compliance officer:
async function generateSARDraft(
userId: string,
transactions: Transaction[],
alerts: Alert[]
): Promise<SARDocument> {
const user = await getUserKYCData(userId);
return {
reportType: 'SUSPICIOUS_ACTIVITY',
filingEntity: COMPANY_DETAILS,
subject: {
name: `${user.firstName} ${user.lastName}`,
address: user.residenceAddress,
dob: user.dateOfBirth,
idNumber: user.documentNumber,
},
suspiciousActivity: {
dateRange: { from: transactions[0].date, to: transactions[transactions.length - 1].date },
totalAmount: transactions.reduce((sum, t) => sum + t.usdValue, 0),
description: generateNarrative(alerts, transactions),
alertTypes: alerts.map(a => a.type),
},
supportingTransactions: transactions.map(formatForSAR),
};
}
Technical Stack
| Component | Technology |
|---|---|
| KYC provider | Sumsub (primary) / Onfido (backup) |
| AML on-chain | Chainalysis KYT + Elliptic |
| PEP/Sanctions | Refinitiv World-Check or ComplyAdvantage |
| Transaction monitoring | custom + Chainalysis Reactor |
| SAR management | custom compliance module |
| Backend | Node.js + TypeScript + PostgreSQL |
| Queue | BullMQ (Redis) for async processing |
Development Timeline
| Component | Timeline |
|---|---|
| KYC tier system + Sumsub integration | 3-4 weeks |
| AML wallet screening | 2 weeks |
| Transaction monitoring engine | 3-4 weeks |
| SAR management system | 2 weeks |
| Compliance dashboard | 2-3 weeks |
| Testing + compliance review | 2-3 weeks |
Complete KYC/AML system for exchange: 3-4 months development.







