I Got Sick of Subscription Budget Apps. So I Built My Own With Telegram, Python, and a $0/Month Stack.
No logins. No monthly fees. No "Premium required to export your own data." Just you, a Telegram message, and a double-click. Try it now: @PennyTrak_bot
Every January I download a new budgeting app. Every March I forget to open it. By April, I'm back to squinting at my bank statement trying to reverse-engineer where โน40,000 went.
It wasn't a discipline problem. It was a friction problem. Every app wanted me to log into a dashboard, find the right category, tap through three menus, and somehow remember to do this after every transaction. I don't do that. Nobody does that.
But I do check Telegram constantly. So I asked myself a dumb question: what if logging money felt exactly like texting a friend?
Me: spent 500 on ola
Me: swiggy 420 dinner
Me: got salary 75000
No menu. No category dropdown. Just words.
That's the whole idea behind Ek Ek Paisa ka Hisab - a personal finance system that lives in Telegram, persists to Supabase, and surfaces in a static HTML dashboard you open with a double-click. Here's how it works, and why a few of its design decisions might be worth stealing.
The Architecture in One Sentence
A Python bot parses your natural-language messages, writes them to a Postgres database, then regenerates a self-contained HTML file that opens offline with no server.
That last part is the unusual bit. Most developers would reach for a React dashboard served from a cloud function. I went the other direction: the dashboard is a single .html file with your spending data baked in as a JavaScript variable. Double-click it. Done. No internet, no login, no third party seeing your rent figure.
window.EXPENSE_DATA = [
{ "date": "2026-08-01", "category": "food", "amount": 420, "note": "swiggy dinner", "type": "expense" },
...
];
The file regenerates after every Telegram message. The dashboard reads it via a <script src="data.js"> tag - not fetch(), which would require a server for the file:// protocol. It's an old trick, but it works perfectly and the whole thing loads in a blink with no CDN dependency.
The Parser Is the Product
The hardest part of this project wasn't Supabase or Telegram. It was making the parser genuinely robust. spent 500 on ola is easy. But real people type things like:
1.5k myntra shirt(k = thousands)2l rent paid(l = lakhs, Indian notation)rs 500 groceries(currency prefix)got salary 75k(income, not expense)ola 250 last night(amount anywhere in the sentence)coffee 3(small amounts without a unit)
The parser handles all of these. It finds amounts using regex that understands Indian notation, classifies the type as expense or income based on trigger words (salary, credited, refund, cashback), and assigns a category by matching the note against keyword sets.
"spent 500 on ola" โ {amount: 500, category: "travel", type: "expense"}
"swiggy 420 dinner" โ {amount: 420, category: "food", type: "expense"}
"got salary 75000" โ {amount: 75000, category: "income", type: "income"}
"1.5k myntra shirt" โ {amount: 1500, category: "clothes", type: "expense"}
"2l rent paid" โ {amount: 200000, category: "rent", type: "expense"}
91 test cases cover the parser alone, including adversarial inputs: a message like call me at 7pm should return null, not {amount: 7}. Getting that boundary right took more iteration than any other part of the system.
Per-User Isolation From Day One
Most personal bots are built for one person. This one is built to handle many people using the same bot - each completely isolated from the other. Every transaction carries a chat_id. Every query is scoped to the sender. Nobody sees anyone else's spending. And the dashboard - which has no login - shows exactly one owner's data, set in config via a single field: owner_chat_id.
{ "owner_chat_id": 123456789 }
Not sure what your chat id is? /whoami tells you. It even detects the single-user case automatically - if only one person has ever messaged the bot, it adopts them as the owner without requiring any configuration at all.
Per-user budgets extend this. You can set category caps without touching a config file:
/setcap food 8000
/setbudget 50000
Your caps layer over the defaults. Setting one doesn't wipe the others. And when the dashboard regenerates, it reads your caps - not the install defaults - so the budget bars actually reflect what you told it.
The Projection That Doesn't Lie to You
Most budget apps project your monthly spend by multiplying today's daily average by 30. This sounds reasonable until you pay rent on the 1st and the app tells you you're on track to spend โน6,00,000 this month.
Ek Ek Paisa ka Hisab flags recurring costs before extrapolating. Rent, EMIs, subscriptions - large one-time payments that appear in history at roughly monthly intervals - get pulled out before the daily rate is calculated. What's left is genuine variable spending: food, travel, coffee. That gets projected. Rent doesn't.
The result is a projection that feels honest. If it says you'll overshoot by โน3,000 on food by the end of the month, it probably means it.
The Health Check That Started a Security Story
I built a /health route because I wanted to point an uptime monitor at the bot. GET /health/live for a liveness probe, /health/ready if you want it to actually hit Postgres first. Standard stuff.
But building it forced me to think about what a health endpoint is actually exposing. It knows your database URL. It knows whether your bot token is set. And Postgres error messages - as I found out the hard way - will sometimes quote your connection string back at you when they fail.
So the health endpoint reports credentials as shapes, not values:
{
"supabase_key": "sb_secret_โฆ(41)",
"telegram_token": "set",
"supabase_project": "hfmmโฆyc"
}
It doesn't use SimpleHTTPRequestHandler, which would cheerfully serve the .env file sitting beside it. It answers exactly three string paths and has no concept of the filesystem. And report() runs a final sweep over the assembled JSON to catch any chat id that snuck into an error message - because Telegram chat
Comments
No comments yet. Start the discussion.