I Built a Budget Tracker That Doesn't Need Your Bank Login
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.