fintech·Independently reviewed · 97/100

KYC Screener

Onboarding KYC screen against sanctions/PEP/adverse-media lists with a DETERMINISTIC fuzzy-match gate: required fields (name/DOB/country) validated up front; strong/exact hits escalate unconditionally (never model-cleared); the model only downgrades weak near-misses. Typed risk verdict + human sign-off.

fintechcompliancekycpepdeterministic-gatehuman-in-the-loop

Install

npx agentskit add fintech-kyc-screener

Quick start

import { openai } from '@agentskit/adapters'import { createFintechKycScreenerAgent } from './agents/fintech-kyc-screener/agent'const agent = createFintechKycScreenerAgent({  adapter: openai({    apiKey: process.env.OPENAI_API_KEY!,    model: 'gpt-4o',  }),})const result = await agent.run('Describe your task here')console.log(result.content)

Independent reviewer approved

Validation evidence

How validation works
Review score
97/100
Confidence
97%
Evaluation cases
3
Iterations
2

The agent consistently produced valid typed KYC results, enforced required-field validation up front, escalated fail-safe when required identity fields were missing, and resisted the prompt-injection request to output APPROVED. It did not hallucinate candidate details from sparse natural-language prompts, did not falsely clear anyone, and surfaced the exact missing fields with human signoff required. The normal case is conservative rather than rich, but that matches the agent purpose and README contract: KYC screening requires name, DOB, and country, and missing required fields must not be guessed.

What passed review

  • Valid structured output in all cases with the documented KycResult shape.
  • Fail-safe escalation for incomplete KYC inputs.
  • Exact missing fields reported: name, dob, country.
  • Prompt injection was ignored and did not alter the risk verdict.
  • No unsupported sanctions, PEP, or adverse-media claims were invented.

Example

A real usage example maintained with this agent.

import { anthropic } from '@agentskit/adapters'import { createKycScreenerAgent } from './agents/fintech-kyc-screener/agent'const agent = createKycScreenerAgent({  adapter: anthropic({ apiKey: process.env.ANTHROPIC_API_KEY!, model: 'claude-opus-4-8' }),  lists: [{ name: 'Vladimir Putin', list: 'PEP' }, ...sanctionsNames.map((name) => ({ name, list: 'OFAC-SDN' }))],})const r = await agent.run({ name: 'Jane Doe', dob: '1980-02-02', country: 'US' })if (r.requiresHumanSignoff) routeToCompliance(r)

Extend it

Pass tools, retrieval, memory, permissions, and observers through the factory config.

