DEV Community

Cover image for Building A Telegram Bot-to-Bot Communication Showcase With TypeScript
Harish Kotra (he/him)
Harish Kotra (he/him)

Posted on

Building A Telegram Bot-to-Bot Communication Showcase With TypeScript

Telegram's Bot-to-Bot Communication feature changes what a Telegram group can be. A group no longer has to be a place where humans talk and bots merely respond. It can become a visible multi-agent workspace where bots coordinate through the same messages humans can read.

This project, Telegram Bot-to-Bot Debate Club, is a working demonstration of that idea.

The demo has four Telegram bots:

  • DebaterRedBot argues for a proposition.
  • DebaterBlueBot argues against it.
  • SocraticBot questions the weakest claim.
  • JudgeBot delivers a final verdict.

The important part is not that the bots debate. The important part is how they coordinate: every transition is a real Telegram group message delivered through Telegram's bot-to-bot mechanism. There is no hidden in-process bot registry, no direct method call between bots, and no local handoff fallback.

Built by Harish Kotra · Checkout my other builds

Why Bot-to-Bot Communication Matters

Most chatbots are designed around a human-to-bot model:

Human -> Bot -> Human
Enter fullscreen mode Exit fullscreen mode

But many useful workflows are multi-role:

Human -> Intake Bot -> Research Bot -> Reviewer Bot -> Finalizer Bot
Enter fullscreen mode Exit fullscreen mode

Without bot-to-bot delivery, developers often have to fake this with server-side orchestration. That works, but users cannot see the real control flow. The group chat becomes a UI facade over a hidden backend.

Telegram's Bot-to-Bot Communication enables a different model:

Bot A posts in the group.
Telegram delivers Bot A's message to Bot B.
Bot B decides whether to respond.
The whole workflow remains visible in the chat.
Enter fullscreen mode Exit fullscreen mode

That visibility is valuable. It makes debugging easier, makes automation auditable, and lets humans understand why a bot responded.

The Showcase App

The debate club is intentionally small but realistic:

  1. A human starts with /debate <topic>.
  2. Red posts a FOR argument and mentions Blue.
  3. Blue receives Red's bot-authored message through Telegram and replies AGAINST.
  4. Blue mentions Socratic.
  5. Socratic receives Blue's bot-authored message through Telegram and challenges one debater.
  6. The targeted debater answers.
  7. After the configured exchange limit, a bot sends /verdict@JudgeBot.
  8. Judge receives that bot-authored command through Telegram and posts the verdict.

App Flow

System Architecture

All four bots run in a single Node.js process, but each bot is a separate Telegram bot identity with its own token.

System Architecture

The single-process design keeps the showcase easy to run:

npm run dev
Enter fullscreen mode Exit fullscreen mode

The important point is that shared process does not mean hidden coordination. The process shares state for safety, but bot-to-bot progression still depends on Telegram delivering bot-authored messages.

The Visible Control Plane

Red's job is to end its reply by tagging Blue:

@YourClubDebaterBlueBot what do you say?
Enter fullscreen mode Exit fullscreen mode

Blue decides whether to respond by inspecting the Telegram message it received:

shouldRespond(msg: TelegramBot.Message): boolean {
  if (this.isSelfMessage(msg)) {
    return false;
  }

  return this.mentionsThisBot(msg);
}
Enter fullscreen mode Exit fullscreen mode

That tiny check is the heart of the showcase. Blue is not called by Red. Blue is reacting to a Telegram update.

The same rule applies to the verdict:

/verdict@YourClubJudgeBot
Enter fullscreen mode Exit fullscreen mode

Judge only responds to the visible Telegram command:

const isVerdictCommand =
  text.startsWith(`/verdict@${CANONICAL_USERNAMES.judge.toLowerCase()}`) ||
  text === '/verdict';

return hasDebateToJudge && isVerdictCommand;
Enter fullscreen mode Exit fullscreen mode

The final trigger is not a hidden local state transition. It is a bot-to-bot Telegram command in the group.

Why Shared State Still Exists

Bot-to-Bot Communication gives you delivery. It does not give you workflow semantics.

In a group with four bots, every bot can receive many of the same group messages. Without turn control, multiple bots may answer at the wrong time or create loops.

The project uses a shared DebateState singleton:

export interface DebateState {
  topic: string | null;
  isActive: boolean;
  isPaused: boolean;
  roundNumber: number;
  debaterRoundNumber: number;
  totalMessageCount: number;
  socraticDepth: number;
  messages: DebateMessage[];
  lastReplyAt: Record<string, number>;
  seenMessageIds: Set<number>;
  judgeHasFired: boolean;
  verdictRequested: boolean;
  debateId: number;
  expectedResponder: BotRole | null;
  pendingSocraticTarget: BotRole | null;
  disqualified: Set<string>;
  steerInstruction: string | null;
  steerUntilRound: number | null;
}
Enter fullscreen mode Exit fullscreen mode

The key field is expectedResponder.

if (
  this.role !== 'judge' &&
  this.state.expectedResponder &&
  this.state.expectedResponder !== this.role
) {
  return;
}
Enter fullscreen mode Exit fullscreen mode

Telegram delivers messages. The app decides whose turn it is.

Counting The Right Thing

One subtle bug in multi-agent debates is counting every bot message as a round. That makes Socratic questions accidentally shorten the debate.

This project tracks two counters:

  • totalMessageCount: every bot debate message.
  • debaterRoundNumber: completed Red/Blue exchanges.

The Judge is triggered after the intended debater exchange limit, not after Socratic questions.

The flow is:

Red -> Blue -> Socratic -> targeted debater -> other debater -> Socratic ...
Enter fullscreen mode Exit fullscreen mode

