Skip to content

feat: Telegram Managed Bots — automatic bot creation infrastructure #10591

@teknium1

Description

@teknium1

Overview

Telegram Bot API 9.6 (April 3, 2026) introduced Managed Bots — a feature that lets a "manager bot" create and control child bots on behalf of users. This eliminates the manual BotFather → copy token → paste token setup flow entirely.

User-facing goal: During hermes setup, the Telegram step becomes "scan a QR code, tap confirm, done." No BotFather, no token copy-paste.

Client-side PR: #10589 (hermes-agent changes — QR code, pairing client, setup wizard)

This issue describes the backend infrastructure needed to complete the flow.


How Telegram Managed Bots Work

Reference: https://core.telegram.org/bots/features#managed-bots

  1. A bot enables "Bot Management Mode" via @Botfather MiniApp → can_manage_bots becomes true
  2. Users are sent a deep link: https://t.me/newbot/{manager_bot}/{suggested_username}?name={name}
  3. User opens link → Telegram shows a pre-filled bot creation screen → user taps confirm
  4. Manager bot receives a managed_bot update (ManagedBotUpdated object) with the new bot's info
  5. Manager bot calls getManagedBotToken(user_id) → gets the child bot's full token
  6. Manager bot now has complete control over the child bot via Bot API

Key API methods:

  • getManagedBotToken(user_id) → returns bot token as string
  • replaceManagedBotToken(user_id) → rotates the token, returns new one
  • ManagedBotUpdated — update type received when a managed bot is created or its token changes

Constraints:

  • Bot Management Mode must be enabled via @Botfather MiniApp (one-time)
  • The request_managed_bot keyboard button only works in private chats
  • Child bot is owned by the user on Telegram's side; manager bot holds the token

Architecture

