Connecting Telegram
Telegram is the “talk to your Muse from your phone” connection (PLAN.md §6). It’s built on plain
long-polling (getUpdates), deliberately — no public HTTPS endpoint or webhook registration needed,
so it works the same on a laptop under a desk as it would behind a Cloudflare Tunnel.
One world bot, or your own
TELEGRAM_BOT_TOKEN=123456:ABC-your-bot-token # from @BotFatherSet this once in .env and every being in the install can pair a chat to the same bot; each pairing
is per-being, not per-token. You can instead paste a personal bot token in Settings
(POST /api/connections { "provider": "telegram", "config": { "token": "..." } }) if you’d rather
run your own bot — tokenFor(beingId) prefers a being’s own connection token, falling back to the
env token. One long-polling loop runs per distinct token in the whole install
(apps/api/src/twin/telegram.ts), not per being.
Pairing
GET /api/connections/telegram/pairing → TelegramPairing{ "configured": true, "paired": false, "code": "K7XQPL", "botUsername": "novaterra_world_bot", "deepLink": "https://t.me/novaterra_world_bot?start=K7XQPL", "chatTitle": null}Opening the deepLink (or just messaging the bot /start K7XQPL, or sending the bare code) tells
the bot which being’s Twin that chat belongs to. Codes are six characters from a
Crockford-ish alphabet (ABCDEFGHJKLMNPQRSTUVWXYZ23456789 — no ambiguous 0/O/1/I) generated by
nanoid’s customAlphabet, and persist on the being’s pending connection row until paired. Once
Telegram delivers the /start message, handleMessage() looks the code up
(beingForCode), saves the connection as connected with the chat id and a human-readable title,
and replies “Paired with <name>. Say anything and your Muse will answer here.”
Talking to your Muse from Telegram
Every subsequent message from a paired chat is relayed straight into that being’s Muse thread:
relayToMuse(being, text)This appends the text as a real user Message to the being’s muse-kind Thread (the same thread
/muse shows), triggers exactly the same onUserMessage hook the web UI’s composer triggers, and
waits (up to 120 seconds, REPLY_TIMEOUT_MS) for the assistant’s reply to land as a message.created
WebSocket event before sending it back to Telegram (chunked at ~3,900 characters, Telegram’s message
limit). There is no separate “Telegram bot brain” — it is the same Muse, the same memory, the same
soul, just answering through a different door. If Telegram messages arrive while OpenRouter isn’t
configured, the bot says so plainly rather than hanging: “Your Muse is asleep: connect OpenRouter in
Settings and she will answer here.”
Every inbound and outbound message also writes a Twin activity: “Heard you on Telegram: ’…’” and “Replied on Telegram: ’…’” — so a conversation that happened entirely on your phone still shows up in your Twin’s activity stream on the desktop.
Status and disconnecting
GET /api/twin/status → providers[].telegram: { configured, connected, meta: { botUsername, chatTitle }, hint }DELETE /api/connections/:idconfigured is true as soon as any token (env or per-being) resolves; connected only once a chat
has actually completed pairing. Disconnecting a per-being token connection stops that bot’s poller,
unless it’s the same token as TELEGRAM_BOT_TOKEN, which keeps running for everyone else.