Why one bot per project
Pushing a stock tracker, a scraper, and a deploy notifier all through the same bot is fine until it isn't — you can't mute one, revoke one, or hand one to a teammate without touching the others. Separate bots fix that. And it turns out separate bots are nearly free, because of one detail:
Every bot you own DMs the same person — you. So the chat_id is your user id, identical across all of them. A new project bot needs only a new token.
That reframes "set up a bot" from a chore into: get one token from @BotFather, reuse the chat_id you already have, done. Which is exactly the kind of small, repeatable thing worth teaching an agent to do for you.
Driving @BotFather in the browser
The flow a human does — /newbot, pick a name, pick a username, copy the token — maps cleanly onto browser automation. Claude opens Telegram Web, sends the commands, and reads the reply. Simple. Then reality showed up.
Gotcha 1 — the fake BotFather
Search "BotFather" in Telegram and the real, verified account is joined by a crowd of impostors: @ZF_BotFather88_bot, @SphPay_Bot, and friends, all mimicking the name. Hand a token to one of those and you've handed it to an attacker. The rule the skill encodes: only ever trust the verified @BotFather under the "Chats" section — never a global-search result.
Gotcha 2 — don't read secrets off a screenshot
BotFather returns the token as a message. The obvious move is to read it from a screenshot. The obvious move is wrong: OCR quietly confuses 0 and O, 1 and l and I — and a token that's one character off fails in a way that looks like a network bug. The fix was to stop looking at pixels and pull the exact string from the page's DOM instead:
// read the token from the text layer, not the image
const m = document.body.innerText
.match(/\b(\d{8,10}:[A-Za-z0-9_-]{35})\b/);
Same lesson as everywhere else in this log: when a value has to be exact, get it from the source of truth, never from a lossy render of it.
Gotcha 3 — the dialog that ends the session
One stray click landed on a chat and popped a "Delete chat" confirmation. In a browser-automation loop, a modal like that is a trap: confirm it and you've destroyed something; ignore it and every following action silently targets the wrong place. The discipline is boring but absolute — on any destructive dialog, cancel, never confirm.
Gotcha 4 — a single underscore kills the push
With the bot created, sending a test message failed with 400: can't parse entities. The culprit was Markdown mode: one unbalanced _ in the message body — the kind you get constantly in scraped text or code — and Telegram rejects the whole thing. Push in plain text and the problem vanishes. Fancy formatting isn't worth a notifier that dies on the wrong character.
Oh, and: a bot can't message you until you message it first. Skip pressing Start and every send returns 403. Obvious in hindsight; a confusing five minutes in the moment.
What shipped
All of it became a Claude Code skill — packaged as an installable plugin, MIT-licensed, with a stdlib-only send helper and a test suite. The demo GIF in the README was itself recorded through the browser, cropped to hide the sidebar, and made against a throwaway bot that got deleted so the token on screen is already dead. Practising the paranoia it preaches.