Tech Poly VPN: VPN subscription billing in Telegram
our own product: SBP payments, key issuing in Marzban, a backup VKontakte bot
The task
A VPN is sold by subscription, and every step easily turns into manual work: issue a trial key, accept payment, check the receipt, extend the term in the panel, remind about expiry, work out the discount. A mistake at any step means either a customer without access or access without payment.
Companies add a separate pain. A director pays for employees centrally, the employees do not want to create Telegram accounts, and accounting needs a reconciliation statement based on actual charges at the end of the month. Working that out by hand in a spreadsheet is slow and error-prone.
The goal is one service that takes a customer from first contact to renewal and produces the corporate statement. In this setup the administrator only confirms the receipt.
The solution
Tech Poly VPN (the polybot bot) is the studio's own product. It is an asynchronous Telegram bot that sells subscriptions, issues VLESS keys through the Marzban panel and keeps track of money and terms itself. What is implemented:
- sign-up with an invite code or a corporate promo code, and trial access for a few hours (12 by default);
- SBP payment requests with manual receipt review by an administrator and an atomic confirmation that cannot credit twice;
- creating and renewing a user in Marzban, with the request rolled back to pending if the panel does not respond;
- prices per group, two types of promo codes (percent and days), a referral discount with a cap;
- a corporate track: groups, a reseller role, employee keys not tied to Telegram, a charge ledger and reconciliation statements;
- expiry notifications with per-user quiet hours;
- a backup channel in VKontakte that issues configs when Telegram is unavailable;
- route monitoring, daily backups, broadcasts, an admin panel, statistics and an action log;
- a hidden
/versioncommand available to administrators only.
How it works
Marzban API: the bot as the source of truth for money
Marzban is a VPN node control panel: it creates users and serves traffic. The bot drives it through a REST API and keeps money, terms, discounts and reports itself. The subscription expiry is stored in two places: expire_date in the bot's database and expire in Marzban, which is what actually cuts off traffic. Writes always go one way: Marzban returns the actual date first, and only that date is saved to the database. So the bot never treats a subscription as active when the traffic is already shut off. In the user's note field in the panel the bot writes @username, so the administrator sees people rather than anonymous identifiers.
Idempotent payment: a row lock on confirmation
Idempotency means that repeating an operation does not change the result. It matters here because two administrators clicking “Confirm” at the same moment must not credit the payment twice. The bot opens a transaction, reads the payment with SELECT ... FOR UPDATE and checks its status under that lock. If the status is no longer “pending”, the transaction is rolled back and the administrator sees who processed the request and when. The race is excluded at the database level, not by a convention in the code.
Rollback on a panel failure, discount after issuing
If a payment is already marked as paid but Marzban did not respond, a dedicated function returns the request to pending. The discount is spent only after the panel has answered successfully, so a failed payment does not burn the accumulated discount. The price is fixed in the ledger at the date of the operation: changing a group's tariff neither rewrites history nor breaks statements that have already been issued.
Corporate groups and reconciliation statements
The reseller role lives in a separate table rather than in a user field: a client's director may not use the service at all. The reseller issues keys to employees with a label such as “Accounting, PC-2”, and no registration in the bot is needed. The charge ledger is append-only, so records are never rewritten. The bot uses it to total a period and close it, and sends statements automatically on the 1st. The manual option is the /act command.
Backup bot in VKontakte
In the extended stack a VKontakte bot runs next to the Telegram bot using LongPoll (receiving messages without a public callback address). The account is linked with a one-time code that expires, used trials are tracked, and access can be restored from an old link. Both bots share the Marzban client and retry-policy modules. The client receives a subscription link rather than a raw key, and the admin panel shows a QR code for the user.
Operations: monitoring, backups, request retries
APScheduler jobs run on Moscow time. Monitoring checks the node's TCP port every 5 minutes and alerts the administrator only after several failed checks in a row, so a single network blip does not wake anyone. A JSON backup is created daily, kept with rotation and can be uploaded to Google Drive. In the extended stack, requests to the database, Marzban, Telegram and VKontakte are retried with a growing delay, and containers shut down gracefully.
Results
- Trial keys, purchases and renewals need no manual work except confirming the receipt.
- A second administrator confirming the same payment does not credit it twice.
- A panel failure does not leave a payment without a key: the request goes back to the queue.
- A company gets keys for employees without Telegram accounts and a monthly reconciliation statement built from the charge ledger.
- A customer who cannot reach Telegram gets a config through the backup VKontakte channel.
- The repository has no automated tests; acceptance is manual.
Technologies and why
- Python and aiogram 3 — the asynchronous Telegram bot: menus, payments, admin panel.
- MariaDB (InnoDB) — subscriptions, payments, promo codes and the charge ledger; transactions and row locks protect against double crediting.
- Marzban API — creating, renewing and subscribing VLESS users; the panel and nodes themselves are deployed separately.
- APScheduler — expiry checks, monitoring, backups and reconciliation statements.
- VK API — the backup config-delivery channel.
- Google Drive (PyDrive2) — optional storage for backups.
- Docker — the bot image with a fixed time zone; the extended stack uses a shared docker-compose file.
Status
Version 1.0.1 of 30 September 2026: a cleaned-up bot codebase that prints its version to the log on every start. The bot runs in production as our own product. The extended stack with the VKontakte bot and a shared docker-compose file is kept in a separate repository, where polybot 3.5.0 and vkbot 1.2.0 are listed. The README's list of remaining work includes automated tests, an environment template for the VKontakte bot, moving payment details out of the code into environment variables, and a dedicated schema migration tool.
Questions about this project
How can I sell VPN subscriptions through a Telegram bot with SBP payments?
Can a payment be credited twice if two admins click at the same moment?
What happens if the Marzban panel does not respond after a payment is confirmed?
How do I set up a corporate VPN for employees who do not use Telegram?
What if Telegram is unavailable?
Where is the data stored and what happens when something fails?
More in this area
Finance Assistant: PDF bank statements to a Telegram budget
our own product: a bot and web dashboard, four banks' statements, Gemini
Telegram ticketing system for an IT department
for a museum complex in Moscow: tickets, asset tracking, knowledge base and analytics
Branding e-policies (OSAGO) with a Telegram bot
for accident commissioners: contacts on the form, data and QR code untouched
Need something similar?
Tell us about the task — we'll show how we solved it and estimate the scope.