How to Build a transaction monitoring Agent Using LangGraph in TypeScript for healthcare
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.
- •Produces one of:
- •
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
- •
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.
- •
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.
- •
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.
- •
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
- •The complete AI Agents Roadmap — my full 8-step breakdown
- •Free: The AI Agent Starter Kit — PDF checklist + starter code
- •Work with me — I build AI for banks and insurance companies
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