┌──────────────────┐     ┌──────────────────────┐     ┌─────────────────┐
│   hermes setup   │     │   Pairing API          │     │  Manager Bot    │
│   (user's CLI)   │     │   (Cloudflare Worker)  │     │  (@HermesSetup) │
└────────┬─────────┘     └──────────┬───────────┘     └────────┬────────┘
         │                          │                           │
    1. POST /pair {nonce}           │                           │
    ─────────────────────>          │                           │
    <── 201 {nonce}                 │                           │
         │                          │                           │
    2. Print QR code with           │                           │
       deep link containing         │                           │
       suggested_username           │                           │
         │                          │                           │
         │          3. User opens link in Telegram,             │
         │             confirms bot creation                    │
         │                          │                           │
         │                          │    4. Telegram sends      │
         │                          │       managed_bot update  │
         │                          │    ──────────────────────>│
         │                          │                           │
         │                          │    5. getManagedBotToken  │
         │                          │    ──────────────────────>│
         │                          │    <── bot token          │
         │                          │                           │
         │    6. PUT /pair/{nonce}   │                           │
         │       {token, username}   │<──────────────────────── │
         │                          │                           │
    7. GET /pair/{nonce}            │                           │
    ─────────────────────>          │                           │
    <── 200 {token}                 │                           │
         │                          │                           │
    8. Save token to .env           │                           │

Nonce ↔ Username Correlation

The CLI generates both a nonce and a suggested_username (e.g. hermes_work_a7f3_bot). It registers the nonce with the pairing API. The deep link encodes the suggested_username.

When the manager bot receives the managed_bot update, the ManagedBotUpdated object includes the new bot's User object (with username). The manager bot needs to correlate this username to a registered nonce.

Two approaches (pick one):

Option A: Nonce encoded in username (simplest)

  • CLI generates username as hermes_{nonce_prefix}_bot (e.g. hermes_a7f3b2c1_bot)
  • Manager bot extracts the nonce prefix from the created bot's username
  • Pairing API stores {nonce_prefix: nonce} mapping
  • Pro: No additional state needed between CLI and manager bot
  • Con: Username is less customizable

Option B: Separate lookup table

  • CLI registers {nonce: suggested_username} with pairing API
  • Manager bot queries pairing API to find nonce by username
  • Pro: Username can be anything
  • Con: Extra API call

Recommendation: Option A — simpler, fewer moving parts, the username suffix is already random.


Component 1: Pairing API (Cloudflare Worker + KV)

A minimal HTTP API that mediates the token exchange between the CLI and the manager bot.

Deployment

  • Cloudflare Worker on setup.hermes-agent.nousresearch.com (subdomain of existing docs domain)
  • Cloudflare KV namespace for nonce → token storage with auto-expiration

Endpoints

POST /pair

Register a new pairing nonce. Called by the CLI.

Request:  { "nonce": "a7f3b2c1d4e5f6a7b8c9d0e1f2a3b4c5" }
Response: 201 { "nonce": "...", "expires_at": "2026-04-15T16:15:00Z" }
  • Store nonce in KV with 5-minute TTL
  • Rate limit: 10 requests per IP per minute (prevent abuse)

PUT /pair/{nonce}

Store the bot token. Called by the manager bot (authenticated).

Headers:  Authorization: Bearer <MANAGER_BOT_SECRET>
Request:  { "token": "123456:ABCdefGHI...", "bot_username": "hermes_a7f3_bot" }
Response: 200 { "ok": true }
  • Verify the nonce exists in KV (404 if not)
  • Verify Authorization header matches the shared secret
  • Update KV entry with the token (preserve TTL)

GET /pair/{nonce}

Poll for the token. Called by the CLI.

Response (not ready): 404 { "status": "waiting" }
Response (ready):     200 { "token": "123456:ABCdefGHI...", "bot_username": "hermes_a7f3_bot" }
  • On successful retrieval, delete the KV entry (one-time use)
  • The token should only be retrievable once

Security considerations

  • KV entries auto-expire after 5 minutes (prevents stale token accumulation)
  • Manager bot authenticates via shared secret on PUT (prevents injection)
  • GET deletes the entry on read (one-time retrieval)
  • No user authentication on POST/GET (the nonce itself is the secret — 128-bit random)
  • Rate limiting on POST prevents nonce flooding
  • CORS: restrict to CLI user-agent or disable entirely (CLI uses httpx, not browser)

Cloudflare Worker implementation (~60 lines)

export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    const path = url.pathname;

    // POST /pair — register nonce
    if (request.method === "POST" && path === "/pair") {
      const { nonce } = await request.json();
      if (!nonce || nonce.length !== 32) {
        return Response.json({ error: "invalid nonce" }, { status: 400 });
      }
      await env.PAIRING_KV.put(nonce, JSON.stringify({ status: "waiting" }), {
        expirationTtl: 300, // 5 minutes
      });
      return Response.json({ nonce, expires_in: 300 }, { status: 201 });
    }

    // PUT /pair/:nonce — store token (manager bot only)
    const putMatch = path.match(/^\/pair\/([a-f0-9]{32})$/);
    if (request.method === "PUT" && putMatch) {
      const auth = request.headers.get("Authorization");
      if (auth !== `Bearer ${env.MANAGER_SECRET}`) {
        return Response.json({ error: "unauthorized" }, { status: 401 });
      }
      const nonce = putMatch[1];
      const existing = await env.PAIRING_KV.get(nonce);
      if (!existing) {
        return Response.json({ error: "nonce not found" }, { status: 404 });
      }
      const { token, bot_username } = await request.json();
      await env.PAIRING_KV.put(nonce, JSON.stringify({ token, bot_username }), {
        expirationTtl: 300,
      });
      return Response.json({ ok: true });
    }

    // GET /pair/:nonce — poll for token
    const getMatch = path.match(/^\/pair\/([a-f0-9]{32})$/);
    if (request.method === "GET" && getMatch) {
      const nonce = getMatch[1];
      const data = await env.PAIRING_KV.get(nonce, "json");
      if (!data) {
        return Response.json({ status: "expired" }, { status: 404 });
      }
      if (!data.token) {
        return Response.json({ status: "waiting" }, { status: 404 });
      }
      // One-time retrieval: delete after read
      await env.PAIRING_KV.delete(nonce);
      return Response.json({ token: data.token, bot_username: data.bot_username });
    }

    return Response.json({ error: "not found" }, { status: 404 });
  },
};

