---
title: The Distance From a Message to Your Code
date: '2026-07-21'
description: 'The same bot, a user''s message answered by code I wrote, costs a handful of lines on Telegram, a gateway and a bag of intents on Discord, and a whole headless browser pretending to be a phone on WhatsApp. Four libraries in, the pattern was clear, and the hard part was never the bot.'
author: Pablo Fernandez
tags:
  - Bots
  - Discord
  - Telegram
  - WhatsApp
  - TypeScript
source: 'https://www.pablofr.com/blog/distance-from-a-message-to-your-code'
---
Right now I have four tabs open, and they are all the same task. A person types a message. Some code I wrote reads it and decides what to say back. That is a bot, or a conversational agent if the code that decides is a language model. On Telegram it takes a handful of lines and a token I got by messaging a bot that makes bots. On Discord it takes a socket that stays open, a login, and a list of the kinds of event I am allowed to hear. On WhatsApp it takes a real copy of Chromium, running a real WhatsApp Web session, tapping keys like a person with very fast hands, because there is no front door and I have to climb in through the one the browser uses.

Same task, wildly different amount of machinery. I used to think "build a bot" was one problem. Sitting with these four libraries, [grammY](https://grammy.dev/) for Telegram, [discord.js](https://discord.js.org/) for Discord, [whatsapp-web.js](https://wwebjs.dev/) for WhatsApp, and Vercel's [Chat SDK](https://chat-sdk.dev/) for all of them at once, I think it is two problems, and the split explains why the same idea costs a browser on one platform and a token on another.

## Two problems wearing one name

A bot is transport plus an agent. Transport is the plumbing: take a message off the platform, put a reply back on it. The agent is the decision: given the message, what do I say. For years the agent was a switch on the first word, `!ping` returns `pong`. Now it is often a call to a language model, which is why "bot" and "conversational agent" have collapsed into the same shape.

Here is the part that reorders everything. The agent half has become the easy, portable half. Calling a model looks the same whether the message came from Telegram or Discord, and Vercel's [AI SDK](https://ai-sdk.dev/) streams the answer back the same way regardless. The hard, platform-shaped half is transport. Three of these four libraries are transport for exactly one platform. The fourth tries to be transport for all of them.

```
               a user sends a message
                          │
                          ▼
   ┌─────────────────────────────────────────────┐
   │  transport: get the message in and out      │
   │  grammY · discord.js · whatsapp-web.js      │
   └──────────────────────┬──────────────────────┘
                          │
                          ▼
   ┌─────────────────────────────────────────────┐
   │  the agent: decide what to say              │
   │  your code, or an LLM via the AI SDK        │
   └─────────────────────────────────────────────┘
```

*A bot is two problems: move the message, then decide the reply. These libraries own the first half.*

So the interesting question is not "which library writes the agent". None of them do; you do, or a model does. It is "how far does a message have to travel to reach my code", and that distance is set by the platform, not by me.

## Telegram hands you an API

Telegram decided years ago that it wanted bots. It publishes a full, free, documented [Bot API](https://core.telegram.org/bots/api), you get a token by messaging @BotFather, and you are talking to real users a minute later. No review, no business account, no dedicated phone number.

grammY is what a library looks like when the platform underneath it is friendly. The whole thing:

```typescript:telegram-bot.ts {4}
import { Bot } from "grammy";

const bot = new Bot(""); // token from @BotFather
bot.on("message", (ctx) => ctx.reply("Hi there!"));
bot.start();
```

That runs. `bot.start()` opens a long-polling loop against Telegram; in production you swap it for a webhook and nothing else changes. Above that floor, grammY gives you middleware in the Express sense, every update flows through a stack you compose, sessions for per-user state, and a plugin set for the Telegram surface: inline keyboards, menus, conversations, rate limiting. It tracks the Bot API closely; the current line speaks version 10.2, shipped in July. When Telegram adds a feature, grammY tends to have it within days, because there is a real spec to follow.

The thing to notice is how little of that code is about Telegram. One import, one token, one handler. That is the whole cost when a platform hands you an API on purpose.

## Discord hands you a gateway and a bag of intents

Discord also wants bots, and its API is official and free, but the shape is heavier, because a Discord bot is a standing presence in real-time servers, not a request-and-reply endpoint. You register an application, get a bot token, and open a [Gateway](https://docs.discord.com/developers/events/gateway) connection: a websocket that stays open and streams events. Before Discord sends you anything, you declare intents, the categories of event you want.

```javascript:discord-bot.js {7}
import { Client, GatewayIntentBits, Events } from 'discord.js';

const client = new Client({
  intents: [
    GatewayIntentBits.Guilds,
    GatewayIntentBits.GuildMessages,
    GatewayIntentBits.MessageContent,
  ],
});

client.on(Events.MessageCreate, (message) => {
  if (message.content === '!ping') message.reply('pong');
});

client.login(process.env.TOKEN);
```

That last intent, `MessageContent`, is the one that catches people. It is privileged: Discord will not give your bot the text of messages until you turn it on in the developer portal, and once the bot is in more than a hundred servers you have to apply and be approved to keep it. Discord did this in 2022 to stop bots reading every message by default, which is reasonable, and also the reason a first bot silently receives empty message bodies until the developer finds the toggle.

```
   intents you declare        events you receive
   ──────────────────────     ───────────────────────
   Guilds                     server metadata
   GuildMessages              message events
   MessageContent (gated)     the actual message text
```

*Discord sends your bot only the event categories it declares, and the message text is a gated intent.*

discord.js is the mature end of this whole survey. It is still on the v14 line, it matches Discord's v10 API, and it wraps the parts that get hard at scale: sharding when one process cannot hold every server, a REST client for slash commands, voice, components. The cost compared to grammY is real and honest. The platform wants you there; it just has more surface, so the library does too.

## WhatsApp makes you pretend to be a browser

Then there is WhatsApp, which does not want your bot.

There is no open Bot API for a normal WhatsApp account. There are two doors, and both are awkward. The official one is the [WhatsApp Cloud API](https://developers.facebook.com/docs/whatsapp/cloud-api), part of Meta's WhatsApp Business Platform. It is a real API, but it is built for businesses, not for you messing about on a Saturday. You need a Meta business account, a phone number dedicated to it that can no longer be used in the normal app, message templates approved in advance for anything you send first, and you can only reply freely inside a 24-hour window after the user writes to you. It is the right tool for a company sending order confirmations at volume, and heavy going for anything smaller.

The other door is whatsapp-web.js, and it is the reason this section exists. It does not call an API at all. It launches a headless Chromium with [Puppeteer](https://pptr.dev/), loads the actual WhatsApp Web page, logs in the way you would, and drives the page to read and send messages.

```javascript:whatsapp-bot.js {6}
const { Client, LocalAuth } = require('whatsapp-web.js');
const qrcode = require('qrcode-terminal');

const client = new Client({ authStrategy: new LocalAuth() });

client.on('qr', (qr) => qrcode.generate(qr, { small: true }));
client.on('ready', () => console.log('Client is ready!'));
client.on('message', (msg) => {
  if (msg.body === '!ping') msg.reply('pong');
});

client.initialize();
```

The `qr` event hands you a string you print as a QR code and scan with your phone, the same Linked Devices flow a second laptop would use. `LocalAuth` saves that session to disk so you do not rescan on every restart; `RemoteAuth` puts it in a database so the bot can move between machines. And `message` fires because a browser somewhere is watching a web page on your behalf.

It works, it is pleasant to use, and its own guide tells you plainly what you are signing up for:

> This project is not affiliated, associated, authorized, endorsed by, or in any way officially connected with WhatsApp or any of its subsidiaries or its affiliates. [...] it is not guaranteed you will not be blocked by using this method. WhatsApp does not allow bots or unofficial clients on their platform, so this shouldn't be considered totally safe.

That is the whole WhatsApp situation in two sentences. The library is not doing anything sneaky; it automates the one interface WhatsApp does leave open to a normal account, the web client, and WhatsApp is within its rights to notice and ban the number. Every property that makes the other two libraries dull, a documented API, a stable contract, a blessing from the platform, is missing here, and a headless browser stands in for all of it.

```
   ┌── official ───────────────────────────────────┐
   │  Cloud API                                    │
   │  Meta business · a dedicated number           │
   │  templates approved up front                  │
   │  free replies only in a 24-hour window        │
   └───────────────────────────────────────────────┘

   ┌── unofficial ─────────────────────────────────┐
   │  whatsapp-web.js                              │
   │  Puppeteer drives the real WhatsApp Web page  │
   │  a QR-linked account                          │
   │  "not totally safe"                           │
   └───────────────────────────────────────────────┘
```

*The two ways into WhatsApp, both awkward: the gated official API, or a browser driven like a person.*

## One codebase, every platform

The fourth tab is the odd one, on purpose. Vercel's Chat SDK is not another single-platform library. It is a universal transport layer: `npm install chat`, write your handler once, and deploy it to Slack, Discord, Telegram, WhatsApp, Google Chat, Microsoft Teams, and a growing list of others behind one API. (If you knew chat-sdk.dev as Vercel's Next.js chatbot template, that moved to chatbot.ai-sdk.dev; the domain now hosts this.)

The handler is event-shaped and platform-blind:

```typescript:handler.ts
bot.onNewMention(async (thread) => {
  await thread.subscribe();
  await thread.post("Sure! Here's a quick summary...");
});
```

A `thread` is the same object whether it started in a Discord channel or a WhatsApp chat, and `post` renders to each platform's native format. Rich replies are written as JSX cards that Chat SDK translates per platform. It accepts AI SDK text streams directly, so the agent half plugs straight in: the model streams, Chat SDK delivers, and one handler answers on every platform at once. The naming is not an accident either. The AI SDK calls itself the "universal AI layer"; the Chat SDK calls itself the "universal chat layer". Same company, same shape, one for the model and one for the transport.

The trade is the obvious one. A universal layer can only expose what the platforms share, so you reach for the lowest common denominator and lose the platform-specific surface the single-platform libraries live for: Discord's voice and slash-command autocomplete, Telegram's inline mode, the fiddly native controls. And it does not repeal the WhatsApp problem. Chat SDK's WhatsApp adapter still walks through one of the same two doors, the Cloud API or a browser session; the choice is just made for you, behind the adapter, where you can no longer see it.

| Library | Platform | Official API? | How it reaches the platform |
| --- | --- | --- | --- |
| grammY | Telegram | Yes, open and free | Bot token, long polling or webhook |
| discord.js | Discord | Yes, with gated intents | Bot token, a gateway socket kept open |
| whatsapp-web.js | WhatsApp | No | A headless browser driving WhatsApp Web |
| Chat SDK | Many, all three included | Depends on the adapter | One handler, one adapter per platform |

## Where each one is the wrong choice

None of these is a bad library. Each is the wrong one somewhere.

- **whatsapp-web.js will get a number banned eventually, if you push it.** It is right for personal automation, a home project, a prototype, a bot for a group of friends. It is wrong for a business running support at volume: it breaks whenever WhatsApp ships a change to the web client, it does not fan out across many numbers, and the ban is a question of when. For that, swallow the Cloud API.
- **discord.js is too much if all you want is to post.** If your bot only needs to drop a message into a channel, a plain Discord webhook does that with one HTTP request, no gateway, no intents, no process to keep alive. Reach for the library when you need to listen, not just speak.
- **grammY is single-platform, and honest about it.** If your users are only on Telegram, nothing here beats it. If they are on three platforms, you are either running three of these libraries side by side or reaching for the fourth.
- **Chat SDK is the youngest by years.** It went public in February 2026 and is still in beta, and it buys portability with the lowest common denominator. If you need one platform done deeply, the native library is the safer pick. Chat SDK earns its place when "every platform, one codebase" is the actual requirement.

## The hard part was never the bot

Put the four side by side and the pattern is not about code quality; all four are good. It is about welcome.

```
   Telegram  ├─ token ──────────────────────────────────▶ code
   Discord   ├─ token · gateway · intents ──────────────▶ code
   WhatsApp  ├─ QR · a headless browser · WhatsApp Web ─▶ code
```

*The distance a message travels to reach your code, set by how much each platform wants a bot.*

Telegram says yes and hands you an API, so grammY is a handful of lines. Discord says yes with rules, so discord.js is a gateway and a bag of intents. WhatsApp says no, so whatsapp-web.js runs a browser and pretends to be a person at a keyboard, and tells you in writing that it might get caught. Chat SDK hides all three behind one handler, and folds WhatsApp's no into an adapter so you do not have to look at it.

The agent, the part everyone now calls the interesting bit, the language model deciding what to say, is the same on all four and getting more portable every month. The transport is where the differences live, and the differences are not technical. They are a platform's answer to one question: do we want your bot here. So you are not really choosing a library. You are choosing how much of a platform's no you are willing to carry in your own code, and how much you would rather pay someone to hide.

## References

- [whatsapp-web.js: the guide](https://wwebjs.dev/guide/) — source of the affiliation and ban-risk disclaimer
- [Vercel: Introducing npm i chat](https://vercel.com/changelog/chat-sdk) — Chat SDK, public beta, February 2026
- [Discord: Gateway and intents](https://docs.discord.com/developers/events/gateway) — the privileged Message Content intent
