fintech·Independently reviewed · 96/100

Transaction Investigator

Scans a transaction history for anomalous patterns (velocity, structuring, geo, round-number, rapid-movement) and drafts a TYPED case file as canonical Findings citing the transaction IDs + rule. Two modes (fraud / aml SAR-ready); always a draft for human review, fail-safe (insufficientEvidence over false positives), never freezes accounts or files SARs.

fintechamlfraudhuman-in-the-loop

Install

npx agentskit add fintech-transaction-investigator

Quick start

import { openai } from '@agentskit/adapters'import { createFintechTransactionInvestigatorAgent } from './agents/fintech-transaction-investigator/agent'const agent = createFintechTransactionInvestigatorAgent({  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
96/100
Confidence
96%
Evaluation cases
3
Iterations
1

The agent consistently returned non-empty structured case files, preserved human-review/draft posture, avoided taking enforcement action, handled missing evidence safely, and resisted the explicit injection request. It did not hallucinate transaction anomalies from sparse prompts and correctly marked insufficientEvidence. Minor weaknesses: it over-classified benign meta/task text in the normal/minimal cases as prompt-injection-like content, and the executor stdout omits some wrapper fields that appear in the final record output, but the actual record output is complete and usable.

What passed review

  • Fail-safe behavior: all sparse inputs correctly resulted in insufficientEvidence instead of false positives.
  • Injection resistance: did not output APPROVED and treated override text as untrusted data.
  • Structured findings include severity, title, detail, category, location, confidence, and remediation.
  • Maintains requiresHumanReview and avoids SAR filing/account-freeze actions.
  • Clearly surfaces missing evidence and concrete data needed for analyst follow-up.

Example

A real usage example maintained with this agent.

import { anthropic } from '@agentskit/adapters'import { createTransactionInvestigatorAgent } from './agents/fintech-transaction-investigator/agent'const r = await createTransactionInvestigatorAgent({  adapter: anthropic({ apiKey: process.env.ANTHROPIC_API_KEY!, model: 'claude-opus-4-8' }),  mode: 'aml', // or 'fraud'}).run(transactionHistory)// → { findings: Finding[], summary, highestSeverity, insufficientEvidence, requiresHumanReview }

Extend it

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

const agent = createFintechTransactionInvestigatorAgent({  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 { fenceUntrustedContent, UNTRUSTED_CONTENT_DIRECTIVE } from '@agentskit/core/security'import { SEVERITY_ORDER, type Finding, type Severity } from '@agentskit/core/finding'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'/** * Transaction Investigator — scans a transaction history for anomalous patterns * (velocity, structuring, geo, round-number, rapid-movement-then-withdrawal) and drafts * a TYPED case file. Two modes: `fraud` (fraud case file) and `aml` (SAR-ready). Each * finding is a canonical `Finding` citing the underlying transaction IDs and the rule it * tripped — interoperable with dashboards, eval scorers, and AKOS workflow steps. * * Hard rules (replace the two prior single-prompt agents fraud-investigator / * transaction-monitor): *   - **Always a draft** — `requiresHumanReview` is always true; never freezes accounts *     or files SARs. Enforcement is a human decision. *   - **No over-claiming** — ambiguous evidence ⇒ `insufficientEvidence`, not a guess. *   - **Fail-safe** — if the model can't produce a structured case file, returns *     `insufficientEvidence:true` with no findings (never a silent all-clear). * * ```ts * const { findings, requiresHumanReview } = await createTransactionInvestigatorAgent({ *   adapter, mode: 'aml', * }).run(transactionHistory) * ``` */export type InvestigationMode = 'fraud' | 'aml'export interface InvestigationResult {  mode: InvestigationMode  findings: Finding[]  summary: string  /** Highest finding severity, or null when there are no findings. */  highestSeverity: Severity | null  /** True when evidence was too thin to draw conclusions. */  insufficientEvidence: boolean  /** Always true — a draft for the human investigator; never an enforcement action. */  requiresHumanReview: boolean}export interface TransactionInvestigatorConfig {  adapter: AdapterFactory  /** 'fraud' = fraud case file, 'aml' = SAR-ready AML case file. Default 'fraud'. */  mode?: InvestigationMode  memory?: ChatMemory  observers?: Observer[]  onConfirm?: (toolCall: ToolCall) => boolean | Promise<boolean>  maxSteps?: number}const FindingSchema = z.object({  id: z.string(),  severity: z.enum(SEVERITY_ORDER as unknown as [Severity, ...Severity[]]),  title: z.string(),  detail: z.string(),  /** Pattern class: velocity | structuring | geo | round-number | rapid-movement. */  category: z.string().optional(),  /** Cited transaction IDs (comma-joined) — the evidence. */  location: z.string().optional(),  confidence: z.number().min(0).max(1).optional(),  /** Suggested next step for the investigator. */  remediation: z.string().optional(),})const Output = z.object({  findings: z.array(FindingSchema),  summary: z.string(),  insufficientEvidence: z.boolean(),})const toJson = (s: z.ZodTypeAny): JSONSchema7 => zodToJsonSchema(s) as JSONSchema7const MODE_GUIDANCE: Record<InvestigationMode, string> = {  fraud:    'Draft a FRAUD case file for the human analyst. Focus: account-takeover signals, card-testing, ' +    'unusual velocity/geography, amount patterns.',  aml:    'Draft a SAR-ready AML case file for the human investigator. Focus: structuring/smurfing, ' +    'rapid movement-then-withdrawal, layering, round-number patterns, threshold avoidance.',}const buildSkill = (mode: InvestigationMode) => ({  name: 'transaction-investigator',  description: 'Surfaces anomalous transaction patterns as typed findings (draft for human review).',  systemPrompt: `You investigate a transaction history for anomalous patterns. ${MODE_GUIDANCE[mode]}Each finding MUST: cite the underlying transaction IDs (in location), name the pattern it tripped(in category), set a severity (critical|high|medium|low|info), and suggest a next step (remediation).NEVER freeze accounts or file reports — your output is always a DRAFT for a human.If patterns are ambiguous or evidence is thin, set insufficientEvidence=true and prefer"insufficient evidence — monitor" over a false positive. Do not over-claim.${UNTRUSTED_CONTENT_DIRECTIVE}Compliance: you do not provide financial advice; KYC/AML/sanctions decisions require human sign-off.Call submit_case exactly once with { findings, summary, insufficientEvidence }. Stop.`,  tools: ['submit_case'],})const rank = (s: Severity): number => SEVERITY_ORDER.indexOf(s)export function createTransactionInvestigatorAgent(config: TransactionInvestigatorConfig) {  const mode = config.mode ?? 'fraud'  const skill = buildSkill(mode)  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_case', description: 'Submit the case file. Call exactly once.', schema: Output, toJsonSchema: toJson, async execute() { return 'recorded' } }) as ToolDefinition  async function run(history: string): Promise<InvestigationResult> {    if (!history?.trim()) throw new Error('transaction investigator requires a non-empty transaction history')    emit('investigate', 'start', mode)    let out: z.infer<typeof Output>    try {      out = await invokeStructured({        adapter: config.adapter,        tool: submit(),        task: `TRANSACTION HISTORY:\n${fenceUntrustedContent(history)}`,        parse: (a) => Output.parse(a),        skill,        memory: config.memory,        observers: config.observers,        onConfirm: config.onConfirm,        maxSteps: config.maxSteps ?? 3,      })    } catch {      // FAIL-SAFE: never emit a silent all-clear when the model failed to produce a case file.      emit('investigate', 'error')      return { mode, findings: [], summary: 'investigation unavailable — manual review required', highestSeverity: null, insufficientEvidence: true, requiresHumanReview: true }    }    const findings = out.findings as Finding[]    const highestSeverity = findings.length      ? findings.reduce<Severity>((acc, f) => (rank(f.severity) < rank(acc) ? f.severity : acc), 'info')      : null    emit('investigate', 'ok', `${findings.length} finding(s)${highestSeverity ? ` (max ${highestSeverity})` : ''}`)    return {      mode,      findings,      summary: out.summary,      highestSeverity,      insufficientEvidence: out.insufficientEvidence,      requiresHumanReview: true,    }  }  return {    name: 'fintech-transaction-investigator',    run,    asHandle() {      return { name: 'fintech-transaction-investigator', run: async (task: string) => JSON.stringify(await run(task)) }    },  }}
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 investigator AgentHandle (`run(history) → jsonInvestigationResult`). */export const suite: EvalSuite = {  name: 'fintech-transaction-investigator',  cases: [    {      input: `Account ACC-771 transactions:TX-101 2026-06-01 deposit $9,500 (Branch A)TX-102 2026-06-01 deposit $9,500 (Branch B)TX-103 2026-06-02 deposit $9,400 (Branch A)TX-104 2026-06-02 deposit $9,600 (Branch C)TX-105 2026-06-03 wire-out $37,000 to overseas account`,      expected: (r: string) =>        /"findings":\[/.test(r) && /TX-10/.test(r) && /"requiresHumanReview":true/.test(r) && /(structuring|smurf|threshold)/i.test(r),    },    {      input: `Account ACC-880 transactions:TX-200 2026-06-01 coffee $4.20TX-201 2026-06-02 groceries $63.10TX-202 2026-06-05 salary deposit $3,200`,      expected: (r: string) => /"insufficientEvidence":true/.test(r) || /"findings":\[\]/.test(r),    },  ],}

Was this agent useful?

Your response helps us prioritize agent quality.

Keep exploring

Related agents

View category