A Telegram bot that tracks your spending in the time it takes to type "purse 5.2".
No app to install. No forms. No category dropdown. No "select account → select type → select date → confirm". You already have Telegram open — just tell it what you spent.
you: purse 5.2
bot: 👍
That's the whole interaction. Two seconds, one thumb, done.
Every expense tracker dies the same death: you stop logging.
Not because you don't care about your money. Because logging a $4 coffee costs you six taps across three screens, and doing that at the counter while someone waits behind you is genuinely not worth $4 of information. So you skip one. Then a week. Then the app is a graveyard of March.
The problem was never motivation. It was friction.
So the interface here is the one you're already fluent in: a text message. You don't learn a syntax, you don't pick from a menu, you don't open anything. You type what happened, the way you'd say it out loud, in the app that's already on your screen — and it's logged before the card machine finishes beeping.
It reads English, not forms. "20 credit for groceries" is a complete transaction. Amount, wallet, and note, extracted from one sentence. No fields.
It learns your words for your money.
A wallet has no official name — it has as many nicknames as you want. /newwallet cash money purse means all three words point at the same wallet, so you never have to remember which one you registered. You write however you naturally write.
It doesn't interrupt you. Success is a 👍 reaction on your own message. Not a reply, not a popup, not a confirmation dialog. The chat stays clean and you stay in flow.
Every wallet command doubles as a wizard.
Run /newwallet, /renamewallet, /showwallets, /showwallettransactions, or /setwalletamnt with no arguments and you get inline buttons instead of a usage error. New wallets start from three presets — Cash, Bank, Credit — each already seeded with a few nicknames (cash/money/purse, bank/corporation, credit/online/card), or a custom name if none fit. /showwallets doubles as a small dashboard: tap a wallet to rename it, correct its balance, or see its history, without retyping the name.
AI, but only when it's actually needed. Simple messages are parsed instantly by plain Python — no API call, no latency, no cost. When a message is genuinely ambiguous ("5 in purse and 20 from credit", or "4.35cred") it quietly falls back to an LLM that can split it into multiple transactions and hand them back structured. Fast path for the 95%, smart path for the rest.
It tells you what you didn't log.
Every tracker pretends you logged perfectly. This one doesn't. When you correct a balance with /setwalletamnt, that gap is stored and surfaced later as "untracked drift" — money that moved without being logged. It's the most honest number in any budgeting app, and it's the one nobody else shows you.
It's private by default. No bank linking. No account to create. No subscription. No ads selling you a credit card while you look at your overdraft. Your ledger is siloed to you, backed up nightly, and never sold or shared. And if you'd rather not take anyone's word for that, the whole thing self-hosts onto a SQLite file you own — see Setup.
It has a personality.
"Zero wallets. Which is, itself, a financial position (Haha brokie)."
"Nothing logged yet for that wallet. Suspiciously clean."
You'll open it more often than an app that talks to you like a bank.
/app opens a Telegram Mini App — no login, it knows who you are.
- Ledger — In / Out / Net at the top, your transactions grouped by day. Each day header has a bar filled proportional to your worst day, so the list is the chart. Income in green, corrections flagged amber.
- Patterns — spend by weekday (page back through previous weeks), spend by hour of day with your peak hour called out ("you spend most around 7pm–8pm"), a 6-month trend line, and stat tiles: this month vs last month, average per day, biggest single expense, busiest day.
One Python process runs both the Telegram bot (long polling) and a FastAPI server that hosts the Mini App and its JSON API.
Telegram message
│
▼
on_message ──► token parser (amount? wallet nickname? the rest is the note)
│ │
│ ├─ clean ─────────────► insert transaction ──► 👍
│ ├─ no wallet ─────────► inline buttons: "Which wallet?"
│ └─ ambiguous / unparsed
│ │
│ ▼
│ AI fallback (Bedrock, forced tool call → structured JSON)
│ │
▼ ▼
SQLite ◄──────────── one or more transactions
Parsing rules (src/telebot.py:on_message)
| You type | Result |
|---|---|
purse 5.2 |
−5.20 from purse |
paid xyz for chingchong 3.2 credit |
−3.20 from credit, note "paid xyz for chingchong" |
got +50 from dad! |
+50 — asks which wallet if you have more than one, otherwise logs straight to your only one |
5 in purse and 20 from credit |
two numbers → AI fallback → two transactions |
4.35cred |
unparseable → AI fallback |
hello |
no digits → ignored entirely, chat away |
A bare number is assumed to be a spend and goes negative. An explicit + or - is respected.
Data model (src/db.py) — four tables: user, wallet, walletnickname, transactions. Amounts are signed floats; balances are never stored, always SUM(amnt). Nicknames are unique per user at the database level, which is what makes purse unambiguously resolvable.
Auth (src/auth.py) — the Mini App sends Telegram's initData on every request; the server verifies the HMAC-SHA256 signature against the bot token and rejects anything older than 24h. No route ever trusts a client-supplied user id.
Python 3.11+ · python-telegram-bot · SQLModel + SQLite · Alembic · FastAPI + Uvicorn · AWS Bedrock (Amazon Nova Micro) · Tailwind + Chart.js from CDN, no build step.
| Command | Does |
|---|---|
/start |
Sign up, name your first wallet (buttons or text) |
/newwallet <names...> |
One wallet, all those nicknames |
/renamewallet <wallet> |
Rewrite its nicknames; the first one is the display name |
/showwallets |
Every wallet and its balance |
/showwallettransactions <wallet> [count] |
Recent history (default 40) |
/setwalletamnt <wallet> <amount> |
Correct a balance; the gap is recorded as drift |
/settimezone |
So timestamps aren't a lie |
/app |
Open the Mini App |
/help |
The above, with jokes |
Every command above also works with no arguments — instead of a usage error you get inline buttons (pick a wallet, pick a preset, pick a count), the same BotFather-style flow /start uses to onboard you.
/showwalletlogs is an undocumented alias for /showwallettransactions — same handler, same arguments. It's kept out of the command menu on purpose: a synonym earns its place in muscle memory, not in the picker.
The exact text of every one of these — plus the profile blurb, description, and picture — lives in BOT_DETAILS.md, ready to paste into @BotFather.
website/ is a separate single-page React app (Vite + Tailwind v4, run with Bun) that isn't served by the FastAPI process at all — it deploys on its own to Vercel. It shares the Mini App's design tokens, so the two look like one product.
cd website
bun install
bun run devsrc/config.js holds the whole launch switch: LIVE = false shows a coming-soon banner, disables the Telegram buttons, and collects emails into a Supabase mailing_list table straight from the browser instead — safe because the table's RLS policy grants insert and nothing else. Flipping it to true with a real BOT_URL is the entire go-live step: banner and waitlist disappear, the Telegram CTAs become real links. It's currently live, pointed at @thebeancounter_bot.
git clone https://github.com/KidCoderT/beancounter.git
cd beancounter
uv syncCreate a .env:
TOKEN=<your bot token from @BotFather>
WEBAPP_URL=<https URL where the Mini App is reachable>
Then:
uv run main.pyMigrations run automatically on startup and the SQLite database is created at database.db.
Set the bot up in @BotFather using BOT_DETAILS.md — name, picture, description, and the command list are all in there, ready to paste.
Notes
- The server binds to
127.0.0.1:8000. For the Mini App to work you need a public HTTPS URL — a tunnel (ngrok/cloudflared) in development, a reverse proxy in production — and that URL goes inWEBAPP_URL. The bot itself works fine without it. - The AI fallback needs AWS credentials with Bedrock access in
eu-north-1. Without them, everything else still works — only ambiguous messages fail, with an apology. scripts/seed_dev_data.pygenerates ~60 days of realistic data if you want to see the charts populated. Add--committo actually write.- Backups: the database is snapshotted daily at 03:00 UTC into
backups/, newest 7 kept. - Tests are plain asserts, no framework:
uv run python tests/test_nicknames.py(and so on for each file intests/).
- Automatic sorting — an AI pass that files each transaction into a spending group by itself, plus a daily digest of where the money went. Built once already and pulled back out before launch for being too much surface area; the code and a write-up of what to do differently are in
archive/classification-v1/. - Editing from the Mini App — reading your ledger on the web but having to go back to chat to fix a typo is the obvious missing half.
- Per-wallet currency — designed already, written up in
futureplan.md. - Discord — the parsing, database and AI layers don't know or care that Telegram exists. A Discord bot on top of the same core means the same ledger, from whichever chat app you actually live in.
Honest list, so nothing surprises you:
- The Mini App is read-only — no editing or deleting transactions from the web UI yet
- No spending categories, budgets, or recurring transactions
- Currency is hardcoded to
$ - Wallets can be renamed, but not deleted
- The timezone picker is a fixed list of 8 common zones
