<< All versions
Skill v1.0.0
currentAutomated scan100/100matyasstoch/david-skills/bank-statement-reconciliation
──Details
PublishedOctober 22, 2026 at 10:28 PM
Content Hashsha256:e1e4bc49262b8fd8...
Git SHA9628c8b6a1b2
──Files
Files (1 file, 3.2 KB)
SKILL.md3.2 KBactive
SKILL.md · 59 lines · 3.2 KB
version: "1.0.0" name: bank-statement-reconciliation description: Verify a bank's monthly CSV and PDF exports agree before trusting either for bookkeeping. Use when reconciling bank statements, importing monthly bank exports, or building a local ledger from raw bank data.
Bank Statement Reconciliation
When a bank gives you both a CSV and a PDF for the same statement period, never trust either in isolation. Verify they agree on three things before using them downstream.
When to use
- New monthly bank statement files land in
data/imports/<bank>/ - About to import bank data into Zoho / Xero / a local ledger
- Diffing annual vs monthly exports for the same bank
- Wio Business specifically — see
references/wio-business.md
The 3-point verification
For each (CSV, PDF) pair:
- Row count match.
len(csv_rows) == count(date-prefixed lines in PDF). Mismatch ≠ automatic fail (PDF text-extraction can double-count wrapped descriptions), but flag it for manual eyeball. - Closing balance match. Extract from PDF header (regex:
CLOSING BALANCE.*?([\d,]+\.\d{2})\s*AED). Must equal the lastBalancecolumn value in the CSV to the cent. - Chain integrity. This month's opening balance = previous month's closing balance. Computed:
csv_rows[0].Balance - csv_rows[0].Amount. Off-by-cent = real gap.
If 2 and 3 pass but 1 doesn't, the data is usable. If 2 or 3 fails, stop and investigate.
Standard recipe (Python)
python
import csv, re, subprocesswith open(csv_path) as f:rows = list(csv.reader(f))header, data = rows[0], rows[1:]amt = header.index("Amount"); bal = header.index("Balance"); dt = header.index("Date")opening = float(data[0][bal]) - float(data[0][amt])closing = float(data[-1][bal])inflow = sum(float(r[amt]) for r in data if float(r[amt])>0)outflow = sum(float(r[amt]) for r in data if float(r[amt])<0)txt = subprocess.check_output(["pdftotext","-layout","-enc","UTF-8",pdf_path,"-"]).decode()txn_lines = [ln for ln in txt.split("\n") if re.match(r"^\s*\d{2}/\d{2}/\d{4}", ln)]pdf_close = re.search(r"CLOSING BALANCE.*?([\d,]+\.\d{2})\s*AED", txt, re.S).group(1)assert pdf_close == f"{closing:,.2f}", f"CLOSING MISMATCH: PDF={pdf_close} CSV={closing:,.2f}"
Pitfalls
- Annual / lumped exports lie. Many banks' "full year" CSV silently drops rows that exist in the monthly exports. Always prefer monthly. If you only have a lumped file, treat it as untrusted until proven otherwise.
- PDF date-regex false positives. Multi-line transaction descriptions can produce 2nd/3rd lines that start with a date-shaped string. Off-by-1-or-2 row counts are usually this, not real gaps.
- CSV header names vary. Wio uses
Balancefor the running balance column; some banks useRunning BalanceorClosing Balance. Don't hardcode — useheader.index(...)with a fallback. - File-naming from bank apps is ugly. Mobile-app exports often land as
statement(Mon DD, YYYY - Mon DD, YYYY.csvwith a typo'd year (Wio's puts "2025" on a 2024 export). Rename toYYYY-MM.csv/YYYY-MM.pdfimmediately, before analysis.
Bank-specific notes
- Wio Business — see
references/wio-business.mdfor filename pattern, account filter, and the documented annual-CSV row-drop bug.