How to Build a transaction monitoring Agent Using LangGraph in TypeScript for healthcare

By Cyprian AaronsUpdated 2026-04-21
transaction-monitoringlanggraphtypescripthealthcare

A transaction monitoring agent for healthcare watches claim submissions, payment events, refunds, eligibility checks, and provider activity for suspicious or non-compliant patterns. It matters because healthcare data is regulated, billing fraud is expensive, and false positives can delay care or block legitimate reimbursements.

Architecture

  • Event intake layer

    • Consumes transactions from Kafka, SQS, or a database outbox.
    • Normalizes payloads into a single schema: claim, payment, provider, member, facility, amount, timestamp.
  • Risk scoring node

    • Applies deterministic rules first: duplicate claims, unusual frequency, high-dollar reversals, mismatched provider specialty.
    • Calls an LLM only when the rule engine needs context classification or narrative summarization.
  • Case enrichment node

    • Pulls provider metadata, prior incidents, policy rules, and claim history.
    • Redacts PHI before anything goes to the model unless the model is running in a compliant private environment.
  • Decision node

    • Produces one of: allow, review, hold, escalate.
    • Attaches reason codes for auditability.
  • Audit/logging sink

    • Writes every decision with immutable metadata.
    • Stores prompt version, rule version, model version, and reviewer outcome.
  • Human review handoff

    • Sends borderline cases to operations or compliance staff.
    • Captures feedback for later tuning.

Implementation

1) Define the state and build the graph

Use LangGraph’s StateGraph to model the monitoring flow as a typed state machine. Keep the state small and explicit; do not pass raw PHI around unless your deployment is inside a compliant boundary.

import { StateGraph, START, END } from "@langchain/langgraph";

type Transaction = {
  id: string;
  type: "claim" | "refund" | "payment" | "eligibility";
  amount: number;
  providerId: string;
  memberId: string;
  facilityId?: string;
  notes?: string;
};

type MonitorState = {
  tx: Transaction;
  riskScore: number;
  reasons: string[];
  decision?: "allow" | "review" | "hold" | "escalate";
};

const graph = new StateGraph<MonitorState>()

2) Add deterministic checks before any model call

For healthcare workflows, rules should carry most of the load. They are auditable, easy to explain to compliance teams, and safer than sending sensitive data to an LLM too early.

const scoreTransaction = async (state: MonitorState): Promise<Partial<MonitorState>> => {
  let riskScore = 0;
  const reasons: string[] = [];

  if (state.tx.amount > 10000) {
    riskScore += 40;
    reasons.push("High-value transaction");
  }

  if (state.tx.type === "refund" && state.tx.amount > 5000) {
    riskScore += 25;
    reasons.push("Large refund");
  }

  if (state.tx.notes?.toLowerCase().includes("urgent")) {
    riskScore += 10;
    reasons.push("Urgency language detected");
  }

  return { riskScore, reasons };
};

const decide = async (state: MonitorState): Promise<Partial<MonitorState>> => {
    if (state.riskScore >= 60) return { decision: "hold" };
    if (state.riskScore >= 30) return { decision: "review" };
    return { decision: "allow" };
};

3) Wire enrichment and human review paths

Use conditional edges so only borderline cases go to review. That keeps latency down and reduces unnecessary exposure of sensitive records.

const enrich = async (state: MonitorState): Promise<Partial<MonitorState>> => {
  // Replace with internal service calls:
  // provider specialty lookup, claim history lookup, prior incident lookup
  const extraRisk =
    state.tx.providerId.startsWith("TEMP") ? 20 : 0;

   return extraRisk > 0
    ? { riskScore: state.riskScore + extraRisk, reasons: [...state.reasons, "Temporary provider identifier"] }
    : {};
};

const routeAfterScore = (state: MonitorState) => {
   if (state.riskScore >= 30 && state.riskScore < 60) return "enrich";
   return "decide";
};

const routeAfterDecide = (state: MonitorState) => {
   return state.decision === "review" ? END : END;
};

graph.addNode("score", scoreTransaction);
graph.addNode("enrich", enrich);
graph.addNode("decide", decide);

graph.addEdge(START, "score");
graph.addConditionalEdges("score", routeAfterScore);
graph.addEdge("enrich", "decide");
graph.addConditionalEdges("decide", routeAfterDecide);

const app = graph.compile();

###4) Execute it with real transactions

In production you’ll run this from an API worker or stream consumer. Keep execution isolated per event so retries don’t corrupt shared state.

async function run() {
   const result = await app.invoke({
     tx: {
       id: "tx_123",
       type: "claim",
       amount: 12500,
       providerId: "TEMP_77",
       memberId: "m_456",
       notes: "urgent processing requested"
     },
     riskScore:0,
     reasons:[]
   });

   console.log({
     transactionId: result.tx.id,
     decision: result.decision,
     riskScore: result.riskScore,
     reasons: result.reasons
   });
}

run();

Production Considerations

  • Data residency

    Keep PHI and claim data in-region. If your model endpoint is outside your jurisdiction or cloud boundary, send only redacted summaries or use a private deployment.

  • Auditability

    Log the full decision chain:

    • input event ID
    • rule version
    • graph version
    • output decision
    • reviewer override

    Compliance teams need traceability more than they need clever prompts.

  • Guardrails

    Put hard limits on what the LLM can see:

    • redact member identifiers where possible
    • block free-text fields containing diagnoses unless required
    • reject outputs that do not match allowed decision enums
  • Operational monitoring

    Track:

    • false positive rate by transaction type
    • average time to decision
    • manual review volume
    • drift in provider-specific patterns

    If review queues spike after a policy change, you want that visible before it becomes an operations problem.

Common Pitfalls

  1. Sending raw PHI into the model by default

    Avoid this by redacting first and only enriching with the minimum necessary fields. In healthcare, “just send everything” is how teams create compliance incidents.

  2. Using the LLM as the primary detector

    Don’t do that. Deterministic rules should catch obvious fraud patterns; the model should assist with classification and summarization when rules are inconclusive.

  3. Skipping audit metadata

    If you don’t store prompt version, rule version, and graph version alongside each decision, you can’t explain why a transaction was held. That becomes a problem during audits and incident reviews.

  4. Treating all alerts equally

    Separate low-risk anomalies from high-risk events. A duplicate copay refund is not the same as repeated high-dollar claims across multiple facilities with shared identifiers.


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