Pahlavon Numonjonov Case study
tgagent
← All work

tgagent

A Telegram Business assistant on bare Java — answers private messages as me, keeps tasks, moderates groups. Gemini as the model, Groq as the fallback.

Role
Solo — design, build, run
Timeline
2026 — present
Stack
Java · SQLite · Gemini · Groq
Status
Running

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

Telegram longpolling UpdateRouter three branches BusinessMessage answers as me CommandHandler GroupModeration Gemini primary Groq / Llama fallback SQLite memory, tasks, moderation rules
One router, three branches. The dashed arrow is the fallback that fires on quota or error.

Screens and states

them: are you around tomorrow? me : after 15:00 — want me to put it in the calendar? [sent as me, not as a bot]

Business reply. Sent through business_connection_id, so it appears as my own message.

/task add ship uz-status v2 ✓ #14 added /task list #14 ship uz-status v2 open #13 renew certificate done

Tasks. Plain commands, stored locally. No account, no sync service.

[group] flood: 7 msgs / 4s → user muted 10m → reason logged

Moderation. Stop words, flood limits, mute and ban — rules live in SQLite.

gemini: 429 quota exceeded → falling back to groq → replied in 840ms

Fallback. The provider switch is logged and invisible to whoever is chatting.

Results and lessons

2
providers behind one interface — neither is load-bearing alone
<1 s
typical reply, fallback path included
1 jar
plus a SQLite file — the whole deployment

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.