Customer Screening Workbench
Submit a customer for
risk screening
Five compliance agents run simultaneously — identity verification, sanctions screening, AML risk, fraud detection, and entity due diligence. Results in under 30 seconds.
Identity information
Identity documents
ID document — front
JPG, PNG or PDF · max 10MB
ID document — back
JPG, PNG or PDF · max 10MB
Selfie / liveness photo
JPG or PNG · clear face visible
Financial profile
✓
Politically Exposed Person (PEP)
Self-declaration
CASE REF-0000000
Screening in progress…
All compliance agents running in parallel
KYC
Running…
Identity verification in progress
Sanctions
Queued
Awaiting KYC completion
AML
Queued
Profile risk assessment
Fraud
Queued
Identity integrity check
KYB
Queued
Individual mod
// access modal removed
accessCode = val;
// access modal removed
}
window.addEventListener('load', function() {
if (!REQUIRE_ACCESS_CODE) // access modal removed
});
// ── STATE ────────────────────────────────────────────────────────────────────
var WORKER_URL = 'https://comply-iq.nikhil-dhurandhar.workers.dev';
var REQUIRE_ACCESS_CODE = false;
var accessCode = '';
var currentMode = 'individual';
var sofSelected = [];
var pepChecked = false;
var uploadedFiles = {};
var screeningData = {};
var results = {};
var caseRef = '';
// ── CLOCK ────────────────────────────────────────────────────────────────────
function updateClock() {
var el = document.getElementById('tb-time');
if (el) el.textContent = new Date().toLocaleTimeString('en-US', {hour:'2-digit', minute:'2-digit', second:'2-digit'});
}
setInterval(updateClock, 1000);
updateClock();
// ── API KEY ──────────────────────────────────────────────────────────────────
function setApiKey(){}
// ── MODE TOGGLE ──────────────────────────────────────────────────────────────
function setMode(mode) {
currentMode = mode;
document.getElementById('mode-individual').classList.toggle('active', mode === 'individual');
document.getElementById('mode-entity').classList.toggle('active', mode === 'entity');
var showIndividual = mode === 'individual';
document.getElementById('identity-grid').style.display = showIndividual ? 'grid' : 'none';
document.getElementById('entity-grid').style.display = showIndividual ? 'none' : 'grid';
document.getElementById('individual-docs').style.display = showIndividual ? 'grid' : 'none';
document.getElementById('entity-docs').style.display = showIndividual ? 'none' : 'grid';
document.getElementById('occ-field').style.display = showIndividual ? 'flex' : 'none';
var kybCard = document.getElementById('proc-kyb');
document.getElementById('proc-kyb-detail').textContent = showIndividual ? 'Individual mode — N/A' : 'Entity due diligence';
}
// ── SOURCE OF FUNDS ──────────────────────────────────────────────────────────
function toggleSoF(el, val) {
el.classList.toggle('selected');
if (el.classList.contains('selected')) {
sofSelected.push(val);
} else {
sofSelected = sofSelected.filter(function(v) { return v !== val; });
}
}
// ── PEP ──────────────────────────────────────────────────────────────────────
function togglePEP() {
pepChecked = !pepChecked;
var ui = document.getElementById('pep-check-ui');
ui.classList.toggle('checked', pepChecked);
ui.textContent = pepChecked ? '\u2713' : '';
}
// ── FILE UPLOAD ──────────────────────────────────────────────────────────────
function handleUpload(zoneId, input) {
if (!input.files || !input.files[0]) return;
var file = input.files[0];
var zone = document.getElementById(zoneId);
zone.classList.add('has-file');
var fileNameEl = document.getElementById('file-' + zoneId.replace('zone-', ''));
if (fileNameEl) fileNameEl.textContent = '\u2713 ' + file.name;
uploadedFiles[zoneId] = file.name;
}
// ── COLLECT FORM DATA ────────────────────────────────────────────────────────
function collectFormData() {
if (currentMode === 'individual') {
return {
mode: 'individual',
name: document.getElementById('f-name').value.trim() || 'Not provided',
dob: document.getElementById('f-dob').value || 'Not provided',
nationality: document.getElementById('f-nationality').value.trim() || 'Not provided',
country_of_residence: document.getElementById('f-country').value.trim() || 'Not provided',
address: document.getElementById('f-address').value.trim() || 'Not provided',
document_type: document.getElementById('f-doctype').value || 'Not provided',
document_number: document.getElementById('f-docnum').value.trim() || 'Not provided',
document_expiry: document.getElementById('f-expiry').value || 'Not provided',
occupation: document.getElementById('f-occupation').value.trim() || 'Not provided',
volume: document.getElementById('f-volume').value || 'Not provided',
source_of_funds: sofSelected.length ? sofSelected.join(', ') : 'Not declared',
pep: pepChecked ? 'YES — self-declared PEP' : 'No',
notes: document.getElementById('f-notes').value.trim() || 'None',
documents_uploaded: Object.keys(uploadedFiles).map(function(k) {
return uploadedFiles[k];
}).join(', ') || 'None uploaded'
};
} else {
return {
mode: 'entity',
company_name: document.getElementById('f-company').value.trim() || 'Not provided',
registration_number: document.getElementById('f-regno').value.trim() || 'Not provided',
jurisdiction: document.getElementById('f-jurisdiction').value.trim() || 'Not provided',
incorporation_date: document.getElementById('f-incorp-date').value || 'Not provided',
nature_of_business: document.getElementById('f-business').value.trim() || 'Not provided',
ubos: document.getElementById('f-ubos').value.trim() || 'Not provided',
volume: document.getElementById('f-volume').value || 'Not provided',
source_of_funds: sofSelected.length ? sofSelected.join(', ') : 'Not declared',
notes: document.getElementById('f-notes').value.trim() || 'None',
documents_uploaded: Object.keys(uploadedFiles).map(function(k) {
return uploadedFiles[k];
}).join(', ') || 'None uploaded'
};
}
}
// ── GENERATE CASE REF ────────────────────────────────────────────────────────
function genRef() {
var d = new Date();
var year = d.getFullYear();
var num = Math.floor(Math.random() * 900000) + 100000;
return 'REF-' + year + '-' + num;
}
// ── START SCREENING ──────────────────────────────────────────────────────────
async function startScreening() {
if (WORKER_URL === 'REPLACE_WITH_YOUR_WORKER_URL') { alert('Worker URL not configured. Open index.html and replace REPLACE_WITH_YOUR_WORKER_URL with your Cloudflare Worker URL.'); return; }
screeningData = collectFormData();
caseRef = genRef();
// Validate minimum data
var nameField = currentMode === 'individual' ? screeningData.name : screeningData.company_name;
if (nameField === 'Not provided') {
alert('Please enter at least a name before running a screening.');
return;
}
// Switch to processing state
setState('processing');
var displayName = currentMode === 'individual' ? screeningData.name : screeningData.company_name;
document.getElementById('proc-ref').textContent = caseRef;
document.getElementById('proc-name').textContent = 'Screening: ' + displayName;
results = {};
// Run agents sequentially but update UI as if parallel
var agents = ['kyc', 'sanctions', 'aml', 'fraud', 'kyb'];
var agentPromises = [];
for (var i = 0; i < agents.length; i++) {
await runAgent(agents[i], i, agents.length);
}
// Show results
showResults();
}
// ── RUN A SINGLE AGENT ───────────────────────────────────────────────────────
async function runAgent(agentName, idx, total) {
var card = document.getElementById('proc-' + agentName);
var statusEl = card.querySelector('.agent-card-status');
var detailEl = card.querySelector('.agent-card-detail');
// Mark as running
card.classList.remove('loading');
statusEl.className = 'agent-card-status status-running';
statusEl.textContent = 'Running…';
card.classList.add('loading');
// KYB skipped for individual
if (agentName === 'kyb' && currentMode === 'individual') {
card.classList.remove('loading');
card.classList.add('skip');
statusEl.className = 'agent-card-status status-skip';
statusEl.textContent = 'Not applicable';
detailEl.textContent = 'Individual mode';
results['kyb'] = { verdict: 'skip', badge: 'N/A', summary: 'KYB not applicable for individual screening.', findings: ['Individual mode — entity due diligence not required'], flags: [] };
updateProgress(idx + 1, total);
return;
}
var prompt = buildPrompt(agentName);
try {
var headers = {'Content-Type': 'application/json'};
if(REQUIRE_ACCESS_CODE && accessCode) headers['X-Access-Code'] = accessCode;
var res = await fetch(WORKER_URL, {
method: 'POST',
headers: headers,
body: JSON.stringify({
model: 'claude-sonnet-4-20250514',
max_tokens: 800,
messages: [{ role: 'user', content: prompt }]
})
});
var data = await res.json();
if (data.error) throw new Error(data.error.message);
var text = data.content && data.content[0] ? data.content[0].text : '{}';
// Strip markdown fences if present
if(text.indexOf('```') !== -1) { text = text.slice(text.indexOf('{'), text.lastIndexOf('}') + 1); }
var parsed = JSON.parse(text);
results[agentName] = parsed;
// Update card
card.classList.remove('loading');
var verdictClass = parsed.verdict === 'pass' ? 'done-pass' : parsed.verdict === 'review' ? 'done-review' : 'done-block';
card.classList.add(verdictClass);
statusEl.className = 'agent-card-status status-' + parsed.verdict;
statusEl.textContent = parsed.verdict === 'pass' ? 'Clear' : parsed.verdict === 'review' ? 'Review' : 'Block';
detailEl.textContent = parsed.headline || '';
} catch(e) {
card.classList.remove('loading');
card.classList.add('done-review');
statusEl.className = 'agent-card-status status-review';
statusEl.textContent = 'Error';
detailEl.textContent = e.message.substring(0, 60);
results[agentName] = {
verdict: 'review',
badge: 'ERROR',
headline: 'API error — manual review required',
summary: 'Agent encountered an error: ' + e.message,
findings: ['Manual review required due to API error'],
flags: [{ severity: 'MEDIUM', text: 'Agent error — manual review' }]
};
}
updateProgress(idx + 1, total);
}
// ── BUILD AGENT PROMPT ───────────────────────────────────────────────────────
function buildPrompt(agentName) {
var d = screeningData;
var dataStr = JSON.stringify(d, null, 2);
var agentInstructions = {
kyc: 'You are a KYC compliance agent. Assess the identity data for completeness, consistency, and risk indicators. Consider: name plausibility, document type appropriateness for stated nationality, document expiry, address completeness, consistency between nationality and country of residence. Note that document verification (NFC chip, biometric match, liveness) requires an IDV integration — flag this as requiring technical verification. Assess overall identity risk.',
sanctions: 'You are a Sanctions screening agent. Screen the provided identity information against OFAC SDN, UN Security Council consolidated list, EU consolidated list, and HMT financial sanctions list. Consider: name variations, nationality risk, country of residence risk. Identify whether the name, nationality, or any declared connections suggest potential list matches or high-risk jurisdiction exposure. Note that automated fuzzy-matching against live lists requires system integration — assess risk based on the provided data.',
aml: 'You are an AML risk assessment agent. Assess the anti-money laundering risk profile of this customer based on their declared information. Consider: source of funds consistency with occupation and volume, PEP status (high risk), high-risk country exposure, transaction volume appropriateness for stated occupation, any unusual patterns in the declared information. Assign an AML risk tier: LOW, MEDIUM, or HIGH.',
fraud: 'You are a Fraud detection agent. Assess the customer submission for fraud indicators. Consider: document type and number format plausibility, consistency between declared information fields, any indicators of synthetic identity (mismatched data), application completeness patterns (very sparse data can indicate fraud), stated occupation vs volume consistency. Note that device intelligence, IP geolocation, and biometric verification require technical integration — flag these as required checks.',
kyb: 'You are a KYB (Know Your Business) agent. Assess the entity information for completeness and risk. Consider: company name and registration number plausibility, jurisdiction risk, nature of business risk level, UBO disclosure completeness (anyone owning 25%+ must be declared), beneficial ownership structure complexity, whether the business type is higher risk (MSB, crypto, cash-intensive). Each UBO should be flagged for individual KYC screening.'
};
var systemPrompt = agentInstructions[agentName];
var parts = [
systemPrompt,
'',
'Customer submission data:',
dataStr,
'',
'Return ONLY a JSON object with exactly this structure — no markdown, no explanation, just the JSON:',
'{',
' "verdict": "pass" or "review" or "block",',
' "badge": "CLEAR" or "REVIEW" or "BLOCK" or a short status word,',
' "headline": "one line summary of the key finding (max 60 chars)",',
' "summary": "2-3 sentence assessment",',
' "findings": ["finding 1", "finding 2", "finding 3"],',
' "flags": [{"severity": "HIGH" or "MEDIUM" or "LOW" or "INFO", "text": "flag description"}]',
'}'
];
return parts.join('\n');
}
// ── PROGRESS ─────────────────────────────────────────────────────────────────
function updateProgress(done, total) {
var pct = Math.round(done / total * 100);
document.getElementById('progress-fill').style.width = pct + '%';
document.getElementById('progress-label').textContent = done + ' / ' + total + ' complete';
}
// ── SHOW RESULTS ─────────────────────────────────────────────────────────────
function showResults() {
setState('results');
var d = screeningData;
var displayName = d.mode === 'individual' ? d.name : d.company_name;
// Calculate overall risk
var verdicts = Object.values(results).map(function(r) { return r.verdict; });
var hasBlock = verdicts.some(function(v) { return v === 'block'; });
var hasReview = verdicts.some(function(v) { return v === 'review'; });
var overallVerdict = hasBlock ? 'block' : hasReview ? 'review' : 'pass';
// Risk score (simple heuristic)
var score = 15;
if (hasBlock) score = 85;
else if (hasReview) score = 45;
if (d.pep && d.pep.includes('YES')) score = Math.min(score + 20, 95);
score = Math.max(5, Math.min(95, score));
// Update header
var ring = document.getElementById('risk-ring');
ring.className = 'risk-score-ring ' + (score >= 65 ? 'high' : score >= 35 ? 'medium' : 'low');
document.getElementById('risk-num').textContent = score;
document.getElementById('risk-name').textContent = displayName;
var meta = d.mode === 'individual'
? (d.nationality + ' · DOB ' + d.dob + ' · ' + d.country_of_residence)
: (d.jurisdiction + ' · ' + d.nature_of_business);
document.getElementById('risk-meta').textContent = meta;
var verdict = document.getElementById('risk-verdict');
verdict.className = 'risk-verdict ' + overallVerdict;
document.getElementById('risk-verdict-icon').textContent = overallVerdict === 'block' ? '\u26d4' : overallVerdict === 'review' ? '\u26a0\ufe0f' : '\u2705';
document.getElementById('risk-verdict-text').textContent = overallVerdict === 'block' ? 'Decline — block indicators present' : overallVerdict === 'review' ? 'Manual review required' : 'Approve — all checks clear';
var ts = new Date();
document.getElementById('risk-ref').textContent = caseRef + ' · ' + ts.toLocaleTimeString('en-US', {hour:'2-digit', minute:'2-digit'});
// Build result cards
var agentConfig = [
{ key: 'kyc', name: 'KYC', icon: '\u{1F464}' },
{ key: 'sanctions', name: 'Sanctions', icon: '\u{1F6AB}' },
{ key: 'aml', name: 'AML', icon: '\u{1F4CA}' },
{ key: 'fraud', name: 'Fraud', icon: '\u{1F50E}' },
{ key: 'kyb', name: 'KYB', icon: '\u{1F3DB}\uFE0F' },
];
var grid = document.getElementById('results-grid');
grid.innerHTML = '';
var allFlags = [];
var cardIdx = 0;
agentConfig.forEach(function(cfg) {
var r = results[cfg.key];
if (!r) return;
var card = document.createElement('div');
card.className = 'result-card ' + r.verdict + ' fade-in fade-in-d' + Math.min(cardIdx + 1, 5);
cardIdx++;
var badgeText = r.badge || (r.verdict === 'pass' ? 'CLEAR' : r.verdict === 'review' ? 'REVIEW' : r.verdict === 'skip' ? 'N/A' : 'BLOCK');
var findingsHtml = '';
if (r.findings && r.findings.length) {
r.findings.slice(0, 4).forEach(function(f) {
findingsHtml += '';
});
}
card.innerHTML =
'
' + escHtml(f) + '
' +
'' +
'' + cfg.name + '' +
'' + badgeText + '' +
'
' +
'' + findingsHtml + '
' +
'' +
'';
grid.appendChild(card);
// Collect flags
if (r.flags && r.flags.length) {
r.flags.forEach(function(fl) {
allFlags.push({ severity: fl.severity, source: cfg.name, text: fl.text });
});
}
});
// Build flags panel
var flagsCount = allFlags.filter(function(f) { return f.severity === 'HIGH' || f.severity === 'MEDIUM'; }).length;
document.getElementById('flags-count').textContent = allFlags.length + ' flag' + (allFlags.length !== 1 ? 's' : '') + ' identified';
var flagsList = document.getElementById('flags-list');
flagsList.innerHTML = '';
if (allFlags.length === 0) {
flagsList.innerHTML = 'CLEARAll agentsNo risk flags identified across all compliance checks
';
} else {
// Sort: HIGH first
allFlags.sort(function(a, b) {
var order = { HIGH: 0, MEDIUM: 1, LOW: 2, INFO: 3 };
return (order[a.severity] || 3) - (order[b.severity] || 3);
});
allFlags.forEach(function(fl) {
var sevClass = fl.severity === 'HIGH' ? 'high' : fl.severity === 'MEDIUM' ? 'medium' : fl.severity === 'LOW' ? 'low' : 'info';
var item = document.createElement('div');
item.className = 'flag-item';
item.innerHTML =
'' + fl.severity + '' +
'' + escHtml(fl.source) + '' +
'' + escHtml(fl.text) + '';
flagsList.appendChild(item);
});
}
}
// ── HELPERS ──────────────────────────────────────────────────────────────────
function setState(name) {
document.querySelectorAll('.state').forEach(function(el) {
el.classList.remove('active');
});
document.getElementById('state-' + name).classList.add('active');
window.scrollTo(0, 0);
}
function toggleExpand(btn) {
var expanded = btn.nextElementSibling;
expanded.classList.toggle('open');
btn.textContent = expanded.classList.contains('open') ? '\u25b2 Collapse' : '\u25bc Full assessment';
}
function recordDecision(decision) {
var ts = new Date().toLocaleTimeString('en-US', {hour:'2-digit', minute:'2-digit'});
var ref = document.getElementById('risk-ref');
ref.textContent += ' · Decision: ' + decision + ' at ' + ts;
alert('Decision recorded: ' + decision + '\nCase: ' + caseRef + '\n\nIn production this would log to the case management system and trigger the appropriate workflow.');
}
function newScreening() {
// Reset
sofSelected = [];
pepChecked = false;
uploadedFiles = {};
results = {};
document.getElementById('pep-check-ui').classList.remove('checked');
document.getElementById('pep-check-ui').textContent = '';
document.querySelectorAll('.sof-btn').forEach(function(b) { b.classList.remove('selected'); });
document.querySelectorAll('.upload-zone').forEach(function(z) { z.classList.remove('has-file'); });
document.querySelectorAll('.upload-file-name').forEach(function(el) { el.textContent = ''; });
updateProgress(0, 5);
setState('form');
}
function escHtml(str) {
return String(str)
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"');
}
var sevClass = fl.severity === 'HIGH' ? 'high' : fl.severity === 'MEDIUM' ? 'medium' : fl.severity === 'LOW' ? 'low' : 'info';
var item = document.createElement('div');
item.className = 'flag-item';
item.innerHTML =
'' + fl.severity + '' +
'' + escHtml(fl.source) + '' +
'' + escHtml(fl.text) + '';
flagsList.appendChild(item);
});
}
}
// ── HELPERS ──────────────────────────────────────────────────────────────────
function setState(name) {
document.querySelectorAll('.state').forEach(function(el) {
el.classList.remove('active');
});
document.getElementById('state-' + name).classList.add('active');
window.scrollTo(0, 0);
}
function toggleExpand(btn) {
var expanded = btn.nextElementSibling;
expanded.classList.toggle('open');
btn.textContent = expanded.classList.contains('open') ? '\u25b2 Collapse' : '\u25bc Full assessment';
}
function recordDecision(decision) {
var ts = new Date().toLocaleTimeString('en-US', {hour:'2-digit', minute:'2-digit'});
var ref = document.getElementById('risk-ref');
ref.textContent += ' · Decision: ' + decision + ' at ' + ts;
alert('Decision recorded: ' + decision + '\nCase: ' + caseRef + '\n\nIn production this would log to the case management system and trigger the appropriate workflow.');
}
function newScreening() {
// Reset
sofSelected = [];
pepChecked = false;
uploadedFiles = {};
results = {};
document.getElementById('pep-check-ui').classList.remove('checked');
document.getElementById('pep-check-ui').textContent = '';
document.querySelectorAll('.sof-btn').forEach(function(b) { b.classList.remove('selected'); });
document.querySelectorAll('.upload-zone').forEach(function(z) { z.classList.remove('has-file'); });
document.querySelectorAll('.upload-file-name').forEach(function(el) { el.textContent = ''; });
updateProgress(0, 5);
setState('form');
}
function escHtml(str) {
return String(str)
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"');
}
function submitAccessCode(){} // stub