appscript.dev
Blog Gmail Sheets

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:

  1. A Gmail filter catches every transaction alert from the bank and applies a label.
  2. A daily trigger pulls the labelled threads the script has not seen yet.
  3. Each email body goes to Claude with a prompt asking for strict JSON — { amount, merchant, category }.
  4. The script calls JSON.parse on the result.
  5. The parsed values append as a new row in the Expenses sheet.

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.