How to Build a fraud detection Agent Using LangChain in TypeScript for investment banking

By Cyprian AaronsUpdated 2026-04-21
fraud-detectionlangchaintypescriptinvestment-banking

A fraud detection agent in investment banking is not a chatbot that “spots suspicious activity.” It is a decision-support layer that ingests transaction context, customer metadata, sanctions signals, and internal policy rules, then produces a risk assessment with evidence the bank can audit. That matters because false negatives create regulatory and financial exposure, while false positives create operational drag for analysts and clients.

Architecture

  • Data ingestion layer

    • Pulls transaction events from Kafka, CDC streams, or batch files.
    • Normalizes fields like counterparty, amount, jurisdiction, channel, and timestamp.
  • Policy and controls layer

    • Encodes bank-specific fraud rules.
    • Enforces thresholds for high-risk geographies, unusual counterparties, velocity spikes, and sanctions adjacency.
  • LangChain reasoning layer

    • Uses ChatOpenAI with structured output to classify risk.
    • Combines retrieved policy context with the transaction payload.
  • Evidence retrieval layer

    • Uses MemoryVectorStore or a real vector DB for internal controls, prior cases, and typology notes.
    • Grounds responses in approved documentation.
  • Audit logging layer

    • Persists prompts, model outputs, rule hits, and final decisions.
    • Supports model governance and post-incident review.
  • Human review handoff

    • Routes medium/high-risk cases to an analyst queue.
    • Keeps the agent advisory only unless policy explicitly allows auto-blocking.

Implementation

1) Install the LangChain packages you actually need

Use the OpenAI chat model wrapper plus core message types and structured output support.

npm install langchain @langchain/openai @langchain/core zod

Set your environment variables:

export OPENAI_API_KEY="your-key"

2) Define the fraud assessment schema

For investment banking, you want a strict response shape. Free-form text is useless when compliance teams need deterministic fields for audit trails.

import { z } from "zod";

export const FraudAssessmentSchema = z.object({
  riskScore: z.number().min(0).max(100),
  decision: z.enum(["clear", "review", "block"]),
  reasons: z.array(z.string()).min(1),
  policyHits: z.array(z.string()),
  recommendedAction: z.string(),
});

export type FraudAssessment = z.infer<typeof FraudAssessmentSchema>;

3) Build the agent chain with retrieval + structured output

This pattern uses ChatOpenAI, MemoryVectorStore, Document, and RunnableSequence. The retriever injects internal fraud typologies into the prompt so the model does not invent bank-specific logic.

import { ChatOpenAI } from "@langchain/openai";
import { MemoryVectorStore } from "langchain/vectorstores/memory";
import { Document } from "@langchain/core/documents";
import { RunnableSequence } from "@langchain/core/runnables";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { PromptTemplate } from "@langchain/core/prompts";
import { FraudAssessmentSchema } from "./schema.js";

const llm = new ChatOpenAI({
  model: "gpt-4o-mini",
  temperature: 0,
});

const docs = [
  new Document({
    pageContent:
      "Typology: rapid cross-border wires to newly added beneficiaries within 24 hours are high risk.",
    metadata: { source: "fraud_typology_01" },
  }),
  new Document({
    pageContent:
      "Typology: multiple transfers just below approval thresholds may indicate structuring.",
    metadata: { source: "fraud_typology_02" },
  }),
];

const vectorStore = await MemoryVectorStore.fromDocuments(docs);
const retriever = vectorStore.asRetriever(2);

const prompt = PromptTemplate.fromTemplate(`
You are a fraud detection assistant for an investment bank.
Use only the provided policy context and transaction data.
Return a JSON object matching this schema:
{schema}

Policy context:
{context}

Transaction:
{transaction}

Decision rules:
- block if there is clear structuring, sanctions adjacency, or prohibited jurisdiction exposure
- review if risk indicators exist but evidence is incomplete
- clear only when indicators are low and no policy hits apply
`);

const chain = RunnableSequence.from([
  async (input: { transaction: string }) => {
    const docs = await retriever.invoke(input.transaction);
    return {
      transaction: input.transaction,
      context: docs.map((d) => d.pageContent).join("\n"),
      schema: JSON.stringify(FraudAssessmentSchema.shape),
    };
  },
  prompt,
  llm.withStructuredOutput(FraudAssessmentSchema),
]);

const result = await chain.invoke({
  transaction: JSON.stringify({
    txnId: "TXN-88421",
    amountUSD: 98500,
    currency: "USD",
    counterpartyCountry: "AE",
    beneficiaryAgeDays: 1,
    transferCount24h: 4,
    channel: "wire",
    customerSegment: "prime_brokerage",
    sanctionsScreenHit: false,
    thresholdProximityPct: 97,
  }),
});

console.log(result);

4) Add deterministic pre-checks before the LLM call

Do not ask the model to do everything. In banking systems, hard rules should fire before any probabilistic reasoning. That gives you lower latency and better control.

type Txn = {
  amountUSD: number;
  beneficiaryAgeDays: number;
  transferCount24h: number;
  sanctionsScreenHit?: boolean;
};

function precheck(txn: Txn) {
  const hits: string[] = [];

  if (txn.sanctionsScreenHit) hits.push("sanctions_hit");
  if (txn.beneficiaryAgeDays <= 1) hits.push("new_beneficiary");
  if (txn.transferCount24h >= 3) hits.push("velocity_spike");

   if (txn.amountUSD >= 100000 && txn.beneficiaryAgeDays <= 2) {
    hits.push("high_value_new_beneficiary");
   }

   return hits;
}

In production, you would short-circuit on severe matches:

  • sanctions hit → block
  • prohibited jurisdiction → block
  • otherwise → send to LLM for contextual scoring

Production Considerations

  • Compliance and auditability

    • Persist every request/response pair with model version, prompt version, retrieved documents, and final analyst action.
    • Keep immutable logs for internal audit and regulator review.
  • Data residency

    • Keep PII and transaction data in-region.
    • If your bank has EU or APAC residency constraints, do not send raw customer data to endpoints outside approved regions.
    • Mask account numbers and tokenize identifiers before model calls where possible.
  • Guardrails

    • Use strict schemas with withStructuredOutput.
    • Block free-text outputs from driving downstream automation.
    • Route high-impact decisions through human approval unless policy explicitly permits automation.
  • Monitoring

    • Track precision/recall by fraud typology, false positive rate by business line, and analyst override rates.
    • Watch drift in counterparty geography, ticket sizes, and approval latency.

Common Pitfalls

  • Letting the LLM replace deterministic controls

    If you skip rule-based checks for sanctions or threshold breaches, you will eventually miss something obvious. Use the LLM for contextual ranking, not as the first line of defense.

  • Using unstructured prompts in production

    Free-form responses are hard to validate and harder to audit. Always force structured output with Zod-backed schemas or equivalent validation.

  • Ignoring lineage on retrieved evidence

    If you cannot trace which policies or case notes influenced a decision, your audit trail is weak. Store document IDs, sources, timestamps, and retrieval scores alongside each assessment.


Keep learning

By Cyprian Aarons, AI Consultant at Topiax.

Want the complete 8-step roadmap?

Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.

Get the Starter Kit

Related Guides