← Back to blog
·10 min read

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.

ai agentnet worthbank statementCAS statementsalary slippersonal financeTypeScript

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:

  • Liquid assets: current and savings account balances from bank statements
  • Investments: mutual fund NAV and units from CAS reports
  • Liabilities: outstanding loan balances and EMIs from bank statements
  • Income: monthly CTC and net take-home from salary slips
  • 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 returns current_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

  • No storage: Lekha processes documents in memory and returns results immediately. Documents are never persisted, which keeps you compliant with DPDP data minimization requirements.
  • Parallel extraction: The agent fans out to multiple parsers in parallel using Promise.all. For a typical four-document upload, extraction completes in 3-5 seconds.
  • Error isolation: Wrap each parser call in a try/catch so one failed extraction doesn't block the others. Return partial results with a parsing_errors array rather than failing the entire request.
  • Rate limits: The Lekha API is rate-limited per API key. For batch uploads, use the /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.

    What if a user uploads the same document twice?

    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.