← Back to blog

Why (Not) Build Yet Another Finance App — Part 3: Categorization and AI

Part 3 of 7Series: Why (Not) Build Yet Another Finance App
0 · Manifesto1 · Open Finance2 · Subscriptions3 · Categorization4 · Credit cards5 · Family6 · Privacy
A thoughtful purple bird, hand on its chin, next to a stack of books, a laptop and large question marks
Categorization, or "the AI categorizes it".

In 2026, categorizing spending looks like a solved problem. Send the description to an AI model, get "Groceries" or "Transport" back, done.

In our heads, it was one prompt and one list of categories.

1%
of new transactions in October were categorized by AI
6
attempts, in order, before a transaction is left uncategorized
91
commits with "categor" in the message since Aug 2025

This is Part 3 of the diary we started in Part 0. In Part 1, we covered Open Finance. In Part 2, charging for the app. Now it's time for the question every finance app promises to answer: where did the money go?

Quick context: transactions arrive from the bank through Brazil's Open Finance, with a short description (sometimes just "PIX ENVIADO", meaning "instant transfer sent") and usually a generic category suggested by the bank itself.

What we thought it was

The original plan
  1. A fixed list of categories
  2. The AI reads the description and picks one
  3. If it's wrong, the user fixes it

What it really was

AI is fourth in line

An AI knows "UBER *TRIP" is transport. What it doesn't know is that a PIX transfer to one specific person is your rent, your cleaner or your English lessons. That isn't in the description. It's in what you did last time.

So every new transaction goes through a line of attempts, from the strongest clue to the weakest. The AI is only called when the earlier ones found nothing:

  1. 1Your history. Have you categorized something similar? What you chose wins.
  2. 2The template for your account type. Personal, shared and business accounts organize the same spending in different ways.
  3. 3The bank's category, translated. A dictionary of 175 Open Finance labels, from "Transfers" to "Groceries".
  4. 4The AI. A new label the dictionary doesn't know yet.
  5. 5The bank's label, untranslated.
  6. 6"To be defined". No clue at all. Better to admit it than to guess.

Since early October 2026, every transaction records which of these attempts gave it its category. In the first few weeks, the scoreboard looked like this:

History
67%
Translation
24%
Account template
7%
AI
1%

Just over 10,000 new transactions, not counting those marked "Ignored". The rest (under 2%) came from spreadsheet imports, from the user, or stayed "To be defined".

AI is great at what's the same for everyone. What matters is what's only yours.

The bank already sends a category. It's generic.

Through Open Finance, most transactions arrive with a category suggested by the bank. "Groceries" becomes "Mercado" with no drama. The problem is "Transfers".

In Brazil, almost every payment to a person is a PIX instant transfer, and every PIX to a person is "Transfers". Rent, a kid's allowance, the cleaner, chipping in for a barbecue. Calling it "Transfers" is technically right and useless to someone who wants to know how much the household cost.

The PIX that didn't fit in the window

To fix that, Snabber looks at your history. If June's PIX to a person was categorized as "Cleaner", September's PIX to the same person should be too.

It wasn't. In September 2026, we found out why: the history lookup only checked the 10 most recent transactions with the same bank label. For someone who sends a PIX every week, those 10 rows were transfers to other people. The right transaction, from months earlier, never fit in the window. And the category fell back to "Transfers".

Before
The last 10 with the same bank label
"Transfers" to anyone, only the most recent
Now
Who got the money, across the whole history
up to 3,000 categorized transactions, matched by the other party's name

It sounds like just raising a number. It isn't. Transaction descriptions are encrypted in the database (Part 6 is about that), so you can't ask the database "who has sent a PIX to this person before?". The history has to be decrypted in memory, once per sync, to build a "person → category" index right there.

And the matching has rules you only discover when they break:

  • Direction matters. A PIX you received from someone doesn't inherit the category of the PIX you sent them. Paying the cleaner isn't the same as the cleaner paying you back.
  • Names come in dirty. Dates, document numbers and suffixes like "LTDA" (Brazil's "Ltd.") are stripped before comparing. Sometimes the bank only sends "PIX ENVIADO", and the name is hidden in another field.
  • A human's choice weighs more. Between two possible categories, the one a person picked beats the one that came from the bank.

The generic label won the tiebreak

We fixed the window, tested it, and the bug was still there. Same day.

That person's history had one transaction in the right category (June) and one in "Transfers" (September, the one that had come in wrong). One against one. The tiebreak was "most recent", and the most recent was exactly the wrong one. The mistake was feeding itself.

Today, a generic bank label loses to any specific category, no matter how often it appeared or when.

Renaming wasn't renaming

Until August 2026, each transaction's category was stored by name. That works until someone renames "Groceries" to "Supermarket". Then the next sync looked at the history, found "Groceries" and recreated the old category. The old name came back from the dead.

Shared accounts were worse: each member had their own copy of every category, hidden on screen by a filter that showed only one. And the copies could have different limits for the same category.

The fix took from August 20 to the end of September: give each category a real identifier, merge the copies into one per account, and write that identifier on every path a transaction takes into the app: bank sync, spreadsheet, form, bulk edit, AI and Telegram. In two phases, so that a mistake in the first one would be a badly filled column, not a report silently adding up the wrong numbers.

Not all spending is spending

Paying the credit card bill comes out of your checking account. But the purchases on that bill were already counted, one by one, on the card. If the payment counts as spending too, the month spends double.

The same goes for moving money between your own accounts and putting money into an investment. These are categories that exist to stay out of the totals. Until August 2026, Snabber's AI agents only left out what the user had marked as "Ignored". Bill payments made it into the summary. (The credit card bill deserves a whole part, and it's the next one.)

snabber-fin
$ git log --oneline -i --grep=categor
01d5b6b9 sync: rótulo genérico do provedor perde para categoria específica
91c2b159 sync: categorizar pela contraparte usando o histórico do usuário
96635621 fim fase 1 ID de categorias
c3630c81 Fase 1 do category_id: colapso das categorias, coluna e writers
ae779c64 categorias excluidas da IA
3e73aeb9 categorizacao por IA

Some of the 91 commits with "categor" in the message, from August 2025 to October 2026. (Yes, our commits are in Portuguese. The highlighted one reads "sync: categorize by counterparty using the user's history".)

What we did

  • ✓A line of six attempts, with your history up front and AI as backup.
  • ✓Categories by who received or sent the money, across the whole history and respecting direction.
  • ✓A generic bank label always loses to a category someone chose.
  • ✓Categories with their own identifier: renaming doesn't bring the old name back, and a shared account has a single list.
  • ✓Bill payments, transfers between your own accounts and investments kept out of spending totals, AI agents included.
  • ✓A record of where each category came from, to measure which source gets it right.

What we haven't solved yet

The first time is still a guess. Your first PIX to a new landlord will come in as "Transfers". Snabber learns when you fix it, and only then. Figuring out that "the same person every 5th of the month" is rent, without anyone saying so, isn't something we do well yet.

A transaction has only one category. The grocery run that included medicine and dog food is still one line, all of it under "Groceries".

And measuring accuracy is harder than it looks. Today we know how many categories users kept: about 95%, whatever the source. But transactions left as "To be defined" were also kept 100% of the time. Keeping doesn't mean it was right. Often it just means nobody looked.

If you're going to categorize transactions

Start with the user's history, not the model. The best clue about a transaction is what that same person did with the previous one.

Give categories an identifier from day one, and record where each one came from. You'll want both sooner than you think.

Want to see how it turned out? Create your Snabber account and try the free plan.

In the next part: why a credit card bill isn't just a list of purchases.