skills/pdf-statement-parser/SKILL.md
Parses a single financial statement PDF (checking, savings, credit card, brokerage, 401k, HSA, mortgage, tax form) and emits a normalized JSON record with institution, account mask, statement period, opening/closing balances, line-item transactions or holdings, and a confidence score. Use when extracting structured data from a bank, brokerage, retirement, or HSA PDF statement, when ingesting a drop of household finance documents, or when user mentions parsing a statement, extracting transactions from a PDF, or normalizing statement data.
npx skillsauth add lyndonkl/claude pdf-statement-parserInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Bank, brokerage, retirement, HSA, and mortgage statements arrive as PDFs in many house-styles. This skill is the prompt-driven extraction methodology: identify the document type, locate the canonical fields by their labels (not position), pull every transaction or holding row, and emit a strict JSON record. All monetary values become integer cents, all dates become ISO 8601 (YYYY-MM-DD).
Use directly with Claude's PDF reading: Read the PDF, then run this skill's workflow on the visible content. No external library needed.
The caller provides:
pdf_path — absolute path to a single statement PDF.expected_type (optional) — one of checking_statement | savings_statement | credit_card_statement | brokerage_statement | 401k_statement | hsa_statement | mortgage_statement | tax_form | insurance_doc | unknown. If absent, the skill detects it.known_accounts (optional) — array of {id, institution, mask, type} for matching.- [ ] Step 1: Read the PDF (first pass: pages 1-2 for header)
- [ ] Step 2: Detect document type from header keywords
- [ ] Step 3: Extract account identity (institution + mask + type)
- [ ] Step 4: Extract statement period (start/end dates)
- [ ] Step 5: Extract balance markers (opening / closing / available)
- [ ] Step 6: Extract line items (transactions, holdings, or events)
- [ ] Step 7: Normalize to integer cents and ISO dates
- [ ] Step 8: Score confidence on each field; emit JSON
Use the Read tool with the PDF path. For PDFs > 10 pages, read in chunks: pages 1–5 (header + first transactions), then subsequent ranges as needed. Track total page count.
Look for these header signals (in order of authority):
| Signal | Document type |
|---|---|
| "Statement of Account" + "Available Balance" + checking-style transactions | checking_statement |
| "Savings Statement" / "Money Market Statement" | savings_statement |
| "Credit Card Statement" + "Payment Due" + "Minimum Payment" | credit_card_statement |
| "Account Summary" + "Holdings" + ticker symbols | brokerage_statement |
| "401(k) Statement" / "Retirement Plan" + "Vested Balance" | 401k_statement |
| "Health Savings Account" / "HSA" + "Contribution Limit" | hsa_statement |
| "Mortgage Statement" + "Principal" + "Escrow" | mortgage_statement |
| "W-2" / "1099" / "1098" / "5498" form numbers | tax_form |
| Policy declaration page, premium, deductible | insurance_doc |
If two signals tie, prefer the one in the document title or page header.
****1234, xxxx1234, Account ending in 1234, or the redacted form the institution uses. Always store as ****1234.household.json members downstream).Match against known_accounts by (institution, mask). If no match, mark account_match: "new" and propose an account_id like acc_<type>_<incrementing>.
Find labels: "Statement Period", "Cycle Date", "Closing Date", "Period Beginning … Period Ending". Output:
period_start — first day covered (ISO).period_end — last day covered, also the statement closing date (ISO).If only a closing date is present, set period_end = closing_date and period_start = null with confidence: 0.6 on the period.
For cash/credit accounts: opening_balance_cents, closing_balance_cents, available_balance_cents (if present). For credit cards, also statement_balance_cents, minimum_payment_cents, payment_due_date.
For brokerage / 401k / HSA: total_value_cents, cash_cents, invested_cents. Skip opening/closing transaction balances — these are holdings statements.
For mortgages: current_principal_cents, monthly_pi_cents, monthly_escrow_cents, next_payment_due, extra_principal_ytd_cents.
Three modes:
Cash/credit transactions. For each row in the transactions table, capture: date, post_date (if separate column), description_raw, amount. Sign convention: outflows negative, inflows positive. If statement uses two columns ("Withdrawals" and "Deposits"), set sign accordingly. Preserve description_raw exactly as printed — the bookkeeper will normalize the merchant name later.
Brokerage / 401k / HSA holdings. For each holding row: symbol, description, shares (decimal), price (per-share), value, cost_basis (if shown), asset_class (mapped from fund description: see playbook below).
Mortgage activity. Recent payments table: each entry with date, total_paid_cents, principal_cents, interest_cents, escrow_cents, extra_principal_cents.
Tax forms. Form-specific fields per playbook (W-2 boxes, 1099-DIV ordinary/qualified dividends, 1098 mortgage interest paid, 5498-SA HSA contributions).
$1,247.50 → 124750 cents. Negative outflow: -$87.42 → -8742.01/15/26 → 2026-01-15. Two-digit years assume current century unless context says otherwise.Per field, assign a confidence in [0.0, 1.0]:
1.0 — extracted directly from a labeled field.0.85–0.95 — extracted from positional context (column header inferred, value clear).0.6–0.8 — inferred from surrounding text or partially OCR'd.< 0.6 — flag for human review.The overall record gets confidence = min(per-field confidences for required fields).
Required: institution, mask, period_start, period_end, opening_balance_cents, closing_balance_cents, transactions[]. Reconcile: opening + Σ(transactions.amount) = closing within ±$1.
Required: institution, mask, period_start, period_end, opening_balance_cents, closing_balance_cents, statement_balance_cents, minimum_payment_cents, payment_due_date, transactions[]. Note: credit-card outflows from the cardholder's perspective are purchases (positive on the card balance) — but for the unified household ledger, treat purchases as negative (money leaving the household) and payments to the card as positive (or as internal transfers). Document the sign convention you used.
Required: institution, mask, account_type, period_end, total_value_cents, holdings[]. Optional: cash_cents, transactions[] (buys/sells/dividends). Asset-class mapping:
| Fund hint | asset_class |
|---|---|
| "S&P 500", "Total Stock Market", "Large Cap Blend", VOO/VTI/SPY | us_equity |
| "International", "Developed Markets", "Emerging Markets", VXUS/IXUS | intl_equity |
| "Total Bond", "Aggregate Bond", "Treasury", BND/AGG | us_bond |
| "Money Market", "Cash Reserve", SPAXX | cash |
| "REIT", "Real Estate" | reit |
| "Target Date 20XX" | target_date (note the year) |
If unmappable, leave asset_class: null with confidence 0.5.
Required: institution, owner, period_end, total_balance_cents, ytd_contribution_cents (if shown), employer_match_ytd_cents (if shown), holdings[]. Watch for vesting columns — record vested_cents separately if shown.
Required: institution, period_end, cash_cents, invested_cents, total_cents, ytd_contribution_cents, ytd_distribution_cents (if shown). Note coverage type if printed: self_only or family.
Required: institution, mask, period_end, current_principal_cents, monthly_pi_cents, monthly_escrow_cents, next_payment_due, recent_payments[]. Capture the rate APR if printed.
Required: form_type (W-2, 1099-DIV, 1099-INT, 1099-B, 1098, 5498-SA, 1095), tax_year, issuer, recipient, key_boxes (form-specific). Defer interpretation to the tax-compliance agent — this skill just captures the boxes.
Emit a single JSON object:
{
"document_type": "checking_statement",
"source_path": "inbox/2026-01-15-batch/checking-jan.pdf",
"institution": "Bank Name",
"account": {
"mask": "****1234",
"type": "checking",
"owners": ["Jane Doe", "John Doe"],
"match": "acc_chk_001",
"match_confidence": 0.95
},
"period": {
"start": "2025-12-15",
"end": "2026-01-14"
},
"balances": {
"opening_cents": 1124300,
"closing_cents": 1247500,
"available_cents": 1247500
},
"transactions": [
{
"date": "2025-12-16",
"post_date": "2025-12-17",
"description_raw": "TRADER JOE'S #123 PORTLAND OR",
"amount_cents": -8742,
"confidence": 0.97
}
],
"reconciliation": {
"expected_closing_cents": 1247500,
"computed_closing_cents": 1247500,
"diff_cents": 0,
"ok": true
},
"extracted_at": "2026-01-20T14:00:00Z",
"overall_confidence": 0.94,
"warnings": []
}
For brokerage/401k/HSA replace transactions with holdings and balance fields per playbook.
Reduce overall confidence and add a warnings[] entry whenever:
warning: "reconcile_failed: diff $X.XX" and overall_confidence: max(0.5, current * 0.7).warning: "missing_required: <field>" and overall_confidence ≤ 0.5.warning: "low_ocr_quality_page_<n>" and apply the per-field confidence penalty.warning: "unknown_asset_class_high".null and add a warning. Do not interpolate.description_raw byte-for-byte.transaction-categorizer).ok: true.Given a 4-page Chase checking statement for the cycle 2025-12-15 to 2026-01-14 with opening balance $11,243.00, three deposits totaling $4,832.00, twenty-seven debits totaling $3,600.00, and closing balance $12,475.00, the parser emits:
document_type: "checking_statement"institution: "Chase"account.mask: "****1234", account.type: "checking"period.start: "2025-12-15", period.end: "2026-01-14"balances.opening_cents: 1124300, balances.closing_cents: 1247500transactions: [...] — 30 entries, each with description_raw, amount_cents (signed), and per-field confidence ≥ 0.92reconciliation.ok: true, diff_cents: 0overall_confidence: 0.95, warnings: []testing
Cluster a conference's event records into a small set of coarse themes with finer sub-clusters, an explicit outlier bucket, and soft (multi-membership) affinities — using the hybrid embed-then-label pipeline (embed abstracts, reduce, density-cluster, then LLM-label the clusters) when embedding libraries are available, and an LLM-reasoned hierarchical fallback when they are not. Embeddings do the grouping; the LLM only names the groups. Conference-agnostic. Use when turning structured event records into a navigable theme map for preference elicitation and scheduling, when you need 6-8 reasonable themes rather than 20 muddy ones, or when overlapping talks must belong to more than one theme. Trigger keywords - theme clustering, cluster talks, embed then label, soft membership, outlier talks, conference themes, topic map.
development
Build a personal conference schedule as a constraint-optimization problem — hard constraints (no time overlap, room-to-room travel time, capacity/registration, the attendee's own must-attends and blackouts) plus a user-owned weighted objective trading interest against breadth, pacing (maximize contiguous free time), and serendipity. Surfaces unbreakable conflicts (two high-value overlapping talks the model cannot rank) as decisions for the human rather than silently picking, and reports what each choice traded away. Conference-agnostic. Use to turn a preference profile plus a theme map into a day-by-day plan, to resolve overlapping sessions, or to balance a packed vs paced schedule. Trigger keywords - schedule optimization, conference schedule, constraint optimization, overlapping talks, contiguous free time, conflict surfacing, packed vs paced.
development
Parse a heterogeneous conference program (markdown, HTML, PDF-derived text, or JSON) into normalized event records with per-field confidence scores and independent classification axes (topic, depth, format, prerequisites, recorded, capacity). Detects the program's format before extracting, treats every inferred field as uncertain (present vs inferred vs missing), and flags thin or missing abstracts so downstream enrichment can target them. Conference-agnostic. Use when ingesting a conference or event schedule into a structured store, normalizing a talk/session list, or extracting per-session metadata with calibrated confidence. Trigger keywords - program ingestion, parse schedule, session extraction, event records, conference program, talk metadata, per-field confidence.
development
Build a personalized preference profile from a small number of well-chosen, cluster-grounded questions instead of a long survey. Represents the person's interests as an uncertainty region over the theme map, picks the single highest-information-gain choice-based question (contrasting real talks from different clusters), balances exploiting known interests against exploring uncertain ones, deliberately injects outlier probes to fight selection bias, and stops as soon as the schedule would be stable. Also elicits the user-owned objective weights and hard constraints. Interactive — runs where it can actually ask the person. Conference-agnostic. Use to turn a theme map into a preference profile, to decide what to ask a conference attendee, or to elicit scheduling priorities. Trigger keywords - preference elicitation, ask few questions, information gain, choice-based questions, selection bias probe, objective weights, attendee preferences.