Multi-tenant Telegram SaaS bot that turns curated X (Twitter) and VK feeds into automatic Russian-language Telegram channels. Built as a solo-founder MVP demonstrating end-to-end SaaS architecture: payments, admin panel, AI-powered onboarding, and autonomous content workers.
Live bot: @TwidgestBot
A user opens the bot, picks a topic (from 15 ready templates or describes their own), and gets a fully-automated Telegram news channel. The bot:
- Fetches posts from curated X (Twitter) accounts via twitterapi.io and VK communities via VK API
- Filters by engagement thresholds (likes, retweets) — configurable per channel via
/setthreshold - Runs each candidate through an LLM with niche-specific prompts and a Russian safety filter (no drugs, no dosages, no legally risky content)
- Ranks top candidates with a cheap LLM reviewer (interest score + junk verdicts) before posting — the most substantive tweet wins, not just the most liked one; on any LLM failure it fails open to plain engagement order
- Publishes either individual posts or periodic digests into the user's Telegram channel
- When a channel goes quiet, an AI source scout suggests new X authors — pre-validated against the channel's own thresholds on their real recent tweets, and applied only on the owner's approval (human-in-the-loop)
All multi-tenant: one bot process serves many users, each with their own sources, channels, schedules, and subscription tier.
- Language: Python 3.12, async/await throughout
- Bot framework: aiogram 3.x
- Database: SQLite + SQLAlchemy 2.0 (async)
- Scheduler: APScheduler (in-process)
- LLM: OpenRouter — Llama 3.3 70B (default) and Claude Sonnet 4 (Pro tier)
- Sources: twitterapi.io + VK API, both with in-memory TTL cache
- Payments: Telegram Stars (XTR) + RUB cards via Telegram Payments provider (
PAYMENT_PROVIDER_TOKENfrom BotFather, e.g. YooKassa;PRICE_RUB) - Deployment: systemd service on a single VPS; SQLite in WAL mode (multi-process safe), nightly
.backupsnapshots with 14-day rotation via systemd timer
One process runs both the aiogram Telegram dispatcher (handling user commands in real time) and an APScheduler (running background workers). Everything shares a single async SQLAlchemy session factory.
flowchart TB
subgraph Process["TwidgestBot process"]
direction TB
Dispatcher["aiogram Dispatcher<br/>(Telegram polling)"]
Scheduler["APScheduler<br/>(background jobs)"]
subgraph Jobs[" "]
direction LR
Collector["collector<br/>every 30m"]
Publisher["publisher<br/>every 1h"]
ViralPicker["viral_picker<br/>every 1h"]
ChannelHealth["channel_health<br/>every 1h"]
Expiry["expiry-check<br/>every 24h"]
Cleanup["queue_cleanup<br/>every 24h"]
end
subgraph Handlers["Handlers"]
direction TB
H1["start / help / me"]
H2["channels / templates"]
H3["createchannel"]
H4["admin"]
H5["billing (Stars)"]
H6["forward (auto-bind)"]
H7["sources / targets"]
H8["scout (AI source discovery)"]
end
subgraph Workers["Workers"]
direction TB
W1["fetch Twitter + VK"]
W2["filter engagement"]
W3["LLM rewrite or enqueue digest"]
W4["post / publish"]
end
DB[("SQLAlchemy ORM<br/>async SQLite")]
Dispatcher --> Handlers
Scheduler --> Jobs
Jobs --> Workers
Handlers --> DB
Workers --> DB
end
style Process fill:transparent,stroke:#888
style Jobs fill:transparent,stroke:none
Multi-tenant from day one. The data model is User -> Channel -> ChannelSource, not a single global config. A user can run multiple channels on different topics, each with its own sources, filters, niche prompts.
Dual source support: Twitter + VK. Each ChannelSource has a source_type field (twitter or vk). Twitter sources use @username, VK sources use vk:domain format. Both are fetched in parallel each collector cycle. VK posts are already in Russian — a separate build_vk_prompt() skips translation and focuses on editing/shortening.
Shared source cache. If 50 users monitor the same X account or VK community, the API is called once per cycle, not 50 times. In-memory TTL cache with per-source async locks prevents thundering herd on cold start.
Three-tier content filter. Each channel has its own filter_preset:
- 🎯
strict— facts and events only, high bar (legacy alias:news) - 📡
loose— news, reactions, community posts (legacy aliases:community,entertainment) - ⚡
unfiltered— publishes everything except legally risky content; engagement thresholds auto-zeroed; user opt-in with ToS disclaimer; digest published without LLM filtering
Admins can override any channel's filter via /admin setfilter CHANNEL_ID PRESET.
Configurable engagement thresholds. Each channel has min_likes and min_retweets settings, configurable via /setthreshold. For unfiltered channels, thresholds are automatically ignored. Admins can override via /admin setthreshold.
Billing via Telegram sendInvoice. Two rails: Telegram Stars (XTR, works globally, no provider needed) and RUB card payments through a Telegram Payments provider token from BotFather (YooKassa/Robokassa; set PAYMENT_PROVIDER_TOKEN + PRICE_RUB in .env — without them the bot is Stars-only).
Free trial model. New users get 30 days of full Free-tier access. After expiry, posting stops and users are prompted to upgrade to Pro (2999 ⭐/month).
Safety-first content filter. The LLM prompt explicitly rejects content that would be problematic under Russian law (military critique, drug references, specific medication dosages). Caught before posting, not after. The unfiltered preset bypasses quality filtering but retains all safety rules.
Template + AI hybrid onboarding. 15 curated templates for popular niches (AI, crypto, longevity, F1, NBA, etc.) give instant-start UX. If the topic isn't in the catalog, /createchannel ai <description> asks the LLM to suggest 12 relevant X accounts with one-line explanations for each.
Separate windows for single and digest. Hybrid channels publish two types of content: individual fresh tweets (single posts, 24-hour freshness window) and periodic digests (14-hour window, "best of period"). Single posts mark tweets with posted_at_single instead of removing them — so digests can include both fresh tweets and already-published singles in a unified "overview". A daily cleanup job removes queue entries older than 7 days to keep disk usage stable.
| Tier | Price | Sources | Channels | Posts/day |
|---|---|---|---|---|
| Free (trial) | Free, 30 days | 10 | 3 | 50 |
| Pro | 2999 ⭐/month | 10 | 3 | 50 + Claude LLM |
twidgest-bot/
├── main.py Entry point: dispatcher + scheduler
├── config.py Env-based config (incl. VK_ACCESS_TOKEN)
├── tiers.py Pricing tiers (source of truth)
├── templates.py 15 built-in channel templates
├── prompts.py LLM prompts + filter mode definitions
├── engagement_defaults.py Per-niche engagement defaults
│
├── bot/handlers/
│ ├── start.py /start, /help, /me
│ ├── channels.py /channels, /createchannel, /templates
│ ├── sources.py /sources, /addsource, /removesource,
│ │ /setfilter, /setthreshold, /filters
│ ├── targets.py Target channel management
│ ├── forward.py Auto-bind channel on forwarded message
│ ├── admin.py /admin grant|stats|user|broadcast|
│ │ setfilter|setthreshold|addsource|removesource
│ └── billing.py /upgrade, Stars payment flow
│
├── core/
│ ├── twitter_client.py twitterapi.io wrapper
│ ├── twitter_cache.py Shared TTL cache across users
│ ├── vk_client.py VK API client (wall.get, groups.search)
│ ├── llm_client.py OpenRouter with retry/backoff
│ ├── safe_sender.py Telegram send with auto-deactivation
│ ├── image_picker.py Unsplash image fetcher
│ └── topic_dedup.py Duplicate detection
│
├── db/
│ ├── models.py SQLAlchemy models
│ ├── session.py Async engine setup
│ └── repositories/
│ ├── users.py incl. 30-day trial logic
│ ├── channels.py
│ ├── tweets.py
│ ├── billing.py
│ └── admin.py
│
└── workers/
├── collector.py Fetch Twitter+VK, filter, post/enqueue
├── publisher.py Digest builder (14h window, includes single posts)
├── viral_picker.py Single-style posts for hybrid mode (24h window)
├── channel_health.py Channel health checks
├── expiry_check.py Daily tier downgrade + T-1 trial notification
└── queue_cleanup.py Daily TTL cleanup of digest_queue (7-day retention)
git clone https://github.com/kelbic/twidgest-bot.git
cd twidgest-bot
cp .env.example .env
# Fill in: TELEGRAM_BOT_TOKEN, TWITTER_API_KEY, OPENROUTER_API_KEY,
# ADMIN_USER_ID, VK_ACCESS_TOKEN (optional)
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
python main.pyCopy deploy/twidgest-bot.service to /etc/systemd/system/ and run:
systemctl daemon-reload
systemctl enable --now twidgest-bot
journalctl -u twidgest-bot -f- Shipping beats perfection. The MVP reached production at roughly 3000 LOC across one focused weekend. Every architectural "nice to have" was deferred until a real user asked for it.
- LLM as a quality filter, not a worker. The LLM doesn't do the work — it evaluates whether content deserves to exist in the channel. The hard engineering is everywhere else (dedup, rate-limits, quota enforcement, failure recovery).
- Dual-source architecture pays off early. Adding VK alongside Twitter required touching 7 files but zero breaking changes — because
ChannelSourcewas already abstracted from the start with asource_typefield. - Telegram Stars beats Stripe for certain audiences. Native payment flow, no PCI compliance, no KYC for users, works globally out of the box.
- Multi-tenant beats single-user from day one. Refactoring a "my-personal-bot" into multi-tenant later is brutal. Start with
User -> Resourceeven if you're the only user at launch. - Keep a session document. When working with AI assistants across multiple sessions, a
VK_INTEGRATION.mdplan file saved hours — the next session picked up exactly where the last one left off.
MIT — see LICENSE.
@kelbic. Feedback and pull requests welcome.