← Back to blog
·9 min read

Working Capital Assessment Agent for SME Lending

Build an AI agent that analyzes bank statements, GST data, and balance sheets to automate working capital credit decisions for Indian SMEs.

sme lendingworking capitalcredit assessmentbank statement analysisai agentfintechnbfc

Working capital loans power Indian SMEs — but underwriting them manually is slow, error-prone, and expensive. An AI agent that reads bank statements, GST returns, and balance sheets can compress a three-day credit review into minutes. This guide shows you exactly how to build one with the Lekha API.

What Is Working Capital Assessment?

Working capital assessment is the process lenders use to evaluate whether a business generates enough cash flow to service a short-term loan. Unlike home loans (which use salary slips), SME working capital underwriting depends on:

  • Bank statement analysis — average monthly balance, inflow consistency, EMI obligations already running
  • GST turnover — declared sales vs. actual credits to confirm revenue authenticity
  • Balance sheet ratios — current ratio, quick ratio, debtor days, creditor days
  • Traditionally, a credit analyst pulls PDFs, pastes numbers into Excel, and writes a memo. Lekha lets an AI agent do all of that in seconds.

    Architecture Overview

    The agent follows a simple pipeline:

  • Accept uploaded documents (bank statement PDF + GST returns + balance sheet)
  • Call Lekha to extract structured JSON from each document
  • Run working capital calculations on the extracted data
  • Generate a credit recommendation with a risk flag
  • SME Application → Document Upload → Lekha Extraction → Credit Engine → Underwriter Dashboard
    

    You can run this as a Next.js API route, a serverless function, or embed it inside a CrewAI / LangGraph workflow.

    Setting Up the Lekha Client

    Install the SDK and configure your API key:

    bun add lekha-sdk
    
    import { LekhaClient } from "lekha-sdk";
    

    const lekha = new LekhaClient({ apiKey: process.env.LEKHA_API_KEY!, });

    Get your API key at lekhadev.com/docs.

    Step 1: Extract Bank Statement Data

    A 12-month bank statement is the heart of any working capital assessment. Lekha handles every major Indian bank format — HDFC, SBI, ICICI, Axis, Kotak, and 30+ others.

    interface WorkingCapitalMetrics {
      avgMonthlyCredit: number;
      avgMonthlyDebit: number;
      avgClosingBalance: number;
      bouncedCheques: number;
      emiObligations: number;
      monthlyTurnover: number[];
      cashFlowVariance: number;
    }
    

    async function extractBankStatement( pdfBuffer: ArrayBuffer, ): Promise { const result = await lekha.extract({ document: pdfBuffer, type: "bank_statement", });

    const transactions = result.data.transactions; const monthlyGroups = groupTransactionsByMonth(transactions);

    const monthlyCredits = monthlyGroups.map((m) => m.filter((t) => t.type === "credit").reduce((sum, t) => sum + t.amount, 0), );

    const monthlyDebits = monthlyGroups.map((m) => m.filter((t) => t.type === "debit").reduce((sum, t) => sum + t.amount, 0), );

    const avgMonthlyCredit = monthlyCredits.reduce((a, b) => a + b, 0) / monthlyCredits.length; const avgMonthlyDebit = monthlyDebits.reduce((a, b) => a + b, 0) / monthlyDebits.length;

    const bouncedCheques = transactions.filter( (t) => t.narration?.toLowerCase().includes("bounce") || t.narration?.toLowerCase().includes("return"), ).length;

    const emiObligations = transactions .filter( (t) => t.category === "emi" || t.narration?.toLowerCase().includes("emi"), ) .reduce((sum, t) => sum + t.amount, 0) / monthlyGroups.length;

    const variance = calculateVariance(monthlyCredits);

    return { avgMonthlyCredit, avgMonthlyDebit, avgClosingBalance: result.data.summary.average_closing_balance, bouncedCheques, emiObligations, monthlyTurnover: monthlyCredits, cashFlowVariance: variance, }; }

    function groupTransactionsByMonth( transactions: Transaction[], ): Transaction[][] { const groups: Record = {}; for (const t of transactions) { const month = t.date.substring(0, 7); // YYYY-MM if (!groups[month]) groups[month] = []; groups[month].push(t); } return Object.values(groups); }

    function calculateVariance(values: number[]): number { const mean = values.reduce((a, b) => a + b, 0) / values.length; const squaredDiffs = values.map((v) => Math.pow(v - mean, 2)); return ( Math.sqrt(squaredDiffs.reduce((a, b) => a + b, 0) / values.length) / mean ); }

    Step 2: Validate GST Turnover Against Bank Credits

    One of the most important fraud checks in SME lending is comparing declared GST turnover with actual bank credits. A large gap signals potential revenue inflation.

    interface GSTData {
      annualTurnover: number;
      quarterlyTurnover: number[];
      gstRate: number;
      filingRegularity: "regular" | "irregular" | "defaulter";
    }
    

    async function extractGSTData(pdfBuffer: ArrayBuffer): Promise { const result = await lekha.extract({ document: pdfBuffer, type: "gst_return", });

    return { annualTurnover: result.data.annual_turnover, quarterlyTurnover: result.data.quarterly_breakdown.map( (q: { turnover: number }) => q.turnover, ), gstRate: result.data.applicable_gst_rate, filingRegularity: result.data.filing_status, }; }

    function validateTurnoverConsistency( bankMetrics: WorkingCapitalMetrics, gstData: GSTData, ): { score: number; flags: string[] } { const flags: string[] = []; const annualBankCredits = bankMetrics.avgMonthlyCredit * 12;

    // GST-reported turnover should roughly match bank credits const turnoverRatio = gstData.annualTurnover / annualBankCredits; if (turnoverRatio < 0.6 || turnoverRatio > 1.4) { flags.push( Turnover mismatch: GST ₹${(gstData.annualTurnover / 1e5).toFixed(1)}L vs Bank Credits ₹${(annualBankCredits / 1e5).toFixed(1)}L, ); }

    if (gstData.filingRegularity === "defaulter") { flags.push("GST filing defaults detected — high compliance risk"); }

    // Score: 100 = perfect match, deduct for flags const score = Math.max(0, 100 - flags.length * 25); return { score, flags }; }

    Step 3: Extract Balance Sheet Ratios

    For businesses that provide audited accounts, Lekha can parse balance sheets to compute working capital ratios.

    interface BalanceSheetRatios {
      currentRatio: number;
      quickRatio: number;
      debtEquityRatio: number;
      debtorDays: number;
      creditorDays: number;
    }
    

    async function extractBalanceSheet( pdfBuffer: ArrayBuffer, ): Promise { const result = await lekha.extract({ document: pdfBuffer, type: "balance_sheet", });

    const bs = result.data; const currentRatio = bs.current_assets / bs.current_liabilities; const quickRatio = (bs.current_assets - bs.inventory) / bs.current_liabilities; const debtEquityRatio = bs.total_debt / bs.shareholders_equity;

    // Debtor days = (Debtors / Annual Revenue) * 365 const debtorDays = bs.trade_receivables ? (bs.trade_receivables / bs.annual_revenue) * 365 : 0; const creditorDays = bs.trade_payables ? (bs.trade_payables / bs.annual_purchases) * 365 : 0;

    return { currentRatio, quickRatio, debtEquityRatio, debtorDays, creditorDays, }; }

    Step 4: The Credit Decision Engine

    With all three data sources extracted, the agent computes a working capital eligibility score and recommended loan amount.

    interface CreditDecision {
      eligible: boolean;
      recommendedLimit: number;
      riskCategory: "low" | "medium" | "high" | "decline";
      score: number;
      reasoning: string[];
    }
    

    function assessWorkingCapital( bankMetrics: WorkingCapitalMetrics, gstData: GSTData, balanceSheet: BalanceSheetRatios | null, requestedAmount: number, ): CreditDecision { const reasoning: string[] = []; let score = 100;

    // Rule 1: Minimum 6-month average credit threshold if (bankMetrics.avgMonthlyCredit < 200000) { score -= 30; reasoning.push("Average monthly credits below ₹2L minimum threshold"); }

    // Rule 2: Cheque bounces are a hard negative signal if (bankMetrics.bouncedCheques > 3) { score -= 25; reasoning.push( ${bankMetrics.bouncedCheques} cheque bounces detected in statement period, ); }

    // Rule 3: High cash flow variance = seasonal business risk if (bankMetrics.cashFlowVariance > 0.5) { score -= 15; reasoning.push( "High revenue seasonality detected — consider seasonal loan structure", ); }

    // Rule 4: GST compliance matters for formal credit if (gstData.filingRegularity === "irregular") { score -= 20; reasoning.push("Irregular GST filings indicate compliance risk"); }

    // Rule 5: Balance sheet ratios (if available) if (balanceSheet) { if (balanceSheet.currentRatio < 1.2) { score -= 20; reasoning.push( Current ratio ${balanceSheet.currentRatio.toFixed(2)} below 1.2 benchmark, ); } if (balanceSheet.debtEquityRatio > 3) { score -= 15; reasoning.push( High leverage: D/E ratio ${balanceSheet.debtEquityRatio.toFixed(1)}, ); } }

    // Working capital limit = 25% of annual bank credits (standard NBFC heuristic) const maxEligibleLimit = bankMetrics.avgMonthlyCredit 12 0.25; const recommendedLimit = Math.min(requestedAmount, maxEligibleLimit);

    const riskCategory = score >= 80 ? "low" : score >= 60 ? "medium" : score >= 40 ? "high" : "decline";

    if (riskCategory === "low") { reasoning.push( Strong cash flows support up to ₹${(maxEligibleLimit / 1e5).toFixed(1)}L, ); }

    return { eligible: score >= 40, recommendedLimit, riskCategory, score, reasoning, }; }

    Step 5: Wire It Into an API Route

    Here's the complete endpoint that accepts multipart form uploads and returns a credit decision:

    // app/api/working-capital/assess/route.ts
    import { NextRequest, NextResponse } from "next/server";
    

    export async function POST(req: NextRequest) { const form = await req.formData();

    const bankStatementFile = form.get("bank_statement") as File; const gstFile = form.get("gst_return") as File; const balanceSheetFile = form.get("balance_sheet") as File | null; const requestedAmount = Number(form.get("requested_amount") ?? 500000);

    const [bankBuffer, gstBuffer] = await Promise.all([ bankStatementFile.arrayBuffer(), gstFile.arrayBuffer(), ]);

    const [bankMetrics, gstData] = await Promise.all([ extractBankStatement(bankBuffer), extractGSTData(gstBuffer), ]);

    let balanceSheet = null; if (balanceSheetFile) { const bsBuffer = await balanceSheetFile.arrayBuffer(); balanceSheet = await extractBalanceSheet(bsBuffer); }

    const decision = assessWorkingCapital( bankMetrics, gstData, balanceSheet, requestedAmount, );

    return NextResponse.json({ success: true, data: { decision, extracted: { avgMonthlyCredit: bankMetrics.avgMonthlyCredit, avgMonthlyDebit: bankMetrics.avgMonthlyDebit, gstTurnover: gstData.annualTurnover, bouncedCheques: bankMetrics.bouncedCheques, }, }, }); }

    Try the extraction live at lekhadev.com/playground before writing any code.

    What Good Looks Like: Sample Output

    For a healthy SME applying for a ₹5L working capital loan:

    {
      "decision": {
        "eligible": true,
        "recommendedLimit": 500000,
        "riskCategory": "low",
        "score": 85,
        "reasoning": ["Strong cash flows support up to ₹7.2L"]
      },
      "extracted": {
        "avgMonthlyCredit": 240000,
        "avgMonthlyDebit": 195000,
        "gstTurnover": 2800000,
        "bouncedCheques": 0
      }
    }
    

    A ₹5L request against ₹24L in monthly credits and zero bounces — the agent green-lights it in under two seconds.

    Production Considerations

    Multi-bank statements: Many SMEs bank with two or three banks. Run parallel extractions and sum the monthly credit figures before scoring. 12-month minimum: Lekha extracts whatever date range is in the document. Validate result.data.period_months >= 6 before running the decision engine — shorter windows inflate variance scores. Confidence scores: Lekha returns a confidence field per extraction. Set a threshold (e.g. > 0.85) and route low-confidence documents to a human reviewer rather than auto-deciding. Audit trail: Store the raw Lekha JSON alongside the credit decision for RBI/NBFC audit compliance. Never store the original PDF — process in memory and discard.

    FAQ

    What document formats does Lekha accept for bank statements? Lekha accepts PDFs and images (JPG, PNG) from 35+ Indian banks including SBI, HDFC, ICICI, Axis, Kotak, PNB, Canara, and regional co-operative banks. Password-protected PDFs are supported when you pass the password via the API. How accurate is the extraction for working capital calculations? Extraction accuracy exceeds 98% on digital PDFs from major Indian banks. Scanned statements from co-operative banks or older formats may score lower — use the confidence field to route those for human review. Can I use this for NBFC regulatory compliance? The extracted data gives you a structured audit trail, but your credit policy — minimum score thresholds, loan-to-turnover ratios, bureau checks — must be defined by your risk team per RBI NBFC guidelines. Lekha handles the document intelligence layer; compliance logic lives in your code. Does Lekha store the uploaded financial documents? No. Lekha processes documents entirely in memory and returns structured JSON. No PDF is stored on Lekha's servers, keeping you compliant with India's DPDP Act.

    Working capital lending for Indian SMEs is a ₹25 trillion market, and most of the friction sits in the document review step. With Lekha handling extraction, your engineering team can focus on the credit models and the underwriter experience — not on parsing PDFs.

    Start building for free at lekhadev.com — no credit card required.