Back to blog
5 min readMarch 2, 2026

I Built a Budget Tracker That Doesn't Need Your Bank Login

#webdev#typescript#javascript#productivity

Every few months I'd catch myself wondering where my money went. Not in a panicked way — just that quiet, nagging feeling after a week of eating out. I'd open Mint, remember I deleted it, think about YNAB, balk at the subscription, and close my laptop.

The problem isn't that budgeting apps don't exist. It's that they all want the same thing: your bank credentials, a monthly fee, and a commitment. For something I only need once a month for about ten minutes, that's too much.

So I built Monee. You paste your transactions. It tells you where your money went. That's it.

The Core Insight

Banks already give you your data. Every bank I've ever used has a "download transactions" button — usually CSV, sometimes just a table you can copy. The insight is: that's enough. You don't need a live connection. You need a parser smart enough to handle messy, inconsistent bank exports.

The hard part isn't the UI. It's the parsing.

Two-Pass Column Detection

Bank CSV formats are a disaster. Some banks put the date first, some put it third. Amount columns might be "Debit" / "Credit" as separate columns, or a single signed "Amount" column. Descriptions might be "Merchant" or "Payee" or "Narrative".

I wrote a two-pass detector:

function detectColumns(headers: string[], rows: string[][]): ColumnMap {
  // Pass 1: match headers against known aliases
  const aliases = {
    date: ['date', 'posted', 'transaction date', 'trans date'],
    description: ['description', 'merchant', 'payee', 'narrative'],
    amount: ['amount', 'debit', 'credit', 'value']
  };

  const map: ColumnMap = {};
  headers.forEach((h, i) => {
    const lower = h.toLowerCase().trim();
    for (const [field, names] of Object.entries(aliases)) {
      if (names.some(n => lower.includes(n))) {
        map[field] = map[field] ?? i; // first match wins
      }
    }
  });

  // Pass 2: if still missing, infer from data shape
  if (!map.amount) {
    const amountCol = headers.findIndex((_, i) =>
      rows.slice(0, 5).every(r => {
        const val = r[i]?.replace(/[$£€,]/g, '') ?? '';
        return /^-?[\d]+\.?\d*$/.test(val);
      })
    );
    if (amountCol >= 0) map.amount = amountCol;
  }

  return map;
}

This handles about 90% of real-world bank exports without any user configuration.

Categorization by Keyword

Once I have clean transactions, I categorize them by matching description keywords against a weighted dictionary. "WHOLE FOODS", "TRADER JOE'S", "SAFEWAY" → Groceries. "SHELL", "EXXON", "CHEVRON" → Transport. "NETFLIX", "SPOTIFY", "HULU" → Subscriptions.

The dictionary has about 80 merchants and keywords out of the box. Users can override any categorization by clicking the badge inline. I store it all in localStorage — no backend, no accounts, no GDPR headaches.

What Surprised Me

The empty state is the hardest part. An app with no data looks broken. I added blurred ghost pills in the background and a "Try with sample data" button that loads a realistic February's worth of transactions. The moment you click it, the app looks alive — budget bars half-filled, a couple in warning territory, the spending chart populated. That one button made the product feel real.

Dark mode isn't a theme, it's a constraint. Designing dark-first forced me to be deliberate about color. Purple for primary actions. Green for healthy budgets. Amber for warnings. Red only when you've actually blown past a limit. Every color earns its place or it doesn't exist.

What I'd Do Next

  • Import from email — forward your bank statement email, parse the attachment automatically
  • Multi-month view — compare February vs January at a glance
  • Recurring detection — automatically flag subscriptions and flag new ones
  • Export to CSV — with your categories applied, so you can drop it into a spreadsheet

Try It

Monee is free, open source, and runs entirely in your browser. Your transactions never leave your device.

→ monee-budget-tracker.vercel.app

Paste your transactions. Find out where your money went. Takes about 30 seconds.