How to Build a loan approval Agent Using LlamaIndex in TypeScript for retail banking

By Cyprian AaronsUpdated 2026-04-21
loan-approvalllamaindextypescriptretail-banking

A loan approval agent in retail banking takes a customer’s application, pulls the right policy and product rules, checks the application against those rules, and produces a decision with an audit trail. It matters because loan ops teams need faster turnaround without losing control over compliance, explainability, and data handling.

Architecture

  • Application intake
    • Receives structured inputs like income, employment type, existing debt, requested amount, and country/branch.
  • Policy retrieval layer
    • Pulls the latest lending policy, product eligibility rules, and exception handling from approved documents.
  • Decision engine
    • Uses LlamaIndex to route the application to rule-based checks and document-grounded reasoning.
  • Audit logger
    • Stores the retrieved sources, model output, decision reason codes, and timestamps for compliance review.
  • Guardrail layer
    • Blocks missing KYC fields, unsupported jurisdictions, and policy violations before a decision is returned.
  • Human review handoff
    • Sends borderline or low-confidence cases to an underwriter instead of auto-approving.

Implementation

1) Index your lending policy documents

In retail banking, the agent should not “know” policy from memory. It should retrieve from approved sources like underwriting manuals, product sheets, and exception matrices.

import { Document, VectorStoreIndex } from "llamaindex";

async function buildPolicyIndex() {
  const docs = [
    new Document({
      text: `
        Personal Loan Policy v3:
        - Minimum age: 21
        - Minimum monthly income: 2500
        - Maximum unsecured debt-to-income ratio: 40%
        - Employment must be permanent or contract > 12 months
        - Manual review required for self-employed applicants
      `,
      metadata: { source: "policy-manual", version: "v3", jurisdiction: "KE" },
    }),
    new Document({
      text: `
        Exception Matrix:
        - Credit score 620-659 with salary account at bank: refer to underwriter
        - Existing customer with 12 months clean repayment history: allow override up to 5%
      `,
      metadata: { source: "exception-matrix", version: "v1", jurisdiction: "KE" },
    }),
  ];

  return await VectorStoreIndex.fromDocuments(docs);
}

2) Query the index with a structured approval prompt

Use asQueryEngine() to ground the decision in policy text. Keep the prompt tight so the model returns a decision plus reasons you can log.

import { QueryEngine } from "llamaindex";

type LoanApplication = {
  applicantId: string;
  age: number;
  monthlyIncome: number;
  debtToIncomeRatio: number;
  employmentType: "permanent" | "contract" | "self-employed";
  requestedAmount: number;
  creditScore: number;
};

async function evaluateApplication(app: LoanApplication) {
  const index = await buildPolicyIndex();
  const queryEngine = index.asQueryEngine();

  const prompt = `
You are a retail banking loan approval assistant.
Use only the retrieved policy context.

Application:
${JSON.stringify(app, null, 2)}

Return:
- decision: APPROVE | REFER | DECLINE
- reasonCodes: string[]
- summary: one short paragraph
`;

  const response = await queryEngine.query({ query: prompt });
  return response.toString();
}

3) Add hard guardrails before calling the model

Do not let LlamaIndex be the first line of defense. Basic eligibility checks should fail fast before any retrieval or generation happens.

function precheck(app: LoanApplication) {
  const errors: string[] = [];

  if (app.age < 21) errors.push("AGE_BELOW_MINIMUM");
  if (app.monthlyIncome < 2500) errors.push("INCOME_BELOW_MINIMUM");
  if (app.debtToIncomeRatio > 0.4) errors.push("DTI_ABOVE_LIMIT");
  if (app.employmentType === "self-employed") errors.push("SELF_EMPLOYED_MANUAL_REVIEW");

  return errors;
}

async function decideLoan(app: LoanApplication) {
  const precheckErrors = precheck(app);

  if (precheckErrors.includes("AGE_BELOW_MINIMUM") || precheckErrors.includes("INCOME_BELOW_MINIMUM")) {
    return {
      decision: "DECLINE",
      reasonCodes: precheckErrors,
      summary: "Failed mandatory eligibility checks.",
    };
    }

    if (precheckErrors.length > 0) {
      return {
        decision: "REFER",
        reasonCodes: precheckErrors,
        summary: "Case requires manual underwriting review.",
      };
    }

    return await evaluateApplication(app);
}

4) Persist an audit record for compliance

Retail banking needs traceability. Store the input payload, retrieved policy version, final outcome, and who/what made the decision.

type AuditRecord = {
  applicantId: string;
  timestampUtc: string;
  inputSnapshot: LoanApplication;
};

async function writeAudit(recordBody: AuditRecord & { decisionPayload?: string }) {
  
  console.log(JSON.stringify(recordBody));
}

async function processLoan(app: LoanApplication) {
  
const result = await decideLoan(app);

await writeAudit({
    applicantId:
      app.applicantId,
    timestampUtc:
      new Date().toISOString(),
    inputSnapshot:
      app,
    decisionPayload:
      result,
});

return result;
}

Production Considerations

  • Data residency

Keep policy indexes and application data in-region. If your bank operates in Kenya or Nigeria, do not send customer PII to a foreign-hosted vector store or inference endpoint without legal approval.

  • Compliance logging

Log retrieved document IDs, versions, timestamps, and final reason codes. Auditors will ask why a case was declined; “the model said so” is not acceptable.

  • Human-in-the-loop routing

Auto-decline only when mandatory criteria fail. Everything else with missing data, self-employment income complexity, thin-file credit history, or exception eligibility should go to an underwriter.

  • Monitoring

Track approval rate by segment, referral rate by branch/channel, and drift in reason codes. If one channel suddenly gets more declines than others, you likely have bad intake data or a broken rule mapping.

Common Pitfalls

  • Using unapproved documents as retrieval sources

If your index includes draft policies or outdated PDFs, you will approve or decline based on stale rules. Version your documents and load only signed-off policy content.

  • Letting the model make hard eligibility decisions without deterministic checks

Age minimums, income floors, DTI thresholds, and jurisdiction restrictions should be code-first rules. Use LlamaIndex for grounded reasoning and explanation generation after those checks pass.

  • Skipping explainability fields in the output

A bare “APPROVE” is useless in production. Always return decision, reasonCodes, summary, and source references so operations teams can defend the outcome during reviews.


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