How to Build a underwriting Agent Using LlamaIndex in TypeScript for retail banking
An underwriting agent for retail banking takes a loan application, pulls the right policy and customer context, checks the request against credit and compliance rules, and returns a decision recommendation with evidence. It matters because retail banks need faster turnaround on personal loans, overdrafts, and small-ticket credit without losing auditability, consistency, or control.
Architecture
- •
Application intake layer
- •Accepts structured inputs like income, employment type, existing obligations, requested amount, and term.
- •Normalizes data before it hits the agent.
- •
Policy knowledge base
- •Stores underwriting policy documents, product rules, exception handling guides, and regulatory notes.
- •Indexed with LlamaIndex so the agent can retrieve the exact rule behind a recommendation.
- •
Risk decision engine
- •Combines retrieved policy context with application facts.
- •Produces approve / refer / decline recommendations with reasons.
- •
Evidence and audit trail
- •Captures retrieved passages, prompts, model outputs, and final decision payloads.
- •Required for model risk management and post-decision review.
- •
Guardrails layer
- •Blocks unsupported recommendations.
- •Enforces no-use fields like protected attributes or prohibited proxies.
- •
Human review handoff
- •Routes borderline cases to an underwriter with a compact summary.
- •Keeps final authority where policy requires it.
Implementation
- •Install LlamaIndex for TypeScript and set up your environment
Use the TypeScript SDK and keep your configuration explicit. For banking workloads, do not hardcode keys or embed policy documents in source control.
npm install @llamaindex/core @llamaindex/openai dotenv
import "dotenv/config";
import { OpenAI } from "@llamaindex/openai";
import {
Document,
VectorStoreIndex,
Settings,
} from "@llamaindex/core";
Settings.llm = new OpenAI({
model: "gpt-4o-mini",
apiKey: process.env.OPENAI_API_KEY,
});
- •Load underwriting policies into an index
This is the part most teams skip and then regret later. The agent should answer from bank-approved policy text, not from memory.
const policyDocs = [
new Document({
text: `
Personal Loan Underwriting Policy:
- Minimum monthly net income: $2,500
- Maximum debt-to-income ratio: 45%
- Employment tenure must be at least 6 months
- Decline if recent delinquency within last 90 days
- Refer to manual review if income is variable or unverifiable
`,
metadata: { source: "policy/personal-loan-v3", jurisdiction: "US" },
}),
new Document({
text: `
Exceptions:
- Existing customers with salary credits for 12 months may be considered at DTI up to 50%
- Manual approval required for self-employed applicants
`,
metadata: { source: "policy/exceptions-v2", jurisdiction: "US" },
}),
];
const index = await VectorStoreIndex.fromDocuments(policyDocs);
const retriever = index.asRetriever({ similarityTopK: 3 });
- •Build the underwriting workflow
The pattern here is retrieval first, then constrained reasoning. You want the model to explain a recommendation using retrieved policy snippets and application facts.
type Application = {
applicantId: string;
monthlyNetIncome: number;
monthlyDebtPayments: number;
employmentMonths: number;
recentDelinquency90d: boolean;
variableIncome: boolean;
};
const app: Application = {
applicantId: "A-10291",
monthlyNetIncome: 4200,
monthlyDebtPayments: 1600,
employmentMonths: 14,
recentDelinquency90d: false,
variableIncome: false,
};
const query = `
Underwrite this retail banking loan application:
${JSON.stringify(app, null, 2)}
Return:
1) decision as APPROVE, REFER, or DECLINE
2) reason grounded in policy
3) key evidence citations
`;
const response = await retriever.retrieve(query);
const context = response.map((node) => node.node.getContent()).join("\n\n");
const prompt = `
You are a retail banking underwriting assistant.
Use only the provided policy context.
Do not mention protected attributes.
Do not invent rules.
Policy Context:
${context}
Application:
${JSON.stringify(app, null, 2)}
Decision format:
decision: APPROVE | REFER | DECLINE
reason: string
evidence: string[]
`;
const result = await Settings.llm.complete(prompt);
console.log(result.text);
- •Add an explicit decision rule before exposing output
Do not let the model be the only gatekeeper. Compute deterministic ratios in code and use the LLM for explanation and summarization.
function computeDTI(applicantMonthlyDebtPayments: number, applicantMonthlyNetIncome: number) {
return applicantMonthlyDebtPayments / applicantMonthlyNetIncome;
}
const dti = computeDTI(app.monthlyDebtPayments, app.monthlyNetIncome);
let hardDecision: "APPROVE" | "REFER" | "DECLINE" = "REFER";
if (app.recentDelinquency90d) {
hardDecision = "DECLINE";
} else if (dti <= 0.45 && app.employmentMonths >= 6 && !app.variableIncome) {
hardDecision = "APPROVE";
}
console.log({ applicantId: app.applicantId, dti, hardDecision });
This gives you a production-safe split:
| Layer | Responsibility |
|---|---|
| Code | Hard eligibility checks |
| LLM | Policy-grounded explanation |
| Retriever | Evidence lookup |
| Human reviewer | Borderline or exception cases |
Production Considerations
- •
Data residency
- •Keep customer PII and underwriting artifacts in-region.
- •If your bank requires local processing, use a deployment path that keeps embeddings, logs, and prompts inside approved jurisdictions.
- •
Auditability
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