Бот продаёт подписки на VPN: пользователь выбирает тариф, платит (Telegram Stars, СБП переводом по номеру, карта через ЮKassa или криптовалюта через CryptoBot) и сразу получает ключ. Ключи создаются и продлеваются в вашей VPN-панели автоматически, просроченные доступы отключаются по расписанию.
Стек: Python 3.10+, aiogram 3, SQLAlchemy 2 (SQLite или PostgreSQL), APScheduler, aiohttp.
Для пользователя
- Главное меню: доступ, покупка, профиль, пробный период, инструкция, поддержка
- Пробный период (по умолчанию 3 дня / 10 ГБ), один раз на аккаунт; опционально — только после подписки на канал
- Оплата: Telegram Stars, СБП переводом по номеру, карта (ЮKassa), криптовалюта (CryptoBot)
- Покупка и продление тарифа; продление добавляется к остатку, а не обнуляет его
- Всё для подключения приходит само — сразу после оплаты, продления и выдачи триала: QR-код ссылки-подписки, сама ссылка, срок, остаток трафика и пошаговая инструкция одним сообщением; отдельные ключи
vless://…— по кнопке - Продление (
/renew): витрина показывает текущую дату окончания и во что она превратится, дни прибавляются к остатку - Напоминания за 3 дня и за сутки до окончания, уведомление об истечении
Для администратора (/admin, доступ по ADMIN_IDS)
- Редактор тарифов (
/plans): цена, название, срок, трафик, число устройств, порядок; создание, скрытие и удаление — без перезапуска - Редактор настроек (
/settings): триал, лимит трафика по умолчанию, курс звёзд, ссылки на поддержку и канал, реквизиты СБП — меняются на лету и хранятся в БД - Статистика: пользователи, активные подписки, выручка за сутки/30 дней/всё время
- Список всех пользователей (
/users): постраничный, с фильтрами «все / с подпиской / без» и выгрузкой в CSV; клиент открывается нажатием - Карточка клиента (
/user 123456789или/user @username, поиск в/admin): подписка, трафик, платежи — и правка в пару нажатий: прибавить/убавить дни, точная дата окончания, лимит трафика, сброс трафика, приостановка доступа, бан, повторный триал, подтверждение висящих счетов СБП, сообщение клиенту. Всё пишется сначала в VPN-панель, потом в базу — правки не откатываются при сверке - Выдача дней вручную (
/give 123456789 30), бан/разбан (/ban 123456789) - Заявки по СБП:
/claimsили кнопка «🏦 Заявки СБП» — список открытых счетов с подтверждением - Рассылка по всем пользователям
- Проверка связи с VPN-панелью, возврат Telegram Stars (
/refund user_id charge_id)
Под капотом
- Подтверждение платежа идемпотентно: повторный вебхук, кнопка «Я оплатил» и фоновый опрос не выдадут подписку дважды
- Вебхуки платёжек перед выдачей подписки перепроверяются запросом в API платёжной системы — подделанный колбэк ничего не активирует
- Панель — источник правды: срок, трафик и включённость сверяются с ней раз в час и при каждом открытии «Мой доступ», поэтому ручные правки в панели бот видит
- Если панель недоступна, оплата не теряется: платёж сохранён, пользователь получает понятное сообщение
- Фоновые задачи: напоминания, отключение просрочки, синхронизация трафика, добивка неподтверждённых платежей
python -m venv .venv && .venv/Scripts/activate # Linux/macOS: source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .envЗаполните в .env минимум два параметра:
BOT_TOKEN=токен от @BotFather
ADMIN_IDS=ваш_telegram_id # узнать можно командой /id в боте
Запуск:
python -m bot.mainКороткий вариант python -m bot делает то же самое.
С VPN_PROVIDER=mock бот работает без реального сервера и выдаёт демонстрационные ключи —
удобно, чтобы прокликать все сценарии. Проверить логику, не открывая Telegram:
python scripts/smoke_test.py # БД, оплата, продление, истечение
python scripts/dialog_test.py # реальные апдейты Telegram через Dispatcher с подставным Bot
python scripts/renew_test.py # продление, сброс трафика, СБП из «Мой доступ»
python scripts/admin_test.py # редакторы тарифов и настроек
python scripts/safety_test.py # гонки и защитные проверкиСкрипты работают на отдельных БД (data/smoke.db, data/dialog.db и т. д.) и ничего не
отправляют наружу — их можно запускать на боевом сервере.
VPN_PROVIDER=marzban
MARZBAN_URL=https://panel.example.com
MARZBAN_USERNAME=admin
MARZBAN_PASSWORD=пароль
MARZBAN_PROXIES=vless # можно vless,vmess,trojan
MARZBAN_INBOUNDS= # пусто = все inbound'ы панели
MARZBAN_SUB_DOMAIN= # если ссылка-подписка отдаётся с другого домена
Бот создаёт пользователей с именем tg<telegram_id> через /api/user, продлевает их при
оплате и переводит в статус disabled при истечении срока.
VPN_PROVIDER=xui
XUI_URL=https://panel.example.com:2053/секретный_путь # без /panel в конце
XUI_USERNAME=admin
XUI_PASSWORD=пароль
XUI_INBOUND_IDS= # пусто = все inbound'ы панели
XUI_SUB_BASE=https://panel.example.com:2096/sub # сервис подписок
Клиент заводится с email = tg<telegram_id> сразу во всех inbound'ах (или в тех, что
перечислены в XUI_INBOUND_IDS) и получает единый subId. Благодаря этому ссылка-подписка
отдаёт пользователю все протоколы разом — vless, hysteria2 и что там ещё настроено, а при
добавлении нового сервера в панель ссылка у клиентов обновится сама.
При продлении идентификаторы не меняются: бот подхватывает уже выданные uuid, пароль и
subId, так что ранее розданные ключи продолжают работать.
Клиенты заводятся через обновление inbound целиком (/panel/api/inbounds/update/{id}):
бот читает inbound, подставляет запись клиента и записывает обратно. Этот маршрут есть и в
классических сборках, и в свежих, где отдельных ручек addClient уже нет.
Свежие сборки панели защищены CSRF: бот забирает токен из <meta name="csrf-token"> страницы
входа и шлёт его заголовком X-CSRF-Token. Без этого панель отвечает пустым 403.
Диагностика — покажет inbound'ы, их ID и проверит выдачу ключа:
python scripts/panel_check.py # логин + список inbound'ов
python scripts/panel_check.py --full # плюс создание и удаление тестового клиентаПроверить связь с панелью:
/admin→ «🩺 Проверить панель».
Бот не живёт «своей» датой окончания. Перед выдачей и продлением он читает клиента из панели и отсчитывает срок от того, что там реально стоит; раз в час и при каждом открытии «🔑 Мой доступ» сверяет трафик, срок и включённость. Практические следствия:
- продлили клиента руками в панели — бот покажет новую дату и продлит уже от неё;
- выключили клиента в панели — пользователь увидит «Доступ приостановлен», а не «активна»;
- удалили клиента из панели — подписка в боте помечается истёкшей.
Рекомендуемый клиент — Happ: есть под iOS, Android, Windows, macOS, Linux и TV, понимает ссылку-подписку и поддерживает маршрутизацию (российские сайты напрямую, остальное через VPN).
Маршрутизацию раздаёт панель, а не бот: Happ принимает профиль правил через deep-link
happ://routing/add/{base64} либо прямо из ответа подписки — заголовком routing: или
строкой в теле. Настраивается это на стороне 3x-ui, боту делать ничего не нужно: он отдаёт
ссылку-подписку, а всё остальное приезжает вместе с ней.
Работает сразу, без договоров и подключения платёжек: Telegram сам проводит оплату.
Цена тарифа хранится в рублях и пересчитывается в звёзды по курсу STARS_RUB_RATE
(сколько рублей в одной звезде — уточните актуальный курс вывода в @BotFather).
Возврат звёзд: /refund <user_id> <charge_id> (charge_id хранится в поле external_id платежа).
PAY_SBP_ENABLED=true
SBP_PHONE=+7 900 000-00-00
SBP_BANK=Т-Банк
SBP_RECEIVER=Иван И.
ADMIN_IDS=ваш_telegram_id
Самый простой способ, когда клиентов немного: бот показывает ваш номер для перевода по СБП,
сумму и код платежа (VPN-<номер счёта>). Клиент переводит и жмёт «✅ Я перевёл» — вам
в личку прилетает заявка с кнопками «Деньги пришли» / «Отклонить». По нажатию подписка
выдаётся автоматически, клиент получает ключ; при отказе он видит код платежа и предложение
написать в поддержку.
Заявку может подать только владелец счёта, а подтвердить — только ADMIN_IDS.
Повторное подтверждение ничего не удваивает. Если заявка не дошла в личку (админ не нажимал
/start в боте или в ADMIN_IDS неверный ID), счёт не теряется — он виден по команде
/claims и подтверждается оттуда теми же кнопками.
Ограничения, о которых стоит знать: подтверждение ручное (нужно смотреть в банк), поступления на карту физлица регулярными платежами — зона вашей ответственности перед налоговой, а при росте до сотни клиентов это станет утомительным — тогда переходите на ЮKassa.
PAY_YOOKASSA_ENABLED=true
YOOKASSA_SHOP_ID=...
YOOKASSA_SECRET_KEY=...
YOOKASSA_RETURN_URL=https://t.me/ваш_бот
Нужен подтверждённый магазин в ЮKassa (юрлицо, ИП или самозанятый). СБП там доступен как
способ оплаты «из коробки»: включите его в личном кабинете ЮKassa — на странице оплаты
появится QR-код, никаких правок в боте не потребуется. Бот создаёт платёж
через API v3 и подтверждает его двумя путями: вебхук payment.succeeded и фоновый опрос
каждые 3 минуты — то есть даже без вебхука подписка выдастся сама.
54-ФЗ: если вы обязаны выдавать чеки, добавьте объект
receiptв тело запроса вbot/services/payments/yookassa.py:create_invoice(или включите чеки на стороне ЮKassa).
PAY_CRYPTOBOT_ENABLED=true
CRYPTOBOT_TOKEN=... # получить у @CryptoBot -> Crypto Pay -> Create App
CRYPTOBOT_ASSET=USDT
USD_RUB_RATE=95 # курс для пересчёта цены из рублей
Без вебхуков оплаты подтверждаются опросом (до 3 минут задержки). Чтобы получать подтверждение мгновенно:
WEBHOOK_ENABLED=true
WEBHOOK_PORT=8080
WEBHOOK_BASE_URL=https://bot.example.com
WEBHOOK_SECRET=длинная_случайная_строка
Адреса, которые нужно указать в панелях платёжек (они печатаются в лог при старте):
- ЮKassa:
https://bot.example.com/pay/yookassa/<WEBHOOK_SECRET> - CryptoBot:
https://bot.example.com/pay/cryptobot/<WEBHOOK_SECRET>
Поставьте перед ботом nginx с TLS — платёжки принимают только HTTPS.
По умолчанию это файл SQLite data/vpnbot.db. Подключаться можно, не останавливая бота
(включён режим WAL — параллельные чтение и запись разрешены).
Встроенная консоль — сама находит файл по DATABASE_URL:
python scripts/db.py --tables # таблицы и число строк
python scripts/db.py "SELECT id, title, price_rub FROM plans"
python scripts/db.py # интерактивный SQLПолезные запросы:
-- кто и до когда подписан
SELECT u.id, u.username, s.expires_at, s.status FROM subscriptions s JOIN users u ON u.id = s.user_id;
-- открытые счета по СБП (их же показывает /claims)
SELECT id, user_id, amount, created_at FROM payments WHERE provider='sbp' AND status='pending';
-- выручка по месяцам
SELECT substr(paid_at,1,7) AS month, sum(amount) FROM payments WHERE status='paid' GROUP BY month;Альтернативы: GUI DB Browser for SQLite (открыть data/vpnbot.db),
любой клиент с поддержкой SQLite или консольный sqlite3.
Клиентов правьте в карточке (
/user <id>), а не SQL-запросами: срок, трафик и включённость бот при каждой сверке берёт из VPN-панели, поэтому правкаsubscriptions«только в базе» откатится в течение часа. Карточка пишет сначала в панель, потом в базу.
Всё это правится прямо в боте: /admin → «💼 Тарифы» и «⚙️ Настройки». Изменения
применяются сразу — пользователи видят новую цену со следующего нажатия, перезапуск не нужен.
Тарифы (/plans) — цена, название, срок, лимит трафика, число устройств, порядок
в списке. Тариф можно скрыть кнопкой «Выключить», создать новый строкой вида Неделя | 7 | 59
или удалить. Тариф, по которому уже были платежи, удалить нельзя — иначе развалится
статистика; такой скрывается.
Настройки (/settings) — пробный период и его длительность, трафик триала,
лимит трафика по умолчанию, курс рублей к звезде,
ссылки на поддержку и канал, реквизиты СБП. Хранятся в таблице settings и переживают перезапуск; значение из .env
работает как значение по умолчанию, пока настройку не поменяли в боте.
Задаются в /settings («Ссылка на поддержку», «Ссылка на канал») или в .env
(SUPPORT_URL, CHANNEL_URL). Значения-заглушки из шаблона (https://t.me/your_support
и подобные) бот распознаёт и кнопку не показывает — лучше без кнопки, чем ведущая в никуда.
Чтобы убрать уже заданную ссылку, отправьте в редакторе -.
0в любом месте означает безлимит;- у каждого тарифа свой лимит (поле «📊 Трафик» в карточке) — он и уезжает в панель как
totalGB; DEFAULT_TRAFFIC_GB(«Лимит трафика по умолчанию») применяется к новым тарифам, созданным через админку, и к ручной выдаче дней командой/give;- у пробного периода лимит отдельный —
TRIAL_TRAFFIC_GB; - израсходованное подтягивается из панели раз в час и видно пользователю в «🔑 Мой доступ»;
- при оплате (покупка или продление) израсходованный трафик обнуляется в панели — новый период
начинается с полного лимита; ручная выдача дней
/giveрасход не трогает.
В .env остаётся то, что менять на лету небезопасно: токен бота, ADMIN_IDS, доступы
к VPN-панели, ключи платёжных систем, включение способов оплаты. После их правки —
перезапуск бота.
Подойдёт любой VPS с Ubuntu 22.04/24.04 или Debian 12 — боту хватает 1 vCPU и 512 МБ RAM. Его можно поставить на тот же сервер, где крутится 3x-ui: нагрузки он почти не создаёт. Открытые порты не нужны — бот работает на long polling и сам ходит в Telegram.
Через git (удобно для обновлений):
ssh root@ВАШ_СЕРВЕР
apt update && apt install -y git
git clone ВАШ_РЕПОЗИТОРИЙ /root/vpn && cd /root/vpnИли напрямую с Windows — в PowerShell из папки проекта (.env копируется отдельно,
чтобы секреты не попали в репозиторий):
scp -r bot scripts deploy requirements.txt .env.example root@ВАШ_СЕРВЕР:/root/vpn/
scp .env root@ВАШ_СЕРВЕР:/root/vpn/.envsudo bash deploy/install.shСкрипт создаёт системного пользователя vpnbot, разворачивает проект в /opt/vpn-bot,
ставит зависимости в venv, регистрирует сервис и запускает его. Повторный запуск обновляет
код и перезапускает бота, не трогая .env и базу.
sudo nano /opt/vpn-bot/.env
sudo systemctl restart vpn-bot
journalctl -u vpn-bot -fВ логе при старте должно быть Бот @имя запущен, а в Telegram придёт «🤖 Бот запущен».
Если админ не получил сообщение — в логе будет строка с причиной.
Если бот стоит на одном сервере с 3x-ui, укажите панель по локальному адресу:
XUI_URL=http://127.0.0.1:2053/секретный_путь. Так запросы не выходят наружу и не зависят от сертификата.
cd /root/vpn
cp .env.example .env && nano .env # если .env ещё не скопирован
docker compose up -d --build
docker compose logs -fБаза лежит в ./data на хосте, поэтому переживает пересборку образа.
| Задача | systemd | Docker |
|---|---|---|
| Логи | journalctl -u vpn-bot -f |
docker compose logs -f |
| Перезапуск | systemctl restart vpn-bot |
docker compose restart |
| Остановить | systemctl stop vpn-bot |
docker compose down |
| Обновить код | залить файлы, sudo bash deploy/install.sh |
git pull && docker compose up -d --build |
| База | sudo -u vpnbot /opt/vpn-bot/.venv/bin/python /opt/vpn-bot/scripts/db.py --tables |
docker compose exec bot python scripts/db.py --tables |
Резервные копии базы (горячий бэкап, бота останавливать не нужно):
sudo bash /opt/vpn-bot/deploy/backup.sh
sudo crontab -e # 0 4 * * * bash /opt/vpn-bot/deploy/backup.shКопии складываются в /opt/vpn-bot/backups, хранятся 14 дней. Файл data/vpnbot.db —
это все ваши пользователи, подписки и платежи, так что бэкап стоит настроить сразу.
BOT_TOKENиADMIN_IDSзаполнены реальными значениями, админ нажал/startVPN_PROVIDERнеmock, «🩺 Проверить панель» в/adminотвечает «панель отвечает»- Куплен ключ самому себе через тестовый тариф — доступ пришёл и подключается
- Настроен бэкап базы
bot/
main.py точка входа, сборка Dispatcher, запуск планировщика и вебхуков
config.py все настройки из .env
tasks.py фоновые задачи (напоминания, отключение, опрос платежей)
webserver.py aiohttp-сервер для вебхуков платёжных систем
db/ модели, сессия, запросы
handlers/ admin, admin_users (карточка клиента), admin_config, common,
access, plans, payments, profile, trial
keyboards/ inline- и reply-клавиатуры, callback-данные
middlewares/ сессия БД, регистрация пользователя, антифлуд
services/
subscriptions.py выдача/продление/отключение — единая точка правды
vpn/ панели: base, marzban, xui, mock + фабрика
payments/ провайдеры: yookassa, cryptobot + реестр
texts/ru.py все пользовательские тексты
utils/ рендер сообщений, логи, проверка подписки на канал
scripts/db.py консоль к базе бота
scripts/panel_check.py диагностика VPN-панели
scripts/renew_test.py продление подписки и лимиты трафика
scripts/admin_test.py проверка админки: редактор тарифов и настроек
scripts/smoke_test.py сквозная проверка логики без Telegram
scripts/safety_test.py гонки и защитные проверки: двойные нажатия, просрочка, бан, ошибки
scripts/migrate_legacy.py чистка базы от столбцов рефералки и промокодов
Добавить свою панель или платёжку — значит написать один класс: VpnPanel
(bot/services/vpn/base.py) или PaymentProvider (bot/services/payments/base.py)
и зарегистрировать его в фабрике.
- Хранить
.envтолько на сервере, праваchmod 600; токены в репозиторий не коммитить. - Перейти на PostgreSQL, если ожидается больше нескольких тысяч пользователей
(
DATABASE_URL=postgresql+asyncpg://…), и настроить бэкап БД. - FSM-состояния хранятся в памяти: при рестарте недозаполненные диалоги сбрасываются. Для нескольких реплик подключите Redis-хранилище aiogram.
- Проверить юридическую сторону приёма платежей (статус самозанятого/ИП, чеки, оферта,
возвраты) — текст условий лежит в
/termsвbot/handlers/common.py.