> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nanoclaw.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Telegram

> Connect NanoClaw to a Telegram bot via the Chat SDK adapter — BotFather token, polling mode, and code-based chat pairing.

The Telegram adapter connects NanoClaw to a bot you create through @BotFather. It's built on the Chat SDK bridge (`@chat-adapter/telegram` 4.29.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](/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:

<Steps>
  <Step title="Create the bot">
    Message [@BotFather](https://t.me/BotFather) in Telegram, send `/newbot`, and follow the prompts (the username must end in "bot"). Copy the token it gives you.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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).
  </Step>

  <Step title="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).
  </Step>

  <Step title="Name the agent and get the welcome DM">
    The wizard asks for your operator role and an agent name (default `Nano`), wires the paired chat to your first agent group, and sends a welcome message.
  </Step>
</Steps>

To wire more chats or groups later, 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 in `data/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 `/commands` in 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 with `per-thread` session mode behave like `shared` here — see the [entity model](/concepts/entity-model).
* **Markdown sanitization** — the Chat SDK adapter sends with Telegram's strict legacy `Markdown` parse 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"** — `getMe` rejected 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.

For service-level checks (logs, restarts, wiring queries), see [troubleshooting](/operate/troubleshooting).
