💰

Finance Assistant: PDF bank statements to a Telegram budget

our own product: a bot and web dashboard, four banks' statements, Gemini

In production · in-house product
4 banks
VTB, Sberbank, T-Bank and Ozon Bank: the bank is detected from the PDF itself
~120 tests
pytest functions: text parsing, statement matching, debts, cash, export; no database or network needed
1 phrase
in the chat: an expense, a debt or a recurring payment is recorded and categorised

The task

Money flows through several banks, relatives' cards and cash. Each banking app shows only its own slice. Transfers between your own accounts look like income and expenses, and a hand-kept spreadsheet usually dies after a couple of weeks. On top of that, some spending belongs to work and your own projects and some to personal life, and in a bank app they are mixed together.

What was needed was one honest picture of the money: how much came in, where it went, which part is business and which is personal. And without typing in every operation by hand: a bank statement should simply be dropped into the chat, and a small expense told in an ordinary sentence.

The solution

Finance Assistant is our own Telegram bot with a web dashboard on top of the same data. Implemented in version 1.14.0:

  • Statements from four banks. PDFs from VTB (credit card, credit card account, debit cards), Sberbank, T-Bank and Ozon Bank. The bank is detected automatically, and a universal CSV and XLSX parser is the fallback.
  • A register of accounts and cards. Account number, contract, last four card digits, owner. The next statement lands on the right account by itself; for a VTB credit card the debt, limit, rate and payment date are shown.
  • Expenses in words. "Coffee 250 yesterday", "+65000 salary cash", "recurring expenses 2500 subscription". One message can hold several operations, and one purchase can be split across categories.
  • Voice and receipt photos. Gemini transcribes speech; a receipt becomes line items with categories that are matched to the card operation.
  • Account in words. "By credit card", "from Sber", "card 5515": the record goes to the right account, and the bot asks rather than guessing when it is ambiguous.
  • Night run. Unknown merchants go to the model in one request, categories are set by morning, and doubtful ones arrive as a list of buttons.
  • Questions about your money. "What do I spend the most on?", "compare with last month": the answer is built from the bot's own data, and the bot gives no investment advice.
  • Debts, recurring payments and budgets. A debt ledger per person, payments with a charge day, category budgets with warnings at 80 % and 100 %.
  • Directions and reinvestments. Income, expenses and 12-month dynamics for each direction.
  • A dashboard as a Telegram Mini App. Business, Purchases and Debts tabs, a spending calendar, comparison with the previous period, CSV and XLSX export.
  • Access for two people. The owner and a close relative, each with their own accounts and summary.

How it works

Detecting the bank, account and card from a PDF

Each bank has its own parser (a program that reads the structure of a file), and a separate module reads the statement header to detect the bank, owner, account, cards, period and balance. If the bank format is unknown, the fallback CSV and XLSX parser takes over. The parsers are built for each bank's current PDF layout: when a bank changes it, the sums stop matching the statement totals and the parser has to be fixed. A VTB debit card statement has no account number, so cards of one account are joined by an identical balance on the statement date, and a "Same account" button helps when that is not enough.

Reconciling overlapping statements without duplicates

A bank often issues a statement with spare dates, so the same day lands in two files. The bot compares the incoming operations with the saved ones and does not record them again; mismatches in the overlap are listed in the reply. Uploading the same file twice changes nothing.

Transfers between your own accounts are not income

A transfer from card to card looks like an expense in one place and income in another. The bot recognises such transfers by the banks' wording, by the owner's own name and by "out, in" pairs of the same amount, and they never reach income and expenses. Any account can be excluded from the overall totals with one button.

Merging a manual entry with the statement operation

An expense you typed into the chat is not doubled when the statement arrives. The amount may differ slightly: the tolerance is 2 % or 100 currency units, plus a small date window. A place name from the message is searched in the bank's description with transliteration taken into account, so a Cyrillic name and its Latin spelling count as the same place. If several operations fit, the bot offers a choice with buttons, and a merge can be undone. The weak spot: two different expenses of the same amount on the same day may merge, and a button separates them.

Cash withdrawal: a transfer, not an expense

It used to be impossible to tell the bot about a withdrawal: from the statement it recognised the ATM operation, but from a message it did not, and the withdrawal either vanished or became an expense. Now a withdrawal is recorded as money moving from the card to the pocket, while expenses from the same message stay separate records. A withdrawal recorded in words merges with the ATM operation when the statement arrives, so the pocket is not counted twice. The same withdrawal is not recorded twice in one day; a genuine second withdrawal of the same amount is recorded with the word "another".

Night run and merchant rules

