A Telegram bot in one evening: what breaks on day two
You really can build a lead bot in an evening. Then someone types HTML into the name field. Seven things I build into Bot Kit from day one.
“We need a simple bot: a menu, a few questions and a lead sent to the manager. Can you do it in an evening?” Yes, I can . The bot will work and the client will be happy… until day two.
Here’s what I put into Bot Kit from the start, so day two never happens.
1. A form is a state machine, not a pile of ifs
Each question is its own state: name → contact → task → confirm. A user can vanish at any step, come back an hour later or hit /start mid-form. With a state machine (a StatesGroup in aiogram) the bot always knows what it’s waiting for and never mistakes a phone number for a name.
2. State lives in Redis, not in memory
The default storage is process memory. A restart, an update, a crash — and all unfinished forms are gone. Redis lives separately from the bot:
def storage() -> BaseStorage:
url = os.getenv("REDIS_URL")
return RedisStorage.from_url(url) if url else MemoryStorage()
3. Always escape user input
The bot sends the lead to the admin with HTML markup (bold labels look nice). But paste the user’s text as is, and a <b> in the name field becomes markup — and an unclosed tag makes Telegram reject the message, so the lead never arrives.
def summary(data: dict) -> str:
esc = lambda k: html.escape(str(data.get(k, "—"))) # user input never becomes markup
return f"<b>Name:</b> {esc('name')}\n<b>Contact:</b> {esc('contact')}\n<b>Task:</b> {esc('task')}"
4. Limit the length
Someone will paste an entire contract into the “Task” field. A Telegram message is capped at 4096 characters, so I trim every field up front: the name to 100 characters, the task to 1500.
5. The token lives only in environment variables
BOT_TOKEN and ADMIN_ID come from environment variables; the code has neither. If the token leaks (pasted into a chat, committed by accident), you reissue it in @BotFather in a minute without touching the code. Yes, I’ve received a token in a chat too .
6. A test without the network
You can test a bot without Telegram: swap the network layer and “play” the conversation as updates.
class FakeSession(BaseSession):
async def make_request(self, bot, method, timeout=None):
sent.append((type(method).__name__, getattr(method, "chat_id", None), getattr(method, "text", None)))
return Message.model_validate({...}) # a reply "from Telegram"
for up in [msg("/start"), cb("lead"), msg("Olha"), msg("@olha"), msg("Need a website"), cb("send")]:
await dp.feed_update(bot, up)
assert any(chat == ADMIN_ID for _, chat, _ in sent) # the lead reached the admin
Ten seconds, and you know the whole form works, from /start to “Thanks!”.
7. Logs and auto-restart
A bot should live on a server that restarts it after a crash and keeps logs you can read. Then “why didn’t the lead arrive?” has an answer instead of a guess. On bebb Cloud that’s searchable “Logs” and a Telegram alert when a bot is crash-looping.
Bottom line
A bot in an evening is real. A bot that won’t let you down on day two is a few more hours of states, escaping and tests. Those hours are already in Bot Kit — take it and adapt it .