KV namespace setup

wrangler kv:namespace create PAIRING_KV
# Add the ID to wrangler.toml

wrangler.toml

name = "hermes-pairing"
main = "src/index.js"
compatibility_date = "2026-04-01"

[vars]
MANAGER_SECRET = ""  # Set via wrangler secret put MANAGER_SECRET

[[kv_namespaces]]
binding = "PAIRING_KV"
id = "<from kv:namespace create>"

Component 2: Manager Bot (@HermesSetupBot)

A lightweight Python bot that:

  1. Listens for managed_bot updates
  2. Calls getManagedBotToken to get the child bot's token
  3. Sends the token to the pairing API

Setup (one-time)

  1. Create a bot via @Botfather (or use an existing Nous bot)
  2. Open BotFather's MiniApp → enable "Bot Management Mode"
  3. Verify: getMe should show can_manage_bots: true
  4. Deploy the bot process

Implementation (~80 lines)

"""Hermes Managed Bot — Nous-hosted manager bot for automatic Telegram setup."""

import os
import logging
import httpx
from telegram import Update
from telegram.ext import Application, MessageHandler, filters, ContextTypes

logger = logging.getLogger(__name__)

MANAGER_TOKEN = os.environ["MANAGER_BOT_TOKEN"]
PAIRING_API_URL = os.environ.get("PAIRING_API_URL", "https://setup.hermes-agent.nousresearch.com")
PAIRING_API_SECRET = os.environ["PAIRING_API_SECRET"]


