Parse CAS Statements with AI: Build a Mutual Fund Portfolio Agent
Extract structured data from CDSL, NSDL, and CAMS Consolidated Account Statements with AI. Build portfolio analysis agents for Indian mutual fund investors.
Every Indian mutual fund investor holds a Consolidated Account Statement — a single PDF that snapshots every scheme, SIP, redemption, and NAV across all their folios. For developers building wealth management apps, portfolio trackers, rebalancing advisors, or tax planning tools, parsing CAS is the entry point to the entire Indian mutual fund universe.
The challenge: CAS documents come from multiple issuers (CDSL, NSDL, CAMS, KFintech), each with a distinct layout. They are almost always password-protected. And they contain dense tables — hundreds of transactions across dozens of schemes — that defeat legacy OCR tools.
This guide shows how to extract clean, structured JSON from any CAS document using Lekha's AI extraction API, then build a portfolio analysis agent on top of it.
What Is a CAS and Who Issues It?
A Consolidated Account Statement is the official record of all mutual fund holdings linked to a PAN. It is generated by two types of entities:
| Issuer | Type | Source | | ----------------------------------------- | ---------- | ---------------------------- | | CDSL (Central Depository Services Ltd) | Depository | cdslIndia.com, monthly email | | NSDL (National Securities Depository Ltd) | Depository | nsdl.co.in | | CAMS (Computer Age Management Services) | RTA | camsonline.com | | KFintech (formerly Karvy) | RTA | kfintech.com |
Each issuer produces a slightly different PDF layout. CDSL uses a two-column transaction table; CAMS puts returns (XIRR) inline; KFintech uses a scheme-level grouping that differs from both. A parser trained on one format will silently misread another.
Lekha handles all four through the same endpoint — you send the PDF, you get JSON. Bank routing is automatic.
Quickstart: Parse a CAS Document
bun add @lekhadev/sdk
or: npm install @lekhadev/sdk
import Lekha from "@lekhadev/sdk";
import { readFileSync } from "fs";
const lekha = new Lekha({ apiKey: process.env.LEKHA_API_KEY! });
const pdf = readFileSync("cas-statement.pdf");
const result = await lekha.extract({
document: pdf,
type: "cas",
password: "ABCDE12341234", // PAN prefix + DOB (CDSL default)
});
console.log(result.data);
Lekha decrypts the PDF in memory, extracts all holdings and transactions, then discards the decrypted content. Nothing is written to disk.
The Structured JSON Response
A successful CAS extraction returns a response shaped like this:
{
success: true,
data: {
issuer: "CDSL",
pan: "ABCDE1234F",
name: "Ananya Krishnamurthy",
email: "ananya@example.com",
period: {
from: "2025-01-01",
to: "2025-12-31"
},
folios: [
{
folio_number: "1234567/89",
amc: "Mirae Asset Mutual Fund",
schemes: [
{
scheme_name: "Mirae Asset Large Cap Fund - Direct Growth",
isin: "INF769K01EV0",
plan: "direct",
option: "growth",
units: 1245.876,
nav: 112.43,
current_value: 140082.45,
invested_amount: 120000.00,
xirr: 8.94,
transactions: [
{
date: "2025-01-10",
type: "purchase",
amount: 10000.00,
units: 95.231,
nav: 104.99,
description: "SIP - Regular"
},
{
date: "2025-03-10",
type: "purchase",
amount: 10000.00,
units: 92.101,
nav: 108.57,
description: "SIP - Regular"
}
]
}
]
}
],
summary: {
total_invested: 480000.00,
current_value: 531240.80,
total_gain: 51240.80,
overall_xirr: 9.12
}
}
}
All amounts are numbers, all dates are ISO 8601, and XIRR is a percentage value (not a decimal fraction).
Handling CAS-Specific Edge Cases
Password Protection
Almost all CAS PDFs are encrypted. The password convention:
| Issuer | Default Password Format |
| -------- | -------------------------------------------------------------------- |
| CDSL | First 8 characters of PAN + DOB as DDMMYYYY (e.g. ABCDE1231011990) |
| NSDL | First 8 characters of PAN (uppercase) |
| CAMS | PAN in uppercase |
| KFintech | PAN in uppercase |
Pass the password in the request — Lekha handles decryption in memory:
const result = await lekha.extract({
document: pdf,
type: "cas",
password: userProvidedPassword,
});
If the wrong password is supplied, Lekha returns a structured error:
{
success: false,
error: {
code: "incorrect_password",
message: "PDF decryption failed — check the password and try again",
suggestion: "CDSL passwords are: first 8 chars of PAN + DOB as DDMMYYYY"
}
}
Zero-Balance Folios
CAS documents include folios with zero units (fully redeemed). These are included in folios with units: 0 and current_value: 0, preserving the transaction history for capital gains calculations.
Direct vs Regular Plans
The plan field ("direct" or "regular") is extracted from the scheme name. This matters for fee analysis — regular plan investors typically pay 0.5–1.5% more in expense ratio annually. Your agent can flag this:
const regularPlanHoldings = result.data.folios
.flatMap((f) => f.schemes)
.filter((s) => s.plan === "regular" && s.current_value > 10000);
Building a Portfolio Analysis Agent
Here's a practical agent that parses a CAS, computes allocation, and flags concentration risk and direct-vs-regular plan leakage:
import Lekha from "@lekhadev/sdk"; import Anthropic from "@anthropic-ai/sdk";, }, ], });const lekha = new Lekha({ apiKey: process.env.LEKHA_API_KEY! }); const claude = new Anthropic();
interface PortfolioSummary { total_value: number; xirr: number; allocation: Record
; // category → percentage regular_plan_value: number; flags: string[]; } async function analysePortfolio( casPdf: Buffer, password: string, ): Promise
{ // Step 1: extract CAS const { data } = await lekha.extract({ document: casPdf, type: "cas", password, }); const allSchemes = data.folios.flatMap((f) => f.schemes);
// Step 2: compute allocation by scheme name keywords const categories: Record
= { large_cap: 0, mid_cap: 0, small_cap: 0, debt: 0, hybrid: 0, international: 0, other: 0, }; for (const scheme of allSchemes) { const name = scheme.scheme_name.toLowerCase(); const val = scheme.current_value; if (/large.cap|bluechip|nifty 50|sensex/i.test(name)) categories.large_cap += val; else if (/mid.cap|midcap/i.test(name)) categories.mid_cap += val; else if (/small.cap|smallcap/i.test(name)) categories.small_cap += val; else if (/debt|liquid|overnight|short.term|bond|gilt/i.test(name)) categories.debt += val; else if (/hybrid|balanced|aggressive|conservative/i.test(name)) categories.hybrid += val; else if (/international|us|global|nasdaq|s&p/i.test(name)) categories.international += val; else categories.other += val; }
const total = data.summary.current_value; const allocation = Object.fromEntries( Object.entries(categories).map(([k, v]) => [ k, Math.round((v / total) * 100), ]), );
const regularPlanValue = allSchemes .filter((s) => s.plan === "regular") .reduce((sum, s) => sum + s.current_value, 0);
// Step 3: ask Claude for portfolio advice const response = await claude.messages.create({ model: "claude-sonnet-4-6", max_tokens: 512, messages: [ { role: "user", content:
Analyse this Indian mutual fund portfolio and return flags.Portfolio value: ₹${total.toLocaleString("en-IN")} XIRR: ${data.summary.overall_xirr}% Allocation: ${JSON.stringify(allocation)} Regular plan value: ₹${regularPlanValue.toLocaleString("en-IN")} (${Math.round((regularPlanValue / total) * 100)}% of portfolio)
Return JSON: { flags: string[] } Flag: concentration risk if any category > 60%, under-diversification if < 3 categories, regular plan leakage if regular plan value > ₹50,000, low XIRR if < 8% over 3+ years.
const { flags } = JSON.parse(response.content[0].text);
return { total_value: total, xirr: data.summary.overall_xirr, allocation, regular_plan_value: regularPlanValue, flags, }; }
// Example output // { // total_value: 531240, // xirr: 9.12, // allocation: { large_cap: 55, mid_cap: 20, small_cap: 15, debt: 10, ... }, // regular_plan_value: 85000, // flags: [ // "₹85,000 invested in regular plans — switching to direct could save ~₹1,200/year in fees" // ] // }
This pattern powers the core of a robo-advisory onboarding flow: users upload their CAS, your agent instantly understands their existing portfolio before making any recommendations.
Use Cases for CAS Parsing
Wealth management onboarding: Parse a new user's CAS to understand their existing holdings before suggesting a financial plan. Faster and more accurate than manual form-filling. Tax planning (LTCG/STCG): CAS transaction history contains purchase dates and NAVs. Use this to compute long-term vs short-term capital gains before March 31. SIP health checks: Detect paused or discontinued SIPs by comparing the last transaction date per scheme against expected SIP dates. Fee leakage detection: Flag regular plan investments where the same scheme exists as a direct plan. Calculate the annual fee saving if the investor switches. Portfolio rebalancing: Compare current allocation against a target allocation (e.g. 60/20/20 equity/debt/international) and compute the buy/sell orders needed.Integrating into an API Route
// app/api/analyse-portfolio/route.ts
import { NextRequest, NextResponse } from "next/server";
import Lekha from "@lekhadev/sdk";
const lekha = new Lekha({ apiKey: process.env.LEKHA_API_KEY! });
export async function POST(req: NextRequest) {
const formData = await req.formData();
const file = formData.get("cas") as File;
const password = formData.get("password") as string;
if (!file) {
return NextResponse.json(
{
success: false,
error: { code: "missing_file", message: "No CAS document uploaded" },
},
{ status: 400 },
);
}
const bytes = await file.arrayBuffer();
const pdf = Buffer.from(bytes);
const result = await lekha.extract({
document: pdf,
type: "cas",
password: password ?? undefined,
});
if (!result.success) {
return NextResponse.json(result, { status: 422 });
}
return NextResponse.json({
success: true,
holder: result.data.name,
total_value: result.data.summary.current_value,
xirr: result.data.summary.overall_xirr,
scheme_count: result.data.folios.flatMap((f) => f.schemes).length,
});
}
Test your integration with real CAS documents at the Lekha playground before going to production. Upload a sample CAS to see the exact JSON shape your agent will work with.
Accuracy Across CAS Formats
| Format | Transaction Accuracy | Holding Value Accuracy | | ----------------------- | -------------------- | ---------------------- | | CDSL CAS (demat-linked) | 99.2% | 99.6% | | NSDL CAS | 98.9% | 99.4% | | CAMS Statement | 99.1% | 99.7% | | KFintech Statement | 98.7% | 99.3% |
Accuracy measured against ground-truth transaction counts and NAV values across 300 real-world CAS documents per issuer. Multi-folio documents with 50+ schemes are handled correctly — Lekha processes the entire document in a single request.
FAQ
What CAS formats does Lekha support? Lekha supports CAS documents from all four major issuers: CDSL, NSDL, CAMS (camsonline.com), and KFintech. The issuer is auto-detected from the document — you use the sametype: "cas" for all of them.
How do I handle a CAS where I don't know the password?
The most common convention is PAN (uppercase) for CAMS and KFintech, and first-8-of-PAN + DDMMYYYY date of birth for CDSL. If you're building a user-facing form, ask the user to enter their PAN and date of birth, then try both patterns programmatically.
Does Lekha return XIRR per scheme?
Yes, when the issuer includes XIRR in the PDF (CAMS and some CDSL formats). When it is absent, the xirr field is null for that scheme — you can compute it from the transaction history and current NAV if needed.
Can I parse a CAS with 100+ schemes?
Yes. CAS documents for long-tenure investors routinely contain 80–150 schemes across multiple AMCs. Lekha handles the entire document in one request, returning all folios and schemes. For batch onboarding flows, see the batch processing guide for concurrency and retry patterns.
Ready to build your mutual fund portfolio agent? Create a free Lekha account and get 50 extractions free — no credit card required. Full CAS schema documentation is at lekhadev.com/docs.