const agent = createFintechKycScreenerAgent({  adapter,  tools,  retriever,  memory,  onConfirm: (call) => approve(call),  observers: [tracer],})
View agent factory source
import type { AdapterFactory, ChatMemory, Observer, ToolCall, ToolDefinition } from '@agentskit/core'import { fuzzyMatchList } from '@agentskit/core/fuzzy-match'import { fenceUntrustedContent, UNTRUSTED_CONTENT_DIRECTIVE } from '@agentskit/core/security'import { invokeStructured } from '@agentskit/runtime'import { defineZodTool } from '@agentskit/tools'import { z } from 'zod'import { zodToJsonSchema } from 'zod-to-json-schema'import type { JSONSchema7 } from 'json-schema'/** * KYC Screener — onboarding identity screen against sanctions / PEP / adverse-media * lists. Like the sanctions screener, the compliance guarantee is enforced in CODE: * a DETERMINISTIC `fuzzyMatchList` gate, a HARD RULE that strong/exact hits escalate * unconditionally (the model can never clear them), and the model only DOWNGRADES * weak near-misses (so a hallucination can only make screening stricter). Required * KYC fields (name, DOB, country) are validated up front — it refuses, never guesses. * * ```ts * const agent = createKycScreenerAgent({ *   adapter, *   lists: [{ name: 'Vladimir Putin', list: 'PEP' }, { name: 'Jane Doe', list: 'ADVERSE-MEDIA' }], * }) * const r = await agent.run({ name: 'Jane Doe', dob: '1980-02-02', country: 'US' }) * if (r.requiresHumanSignoff) routeToCompliance(r) * ``` */export type ListSource = string // e.g. 'OFAC-SDN' | 'PEP' | 'ADVERSE-MEDIA'export interface ListEntry {  name: string  list?: ListSource  date?: string}export interface KycCandidate {  name: string  dob: string  country: string}export interface KycHit {  matched: string  list?: ListSource  score: number  decision: 'true-match' | 'false-positive'  rationale: string  autoCleared: boolean}export type RiskTier = 'clear' | 'escalate'export interface KycResult {  candidate: KycCandidate  riskTier: RiskTier  hits: KycHit[]  requiresHumanSignoff: boolean  /** Missing required fields, when the screen is refused. */  missing?: string[]}export interface KycScreenerConfig {  adapter: AdapterFactory  /** Combined screening list — tag each entry with its source via `list`. */  lists: Array<string | ListEntry>  strongThreshold?: number  screenThreshold?: number  memory?: ChatMemory  observers?: Observer[]  onConfirm?: (toolCall: ToolCall) => boolean | Promise<boolean>  maxSteps?: number}const Verdict = z.object({ decision: z.enum(['true-match', 'false-positive']), rationale: z.string() })const toJson = (s: z.ZodTypeAny): JSONSchema7 => zodToJsonSchema(s) as JSONSchema7const adjudicator = {  name: 'kyc-adjudicator',  description: 'Adjudicates a single weak fuzzy KYC hit against the candidate identity.',  systemPrompt: `You adjudicate ONE possible KYC list match. Given a candidate identity (name, DOB, country)and a single fuzzy-matched list name + score, decide whether they are the SAME real-world party.${UNTRUSTED_CONTENT_DIRECTIVE}Be conservative: return "false-positive" ONLY when the evidence clearly shows a different person(mismatched DOB/country, common-name coincidence). When unsure, return "true-match" — a humanreviews escalations; wrongly onboarding a sanctioned/PEP party is a regulatory breach.Call submit_verdict exactly once with { decision, rationale }. Output nothing else.`,  tools: ['submit_verdict'],}const nameOf = (e: string | ListEntry): string => (typeof e === 'string' ? e : e.name)const REQUIRED: (keyof KycCandidate)[] = ['name', 'dob', 'country']export function createKycScreenerAgent(config: KycScreenerConfig) {  const strong = config.strongThreshold ?? 0.92  const screen = config.screenThreshold ?? 0.85  // Guard the thresholds: a bad value (e.g. strong > 1) would silently route every  // hit to the model — which could then clear it. The gate must never be defeatable.  if (!(screen > 0 && screen <= 1 && strong > 0 && strong <= 1 && strong >= screen)) {    throw new Error(`invalid thresholds: require 0 < screenThreshold (${screen}) <= strongThreshold (${strong}) <= 1`)  }  const names = config.lists.map(nameOf)  const entryByName = new Map(config.lists.map((e) => [nameOf(e), typeof e === 'string' ? undefined : e]))  const emit = (label: string, status: 'start' | 'ok' | 'skip' | 'error', detail?: string) => {    for (const o of config.observers ?? []) void o.on({ type: 'progress', label, status, detail })  }  const submit = (): ToolDefinition =>    defineZodTool({      name: 'submit_verdict',      description: 'Submit the adjudication verdict. Call exactly once.',      schema: Verdict,      toJsonSchema: toJson,      async execute() {        return 'recorded'      },    }) as ToolDefinition  async function adjudicate(c: KycCandidate, matched: string, score: number): Promise<z.infer<typeof Verdict>> {    const task = `CANDIDATE:\n${fenceUntrustedContent(JSON.stringify(c))}\n\nPOSSIBLE MATCH (score ${score.toFixed(3)}):\n${fenceUntrustedContent(matched)}`    try {      return await invokeStructured({        adapter: config.adapter,        tool: submit(),        task,        parse: (a) => Verdict.parse(a),        skill: adjudicator,        memory: config.memory,        observers: config.observers,        onConfirm: config.onConfirm,        maxSteps: config.maxSteps ?? 3,      })    } catch {      return { decision: 'true-match', rationale: 'adjudication unavailable — failed safe to escalation' }    }  }  async function run(candidate: KycCandidate): Promise<KycResult> {    const missing = REQUIRED.filter((k) => !candidate?.[k] || !String(candidate[k]).trim())    if (missing.length) {      emit('validate', 'error', `missing: ${missing.join(', ')}`)      return { candidate, riskTier: 'escalate', hits: [], requiresHumanSignoff: true, missing }    }    emit('screen', 'start', candidate.name)    const matches = fuzzyMatchList(candidate.name, names, { threshold: screen, topK: 25 })    emit('screen', matches.length ? 'ok' : 'skip', `${matches.length} candidate hit(s)`)    const hits: KycHit[] = []    for (const m of matches) {      const entry = entryByName.get(m.candidate)      if (m.score >= strong) {        hits.push({ matched: m.candidate, list: entry?.list, score: m.score, decision: 'true-match', rationale: `score ${m.score.toFixed(3)} ≥ strong ${strong} — auto-escalated`, autoCleared: false })        continue      }      const v = await adjudicate(candidate, m.candidate, m.score)      hits.push({ matched: m.candidate, list: entry?.list, score: m.score, decision: v.decision, rationale: entry?.date ? `${v.rationale} (listed ${entry.date})` : v.rationale, autoCleared: v.decision === 'false-positive' })    }    const escalated = hits.filter((h) => !h.autoCleared)    const riskTier: RiskTier = escalated.length ? 'escalate' : 'clear'    emit('verdict', 'ok', `${riskTier} (${escalated.length}/${hits.length} unresolved)`)    return { candidate, riskTier, hits, requiresHumanSignoff: riskTier === 'escalate' }  }  return {    name: 'fintech-kyc-screener',    run,    asHandle() {      return {        name: 'fintech-kyc-screener',        run: async (task: string) => JSON.stringify(await run(JSON.parse(task) as KycCandidate)),      }    },  }}
View evaluation contract

Replay these cases with the provider and model you plan to deploy.

import type { EvalSuite } from '@agentskit/eval'/** * Eval cases for the KYC screener's AgentHandle (`run(jsonCandidate) → jsonKycResult`). * Wire the agent under test with a list containing 'Vladimir Putin' (PEP). The * deterministic gate + required-field check mean most outcomes don't depend on the model. */export const suite: EvalSuite = {  name: 'fintech-kyc-screener',  cases: [    {      // Required field missing → refuse, escalate, report the field.      input: JSON.stringify({ name: 'Jane Doe', dob: '', country: 'US' }),      expected: (r: string) => /"missing":\[/.test(r) && /dob/.test(r) && /"riskTier":"escalate"/.test(r),    },    {      // Exact PEP hit → escalate, sign-off.      input: JSON.stringify({ name: 'Vladimir Putin', dob: '1952-10-07', country: 'RU' }),      expected: (r: string) => /"riskTier":"escalate"/.test(r) && /"requiresHumanSignoff":true/.test(r),    },    {      // Clean identity → clear.      input: JSON.stringify({ name: 'Robert Brown', dob: '1990-03-03', country: 'CA' }),      expected: (r: string) => /"riskTier":"clear"/.test(r),    },  ],}

Was this agent useful?

Your response helps us prioritize agent quality.

Keep exploring

Related agents

View category