{"id":"fintech-transaction-investigator","title":"Transaction Investigator","description":"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.","category":"fintech","version":"2.0.0","source":"AKOS","license":"MIT","tags":["fintech","aml","fraud","human-in-the-loop"],"packages":["@agentskit/core","@agentskit/runtime","@agentskit/tools"],"files":["agent.ts","README.md","eval.ts"],"requires":{"zod":"^3","zod-to-json-schema":"^3"},"status":"validated","skill":{"name":"fintech-transaction-investigator","description":"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.","systemPrompt":"You investigate a transaction history for anomalous patterns. ${MODE_GUIDANCE[mode]}\n\nEach finding MUST: cite the underlying transaction IDs (in location), name the pattern it tripped\n(in category), set a severity (critical|high|medium|low|info), and suggest a next step (remediation).\nNEVER freeze accounts or file reports — your output is always a DRAFT for a human.\nIf patterns are ambiguous or evidence is thin, set insufficientEvidence=true and prefer\n\"insufficient evidence — monitor\" over a false positive. Do not over-claim.\n\n${UNTRUSTED_CONTENT_DIRECTIVE}\nCompliance: you do not provide financial advice; KYC/AML/sanctions decisions require human sign-off.\n\nCall submit_case exactly once with { findings, summary, insufficientEvidence }. Stop."},"flow":null,"a2a":{"id":"io.agentskit.registry.fintech-transaction-investigator","name":"Transaction Investigator","description":"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.","version":"2.0.0","homepage":"https://registry.agentskit.io","skills":[{"name":"fintech-transaction-investigator","description":"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.","capabilities":{"streaming":true,"cancellation":true,"requiresApproval":false}}]},"sources":[{"path":"agent.ts","content":"import type { AdapterFactory, ChatMemory, Observer, ToolCall, ToolDefinition } from '@agentskit/core'\nimport { fenceUntrustedContent, UNTRUSTED_CONTENT_DIRECTIVE } from '@agentskit/core/security'\nimport { SEVERITY_ORDER, type Finding, type Severity } from '@agentskit/core/finding'\nimport { invokeStructured } from '@agentskit/runtime'\nimport { defineZodTool } from '@agentskit/tools'\nimport { z } from 'zod'\nimport { zodToJsonSchema } from 'zod-to-json-schema'\nimport type { JSONSchema7 } from 'json-schema'\n\n/**\n * Transaction Investigator — scans a transaction history for anomalous patterns\n * (velocity, structuring, geo, round-number, rapid-movement-then-withdrawal) and drafts\n * a TYPED case file. Two modes: `fraud` (fraud case file) and `aml` (SAR-ready). Each\n * finding is a canonical `Finding` citing the underlying transaction IDs and the rule it\n * tripped — interoperable with dashboards, eval scorers, and AKOS workflow steps.\n *\n * Hard rules (replace the two prior single-prompt agents fraud-investigator /\n * transaction-monitor):\n *   - **Always a draft** — `requiresHumanReview` is always true; never freezes accounts\n *     or files SARs. Enforcement is a human decision.\n *   - **No over-claiming** — ambiguous evidence ⇒ `insufficientEvidence`, not a guess.\n *   - **Fail-safe** — if the model can't produce a structured case file, returns\n *     `insufficientEvidence:true` with no findings (never a silent all-clear).\n *\n * ```ts\n * const { findings, requiresHumanReview } = await createTransactionInvestigatorAgent({\n *   adapter, mode: 'aml',\n * }).run(transactionHistory)\n * ```\n */\n\nexport type InvestigationMode = 'fraud' | 'aml'\n\nexport interface InvestigationResult {\n  mode: InvestigationMode\n  findings: Finding[]\n  summary: string\n  /** Highest finding severity, or null when there are no findings. */\n  highestSeverity: Severity | null\n  /** True when evidence was too thin to draw conclusions. */\n  insufficientEvidence: boolean\n  /** Always true — a draft for the human investigator; never an enforcement action. */\n  requiresHumanReview: boolean\n}\n\nexport interface TransactionInvestigatorConfig {\n  adapter: AdapterFactory\n  /** 'fraud' = fraud case file, 'aml' = SAR-ready AML case file. Default 'fraud'. */\n  mode?: InvestigationMode\n  memory?: ChatMemory\n  observers?: Observer[]\n  onConfirm?: (toolCall: ToolCall) => boolean | Promise<boolean>\n  maxSteps?: number\n}\n\nconst FindingSchema = z.object({\n  id: z.string(),\n  severity: z.enum(SEVERITY_ORDER as unknown as [Severity, ...Severity[]]),\n  title: z.string(),\n  detail: z.string(),\n  /** Pattern class: velocity | structuring | geo | round-number | rapid-movement. */\n  category: z.string().optional(),\n  /** Cited transaction IDs (comma-joined) — the evidence. */\n  location: z.string().optional(),\n  confidence: z.number().min(0).max(1).optional(),\n  /** Suggested next step for the investigator. */\n  remediation: z.string().optional(),\n})\nconst Output = z.object({\n  findings: z.array(FindingSchema),\n  summary: z.string(),\n  insufficientEvidence: z.boolean(),\n})\nconst toJson = (s: z.ZodTypeAny): JSONSchema7 => zodToJsonSchema(s) as JSONSchema7\n\nconst MODE_GUIDANCE: Record<InvestigationMode, string> = {\n  fraud:\n    'Draft a FRAUD case file for the human analyst. Focus: account-takeover signals, card-testing, ' +\n    'unusual velocity/geography, amount patterns.',\n  aml:\n    'Draft a SAR-ready AML case file for the human investigator. Focus: structuring/smurfing, ' +\n    'rapid movement-then-withdrawal, layering, round-number patterns, threshold avoidance.',\n}\n\nconst buildSkill = (mode: InvestigationMode) => ({\n  name: 'transaction-investigator',\n  description: 'Surfaces anomalous transaction patterns as typed findings (draft for human review).',\n  systemPrompt: `You investigate a transaction history for anomalous patterns. ${MODE_GUIDANCE[mode]}\n\nEach finding MUST: cite the underlying transaction IDs (in location), name the pattern it tripped\n(in category), set a severity (critical|high|medium|low|info), and suggest a next step (remediation).\nNEVER freeze accounts or file reports — your output is always a DRAFT for a human.\nIf patterns are ambiguous or evidence is thin, set insufficientEvidence=true and prefer\n\"insufficient evidence — monitor\" over a false positive. Do not over-claim.\n\n${UNTRUSTED_CONTENT_DIRECTIVE}\nCompliance: you do not provide financial advice; KYC/AML/sanctions decisions require human sign-off.\n\nCall submit_case exactly once with { findings, summary, insufficientEvidence }. Stop.`,\n  tools: ['submit_case'],\n})\n\nconst rank = (s: Severity): number => SEVERITY_ORDER.indexOf(s)\n\nexport function createTransactionInvestigatorAgent(config: TransactionInvestigatorConfig) {\n  const mode = config.mode ?? 'fraud'\n  const skill = buildSkill(mode)\n  const emit = (label: string, status: 'start' | 'ok' | 'skip' | 'error', detail?: string) => {\n    for (const o of config.observers ?? []) void o.on({ type: 'progress', label, status, detail })\n  }\n  const submit = (): ToolDefinition =>\n    defineZodTool({ name: 'submit_case', description: 'Submit the case file. Call exactly once.', schema: Output, toJsonSchema: toJson, async execute() { return 'recorded' } }) as ToolDefinition\n\n  async function run(history: string): Promise<InvestigationResult> {\n    if (!history?.trim()) throw new Error('transaction investigator requires a non-empty transaction history')\n    emit('investigate', 'start', mode)\n    let out: z.infer<typeof Output>\n    try {\n      out = await invokeStructured({\n        adapter: config.adapter,\n        tool: submit(),\n        task: `TRANSACTION HISTORY:\\n${fenceUntrustedContent(history)}`,\n        parse: (a) => Output.parse(a),\n        skill,\n        memory: config.memory,\n        observers: config.observers,\n        onConfirm: config.onConfirm,\n        maxSteps: config.maxSteps ?? 3,\n      })\n    } catch {\n      // FAIL-SAFE: never emit a silent all-clear when the model failed to produce a case file.\n      emit('investigate', 'error')\n      return { mode, findings: [], summary: 'investigation unavailable — manual review required', highestSeverity: null, insufficientEvidence: true, requiresHumanReview: true }\n    }\n\n    const findings = out.findings as Finding[]\n    const highestSeverity = findings.length\n      ? findings.reduce<Severity>((acc, f) => (rank(f.severity) < rank(acc) ? f.severity : acc), 'info')\n      : null\n    emit('investigate', 'ok', `${findings.length} finding(s)${highestSeverity ? ` (max ${highestSeverity})` : ''}`)\n    return {\n      mode,\n      findings,\n      summary: out.summary,\n      highestSeverity,\n      insufficientEvidence: out.insufficientEvidence,\n      requiresHumanReview: true,\n    }\n  }\n\n  return {\n    name: 'fintech-transaction-investigator',\n    run,\n    asHandle() {\n      return { name: 'fintech-transaction-investigator', run: async (task: string) => JSON.stringify(await run(task)) }\n    },\n  }\n}\n"},{"path":"README.md","content":"# Transaction Investigator\n\nScans a transaction history for anomalous patterns and drafts a **typed case file** — `fraud` or `aml` (SAR-ready) mode. Always a draft for a human; never an enforcement action.\n\n```bash\nnpx agentskit add fintech-transaction-investigator\n```\n\n```ts\nimport { anthropic } from '@agentskit/adapters'\nimport { createTransactionInvestigatorAgent } from './agents/fintech-transaction-investigator/agent'\n\nconst r = await createTransactionInvestigatorAgent({\n  adapter: anthropic({ apiKey: process.env.ANTHROPIC_API_KEY!, model: 'claude-opus-4-8' }),\n  mode: 'aml', // or 'fraud'\n}).run(transactionHistory)\n// → { findings: Finding[], summary, highestSeverity, insufficientEvidence, requiresHumanReview }\n```\n\n> Replaces the two near-duplicate single-prompt agents `fintech-fraud-investigator` and `fintech-transaction-monitor` with one configurable, typed agent.\n\n- **Canonical `Finding[]`** — `invokeStructured` + zod against `@agentskit/core/finding`. Each finding cites the transaction IDs (`location`), the pattern tripped (`category`), a severity, and a next step (`remediation`) — interoperable with dashboards, eval scorers, and AKOS steps.\n- **Always a draft** — `requiresHumanReview` is always true; never freezes accounts or files SARs. `highestSeverity` summarises the case.\n- **No over-claiming / fail-safe** — ambiguous evidence ⇒ `insufficientEvidence`; an unparseable model response ⇒ `insufficientEvidence:true` with zero findings, **never** a silent all-clear.\n- Untrusted transaction text is **fenced**; compliance footer enforced.\n\n`run(history)` → `InvestigationResult`. `asHandle()` is JSON-out. See [composing agents](../../COMPOSING.md). Pairs with [`fintech-sanctions-screener`](../fintech-sanctions-screener) and [`fintech-kyc-screener`](../fintech-kyc-screener).\n"},{"path":"eval.ts","content":"import type { EvalSuite } from '@agentskit/eval'\n\n/** Eval cases for the investigator AgentHandle (`run(history) → jsonInvestigationResult`). */\nexport const suite: EvalSuite = {\n  name: 'fintech-transaction-investigator',\n  cases: [\n    {\n      input: `Account ACC-771 transactions:\nTX-101 2026-06-01 deposit $9,500 (Branch A)\nTX-102 2026-06-01 deposit $9,500 (Branch B)\nTX-103 2026-06-02 deposit $9,400 (Branch A)\nTX-104 2026-06-02 deposit $9,600 (Branch C)\nTX-105 2026-06-03 wire-out $37,000 to overseas account`,\n      expected: (r: string) =>\n        /\"findings\":\\[/.test(r) && /TX-10/.test(r) && /\"requiresHumanReview\":true/.test(r) && /(structuring|smurf|threshold)/i.test(r),\n    },\n    {\n      input: `Account ACC-880 transactions:\nTX-200 2026-06-01 coffee $4.20\nTX-201 2026-06-02 groceries $63.10\nTX-202 2026-06-05 salary deposit $3,200`,\n      expected: (r: string) => /\"insufficientEvidence\":true/.test(r) || /\"findings\":\\[\\]/.test(r),\n    },\n  ],\n}\n"}],"installable":true,"validation":{"status":"approved","score":96,"confidence":0.96,"method":"codex-executor-independent-reviewer","iterations":1,"cases":3,"summary":"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.","strengths":["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."],"notes":[]}}