async def handle_managed_bot(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Handle managed_bot updates — new bot created by a user."""
    managed = update.managed_bot
    if not managed:
        return

    bot_user = managed.bot
    creator = managed.user
    bot_username = bot_user.username or ""
    bot_id = bot_user.id

    logger.info(f"Managed bot created: @{bot_username} (id={bot_id}) by user {creator.id}")

    # Get the child bot's token
    try:
        token = await context.bot.get_managed_bot_token(user_id=bot_id)
    except Exception as e:
        logger.error(f"Failed to get managed bot token: {e}")
        return

    # Extract nonce from username (hermes_{nonce_prefix}_bot pattern)
    # The CLI generates usernames like hermes_a7f3b2c1_bot
    nonce_prefix = _extract_nonce_from_username(bot_username)
    if not nonce_prefix:
        logger.warning(f"Could not extract nonce from username: {bot_username}")
        # Try to notify the user directly
        try:
            await context.bot.send_message(
                chat_id=creator.id,
                text=f"Your Hermes bot @{bot_username} was created!\n\n"
                     f"Token: `{token}`\n\n"
                     f"Paste this in your hermes setup if it didn't auto-detect.",
                parse_mode="Markdown",
            )
        except Exception:
            pass
        return

    # Look up full nonce from pairing API and store the token
    try:
        # Find the full nonce that starts with this prefix
        resp = httpx.put(
            f"{PAIRING_API_URL}/pair/{nonce_prefix}",
            json={"token": token, "bot_username": bot_username},
            headers={"Authorization": f"Bearer {PAIRING_API_SECRET}"},
            timeout=10.0,
        )
        if resp.status_code == 200:
            logger.info(f"Token stored for nonce {nonce_prefix[:8]}...")
        else:
            logger.warning(f"Pairing API returned {resp.status_code}")
    except Exception as e:
        logger.error(f"Failed to store token in pairing API: {e}")


def _extract_nonce_from_username(username: str) -> str | None:
    """Extract the pairing nonce from a managed bot username.

    Expected format: hermes_{nonce}_bot or hermes_{profile}_{nonce}_bot
    """
    if not username:
        return None
    parts = username.lower().rstrip("_bot").split("_")
    # The nonce is the last segment before _bot
    if len(parts) >= 2 and parts[0] == "hermes":
        return parts[-1]  # Last segment is the random nonce suffix
    return None


def main():
    app = Application.builder().token(MANAGER_TOKEN).build()

    # Register handler for managed_bot updates
    # Note: python-telegram-bot 9.6+ needed for ManagedBotUpdated support
    app.add_handler(
        MessageHandler(filters.StatusUpdate.MANAGED_BOT_CREATED, handle_managed_bot)
    )

    # Also handle the Update.managed_bot field directly
    # (this may need adjustment based on python-telegram-bot's 9.6 implementation)

    logger.info("Hermes Manager Bot started")
    app.run_polling(allowed_updates=["managed_bot", "message"])


if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    main()

Deployment

  • Container: Single-process Python container
  • Requirements: python-telegram-bot>=22.0 (with Bot API 9.6 support), httpx
  • Env vars: MANAGER_BOT_TOKEN, PAIRING_API_URL, PAIRING_API_SECRET
  • Resources: Minimal — handles maybe a few requests per minute. Smallest available container.
  • Monitoring: Log to stdout, alert on repeated getManagedBotToken failures

Component 3: DNS Configuration

Add a CNAME or A record for setup.hermes-agent.nousresearch.com pointing to Cloudflare Workers.

If the domain is already on Cloudflare DNS:

setup.hermes-agent.nousresearch.com → Cloudflare Worker route

If not, a separate Cloudflare account/zone can host the Worker with a custom domain.


Integration with hermes-agent (PR #10589)

The client-side code in hermes-agent is already built:

  • hermes_cli/telegram_managed_bot.py — QR code rendering, deep link generation, pairing protocol client
  • hermes_cli/setup.py — Setup wizard offers Automatic [1] vs Manual [2] for Telegram
  • Constants (configurable):
    • DEFAULT_API_URL = "https://setup.hermes-agent.nousresearch.com"
    • DEFAULT_MANAGER_BOT = "HermesSetupBot"
    • DEFAULT_POLL_TIMEOUT = 180 (3 minutes)
    • POLL_INTERVAL = 2 (seconds)

The CLI flow:

  1. Generates a 32-char hex nonce
  2. Generates a suggested bot username embedding a random suffix
  3. Registers the nonce via POST /pair
  4. Prints a QR code + URL for the deep link
  5. Polls GET /pair/{nonce} every 2 seconds with an animated spinner
  6. On success, saves the token to .env
  7. Falls back to manual BotFather flow if anything fails

Rate Limits & Scale

  • Telegram: No documented rate limit on managed bot creation. Each creation requires user confirmation (anti-abuse by design).
  • Cloudflare Worker: Free tier handles 100k requests/day. At 2 req/sec poll rate × 3 min timeout = ~90 requests per setup. Supports ~1000 concurrent setups before hitting free tier.
  • Manager bot: Telegram's bot polling has standard rate limits (30 req/sec for getUpdates). Single bot can handle hundreds of concurrent managed bot creations.

No rate limiting from our side needed initially. If we see abuse, add IP-based rate limiting on the Worker (10 POST /pair per IP per minute).


Future Enhancements

  • Token rotation CLI command: hermes gateway telegram rotate-token using replaceManagedBotToken
  • Multi-profile auto-setup: Each hermes -p <name> setup auto-creates a uniquely-named bot
  • Self-hosted manager bot: Config option telegram.manager_bot_token for users who want to run their own manager bot instead of using the Nous-hosted one
  • Webhook mode: Switch manager bot from polling to webhook for lower latency in production

Checklist

  • Create @HermesSetupBot (or chosen name) via @Botfather
  • Enable Bot Management Mode in BotFather MiniApp
  • Deploy Cloudflare Worker with KV namespace
  • Configure DNS: setup.hermes-agent.nousresearch.com
  • Deploy manager bot process
  • Set shared secret between Worker and manager bot
  • Smoke test: run hermes setup → Telegram → auto-create bot → verify token arrives
  • Merge client-side PR feat: automatic Telegram bot creation via Managed Bots (Bot API 9.6) #10589

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions