How I built a credit-card transaction tracker with Claude
Parsing Northwind's alert emails into weekly spend summaries — a weekend project, real failures, real code.
By Awadesh Madhogaria · Published Jun 30, 2025
I wanted one spreadsheet with every Northwind card charge — every card, every month, categorised. No third-party app, no scraping a portal.
The data already existed: the bank emails an alert for every transaction. The problem was that those emails are unstructured prose, and prose is hard to turn into a spreadsheet row. Before this, Northwind’s spending lived in three places — the bank portal, a half-finished spreadsheet, and my memory — and none of them agreed.
The manual alternative was the usual one: log into the portal, export a CSV, clean it, paste it. I did that maybe twice a year, which meant I had no real sense of where money went until it was far too late to react. So I gave the job to Claude.
How it works
The shape of the thing is a short chain that runs on a daily trigger, so the sheet is never more than a day behind reality:
- A Gmail filter catches every transaction alert from the bank and applies a label.
- A daily trigger pulls the labelled threads the script has not seen yet.
- Each email body goes to Claude with a prompt asking for strict JSON —
{ amount, merchant, category }. - The script calls
JSON.parseon the result. - The parsed values append as a new row in the
Expensessheet.
Categorisation happens inside the Claude call — it reads the merchant name and picks a sensible category, so the sheet is useful the moment a row lands.
What broke, and what stayed manual
The first version asked Claude for prose, and parsing prose with regex is an
exercise in pain. Switching to a strict JSON schema in the prompt — and calling
JSON.parse rather than regexing — fixed it. The bank also sometimes formats
numbers as 1.234,56 (German), which crashed my regex, so I added a
normalisation step. Those two failures taught me the same lesson from opposite
ends: control the shape of the data, or it will surprise you. Once the output was
JSON and the numbers were normalised, the script became boring — which is what
you want.
All of that took maybe four hours, including throwing away the first attempt.
That thrown-away attempt was not wasted time; it was the four hours that taught
me to ask for JSON, and the second version came together quickly because the
first one had already found the hard parts. One thing I left manual on purpose: I
still skim the Expenses sheet once a week, because Claude’s categories are
good, not perfect, and a one-off charge occasionally lands in the wrong bucket.
Fixing a category is one click, and doing it weekly keeps the data honest without
me pretending a script can have judgement it does not have.
The takeaway
The payoff is that every card charge now lands in one categorised sheet, automatically, daily — no portal logins, no CSV exports, no third-party app holding the data — and I finally know where Northwind’s money goes while it is still useful to know.
The wider lesson is simple: don’t ask LLMs for unstructured output if you’re
going to parse it. Ask for JSON, give a schema, and JSON.parse the result. The
model is doing the genuinely hard part — reading messy prose — so let your code
handle the easy part with a format it can trust.