A category is chosen in this order: learned merchant rules, then keywords (the longest match wins), then the category the bank supplied. Whatever stays unknown goes to the model in one request overnight. The clear cases are categorised and remembered as a rule, and in the morning a list of buttons arrives: the unclear ones on top, the sorted ones below for correction.

One set of calculations for the bot and the dashboard

All analytics live in one layer shared by the bot and the dashboard, so the numbers in the Telegram menu and on the charts match. The dashboard is a web page with no build step, written in HTML and Chart.js, and it opens as a Telegram Mini App. Login uses the Telegram signature or a personal link from the bot: the link works for an hour, after which the browser is issued a cookie for 30 days.

Backups and liveness monitoring

A daily database dump is checked for integrity and rotated, and a restore can be tried on a separate database without touching the live one. Every 5 minutes an external check looks at the containers, the database, the "bot is alive" mark (the bot sets it once a minute) and the dashboard address. On failure a Telegram message arrives. An error in a handler is never left silent: the person sees a reply, and the owner gets what failed and on which message.

Results

  • A one-message menu: the month at a glance (income, expenses, balance, today, recurring payments until month end, debts).
  • Directions and "who eats the most": directions and personal spending in one row by descending expense, with a share and a shift against the previous month.
  • The dashboard: daily spending, a spending calendar, "what changed", purchases by receipt line, export of operations to CSV and XLSX.
  • Hints: rising expenses, exceeded budgets, outdated statements, an approaching credit card payment.
  • A credit card payment reminder 3 days ahead, a weekly digest on Mondays, cash reconciliation.
  • Automated tests (about 120 pytest functions) cover text parsing, statement matching, debts, bank detection, export and cash handling.

Technologies and why

  • Python 3.13 and aiogram 3 — the Telegram bot: menus, messages, files, the reminder scheduler.
  • FastAPI and uvicorn — the dashboard API; the frontend itself has no build step and uses HTML, CSS, JS and Chart.js.
  • PostgreSQL 17 and SQLAlchemy 2.0 (async) — storage of operations, accounts, rules, debts and budgets.
  • pdfplumber, pandas, openpyxl, chardet — reading PDF statements, the fallback CSV and XLSX, export.
  • Google Gemini — complex text, voice, receipt photos, the night run and questions about money; keys and ordered model lists are set in .env.
  • Docker Compose — three containers: the database, the bot and the web part; tables are created at startup.

Status

Our own product, in production. The current version, 1.14.0 of 22 September 2026, adds cash withdrawals and deposits from a message. Earlier the same day came spreading expenses across directions, questions about your own money, the spending calendar and naming the account in words.

Limitations: there are no migrations, so rolling back a version is done from a database copy; there is no multi-currency support; complex text and receipt photos go to Google. Transfers between your account and a relative's account in the "All" view count as income for one and an expense for the other.

Questions about this project

How do I combine statements from several banks into one picture without duplicates?
Send a PDF statement from VTB, Sberbank, T-Bank or Ozon Bank. The bot detects the bank, account and card itself, reconciles overlapping periods and never records an already-loaded operation twice, and it lists any mismatches in the overlap in its reply. Other banks are covered by a fallback CSV and XLSX parser.
How do I separate business spending from personal spending?
Operations carry directions: work, your own projects, personal. A direction comes from the category or is set on an operation, and the Spread button assigns it to a whole group of expenses at the same place, past and future. The Business tab shows income, expenses and reinvestments over 12 months and a summary of which direction eats the most.
How do I keep track of debts by person?
A debt is recorded in a phrase: “owe Dima 14k”, “returned Dima 5k”. Each person has a ledger of movements with a balance, and different grammatical forms of a name resolve to the same person. Money moved for a debt in a statement counts as neither income nor an expense.
How do I account for cash that the bank does not see?
Cash withdrawals and deposits are recorded in words: “took out 6500 cash”, “put 5000 on the card”. That is a transfer between the card and your pocket, not an expense, so it stays out of the month's spending. After the message the bot writes how much it thinks is left in your pocket, and on Mondays it asks you to reconcile cash.
Where is the data stored, and what goes to Google?
The data lives in a PostgreSQL database in Docker on the server where the bot runs. Only Telegram IDs on the allow-list can open the bot and the dashboard; everyone else is refused and the dashboard API answers 401. Receipt photos and message text go to Google Gemini when a complex parse is needed; without keys only the built-in rules work.
Can a second person use it?
Yes. A second person enters their own expenses, which go to their own accounts, and sees their own summary, operations, accounts, export and a joint summary. The owner's sections (business, debts, budgets, categories, dashboard, night run, admin commands) are closed to them.

Need something similar?

Tell us about the task — we'll show how we solved it and estimate the scope.