I sat down at 12:32 AM last night to solve a boring problem: I could not tell you, without a spreadsheet and thirty minutes, what my SaaS actually costs to run in a given month. The inbox has all the receipts. The inbox is also useless for the question.
Ninety minutes later I had a working finance watchdog posting a daily digest to a Slack channel I made called #freebo-finances. The interesting part wasn’t the Slack cron or the Gmail scraping. The interesting part was realizing the whole design has to start from one honest sentence: a receipt only ever arrives after the money has already left. An inbox is a rearview mirror.
Which means the three questions I actually care about are the three questions a mailbox structurally cannot answer.
Question 1: what bills NEXT?
Not what got billed. What is about to bill. The mailbox has no view of the future.
The forecast has to come from somewhere else, so I gave it its own file: apps/hermes/finance/vendors.yaml. Every service Freebo pays for gets an entry with a cadence, a billing day, and an expected amount. The script reads that ledger and projects the next 21 days.

The match list is a set of substrings I test against the From: header, lowercased. It is deliberately loose. Railway’s invoices, for example, come from stripe.com with a display name of “Railway Corporation” — the display name is the only handle. Loose matching is what lets one entry track one vendor across whatever billing plumbing they happen to use this quarter.
critical: true is the important flag. It marks the vendors where a lapse takes production down or takes the agent down. Supabase, the API host, transactional email, the DNS, the CI. Losing one of those is the difference between “annoying” and “customers see errors.”
Question 2: what did NOT arrive?
This is the one nobody builds for, and the one that costs the most.
A subscription that stops billing is either cancelled or failing, and both are silent.
— From the vendors.yaml header
If your Postgres provider was supposed to charge you on the 9th and it’s now the 14th, that is not good news. It usually means (a) they switched billing systems and are about to double-charge you next month, (b) your card on file expired and the retry ladder is quietly counting down to a suspension, or (c) you actually got downgraded to free and nothing has told you yet. All three look identical from the inbox: no email.
The watchdog compares “vendors I expect to hear from monthly” against “receipts seen in the last 45 days.” Anything expected-but-not-seen gets its own line in the report. Cancelled or failing? Go find out. Some entries opt out of this check with expect_receipt: false — that is how I mark the vendors that are legitimately on a free tier or that net their fees out of payouts instead of invoicing. Stripe never sends me an invoice; its cut comes out of the payout. Marking that explicitly means I never chase a phantom missing receipt.
Question 3: is this charge even mine?
This is the messy one. My personal email carries three things: Freebo, a consulting LLC I use for unrelated work, and my actual personal life. One inbox, three sets of finances.
The ledger has a scope: field on every entry — freebo, consulting, or personal. The watchdog only reports freebo. The others get matched and dropped, silently, on purpose. Listing them isn’t overhead — it is the mechanism. The reason they’re in the file at all is so their mail is recognized and suppressed instead of shouting “unknown vendor” every day.
But then there’s the honest case: a piece of finance-shaped mail from a sender in no entry. Rather than dropping it or forcing it into a bucket, the report surfaces it as UNCLASSIFIED — is this Freebo? That one line is my favorite thing about the design.
The report treats it as a conversation, not an alert. I read the line. If it’s Freebo, I add an entry. If it’s not, I add a suppression entry for its actual scope. Either way the ledger gets slightly more correct every day, and it does it through my hands, not the model’s.
Here is roughly what a morning digest looks like — the shape matters more than the specific numbers:

What the agent does vs. what the script does
The whole thing splits cleanly along one line: anything that could be wrong deterministically is a script; anything that requires judgment is the agent.
The script — a single Python file — does the collector work. It scans the mailbox, reads the ledger, projects due dates, checks for missing receipts, pulls month-to-date revenue and outstanding receivables straight from the production database. It writes nothing. Not even a state file. Every run is a full picture, so a missed run costs nothing.
The agent handles the parts that would be brittle if I hard-coded them: sorting the message by what could actually cost me money (a chargeback outranks a routine renewal, a suspended production vendor outranks either), asking Stripe for the live balance and next payout, phrasing “unclassified” as a question instead of a false positive.
The Stripe pull is the one place I had to be a grown-up about credentials. The MCP OAuth grant I use is not scope-limited to read — the discipline has to be in the skill itself. The instruction file spells it out: only ever call read tools. Never write. Money movement lives behind a different skill with a human approval gate.
The insight I didn’t expect
The mailbox is a record of the past. The ledger is a statement of intent. The gap between them is the whole point.
Where they agree, you learn nothing. Where they disagree — a receipt for more than the ledger expected, a billing day that came and went with no receipt, a sender the ledger has never heard of — you learn exactly the thing that matters. The disagreement is the signal.
I wrote the vendor file for Freebo, but the pattern generalizes. If you run anything with recurring costs and your accounting story is “I read the receipts,” you don’t have a system. You have a rearview mirror. Silence is a bug, unrecognized charges are the ones worth reading, and the fastest way to notice both is a plain-text file that just says what you expect to happen this month.
The whole thing was ninety minutes to build and it’s been running against real mail for one day. That’s the honest state. But the design of the ledger is the part I’ll still be using in a year, on the next thing I build that costs money to run.