Net Worth Calculator Agent: AI for Indian Investors
Build a TypeScript AI agent that calculates net worth from bank statements, CAS reports, and salary slips in seconds using the Lekha API. Full code included.
A complete picture of someone's net worth requires data from at least three different document types: bank statements for liquid assets, Consolidated Account Statements (CAS) for investments, and salary slips for income context. Doing this manually takes hours. With the Lekha API, you can build an agent that does it in seconds.
This tutorial walks through building a TypeScript net worth calculator agent that accepts any combination of bank PDFs, CAS reports, and salary slips, then returns a structured breakdown of assets, liabilities, and net worth — ready to feed into a financial planning UI, a lending decisioning system, or any AI agent pipeline.
What Is a Net Worth Agent?
A net worth agent is an AI pipeline that accepts raw financial documents, extracts structured data from each one, and aggregates the results into a unified financial snapshot. For Indian investors, this means:
The agent handles document parsing so downstream logic only deals with clean, typed JSON — no PDF parsing, no regex, no format juggling.
Architecture
User uploads documents (PDF/image)
│
▼
┌───────────────────┐
│ Lekha API │ classify + extract per doc
│ /extract │
└───────────────────┘
│
Bank Statement → { balance, transactions, loans }
CAS Report → { folios, nav, current_value }
Salary Slip → { ctc, net_pay, employer }
│
▼
┌───────────────────┐
│ Aggregator │ sum assets, liabilities, net worth
└───────────────────┘
│
▼
{ net_worth, assets, liabilities, monthly_income }
The Lekha API handles classification automatically — you don't need to tell it which document type you're sending. It detects the format and applies the right extractor.
Step 1: Install and Configure the Lekha Client
bun add @lekha/sdk
// lib/lekha.ts
import Lekha from "@lekha/sdk";
export const lekha = new Lekha({
apiKey: process.env.LEKHA_API_KEY!,
});
Get your API key from lekhadev.com/playground. The free tier supports up to 50 document extractions per month.
Step 2: Define the Net Worth Schema
Before writing extraction logic, define the aggregated output shape:
// types/net-worth.ts
export interface AssetBreakdown {
bank_balances: number; // sum of all account balances
mutual_fund_value: number; // current NAV × units across all folios
total_assets: number;
}
export interface LiabilityBreakdown {
outstanding_loans: number; // principal outstanding from bank statements
monthly_emis: number; // recurring EMI obligations
total_liabilities: number;
}
export interface NetWorthReport {
net_worth: number; // assets − liabilities
assets: AssetBreakdown;
liabilities: LiabilityBreakdown;
monthly_income: number | null; // net take-home from latest salary slip
documents_processed: number;
as_of_date: string; // ISO 8601
}
Step 3: Parse Bank Statements for Liquid Assets and Liabilities
Bank statements give you two things: the current account balance (an asset) and any outstanding loan accounts or EMI patterns (liabilities).
// agents/parse-bank.ts
import { lekha } from "../lib/lekha";
interface BankResult {
balance: number;
outstanding_loans: number;
monthly_emis: number;
}
export async function parseBankStatement(
fileBuffer: Buffer,
filename: string,
): Promise {
const file = new File([fileBuffer], filename, { type: "application/pdf" });
const result = await lekha.extract(file);
if (result.document_type !== "bank_statement") {
throw new Error(Expected bank_statement, got ${result.document_type});
}
const data = result.data;
// Closing balance is the liquid asset
const balance = data.closing_balance ?? data.current_balance ?? 0;
// Detect loan accounts from transaction narrations
const emiTransactions = (data.transactions ?? []).filter(
(tx: { narration: string }) => /emi|loan|nach|ecs/i.test(tx.narration),
);
const monthlyEmis =
emiTransactions
.slice(0, 3) // use last 3 to average
.reduce((sum: number, tx: { amount: number }) => sum + tx.amount, 0) /
Math.min(emiTransactions.length, 3);
return {
balance,
outstanding_loans: data.loan_outstanding ?? 0,
monthly_emis: isNaN(monthlyEmis) ? 0 : Math.round(monthlyEmis),
};
}
Step 4: Parse CAS Reports for Investment Value
A CAS (Consolidated Account Statement) from CAMS or NSDL lists every mutual fund folio, the scheme NAV, and your current units. Lekha extracts this into a structured folio list so you can sum the current value directly.
// agents/parse-cas.ts
import { lekha } from "../lib/lekha";
export async function parseCAS(
fileBuffer: Buffer,
filename: string,
): Promise {
const file = new File([fileBuffer], filename, { type: "application/pdf" });
const result = await lekha.extract(file);
if (result.document_type !== "cas") {
throw new Error(Expected CAS, got ${result.document_type});
}
const folios = result.data.folios ?? [];
// Sum current_value across all folios
const totalValue = folios.reduce(
(sum: number, folio: { current_value: number }) =>
sum + (folio.current_value ?? 0),
0,
);
return Math.round(totalValue);
}
The current_value field in each folio is already computed as units × nav by the extractor — you don't need to do that math yourself.
Step 5: Parse Salary Slips for Income Data
A salary slip gives you monthly income context: the CTC, gross pay, deductions, and net take-home. For a net worth calculation the key figure is net_pay — the actual cash hitting the bank account each month.
// agents/parse-salary.ts
import { lekha } from "../lib/lekha";
interface SalaryResult {
net_pay: number;
employer: string | null;
month: string | null;
}
export async function parseSalarySlip(
fileBuffer: Buffer,
filename: string,
): Promise {
const file = new File([fileBuffer], filename, { type: "application/pdf" });
const result = await lekha.extract(file);
if (result.document_type !== "salary_slip") {
throw new Error(Expected salary_slip, got ${result.document_type});
}
return {
net_pay: result.data.net_pay ?? 0,
employer: result.data.employer_name ?? null,
month: result.data.pay_period ?? null,
};
}
Step 6: Aggregate Into a Net Worth Report
Now wire everything together. The agent accepts an array of file buffers with their filenames, runs them through the right extractors in parallel, and aggregates:
// agents/net-worth.ts
import { parseBankStatement } from "./parse-bank";
import { parseCAS } from "./parse-cas";
import { parseSalarySlip } from "./parse-salary";
import type { NetWorthReport } from "../types/net-worth";
import { lekha } from "../lib/lekha";
interface DocumentInput {
buffer: Buffer;
filename: string;
}
export async function calculateNetWorth(
documents: DocumentInput[],
): Promise {
// Classify all documents first so we can route to the right parser
const classified = await Promise.all(
documents.map(async (doc) => {
const file = new File([doc.buffer], doc.filename, {
type: "application/pdf",
});
const peek = await lekha.classify(file);
return { ...doc, document_type: peek.document_type };
}),
);
// Fan out to type-specific parsers in parallel
const [bankResults, casValues, salaryResults] = await Promise.all([
Promise.all(
classified
.filter((d) => d.document_type === "bank_statement")
.map((d) => parseBankStatement(d.buffer, d.filename)),
),
Promise.all(
classified
.filter((d) => d.document_type === "cas")
.map((d) => parseCAS(d.buffer, d.filename)),
),
Promise.all(
classified
.filter((d) => d.document_type === "salary_slip")
.map((d) => parseSalarySlip(d.buffer, d.filename)),
),
]);
// Aggregate assets
const bankBalances = bankResults.reduce((s, r) => s + r.balance, 0);
const mutualFundValue = casValues.reduce((s, v) => s + v, 0);
const totalAssets = bankBalances + mutualFundValue;
// Aggregate liabilities
const outstandingLoans = bankResults.reduce(
(s, r) => s + r.outstanding_loans,
0,
);
const monthlyEmis = bankResults.reduce((s, r) => s + r.monthly_emis, 0);
const totalLiabilities = outstandingLoans;
// Income: use the most recent salary slip (last in array)
const latestSalary = salaryResults.at(-1) ?? null;
return {
net_worth: totalAssets - totalLiabilities,
assets: {
bank_balances: bankBalances,
mutual_fund_value: mutualFundValue,
total_assets: totalAssets,
},
liabilities: {
outstanding_loans: outstandingLoans,
monthly_emis: monthlyEmis,
total_liabilities: totalLiabilities,
},
monthly_income: latestSalary?.net_pay ?? null,
documents_processed: documents.length,
as_of_date: new Date().toISOString().split("T")[0],
};
}
Calling the Agent
Wire it to an API route or a CLI tool:
// app/api/net-worth/route.ts (Next.js App Router)
import { calculateNetWorth } from "@/agents/net-worth";
import { NextRequest, NextResponse } from "next/server";
export async function POST(req: NextRequest) {
const formData = await req.formData();
const files = formData.getAll("documents") as File[];
const documents = await Promise.all(
files.map(async (file) => ({
buffer: Buffer.from(await file.arrayBuffer()),
filename: file.name,
})),
);
const report = await calculateNetWorth(documents);
return NextResponse.json({ success: true, data: report });
}
A typical response for someone with two bank accounts, a CAS, and a salary slip:
{
"success": true,
"data": {
"net_worth": 2847500,
"assets": {
"bank_balances": 412500,
"mutual_fund_value": 2685000,
"total_assets": 3097500
},
"liabilities": {
"outstanding_loans": 250000,
"monthly_emis": 18500,
"total_liabilities": 250000
},
"monthly_income": 95000,
"documents_processed": 4,
"as_of_date": "2026-07-06"
}
}
All amounts are in INR, as plain numbers — no currency strings, no comma formatting, ready for downstream math.
Handling Edge Cases
Multiple bank accounts at the same bank: Lekha extracts each statement independently. The aggregator sums balances across all bank documents, so uploading three HDFC statements produces three separate balance entries. CAS with zero-value folios: Some folios have been redeemed to zero units. The extractor returnscurrent_value: 0 for these — they don't inflate the total.
Salary slips from different months: If the user uploads three months of salary slips, use the most recent one for income. The pay_period field in the extracted data (format: YYYY-MM) lets you sort chronologically.
PDFs with passwords: Lekha handles password-protected bank statement PDFs from HDFC, SBI, ICICI, and others automatically. You don't need to strip passwords before uploading.
Production Considerations
Promise.all. For a typical four-document upload, extraction completes in 3-5 seconds.parsing_errors array rather than failing the entire request./batch endpoint covered in the batch processing guide.Try the live API at lekhadev.com/playground before building — paste in a bank statement PDF and see the JSON output in seconds.
FAQ
Which document types does Lekha support for net worth calculation?Lekha supports bank statements (30+ Indian banks), CAS reports from CAMS, NSDL, and CDSL, salary slips, ITR filings, Form 16, and CIBIL reports. For a complete net worth picture, bank statements and CAS cover the largest share of most Indian investors' assets.
How accurate is the extracted balance compared to the actual account balance?Lekha extracts the closing balance printed on the statement, which matches the balance on the statement date. For real-time balances, combine Lekha with an Account Aggregator integration — use AA for live data, and Lekha when the user uploads a PDF manually.
Can the agent handle joint accounts?Yes. Joint account statements extract identically to individual accounts — Lekha returns account holder names in the account_holders field. Your aggregation logic can treat the full balance as an asset or apply a 50% ownership split depending on your use case.
Lekha doesn't deduplicate — that's the responsibility of your application layer. The simplest approach is to track uploaded filenames and reject duplicates at the API route level before sending to Lekha.
Ready to build your net worth agent? Get an API key at lekhadev.com and run your first extraction in under five minutes. The free tier covers 50 documents per month — enough to prototype and test with real documents before committing to a paid plan. See the full API reference at lekhadev.com/docs.