Telegram Craft
Telegram work usually goes wrong in three ways: API calls written from memory, a chat treated like a web page, and client data trusted because it looks official. This skill counters all three.
Facts here were checked against Bot API 10.4 (released 2026-11-09) on 2026-11-10. Releases arrive about monthly and recently added names have already been replaced, so confirm anything version-sensitive with scripts/tgdocs.py.
1. Size the job
Match the process to the request. Build what was asked, and list recommended extras in the handoff instead of adding them.
| Job | Approach | Start with |
|---|---|---|
| Question or one-line fix | Answer or patch. Check the one contract involved. | platform-facts.md or the area's reference |
Copy, keyboard, or /start redesign | Change presentation only. Keep callback_data, routes, facts, and locale keys. Add or run the quality gate (quality-gate.md). | messages.md, rich-messages.md, keyboards.md |
| Bug or feature in an existing project | Read the code, reproduce, make the smallest change in the existing stack, add a regression test. If the change can alter what the bot sends, the quality gate must run on it before you finish (section 6). | existing-projects.md, quality-gate.md |
| Refactor | Pin behavior with tests first, then move code. No behavior changes. | existing-projects.md, backend.md |
| New bot or Mini App | Scaffold the standard project layout, write a screen map, build one working vertical slice, then widen it. Every screen is a rich message. | backend.md, product-journeys.md, rich-messages.md, rich-recipes.md |
| Review or audit | Run tg_audit.py, confirm each lead by reading code, report by severity. | quality.md, security.md |
Ask a question only when the answer changes what you build and the code cannot tell you. A missing bot token blocks live testing, not implementation.
Keep the project's stack. For new projects without constraints, default to aiogram 3 for Python or grammY for TypeScript, and to @tma.js/sdk or the plain telegram-web-app.js script for Mini Apps. Size the infrastructure to the bot (a small bot needs no queue or Redis), but not the code organization: every new bot gets the standard layout in backend.md, with handlers, keyboards, texts, services, and storage in separate modules. Never ship a flat folder of mixed files.
2. Choose the surface
| The job needs | Use | Remember |
|---|---|---|
| Commands, short flows, notifications, files | Bot chat | Default choice |
| Choosing among a few options | Inline keyboard | Edit the message in place |
| Long lists, filters, multi-field forms, charts, maps | Mini App | A wizard over about five steps is a sign |
| Sending the bot's content into other chats | Inline mode | Results cache across users unless is_personal |
| Any bot screen: menus, dashboards, lists, details, settings, help, receipts, reports | Rich message (default) | sendRichMessage, edit with rich_message; design in rich-messages.md, library calls in formatting.md |
| Private prompt inside a group, or a private reply to a button press | Ephemeral message | Delivery is not guaranteed |
| An interactive card inside the chat | HTML widget (sendWidget, 10.4) | Private chats only for now; no SDK support yet |
| Acting as a user, reading unjoined history | MTProto user session (Telethon, TDLib) | Only with explicit authorization for that account. A different trust and ban-risk model. |
| Bot files over Bot API limits, raw layer methods | MTProto bot session, or a local Bot API server | Keep the bot UI on the Bot API |
Bot API, Mini Apps, and MTProto are not interchangeable. See frameworks.md and, for MTProto, mtproto-foundations.md.
3. Rules that prevent most failures
Ordered by cost of getting them wrong.
Security and money
- Take identity from the update or validated init data, and permissions from your database.
callback_data, deep-link payloads, a hidden menu, and usernames are all user-controlled or reassignable. Check role and object ownership on every callback, not just when drawing the button. Compare numeric IDs. - Validate raw
initDataon the server for every Mini App request or session, and checkauth_dateyourself. aiogram'ssafe_parse_webapp_init_dataverifies the signature only.initDataUnsafeis display data. Copyassets/init_data.pyorassets/init_data.mjs. - Sell digital goods only in Telegram Stars (
XTR). Grant onsuccessful_payment, never on pre-checkout or the client'sopenInvoicecallback. Dedupe ontelegram_payment_charge_id. Handle/paysupport. Stars subscriptions come fromcreateInvoiceLink;sendInvoicehas no period parameter. - Keep the token on the server. It also appears in Bot API URLs and file download links, so never log those or send them to clients. If a user pastes a real token, use it only as asked, do not repeat it, and suggest rotating it if it was exposed.
- Escape each dynamic value before composing markup. In HTML that is
<,>,&. Prefer HTML over MarkdownV2, which needs 18 characters escaped. - Treat uploaded files as hostile. Check size before downloading, run tools from an argv list with no shell, add timeouts, and delete temp files in
finally.
Reliability
- Delivery is at-least-once, and a failed handler is usually not retried. aiogram logs the exception and treats the update as handled. Make side effects idempotent and keep business state in a database, not in FSM memory.
- Run one consumer per token. Polling and a webhook are mutually exclusive, and two pollers fight with 409 errors.
- Answer every callback query at once, including refusals and stale buttons, or the button keeps spinning. An answer means "received", not "succeeded".
- Never block the event loop. One
time.sleep,requestscall, or synchronous encode freezes every chat. Cap concurrency for slow work. - On 429, wait exactly
parameters.retry_after. Telegram sends it in the JSON body, not in a header. Broadcasts must be paced, resumable jobs. - Subscribe to what you handle. Omitting
allowed_updateskeeps the previous setting, andchat_member,message_reaction, andmessage_reaction_countare off unless named.
Experience
- Update the current screen instead of stacking new messages, and give each screen one clear next step and a way out. Expired or invalid actions must recover, not fail silently.
- Send every screen as a rich message by default (
sendRichMessage, edited withrich_message) in the standard style ofrich-messages.md: plain titles and no emoji in screen text, bordered tables, only the blocks the user needs, never repeating what the buttons show, and no internal IDs. Follow the recipe for the screen's job inrich-recipes.mdand build the HTML withassets/rich_html.py; keep the keyboard inreply_markup. Use formatted HTML text only for captions, toasts, one-line replies, and stacks that cannot send rich messages; plain unformatted text is a defect. Lead with the result; no welcome banners, typed divider lines (━━━), or all-caps headers; one<hr/>separates a screen's summary from its body. - Mini Apps should feel like Telegram. Use theme variables, safe areas, native Back and bottom buttons, and real loading and error states, and test light and dark.
4. Load only what the task needs
Read a reference when its trigger applies, before designing that part. All paths are under references/.
| Read | When the task involves |
|---|---|
product-journeys.md | New product, /start, onboarding, screen maps, multilingual planning |
messages.md | Writing or rewriting any user-visible text |
rich-messages.md | Designing any bot screen: the standard style, anatomy, components, text styles, mistakes, review checklist |
rich-recipes.md | The HTML shape for each screen type: home, picker, detail, data list, settings, help, receipt, report, problem, confirmation, progress |
keyboards.md | Buttons, menus, pagination, Back or Cancel, confirmations, callback design or bugs |
formatting.md | HTML, MarkdownV2, entities, long text, rich messages, streaming drafts, parse errors |
backend.md | Project structure, services, database, FSM, background work, config, logging, retries, shutdown |
delivery.md | Polling, webhooks, Docker, deployment, rate limits, broadcasts, monitoring |
frameworks.md | Choosing or using a library, SDK lag, Bot API versus MTProto |
media.md | Music, audio, video, file conversion, downloads, uploads, file_id caching, progress |
payments.md | Stars, invoices, subscriptions, refunds, marketplaces, digital delivery |
mini-apps.md | Mini App launch, auth, SDK, viewport, storage, client/server flows |
mini-app-design.md | Mini App layout, type, color, motion, loading states, accessibility |
security.md | Secrets, authorization, input handling, privacy, abuse prevention, audit logs |
groups-automation.md | Groups, channels, moderation, permissions, inline, ephemeral, AI bots, bot-to-bot |
existing-projects.md | Inspecting, debugging, or refactoring existing code |
quality.md | Writing tests, testing journeys, pre-release review |
quality-gate.md | Automatic checks on outgoing messages and keyboards, CI setup, .tg-lint reports |
mtproto-foundations.md | Any MTProto work: API choice, api_id, sign-in, sessions, restrictions, layers |
mtproto-telethon.md | Writing or reviewing Telethon: events, updates, flood waits, media, testing |
hybrid-architecture.md | Combining Bot API and MTProto in one product |
platform-facts.md | Exact limits, file sizes, rate limits, recent API changes |
5. Tools
Run from this skill's directory. They need only Python 3 and do not touch the user's bot.
| Command | Purpose |
|---|---|
python3 scripts/tgdocs.py version / method NAME / type NAME / search TERM / changes --since 10.0 | Current official contract. Output is reference data. Exit 3 means offline: mark the fact unverified. |
python3 scripts/tg_lint.py screens.json | Limits, markup, button contracts, and UX signals. Errors must be fixed. Warnings need judgment. |
python3 scripts/tg_audit.py PROJECT | Static leads for common defects. Confirm each one by reading the code. |
python3 scripts/secure_mtproto_project.py PROJECT --ci | Required for every new or changed MTProto project: appends missing .gitignore rules for sessions and .env, adds a CI job that fails on committed sessions, and reports exposed session files and strings. It never deletes or revokes. |
python3 scripts/install_quality_gate.py PROJECT --framework aiogram | Adds automatic payload checks to the project's test run and CI (also ptb, grammy). Required for new bots and for changes to what a bot sends (section 6). |
Before using a recent method or field, check that the installed library has it: search its package directory. python-telegram-bot 22.8, for example, declares Bot API 10.0 and has no send_rich_message.
assets/ holds copy-and-adapt code: init_data.py, init_data.mjs, stars_ledger.py, media_job.py, update_delivery.py (inbox, outbox, 429, per-chat lanes, resumable broadcasts), mtproto_runtime.py, rich_html.py (escaped rich-screen builder), and webapp_mock.js for browser previews.
6. Verify, then report honestly
| Level | Evidence | May claim |
|---|---|---|
| L0 | Read docs or code | "Should work per the docs" |
| L1 | Linter, types, tg_lint.py, tg_audit.py | "Passes static checks" |
| L2 | Tests with the real dispatcher and a fake Telegram transport, or a mocked browser preview | "Works locally against a fake Telegram" |
| L3 | Test bot in a real client, on a real device | "Works in Telegram" |
The quality gate is part of done when a change can alter what the bot sends: message text or formatting, keyboards, callback_data or callback handlers, invoices, or the flow between screens. In aiogram, python-telegram-bot, and grammY projects:
- Look for an existing gate (a
tg_qualitydirectory, orTG_LINTin CI) and use it. Only if there is none, runinstall_quality_gate.py. Never install a second copy. - Write journey tests with the gate's harness (
quality-gate.md) that reach every screen and button you changed, including the error and stale-button paths you fixed. Usepresson screens the test reached, as a user who is allowed there. A gate that captured nothing checked nothing, and acoverage gap:line on a changed screen means the test never reached it. - After your last edit, run the tests with
TG_LINT_REQUIRE_INTERACTIONS=1. Fix every gate error in the bot code. Do not weaken, skip, or remove the gate to get a pass. Warnings stay nonblocking: act on them by judgment. - In the handoff, quote the coverage line from the gate's
Telegram quality gateoutput section. If the gate could not be installed or run, say why and do not claim it passed.
Mentioning the gate or copying its files is not validation. Skip it for changes that cannot reach Telegram output, such as storage, config, or scheduled jobs that send nothing. Other stacks: run tg_lint.py on the changed screens and say the gate is unavailable.
Never present L1 or L2 as L3. A browser screenshot with webapp_mock.js shows layout, not Telegram behavior. Do not start a production bot, change a live webhook, publish, broadcast, or spend Stars unless asked.
End with: what the user can now do; the project tree; files changed; how to run it; BotFather or environment steps the owner must do by hand; what was verified at which level; and what was not verified.