← Back to blog
·8 min read

MSME Loan Due Diligence Agent: Automate Financial Analysis

Build an AI agent that automates MSME loan due diligence — extracting bank statements, GST data, and balance sheets to assess creditworthiness in minutes.

msme lendingdue diligenceai agentbank statementsgstfinancial analysisloan underwritingindian fintech

MSME lending is one of the largest unsolved problems in Indian fintech. There are 63 million MSMEs in India, but fewer than 15% have ever received formal credit. The bottleneck isn't appetite — banks and NBFCs want to lend to this segment. The bottleneck is underwriting speed. A manual due diligence cycle for an MSME loan takes 5–15 business days and costs ₹3,000–₹8,000 per application in analyst time. For a ₹5 lakh working capital loan, that's not a viable unit economics.

AI agents change this. An MSME due diligence agent can read 12 months of bank statements, 4 quarters of GST returns, and a balance sheet in under 30 seconds — then run 40+ automated checks that would take a human analyst half a day.

This guide walks through building exactly that agent with Lekha's document extraction API.

What the Agent Verifies

A robust MSME due diligence agent checks four dimensions:

1. Revenue consistency — Does declared turnover match bank credits and GST filings? 2. Cash flow health — Is the business consistently cash-flow positive? What's the average monthly ending balance? 3. Debt service coverage — Are existing EMIs being paid on time, and what headroom exists for a new loan? 4. Business stability — How long has the business been operating, and is activity seasonal or consistent?

Each of these requires pulling structured data from multiple document types — which is where Lekha comes in.

Prerequisites

