@chat-adapter/telegram 4.26.0, pinned) and runs in polling mode — NanoClaw polls Telegram’s Bot API for updates, so you don’t need a public URL, webhook, or open port. Works behind NAT and on a laptop.
Because a bot token carries no user binding — anyone who finds the bot’s username can DM it — registration uses pairing: setup prints a one-time 4-digit code, and you prove you own a chat by sending the code from it.
Prerequisites
- A Telegram account (any device) to create the bot and chat with it
- A working NanoClaw install (quickstart)
- A bot token from @BotFather — format
<digits>:<chars>(e.g.123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11). The wizard walks you through creating one if you don’t have it yet.
Install
Telegram is offered in the first-run setup wizard, or add it later by running/add-telegram in Claude Code. The wizard flow:
Create the bot
Message @BotFather in Telegram, send
/newbot, and follow the prompts (the username must end in “bot”). Copy the token it gives you.Paste the token
The wizard format-validates the token, then calls the Bot API’s
getMe to confirm Telegram accepts it and resolve your bot’s username. If a TELEGRAM_BOT_TOKEN already exists in .env, it offers to reuse it.Open the bot's chat
The wizard deep-links you to
https://t.me/<botname> so you’re in the right chat for the next step, then installs the adapter (copies telegram.ts plus its pairing and markdown helpers from the channels branch and installs the pinned package).Pair the chat
Setup prints a one-time 4-digit code. Send exactly those 4 digits as a message to your bot. On match, NanoClaw registers the chat, records you as the paired user, and the bot replies “Pairing success!”. A wrong guess invalidates the code immediately — the wizard auto-issues a fresh one (up to 5 per run).
/manage-channels — group pairing works the same way: post the code in the group you want to register.
Platform notes
- How pairing works — an interceptor wraps the adapter’s inbound handler and checks every message for a pending code before it reaches the router. The message must be exactly the 4 digits (“my pin is 1234” never matches), optionally prefixed with
@<botname>for groups with privacy ON. On match it registers the chat, upserts the paired user, promotes them to owner if the instance has no owner yet, and short-circuits — the code-bearing message never reaches an agent. Pending codes live indata/telegram-pairings.json; they don’t expire, but one wrong guess invalidates them. - The service must be running to pair — the polling adapter is what observes the code. If pairing waits forever, check that NanoClaw is up.
- Group privacy — by default Telegram bots only see @mentions and
/commandsin groups. To let the bot see all messages: @BotFather →/mybots→ your bot → Bot Settings → Group Privacy → Turn off (then remove and re-add the bot to existing groups). With privacy ON you can still pair by prefixing the code:@<botname> 1234. - No threads — the adapter sets
supportsThreads: false; every inbound message has a null thread ID. Wirings withper-threadsession mode behave likesharedhere — see the entity model. - Markdown sanitization — the Chat SDK adapter sends with Telegram’s strict legacy
Markdownparse mode, which rejects (and drops) messages containing**bold**, unbalanced*/_delimiters, or malformed links. NanoClaw sanitizes outbound text first:**bold**→*bold*, list dashes →•bullets, horizontal rules → a plain divider, and unbalanced delimiters or brackets stripped. Code blocks pass through untouched. - Long replies — outbound messages are split at 4,000 characters (paragraph breaks preferred), staying under Telegram’s 4,096 limit. Attachments ride on the first chunk.
- Replies and IDs — replying to a message passes the quoted text and sender to the agent as context. Chats are identified as
telegram:<chatId>; negative chat IDs are groups. - Startup resilience — adapter setup retries transient network failures with exponential backoff (up to 5 attempts) before surfacing the error.
Troubleshooting
- “Telegram didn’t accept that token” —
getMerejected it. Re-copy the full token from @BotFather (it’s easy to truncate); it must match<digits>:<chars>. “Couldn’t reach Telegram” instead means a network problem — check connectivity and retry setup. - Pairing never completes — the service must be running and polling for the code to be observed. Also make sure you sent only the 4 digits, in the chat you want to register. In a group with Group Privacy ON, the bot can’t see the bare code — send
@<botname> 1234. - “Got “NNNN”, not a match” — a wrong code invalidates the pairing on the spot; the wizard prints a fresh code automatically, up to 5 regenerated codes per run. The step fails on the sixth wrong guess — re-run setup for a new batch.
- Bot ignores group messages — that’s Group Privacy doing its job. Either @mention the bot, or turn privacy off in @BotFather and re-add the bot to the group.
- Replies missing or formatting looks off — Telegram’s legacy Markdown mode silently drops messages it can’t parse. The built-in sanitizer handles the known cases; if a reply renders with stripped
*/_characters, that’s the sanitizer rescuing an unbalanced-delimiter message rather than losing it.