×Letting an assistant record my money without letting it break the books

, updated

Letting an assistant record my money without letting it break the books

Waypoint’s MCP server writes as the signed-in user, through the same database checks as the app, and answers every write with what it changed.

Waypoint is my debt-payoff and multi-currency cashflow planner: an Expo app for iOS, Android and web on Supabase, with a projection engine that walks every account forward day by day. Nearly every commit is co-written with Claude Code.

Most of the data entry doesn’t happen in the app. I paste a statement or describe a payment, and Claude Code records it through Waypoint’s MCP server. That makes the server the riskiest part of the system: it’s the one place a model writes money. So it’s built to make a wrong write hard to land and easy to see.

It writes as me, through my checks

The agent signs in as the user, with the publishable key. There’s no service key in the server. Row-level security and every database check apply to its writes exactly as they do to the app’s, and there’s no dry run.

Inputs are Zod schemas shared with the app’s core package, so an unknown argument is refused by name instead of ignored. Amounts go in as decimal strings, "-23.40", and are read once in the account’s currency. A string with more decimals than the currency takes is refused.

Writes are idempotent and state what they expect

post_transactions takes a batch id, and a row id left out is derived from the batch id and the row’s position. Sending the same call twice answers already_applied. A repeated bank reference is refused as already_recorded, naming it. Retries and double pastes are safe.

The call also carries expect_before and expect_after balances. If the account doesn’t hold what the agent thinks it holds, the write is refused as balance_mismatch, with the actual balances. Reconciling against a statement is one call: this is what I think the balance was, this is what it should be after these rows, refuse if either is wrong.

Updates are guarded on what the call read. If a row changed since then, the answer is stale, not an overwrite.

Every write says what it changed

Each write loads the household, writes, loads it again, and runs the projection engine on both. The answer carries each cash account’s lowest coming balance, the days under its floor and the FX margin, before and after, with moved: true when any of them shifted. The agent doesn’t have to work out whether a payment pushes an account under its floor next month. The tool tells it, and it tells me.

Text a person typed is data

Payee names, notes and descriptions are text someone typed, or a bank printed. They only ever appear in fields named data_text, and the tool descriptions tell the agent to read those as data, never as instructions. It’s one layer, not a guarantee, but a payee name is never handed to the model as something to do.

The password never touches a command line

The agent logs in once with the account’s existing password. It goes into the macOS Keychain through security -i on stdin, so it never shows in a process list, on screen or in a log. Every Claude Code session starts its own server with its own in-memory session, so no session can rotate another’s refresh token away, and nothing is written back to disk.

Reading and comparing save nothing

The read tools build a snapshot from one export plus the day’s ECB rates and run the engine: an overview, what’s open or overdue, the plan, a month’s ledger, a card’s statement and what to pay on it. compare runs what-if scenarios through the same engine and saves nothing. It answers with a link that opens the same comparison in the web app.

Update, 7 November 2026: a cleaned copy of the code is now public as waypoint-planner.

All writing