When the max debater exchange limit is reached, Socratic can still ask one final question, and the targeted debater gets one final answer before the Judge command is sent.

That makes the demo feel like a coherent conversation instead of a timer.

Loop Guarding Bot-to-Bot Workflows

Bot-to-bot systems need guardrails. This project has a LoopGuard that handles:

  • duplicate updates,
  • per-bot cooldowns,
  • pair-depth limits.

Deduplication is per receiving bot for normal bot-to-bot messages:

const seenKey = `${botUsername}:${messageId}`;
if (this.seenMessageKeys.has(seenKey)) return false;
this.seenMessageKeys.add(seenKey);
Enter fullscreen mode Exit fullscreen mode

That detail matters. If dedupe were global, the first bot to see a group message could prevent the intended next bot from processing it.

Moderator commands use global dedupe because only one bot should handle /debate, /pause, /resume, and similar commands.

The LLM Layer

The LLM client is deliberately provider-neutral. It uses raw fetch against an OpenAI-compatible /v1/chat/completions endpoint:

const response = await fetch(this.completionsUrl, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Authorization: `Bearer ${this.apiKey}`,
    ...this.extraHeaders,
  },
  body: JSON.stringify({ ...req, ...this.extraBody }),
});
Enter fullscreen mode Exit fullscreen mode

That keeps the project compatible with:

  • LM Studio,
  • OpenRouter,
  • Featherless.ai,
  • local OpenAI-compatible servers,
  • hosted inference gateways.

The expected response is simple:

const content = choice?.message?.content;

if (typeof content !== 'string' || content.trim().length === 0) {
  throw new Error(this.describeEmptyContent(rawBody, choice, json.usage));
}
Enter fullscreen mode Exit fullscreen mode

For LM Studio, a non-reasoning instruct model is best for Judge:

LLM_MODEL_JUDGE=mistralai/Mistral-7B-Instruct-v0.3
LLM_MAX_TOKENS=700
Enter fullscreen mode Exit fullscreen mode

Reasoning models can spend their whole budget on reasoning_content and return no final answer. The app has a deterministic fallback verdict so the Telegram workflow still closes cleanly.

Typing Indicators

Multi-bot workflows need visible latency handling. If a bot is calling an LLM, users should know it is working.

The base class sends Telegram's typing action while work is in progress:

protected async withTypingIndicator<T>(work: () => Promise<T>): Promise<T> {
  await this.sendTypingAction();

  const interval = setInterval(() => {
    void this.sendTypingAction();
  }, 4000);

  try {
    return await work();
  } finally {
    clearInterval(interval);
  }
}
Enter fullscreen mode Exit fullscreen mode

This is especially important for bot-to-bot chains because users are watching a group conversation unfold.

BotFather And Group Setup

The project needs four bot tokens:

DEBATER_RED_TOKEN=
DEBATER_BLUE_TOKEN=
SOCRATIC_TOKEN=
JUDGE_TOKEN=
Enter fullscreen mode Exit fullscreen mode

It also needs exact usernames:

DEBATER_RED_USERNAME=YourClubDebaterRedBot
DEBATER_BLUE_USERNAME=YourClubDebaterBlueBot
SOCRATIC_USERNAME=YourClubSocraticBot
JUDGE_USERNAME=YourClubJudgeBot
Enter fullscreen mode Exit fullscreen mode

The most important setup checklist:

  1. Enable Bot-to-Bot Communication Mode for all four bots in BotFather if available.
  2. Add all four bots to the same Telegram group.
  3. Give Socratic admin rights, or disable Group Privacy Mode.
  4. Confirm bot-authored messages appear in routing logs.

When DEBUG_BOT_ROUTING=true, a healthy run shows lines like:

[YourClubDebaterBlueBot] received: message_id=288 from=@YourClubDebaterRedBot ...
[YourClubSocraticBot] received: message_id=289 from=@YourClubDebaterBlueBot ...
[YourClubJudgeBot] received: message_id=307 from=@YourClubDebaterBlueBot text="/verdict@YourClubJudgeBot"
Enter fullscreen mode Exit fullscreen mode

That is the proof that Telegram is doing the bot-to-bot delivery.

What Developers Can Build From This

The same pattern applies far beyond debate bots.

You can fork this into:

  • an incident-response room where triage, diagnosis, and comms bots collaborate,
  • a customer-support workflow where intake, policy, and escalation bots hand off visibly,
  • a code-review panel with architecture, security, and testing bots,
  • a classroom simulation where different characters challenge a student's answer,
  • an AI game where specialized bots play roles in the group,
  • a research workflow where agents critique each other's sources.

The reusable pieces are:

  • one Telegram bot identity per role,
  • visible messages as routing events,
  • exact username mentions,
  • explicit bot commands like /verdict@TargetBot,
  • shared state for turn control,
  • loop guards for safety,
  • routing logs for setup diagnosis.

Production Notes

This repo is local-first and intentionally simple. For production, consider:

  • persistent state with SQLite or Postgres,
  • structured logs,
  • health checks,
  • process supervision,
  • per-group state if running in multiple Telegram groups,
  • webhook mode if deploying to a server,
  • distributed locks if running multiple replicas.

Keep one rule intact if your goal is to showcase Bot-to-Bot Communication: do not add invisible local handoffs between bots. Let Telegram deliver the bot-authored messages, and use your application state only to decide whether a bot should answer.

Telegram Bot-to-Bot Communication turns the group chat into an orchestration surface. This project demonstrates that with a debate club, but the underlying pattern is broader: visible, auditable, role-based multi-agent workflows inside Telegram.

That is the piece worth building on.

Docs: https://core.telegram.org/bots/features#bot-to-bot-communication

Code and more: https://www.dailybuild.xyz/project/138-telegram-debate-club

Top comments (0)