Skip to content

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

Terminal window
TELEGRAM_BOT_TOKEN=123456:ABC-your-bot-token # from @BotFather

Set 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/:id

configured 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.