All notes

Sep 2026 · 5 min read

Published

Build Log #1: A Window That Says OK

AuthorKervintz Noel
Filed underAI Engineering · Claude Code · Building in Public · Python

I'm building a local-first desktop app that takes years of bank and credit card statement PDFs and turns them into a financial picture you can actually trust. Here is everything it does today:

It looks like nothing. It's also the piece I most wanted to get right before anything else, and that choice is the whole point of this post.

Why this, and why local

I have statements from several banks and cards going back years. Every time I want an honest answer to where the money actually went, I'm staring at a folder of PDFs with different layouts and no way to see across them. Every existing option I looked at wanted the same thing in return: connect your bank, or upload your statements to our servers.

So the app runs entirely on your machine. The database is a SQLite file on your disk. Nothing leaves unless you turn on the optional AI summary, and even then only sanitized, stripped-down facts.

Two rules I set before writing any code

First: every statement gets checked against its own math. A parser that reads 90% of the transactions correctly isn't 90% good — it's broken, the total is wrong, and nothing tells you. So extracted transactions have to reconcile against the opening and closing balances the statement itself reports. If they don't, it gets flagged for review instead of quietly folded into the totals.

Second: the LLM never touches the numbers. Every figure — cash flow, category totals, recurring charges, trends — is computed by deterministic code. The AI layer only explains results in plain English, and every call goes through a sanitizer that strips account numbers and names first. An LLM doing arithmetic is a machine that produces confident, plausible, occasionally wrong numbers, which is the one thing a financial tool cannot do.

So why is the first screen a health check?

The app is two processes. An Electron front end for the window and the UI, and a Python FastAPI backend that does the real work — PDF extraction, parsing, validation, analytics — because that's where the tooling lives. Electron spawns the backend on startup, talks to it over localhost, and kills it on quit.

That seam is the riskiest part of the system. The parsers are just work.

The seam is where it gets genuinely hard: one runtime spawning another, on Windows, with process lifecycle, ports, and eventually shipping a bundled Python executable inside an installer to a machine that has no Python on it. So the first task was to make exactly one thing cross that seam end to end. Electron starts, spawns the backend, the renderer asks it for a health check, and the answer goes on screen. No database, no schema, no parsers.

The screenshot above is that seam working. Which is why a card that says status ok is, to me, the most load-bearing screen in the project.

Under a hundred lines, three bugs

The skeleton is under a hundred lines of meaningful code and it still took three bugs to get there, all of them in the seam. The Electron config was missing its entry point, which produced a launch error containing no other information. Then the backend was spawned with its output set to inherit the parent's console — it threw synchronously in an environment with no console attached and took Electron down with it, which also surfaced that an unhandled error event on a Node child process kills the parent. Both worth hitting now, with one endpoint, rather than later when that child is a bundled binary failing on someone else's machine.

The third one is the interesting one. curl against the health endpoint returned 200 and the expected JSON. The renderer, calling the identical URL, got nothing at all. The backend was fine — the request was cross-origin, there was no CORS middleware, and the browser silently blocked the read. My design docs never mentioned CORS, because on paper "the app calls its own local backend" doesn't sound cross-origin. It is.

The spec was thorough and it was still incomplete, because a document describes a system and a running process is one.

Where the AI fits

I built this with Claude Code, so I want to be precise about the division of labor. What's mine: the problem, the decision to stay local, the two-process split, the rule that the LLM never computes a number, the sequencing that put the riskiest seam first, and the review and approval of each plan before it was implemented. What isn't mine, line by line, is the code — Claude wrote those ninety-odd lines and I read and approved them rather than typing them.

At this stage that's a defensible trade, because process-spawning boilerplate is not where the engineering in this project lives. It stops being defensible the moment the code carries real judgment: the validator, the dedup rules, the sanitizer. So from the data model onward I write down why the design is what it is before it gets built, in my own words, and I don't move past a step whose logic I couldn't reconstruct on a whiteboard without the repo open.

If a future post in this series doesn't show that reasoning, assume I skipped it and hold it against me.

Next

The canonical data model, where every transaction carries its provenance — which statement it came from, which page, which parser version, how confident the extraction was — so that when a number looks wrong six months from now, it traces back to the exact page of the exact PDF. No parsers yet. Just the shape everything else has to fit into.

I write these as I go. You can follow along here or on Hashnode, where I'll start cross-posting.

All notes