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.
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:
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:
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. Validateresult.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.