Bank Statement Reconciliation
SkillFiles & storageVerify 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.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Bank Statement Reconciliation skill
What this skill tells your AI
The instructions your AI receives, as published by matyasstoch/david-skills in skills/productivity/bank-statement-reconciliation/SKILL.md and read by ahel’s review.
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)
import csv, re, subprocess
with 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.
Signals
- GitHub stars
- 59
- Forks
- 11
- Last commit
- Jun 2026
Advanced
- Catalog kind
- skill
- Gateway key
bank-statement-reconciliation- Source
- github.com/matyasstoch/david-skills