My Expense Tracker Invented $4,000 of Phantom Spending: Dissecting SMS Parser False Positives

I built a personal expense tracker that reads card-approval SMS messages and app notifications and records transactions automatically. One day the monthly total looked several thousand dollars larger than life felt. Digging in, the money wasn't spent at all — the parser had been treating any text containing an amount as a transaction. This is a dissection of the four false-positive species that created the phantom spending, and how moving parser rules to the server ended the problem without a single app redeploy.

1. The Smell: A Total That Doesn't Match Reality

The upside of an automatic ledger is that it fills itself; the downside is that you stop looking. When I finally checked, the monthly total was roughly double my gut feeling. Sorting entries by amount, descending, exposed the culprits immediately.

2. Four Species of False Positives

Species 1: Numbers Inside Promotions (the biggest ghost)

"To keep your insurance until maturity,
 your remaining premium is 3,4XX,XXX won."

A fintech app's insurance marketing notification. It describes money I might pay someday — but the parser found an amount ending in "won" and recorded a ~$2,500 expense. The single largest ghost.

Species 2: Billing Notices (double counting)

"[Card] Your upcoming statement total: 3XX,XXX won"

This is the sum of transactions already recorded individually. Recording it again counts the same money twice.

Species 3: Auto-Debit Withdrawal Alerts (double counting, again)

"Withdrawal 2,7XX,XXX won - ○○ Card - balance XXX won"

The bank saying last month's card bill was auto-debited — a payment of expenses already recorded last month, not new spending. Because the card issuer's name appears in the text, the parser mistook it for a card purchase. There were three of these, one per card.

Species 4: Price Targets in Brokerage News (the absurd one)

"Research alert — price target raised to $156.80"

The parser interpreted a stock's price target in a news notification as an overseas card payment, manufacturing a ~$150 expense. Five times over two weeks. News alerts were generating spending.

3. Root Cause: An Optimistic Parser

All four species share one cause: "if there's an amount, it's a transaction" — optimistic matching. Real approval messages almost always carry markers like "approved" or "payment complete," but the early parser didn't require them. Any amount-shaped string passed, so numbers inside ads, notices, and news all became expenses.

4. The Fix: Rules as Server Data, Not App Code

I could have patched the parser and redeployed the app — but false positives keep arriving in new shapes, and waiting for store review each time is not a strategy. So the rules left the code.

  • A server-side rule file. Block keywords (non-payment notices), promo keywords, approval markers, and cancellation markers live in a JSON file on my server. The app fetches it on every sync and overlays it on its built-in defaults. Fixing a rule = editing a file. No redeploy.
  • Layered rules. Even if a block keyword like "premium" matches, an approval marker ("approved", "payment complete") overrides it and lets the transaction through. Real insurance payments survive; insurance ads die. Without this exception layer, every new block keyword risks killing real payments — and you become afraid to add rules.
  • Remote cleanup commands. Already-recorded ghosts were moved to the trash (recoverable) via a server-side command queue the app polls — no tapping through the phone entry by entry.

After clearing a dozen-plus ghosts, the monthly total dropped from the inflated figure to something matching reality. Since then, every new false-positive species costs exactly one keyword line in the server rule file.

5. The Remaining Truth: Statements and Spending Differ by Design

Even with zero false positives, the tracker's "spent this month" will never equal the card issuer's "statement amount." Installment carryovers, auto-payments that send no SMS, and annual fees are money an SMS-based ledger structurally cannot see. That's not a bug to fix but a limitation of the data source — so instead of hiding it, the statement screen now says "based on new spending captured via SMS." Trust comes from disclosing what you can't see, not from pretending you see everything.

6. Lessons

  • Text with an amount ≠ a transaction. Parsers must not match optimistically. Requiring a positive signal (an approval marker) should be the default posture.
  • Classification rules are data, not code. False positives are not software bugs but the world's diversity; they never stop coming. Rules as server data shrink the response cycle from "store review" to "edit a file."
  • Design the exception layer together with the block list. "Block, unless an approval marker is present" is what makes adding rules safe enough to do freely.
  • Label the money you can't see. Automation earns trust by honestly stating its limits, not by its accuracy alone.

FAQ

Q. Why not just use bank APIs?

For a personal tracker, official API access covering every card issuer and bank is a wall of cost and paperwork. SMS/notification parsing is a universal interface that works with every card immediately — and false-positive management, like this post, is its maintenance fee.

Q. Wouldn't an LLM classifier eliminate false positives?

It would reduce them, at the price of per-message cost, latency, and privacy (sending financial texts off-device). Layered keyword rules are free, stay on-device, and — crucially for money — every decision is instantly explainable.

Q. What about ambiguous borderline cases?

Preserve the full original text with every record. If classification goes wrong, the raw message makes after-the-fact judgment and rule improvement possible. Discard the original and keep only the parse, and debugging becomes impossible.