How to Build a loan approval Agent Using CrewAI in TypeScript for fintech

By Cyprian AaronsUpdated 2026-04-21
loan-approvalcrewaitypescriptfintech

A loan approval agent automates the first pass of credit decisioning: it gathers applicant data, checks policy rules, analyzes risk signals, and produces a recommendation with an audit trail. For fintech, that matters because you need faster approvals without losing control over compliance, explainability, and consistent underwriting.

Architecture

  • Applicant intake service

    • Receives the loan application payload from your API or workflow engine.
    • Normalizes fields like income, employment type, debt obligations, and requested amount.
  • Policy retrieval layer

    • Pulls underwriting rules from a versioned source such as a policy store or internal knowledge base.
    • Keeps product-specific constraints separate from agent logic.
  • CrewAI agent layer

    • Uses a Crew with one or more Agent instances to evaluate the application.
    • Separates tasks for document review, risk assessment, and final recommendation.
  • Tooling layer

    • Exposes deterministic tools for credit bureau lookup, affordability calculations, KYC checks, and sanctions screening.
    • Keeps sensitive operations outside the LLM prompt as much as possible.
  • Decision and audit service

    • Persists the recommendation, rationale, model inputs, policy version, and tool outputs.
    • Supports internal review, regulator requests, and dispute handling.

Implementation

1) Install CrewAI in your TypeScript service

For a fintech backend, keep the agent in a dedicated service with strict network boundaries. Install CrewAI and define your runtime environment variables for model access and policy storage.

npm install @crewai/crewai zod dotenv

Then create a small domain model for the application payload:

export type LoanApplication = {
  applicantId: string;
  annualIncome: number;
  monthlyDebt: number;
  requestedAmount: number;
  termMonths: number;
  employmentStatus: "employed" | "self_employed" | "unemployed";
  country: string;
};

2) Define deterministic tools for compliance-sensitive checks

Do not ask the LLM to “calculate” or “verify” anything that should be deterministic. Use tools for affordability and policy checks so the model only interprets results.

import { z } from "zod";

export const affordabilityInput = z.object({
  annualIncome: z.number().positive(),
  monthlyDebt: z.number().nonnegative(),
  requestedAmount: z.number().positive(),
  termMonths: z.number().int().positive(),
});

export function calculateDebtToIncome(input: z.infer<typeof affordabilityInput>) {
  const monthlyIncome = input.annualIncome / 12;
  const dti = input.monthlyDebt / monthlyIncome;
  return {
    monthlyIncome,
    debtToIncomeRatio: Number(dti.toFixed(4)),
    affordable: dti < 0.43,
  };
}

export async function fetchPolicy(country: string) {
  // Replace with your policy store or internal API.
  return {
    country,
    maxDTI: country === "US" ? 0.43 : 0.38,
    minIncome: country === "US" ? 25000 : 30000,
    requiresManualReviewAboveAmount: country === "US" ? 50000 : 25000,
    version: "2026-01",
  };
}

3) Build the CrewAI agents and tasks

The pattern here is simple: one agent reviews facts against policy, another produces the final decision summary. Keep the prompts narrow and force structured output.

import { Agent, Task, Crew } from "@crewai/crewai";
import { calculateDebtToIncome, fetchPolicy } from "./tools";
import type { LoanApplication } from "./types";

const underwriter = new Agent({
  role: "Loan Underwriter",
  goal: "Assess loan applications against policy and risk signals",
  backstory:
    "You are an underwriting analyst working within strict lending policy.",
});

const reviewer = new Agent({
  role: "Compliance Reviewer",
  goal: "Produce an auditable approval recommendation",
});

export async function reviewLoan(application: LoanApplication) {
    const policy = await fetchPolicy(application.country);
    const affordability = calculateDebtToIncome({
      annualIncome: application.annualIncome,
      monthlyDebt: application.monthlyDebt,
      requestedAmount: application.requestedAmount,
      termMonths: application.termMonths,
    });

    const underwritingTask = new Task({
      description: `
        Review this loan application using the provided facts only.
        Application JSON:
        ${JSON.stringify(application)}
        Policy JSON:
        ${JSON.stringify(policy)}
        Affordability JSON:
        ${JSON.stringify(affordability)}

        Return a concise assessment of risk factors and whether manual review is required.
      `,
      expectedOutput:
        "A short underwriting assessment with explicit reasons tied to policy.",
      agent: underwriter,
    });

    const decisionTask = new Task({
      description:
        "Convert the underwriting assessment into an approve/decline/manual_review recommendation with rationale.",
      expectedOutput:
        "JSON containing decision, rationale, policyVersion, and auditNotes.",
      agent: reviewer,
      context: [underwritingTask],
    });

    const crew = new Crew({
      agents: [underwriter, reviewer],
      tasks: [underwritingTask, decisionTask],
    });

    return await crew.kickoff();
}

4) Wrap it in an API endpoint with audit logging

The agent should never be your system of record. Persist inputs, outputs, tool results, policy version, and request IDs before returning anything to downstream systems.

import express from "express";
import { reviewLoan } from "./reviewLoan";
import type { LoanApplication } from "./types";

const app = express();
app.use(express.json());

app.post("/loan-review", async (req, res) => {
  const application = req.body as LoanApplication;

  const result = await reviewLoan(application);

  // Persist these fields to your audit store.
   await saveAuditRecord({
     applicantId: application.applicantId,
     inputSnapshot: application,
     result,
     sourceSystem: "loan-origination-api",
     reviewedAt: new Date().toISOString(),
   });

   res.json(result);
});

async function saveAuditRecord(record: unknown) {
   console.log("AUDIT", JSON.stringify(record));
}

app.listen(3000);

Production Considerations

  • Keep data residency explicit

    • Route EU applications to EU-hosted inference endpoints and storage.
    • Do not send raw PII across regions unless your legal basis and contracts allow it.
  • Log every decision path

    • Store prompt version, policy version, tool outputs, model name, and final recommendation.
    • You need this for adverse action notices and regulator reviews.
  • Add hard guardrails before execution

    • Enforce maximum DTI thresholds, minimum income floors, sanctions screening status, and product caps in code.
    • The LLM should explain decisions; it should not override mandatory rules.
  • Monitor drift in approvals

    • Track approval rate by segment, manual review rate, false positives on declines, and time-to-decision.
    • If one demographic or geography shifts unexpectedly, stop auto-decisioning and inspect policy or prompt changes.

Common Pitfalls

  • Letting the model make numeric decisions

    • Mistake: asking the agent to compute DTI or eligibility from scratch.
    • Fix it by calculating all thresholds in deterministic TypeScript first.
  • Skipping audit context

    • Mistake: storing only the final answer.
    • Fix it by persisting raw inputs, tool outputs, prompt versioning, crew output, and policy snapshot.
  • Mixing compliance logic into prompts

    • Mistake: embedding lending rules only in natural language instructions.
    • Fix it by encoding mandatory rules in code and using the agent for explanation plus edge-case analysis.

A loan approval agent works best when CrewAI handles reasoning orchestration and TypeScript enforces hard business rules. In fintech that separation is not optional; it is what keeps you fast enough to ship and controlled enough to pass audits.


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