You'll need:

  • A Lekha API key (sign up here)
  • Node.js 18+ or Bun
  • The borrower's documents: 12 months of bank statements, last 4 GST returns (GSTR-3B), and the latest balance sheet
  • Install the HTTP client:

    bun add axios
    

    Step 1: Extract Bank Statements

    Start by extracting all bank statements. For a 12-month due diligence, you'll typically have 12 individual monthly PDFs or a single consolidated statement.

    import axios from "axios";
    import fs from "fs";
    

    const LEKHA_API_KEY = process.env.LEKHA_API_KEY!; const BASE_URL = "https://lekhadev.com/api/v1";

    async function extractBankStatement(pdfPath: string) { const fileBuffer = fs.readFileSync(pdfPath); const base64 = fileBuffer.toString("base64");

    const response = await axios.post( ${BASE_URL}/extract, { document: base64, type: "bank_statement", }, { headers: { Authorization: Bearer ${LEKHA_API_KEY}, "Content-Type": "application/json", }, }, );

    return response.data.data; }

    // Extract all 12 months in parallel async function extractAllStatements(pdfPaths: string[]) { const results = await Promise.all(pdfPaths.map(extractBankStatement)); return results; }

    Each extracted statement returns structured fields including opening_balance, closing_balance, transactions[], total_credits, and total_debits. The API auto-detects the bank — SBI, HDFC, ICICI, Axis, Kotak, and 30+ others — so you don't need separate parsers per lender.

    Step 2: Parse GST Returns and Balance Sheet

    GST returns (GSTR-3B PDFs downloaded from the GST portal) and balance sheets get the same treatment:

    async function extractGSTReturn(pdfPath: string) {
      const fileBuffer = fs.readFileSync(pdfPath);
      const base64 = fileBuffer.toString("base64");
    

    const response = await axios.post( ${BASE_URL}/extract, { document: base64, type: "gst_return", }, { headers: { Authorization: Bearer ${LEKHA_API_KEY} }, }, );

    return response.data.data; }

    async function extractBalanceSheet(pdfPath: string) { const fileBuffer = fs.readFileSync(pdfPath); const base64 = fileBuffer.toString("base64");

    const response = await axios.post( ${BASE_URL}/extract, { document: base64, type: "balance_sheet", }, { headers: { Authorization: Bearer ${LEKHA_API_KEY} }, }, );

    return response.data.data; }

    Step 3: Run Automated Due Diligence Checks

    With all documents extracted, run the checks. Each check is a pure function over structured data — no more manual column matching or PDF scanning:

    interface DueDiligenceResult {
      check: string;
      passed: boolean;
      value: string | number;
      flag?: string;
    }
    

    function runBankingChecks(statements: any[]): DueDiligenceResult[] { const checks: DueDiligenceResult[] = [];

    // 1. Average monthly credits const monthlyCredits = statements.map((s) => s.total_credits); const avgCredits = monthlyCredits.reduce((a, b) => a + b, 0) / monthlyCredits.length;

    checks.push({ check: "Average Monthly Credits", passed: avgCredits > 100000, value: Math.round(avgCredits), flag: avgCredits < 50000 ? "Low turnover relative to loan ask" : undefined, });

    // 2. Minimum ending balance (liquidity check) const closingBalances = statements.map((s) => s.closing_balance); const minBalance = Math.min(...closingBalances);

    checks.push({ check: "Minimum Monthly Balance", passed: minBalance > 0, value: minBalance, flag: minBalance < 0 ? "Account went into overdraft" : undefined, });

    // 3. EMI regularity — look for recurring fixed debits const allTransactions = statements.flatMap((s) => s.transactions); const emiTransactions = allTransactions.filter( (tx) => tx.description.match(/EMI|LOAN|ECS|NACH/i) && tx.type === "debit", );

    const monthsWithEmi = new Set( emiTransactions.map((tx) => tx.date.substring(0, 7)), ).size;

    checks.push({ check: "EMI Payment Consistency", passed: monthsWithEmi >= statements.length * 0.9, value: ${monthsWithEmi}/${statements.length} months, flag: monthsWithEmi < statements.length * 0.9 ? "Missed EMI payments detected" : undefined, });

    // 4. Credit-to-debit ratio const totalCredits = statements.reduce((a, s) => a + s.total_credits, 0); const totalDebits = statements.reduce((a, s) => a + s.total_debits, 0); const ratio = totalCredits > 0 ? totalDebits / totalCredits : 99;

    checks.push({ check: "Debit-to-Credit Ratio", passed: ratio < 0.95, value: ratio.toFixed(2), flag: ratio > 0.95 ? "Business spending exceeds income" : undefined, });

    return checks; }

    function reconcileGSTvsBanking( gstReturns: any[], statements: any[], ): DueDiligenceResult { const gstTurnover = gstReturns.reduce( (a, g) => a + (g.total_taxable_value ?? 0), 0, ); const bankCredits = statements.reduce((a, s) => a + s.total_credits, 0);

    // GST turnover should be ≤ bank credits (some credits aren't revenue) const variance = Math.abs(gstTurnover - bankCredits) / bankCredits;

    return { check: "GST vs Bank Reconciliation", passed: variance < 0.25, value: ${(variance * 100).toFixed(1)}% variance, flag: variance > 0.25 ? "Large mismatch between GST filings and bank deposits" : undefined, }; }

    Step 4: Generate a Structured Report

    Combine all checks into a report the underwriting team (or a downstream LLM) can act on:

    async function runDueDiligence(borrowerDocs: {
      bankStatements: string[];
      gstReturns: string[];
      balanceSheet: string;
    }) {
      console.log("Extracting documents...");
    

    const [statements, gstData, balanceSheet] = await Promise.all([ extractAllStatements(borrowerDocs.bankStatements), Promise.all(borrowerDocs.gstReturns.map(extractGSTReturn)), extractBalanceSheet(borrowerDocs.balanceSheet), ]);

    console.log("Running checks...");

    const bankingChecks = runBankingChecks(statements); const gstReconciliation = reconcileGSTvsBanking(gstData, statements);

    const allChecks = [...bankingChecks, gstReconciliation]; const passedCount = allChecks.filter((c) => c.passed).length; const flags = allChecks.filter((c) => c.flag).map((c) => c.flag!);

    const recommendation = passedCount >= allChecks.length * 0.85 && flags.length === 0 ? "APPROVE" : passedCount >= allChecks.length * 0.7 ? "REVIEW" : "DECLINE";

    return { recommendation, score: ${passedCount}/${allChecks.length}, flags, checks: allChecks, summary: { avgMonthlyCredits: Math.round( statements.reduce((a, s) => a + s.total_credits, 0) / statements.length, ), totalAssets: balanceSheet.total_assets, netWorth: balanceSheet.net_worth, }, }; }

    A typical run returns a result like:

    {
      "recommendation": "REVIEW",
      "score": "4/5",
      "flags": ["Large mismatch between GST filings and bank deposits"],
      "summary": {
        "avgMonthlyCredits": 487000,
        "totalAssets": 3200000,
        "netWorth": 1850000
      }
    }
    

    Your underwriters see a one-page summary instead of wading through PDFs. APPROVE decisions can route straight to sanction; REVIEW flags go to a human; DECLINE decisions get a rejection letter drafted automatically.

    Scaling to High Volume

    For lenders processing thousands of applications per month, run extractions asynchronously:

    async function submitForExtraction(pdfBase64: string, type: string) {
      const response = await axios.post(
        ${BASE_URL}/extract/async,
        { document: pdfBase64, type },
        { headers: { Authorization: Bearer ${LEKHA_API_KEY} } },
      );
      return response.data.job_id;
    }
    

    async function pollResult(jobId: string, maxWaitMs = 30000) { const start = Date.now(); while (Date.now() - start < maxWaitMs) { const response = await axios.get(${BASE_URL}/jobs/${jobId}, { headers: { Authorization: Bearer ${LEKHA_API_KEY} }, }); if (response.data.status === "complete") return response.data.data; await new Promise((r) => setTimeout(r, 1000)); } throw new Error("Extraction timed out"); }

    At scale, you can process 500+ applications per hour on a single Node.js instance. Try the async extraction endpoint live at lekhadev.com/playground.

    What This Agent Replaces

    The typical MSME underwriting workflow involves:

  • A credit analyst manually downloading PDFs from applicant portals
  • Copying transaction totals into an Excel model
  • Spot-checking GST figures against bank credits
  • Reviewing EMI bounce reports from CIBIL
  • The agent replaces steps 1–4 entirely. Analysts shift from data entry to exception review — looking at only the flagged cases, not every application.

    FAQ

    Does the agent handle all Indian banks? Yes. Lekha supports 30+ Indian banks including SBI, HDFC, ICICI, Axis, Kotak, PNB, Canara, Bank of Baroda, Federal, IDFC First, and all major cooperative banks. The classifier auto-detects the bank format — your code doesn't change per lender. See the full list in the docs. How accurate is the extraction? Lekha uses vision-language models tuned on Indian financial documents, not regex-based OCR. Field accuracy on clean PDFs is above 99%. On scanned or photographed documents, it's typically 96–98% — well above the 85–90% threshold that makes automation viable in underwriting. Is this DPDP compliant? Lekha processes documents in memory and returns only structured JSON — no raw PDF is stored server-side after extraction. This aligns with DPDP's data minimization principle. You should still obtain explicit borrower consent before submitting documents to any third-party API. Can I customize the checks for my underwriting policy? Yes. The checks in this guide are a starting point. You can adjust thresholds (e.g., require 18 months of statements for loans above ₹25 lakh), add sector-specific logic (seasonal businesses need different averaging windows), or pipe the structured output into your existing credit scoring model.

    MSME lending doesn't have to be a document processing problem. The financial data is already in those PDFs — it just needs to be extracted reliably. Lekha handles the extraction; you build the underwriting logic.

    Sign up for a free Lekha account and run your first extraction in under five minutes. The free tier includes 50 extractions per month — enough to prototype a complete due diligence workflow before committing to production.