Телеграм-бот, который считает, кто кому должен
Мой пет-проект: бот для группового чата, который ведёт учёт совместных расходов и сам сводит, кто кому сколько остался должен. Проверен в поездке к другу в Турцию.
В августе 2025-го я поехал в Турцию к своему другу Лёхе. Жил у него, так что за жильё голова не болела, а вот всё остальное мы делили: то он платит за такси, то я за ужин, то он за продукты в магазине. Через пару дней уже никто не помнит, кто за что отдавал. Обычно это заканчивается неловким вечером с калькулятором и фразой «так, погоди, а кто платил за тот завтрак». Чтобы такого вечера не было, я написал бота.
Живёт он в Telegram под именем @StasBaikalBot. Добавляешь его в групповой чат, и он молча ведёт бухгалтерию: записывает расходы, считает средний чек и держит в закреплённом сообщении актуальный баланс. В Турции мы пользовались им каждый день, и проверку реальной поездкой он прошёл.
Что он делает
Логика такая. В чате есть закреплённое сообщение со сводкой. Каждый раз, когда кто-то пишет расход, бот его записывает и переписывает эту сводку заново. В любой момент можно открыть закреп и увидеть, сколько потрачено всего, сколько в среднем на человека и кто кому в итоге должен.
Записать общий расход просто: пишешь в чат 1500 продукты. Бот понимает, что 1500 — это сумма, а всё остальное — описание, и заносит трату на тебя.
Бывают и личные долги, когда деньги не общие, а конкретно один занял у другого. Для этого есть команда /owe: отвечаешь реплаем на сообщение того, кому должен, и пишешь /owe 1000 майка. Бот фиксирует, что ты должен этому человеку тысячу.
Ошибся — отвечаешь реплаем на свою запись командой /delete, и она пропадает из подсчётов.
Стек
Бот написан на Python, и за время жизни проекта он успел сменить архитектуру. Сейчас это третья версия, и она устроена иначе, чем первые две.
python-telegram-bot— библиотека для работы с Telegram. Бот принимает апдейты вебхуком, а не поллингом, потому что живёт в облаке и не крутится постоянным процессом.firebase-functions— обёртка для serverless. Весь бот — это одна облачная функция, которая просыпается на входящий запрос от Telegram и засыпает обратно.Flask— внутри функции крутится маленькое Flask-приложение с единственным эндпоинтом/webhook, через который Telegram присылает сообщения.Firestore— база данных. Туда складываются расходы, долги, имена участников и id закреплённого сообщения для каждого чата.
Первые версии бота были обычным процессом на VPS: висел демон, держал поллинг, ел память даже когда в чате тишину. Для бота, который реально работает минут двадцать в день, это расточительно. Поэтому третья версия переехала на serverless: функция запускается только когда кто-то что-то написал, а в остальное время не стоит ничего. Для пет-проекта на пару чатов это идеальный режим — платишь за факт использования, а не за то, что сервер просто включён.
Как устроен подсчёт
Самое интересное здесь не в том, как бот записывает расходы, а в том, как он сводит итог.
Когда в чате появляется новая запись, бот не дописывает строчку к старой сводке. Он берёт из базы вообще все расходы и все долги этого чата и пересобирает сводку с нуля. Сначала складывает, сколько потратил каждый, потом считает средний чек — общую сумму делит на число людей, которые вообще тратились. Дальше у каждого появляется баланс: потратил больше среднего — тебе должны, меньше — должен ты.
Поверх общих расходов накладываются личные долги из /owe: они просто двигают балансы двух конкретных людей.
А дальше начинается то, ради чего всё затевалось. Сырые балансы вида «Аня в плюсе на 800, Петя в минусе на 300, я в минусе на 500» — это ещё не ответ. Никому не хочется делать три перевода, если можно обойтись одним. Поэтому бот прогоняет балансы через жадный алгоритм взаимозачёта: берёт самого большого должника и самого большого кредитора, гасит между ними максимально возможную сумму, и так по кругу, пока все не сойдутся в ноль. На выходе получается минимальный список переводов: кто, кому и сколько. Не «все скидываются в общий котёл», а конкретные стрелочки.
Путь одного сообщения выглядит так:
"1500 продукты" → Telegram шлёт вебхук → облачная функция просыпается
│
▼
Flask разбирает апдейт, отдаёт нужному хендлеру
│
▼
запись расхода в Firestore (сумма, кто, описание)
│
▼
читаем ВСЕ расходы и долги чата заново
│
▼
средний чек → балансы → взаимозачёт долгов
│
▼
переписываем закреплённое сообщение
Пересобирать сводку целиком на каждое сообщение — звучит избыточно, но для группового чата это копейки: записей за поездку набирается несколько десятков, не миллионы. Зато удаление любой записи не требует никакой хитрой математики: убрал строчку из базы, пересчитал всё с нуля, и баланс снова сходится. Простота тут дороже мнимой оптимизации.
Как начать
- Добавить @StasBaikalBot в групповой чат.
- Сделать его администратором с правом закреплять сообщения. Без этого права он не сможет держать сводку в закрепе.
- Отправить
/start_tracking. Бот создаст и закрепит таблицу, которая дальше будет обновляться сама.
Дальше просто пишете расходы в формате сумма описание, а личные долги через /owe реплаем. Есть ещё /ping, чтобы проверить, что бот жив, и /start с краткой инструкцией.
Чего тут осознанно нет
Это бот для своих, а не сервис на тысячу чатов, и многое в нём сделано «достаточно», а не «правильно».
Валюта зашита турецкими лирами прямо в текст ответов, потому что писался он под конкретную поездку. Нет ни мультивалютности, ни курсов, ни выбора языка. Сводка обрезается, если перевалит за лимит Telegram в 4096 символов, — для отпуска на неделю столько расходов не набирается, так что я не стал городить разбивку на несколько сообщений. Нет экспорта в таблицу, нет графиков, нет напоминаний.
Зато он делает ровно одно дело и делает его без нервотрёпки: к концу поездки в закрепе висит честный список, кто кому сколько должен, и вечер с калькулятором отменяется.
Код целиком тут, если захотите поковыряться или поднять свою копию: