Stay Steady 1.3: Multiple Accounts, and Teaching a Budget App What a Transfer Actually Is
Most people in Belgium have more than one account. A zichtrekening for daily life, a spaarrekening the money disappears into, maybe a pension account, maybe a business account if you’re self-employed. Until now Stay Steady treated all of that as one undifferentiated pile of transactions.
Version 1.3 fixes that — but the interesting part isn’t the account list. It’s what the app can finally work out once it knows which account a transaction belongs to, and what kind of account that is.
Accounts have types, and the type does real work
When you add an account you give it a type: zichtrekening, spaarrekening, pensioensparen, beleggingen, kredietkaart or bedrijfsrekening. That isn’t a label for a filter chip. It changes how transactions get categorised.
Money leaving a savings account is a withdrawal by definition. Money arriving is a deposit. The description on the line doesn’t matter — “OVERSCHRIJVING” tells you nothing, and the ML model, which only ever sees a counterparty name, would happily file your own transfer under something else entirely. Once the account has a type, the app stops guessing about the cases where guessing is unnecessary.
Two types deliberately say nothing: current and business. A current account is where ordinary life happens, and a business account holds groceries, fuel and software like any other. Inferring “business” from the account alone would mislabel most of it and leave you correcting more than you gained.
Money between your own accounts is not income. It is also not spending.
This is the problem every multi-account budgeting app has to solve, and it’s easy to get subtly wrong.
Move €500 from your current account to your savings account and import both statements, and a naive app sees two transactions: €500 of spending on one side, €500 of income on the other. Your spending is inflated, your income is inflated, and your savings rate is nonsense.
Stay Steady recognises the pair. When a transaction’s counterparty IBAN matches one of your own accounts, both legs are understood as one movement seen from two sides — and both account types get a say in what to call it. The counterparty sees the same movement with the sign reversed, which is what makes the two legs agree: your current account’s −500 and your savings account’s +500 both come out as a savings deposit. Not two bare “transfers” that look like the same row imported twice.
There’s a case underneath that which took us longer to get right than we’d like to admit. If you’re self-employed and your salary arrives from your own company account, is that income?
It depends on something the app can actually check: whether that company account’s own transactions are in the ledger. If they are, the revenue was already counted when it landed there, and counting the salary again would double it — so it’s an internal move. If they aren’t — you added the account so transfers would read nicely, but never imported its statements — that money was never counted anywhere, and it is unambiguously your income. Getting this backwards silently zeroes a freelancer’s entire income on every screen that mentions it. Ask us how we know.
Money going out to an account you own is never spending, whether or not you track the destination. You still own it.
IBANs, validated without asking anyone
Add an account and the IBAN is checked as you type — the ISO 13616 MOD-97 checksum plus the Belgian national two-digit check — and your bank’s name is filled in from the official NBB institution list.
All of it happens on your device. No API, no lookup service, no network call. We looked at the commercial IBAN-validation APIs, and then noticed that the thing they do is arithmetic we can perform locally in about a millisecond, against a bank directory that changes a few times a year and ships inside the app. Sending your account number to a third party to be told it has a valid checksum is a strange trade.
Business and private, kept apart
If you’re self-employed, 1.3 stops mixing the two.
New business categories: boekhouder, btw-teruggave, leveranciersfacturen, leasing, toestelherstel, AI-tools. Leasing gets its own business-side entry because a lease can be private or professional and a subcategory only has one parent.
Subscriptions split into business and private, with separate totals — because “what am I paying every month” is two different questions depending on which hat you’re wearing.
The FIRE calculator ignores business accounts entirely. This one deserves its own note. A FIRE target is annual expenses divided by your withdrawal rate, so at the default 4% every €1,000/month that reaches the expense base adds €300,000 to the number you’re told you need. One imported month of a business statement was enough to turn a €600k target into €16.6M. The arithmetic was never wrong; it was faithfully multiplying a contaminated input by 300.
What was quietly broken
In the spirit of the rest of this blog, here’s what 1.3 fixes that shouldn’t have shipped in the first place.
Correcting a recurring charge didn’t offer to fix the other months. The “apply to all similar” prompt compared only the top-level category. Change a monthly car lease from maintenance to leasing and both stay under Car — so every other month looked like it already agreed, and the prompt never appeared. It now compares the subcategory too.
The account filter wasn’t applied everywhere in Insights. The tab kept cached copies of the filtered data and refreshed them on a counter that doesn’t move when you switch accounts. Several cards kept showing the previous account.
Both home-screen widgets never worked. The app and the widget were pointing at two different App Groups — a leftover from an old bundle identifier — so the widget could never read what the app wrote, and fell back to placeholder data. Anyone who added a widget has been looking at €2.850 of income that belongs to nobody. They now show your real figures, and fall back to the most recent month that has data rather than an empty current one, because in an import-driven app the current month is empty for the first stretch of every month.
iCloud sync never worked. Not regressed — never, from the day it shipped. CloudKit refuses a schema unless every attribute is optional or has a default value, and it refuses the whole container when one doesn’t. Sixty-five properties across eight models were declared without defaults, because the initialiser always set them and nothing complained. The app caught the failure, quietly fell back to local storage, and displayed “Synced with iCloud” regardless. It now syncs, and when something is wrong it says so instead.
Availability
Stay Steady 1.3 is live on both the App Store and Google Play. If your phone still shows 1.2.0, give it a little — the stores take a while to reach every device.
1.3.1 follows shortly, and in the spirit of the section above it is all bug fixes: an import now starts in the account you are looking at instead of the first in the list, the “already imported” count is measured against the account the import will actually land in, and the summary at the top of the Journal follows the account filter rather than totalling every account underneath it.
Still free, still no subscription, still no ads, still no bank connection — every transaction comes from a file you import or a line you type. The account types, the IBAN validation and the categorisation all run on your device.
If you want the detail on what does and doesn’t leave your phone, the privacy policy spells it out per service — and we corrected a few things in it recently that hadn’t kept up with the code.