The problem
Telegram Business lets a bot answer on behalf of a personal account. Almost every bot library
still treats this as an afterthought: business messages arrive as a different update type,
carry a business_connection_id, and must be answered through it. Reply the
ordinary way and the message goes out from the bot instead of from you — visible,
wrong, and exactly what you were trying to avoid.
I wanted an assistant that handles the first round of routine messages, keeps my tasks, and moderates a couple of groups — without a framework between me and the API.
Constraints and tradeoffs
Bare Java, no Spring, no bot library
The Telegram types are modelled by hand and parsed with Jackson; HTTP calls live in one
TelegramClient. The cost is real: adding a field means editing both the DTO and
the parsing. The benefit is that the part the libraries handle worst — Business API
semantics — is the part I control completely.
Two model providers, one chain
Gemini 2.5 Flash-Lite answers by default; on an error or a spent quota the request falls through to Groq running Llama 3.3 70B. A free tier that runs out on a Tuesday afternoon is not an exception — it is the normal operating condition, so the fallback is part of the design rather than an error handler.
Memory that stays small
Conversation context lives in SQLite next to the jar, trimmed per chat. Sending an entire history on every turn costs tokens and makes replies worse, not better — a short window of recent turns plus a fixed instruction beats a long transcript.
What I traded away
- No webhooks. Longpolling needs no public address and no certificate. For one account this is strictly simpler.
- No autonomy. The assistant drafts and answers routine things; anything with a commitment in it is left for me. An assistant that agrees to meetings on your behalf is a liability, not a feature.
- No RAG, no embeddings. The useful context here is the last few messages, and it already fits in the prompt.
Architecture
Screens and states
Business reply. Sent through business_connection_id, so it appears as my own message.
Tasks. Plain commands, stored locally. No account, no sync service.
Moderation. Stop words, flood limits, mute and ban — rules live in SQLite.
Fallback. The provider switch is logged and invisible to whoever is chatting.
Results and lessons
What I would tell myself at the start
- Read the Business API docs before writing the handler, not after. The difference between a business update and an ordinary message is the entire feature, and it is invisible until you see your bot's name on a message meant to come from you.
- Design for the quota, not the happy path. Free tiers end mid-conversation. Treating that as normal made the fallback a first-class path instead of a rescue attempt.
- Hand-written DTOs are fine until they are not. Six types is pleasant. Forty would not be — and I would want a generator long before that.