How to Build a loan approval Agent Using LangGraph in TypeScript for payments

By Cyprian AaronsUpdated 2026-04-21
loan-approvallanggraphtypescriptpayments

A loan approval agent for payments takes a loan application, checks identity and financial signals, runs policy rules, and returns an approve, reject, or manual-review decision. In payments, that matters because every decision can affect fraud exposure, compliance posture, settlement risk, and customer experience.

Architecture

  • Input normalization

    • Convert raw application payloads into a strict internal schema.
    • Validate required fields before any model or policy call.
  • Eligibility and policy node

    • Apply deterministic rules first: KYC status, country restrictions, minimum income, debt-to-income thresholds.
    • Keep this logic outside the LLM so decisions stay auditable.
  • Risk scoring node

    • Combine payment history, chargeback signals, transaction velocity, and bureau data.
    • Return a structured risk score with reasons.
  • Decision node

    • Map policy + risk outputs into approved, rejected, or manual_review.
    • Use explicit thresholds so the behavior is stable in production.
  • Audit trail

    • Persist every input, intermediate state, and final decision.
    • Required for compliance reviews and post-incident analysis.
  • Human review handoff

    • Route borderline cases to an operations queue.
    • Keep the agent from auto-deciding when data is incomplete or high risk.

Implementation

1) Define the state and graph structure

Use a typed state object so each node reads and writes predictable fields. In LangGraph TypeScript, Annotation.Root gives you a clean way to define the shared state shape.

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

type LoanApplication = {
  applicantId: string;
  country: string;
  kycStatus: "verified" | "pending" | "failed";
  monthlyIncome: number;
  monthlyDebt: number;
  requestedAmount: number;
};

type LoanDecision = "approved" | "rejected" | "manual_review";

const LoanState = Annotation.Root({
  application: Annotation<LoanApplication>(),
  eligible: Annotation<boolean>(),
  riskScore: Annotation<number>(),
  reasons: Annotation<string[]>(),
  decision: Annotation<LoanDecision>(),
});

const graph = new StateGraph(LoanState)
  .addNode("validate", async (state) => {
    const reasons = [...(state.reasons ?? [])];
    if (!state.application.applicantId) reasons.push("missing_applicant_id");
    if (!state.application.country) reasons.push("missing_country");
    return { reasons };
  })
  .addNode("policy", async (state) => {
    const app = state.application;
    const eligible =
      app.kycStatus === "verified" &&
      app.country !== "restricted_country" &&
      app.monthlyIncome > app.monthlyDebt;

    return {
      eligible,
      reasons: [
        ...(state.reasons ?? []),
        eligible ? "policy_passed" : "policy_failed",
      ],
    };
  })

2) Add risk scoring and decision logic

Keep scoring deterministic unless you have a strong reason to involve an LLM. For payments workflows, deterministic logic is easier to audit and simpler to explain to compliance teams.

  .addNode("risk", async (state) => {
    const { monthlyIncome, monthlyDebt, requestedAmount } = state.application;
    const dti = monthlyDebt / Math.max(monthlyIncome, 1);
    const amountRatio = requestedAmount / Math.max(monthlyIncome * 6, 1);

    let score = 100;
    score -= Math.min(dti * 60, 60);
    score -= Math.min(amountRatio * 40, 40);

    return {
      riskScore: Math.max(0, Math.round(score)),
      reasons: [...(state.reasons ?? []), `risk_score_${Math.max(0, Math.round(score))}`],
    };
  })

3) Route to approve, reject, or manual review

LangGraph’s addConditionalEdges is the right tool here. It lets you branch based on the current state without burying business logic inside one giant function.

const router = (state: typeof LoanState.State) => {
  if (!state.eligible) return "reject";
  if ((state.riskScore ?? 0) >= 75) return "approve";
  if ((state.riskScore ?? 0) >= 55) return "review";
  return "reject";
};

const compiled = graph
  .addNode("approve", async () => ({
    decision: "approved" as const,
    reasons: ["auto_approved"],
  }))
export {};
const fullGraph = new StateGraph(LoanState)

Let's complete it properly:

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

type LoanApplication = {
  applicantId: string;
};

type LoanDecision = "approved" | "rejected" | "manual_review";

const LoanState = Annotation.Root({
  
})

---

## Keep learning

- [The complete AI Agents Roadmap](/blog/ai-agents-roadmap-2026) — my full 8-step breakdown
- [Free: The AI Agent Starter Kit](/starter-kit) — PDF checklist + starter code
- [Work with me](/contact) — I build AI for banks and insurance companies

*By Cyprian Aarons, AI Consultant at [Topiax](https://topiax.xyz).*

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