The Date Was Forty Rows Away From the Transaction

I built a tool to catch my overspending. It told me I have an earning problem instead. On layout-encoded data, naming primitives, and hunting frictions instead of concepts.

This started as a money problem, not a code problem. The idea came out of a conversation at work about a shared pain: bank data is miserable to get at for AI work — locked behind portals, exported as PDFs, structured for eyeballs and nothing else. I had the same itch personally. I wanted to feed my own statements through my own tooling and have an AI lens go looking for gaps in my budget — places I was leaking money without noticing.

So I pulled my Chime transaction pages and started parsing. And immediately hit the wall.

Each transaction renders as a row — merchant, amount, the usual. But the date isn’t in the row. The date lives in a header element somewhere above, visually grouping everything beneath it until the next header shows up. A human reads this instantly. A parser reads a transaction row and finds, structurally, no date anywhere in it. The most important field of every record was encoded in *layout* — in the positional relationship between a row and a header that might be forty rows up the page. Walk up the DOM tree and you don’t find it, because the header isn’t an ancestor. It’s a sibling of a sibling, related to the transaction only by where things sit on screen.

I was annoyed in the specific way that means something. And then the thing happened that’s happened maybe eight times in my career, the most reliable moment of engagement I know: this wasn’t a Chime problem. It was a class. Record context encoded in layout. Dashboards do it. Admin panels do it. Half the operational web UIs I’ve ever automated against do it, because the page was built for eyes.

So I named it — ReDOM — and built it as a primitive instead of a fix: a schema-driven engine where you declare which elements carry context, which carry records, and how position binds them. The certified floor is 9 passed, 0 failed, 0 skipped, on Python 3.12.10 via uv, against a real sanitized fixture from the actual statement pages. And the part I’m proudest of: three records in that fixture come back explicitly flagged as unresolved, because their context genuinely couldn’t be bound. The engine admits what it doesn’t know instead of guessing.

The middle had the usual struggle with a twist. At one point the coding agent quietly swapped my real fixture for a synthetic one when the real data got inconvenient — tests green, scope silently departed. I caught it only by reading the test file instead of the summary. The engine got a real fix; the process got a dedicated selector-parsing test and a harder rule about fixture provenance.

And the original money question? Here’s the punchline I didn’t see coming. Once the pipeline worked and the analysis ran, it didn’t find waste. It found that my budget was already tight and already working — and that the actual gap was roughly five hundred dollars of *income*, not spending. I built a tool to catch my overspending, and the tool told me I don’t have a spending problem. I have an earning problem. That answer redirected more of my energy than any budget tip could have.

The lesson is bigger than scrapers: the projects that hold my attention are never found by browsing ideas. They’re extracted from walls I personally hit. Hunt frictions, not concepts. Every primitive I’ve ever named started as a specific annoyance with a timestamp — never as “what should I build next.”

If you’ve got a page where the data you need is “obviously right there” and structurally nowhere — that’s the friction. It might be your primitive.

Talk to Robert about your contact center

Convoso, dialer ops, DNC compliance, and the spreadsheets holding it all together — this is the day job.

Get in touch

Play what I’m building

The arcade gets the experiments first — dev notes and session write-ups land here after.

Visit the arcade   GitHub

Robert Floyd Dugger Avatar