The 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 for Telegram, discord.js for Discord, whatsapp-web.js for WhatsApp, and Vercel's Chat SDK 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 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.
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, 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:
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 connection: a websocket that stays open and streams events. Before Discord sends you anything, you declare intents, the categories of event you want.
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.
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, 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, loads the actual WhatsApp Web page, logs in the way you would, and drives the page to read and send messages.
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.
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:
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.
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 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 source of the affiliation and ban-risk disclaimer
- Vercel: Introducing npm i chat Chat SDK, public beta, February 2026
- Discord: Gateway and intents the privileged Message Content intent
Related articles
The request that takes a payment is four lines. Everything that makes it safe to build a business on, so a retry never double-charges and a dropped webhook never loses an order, is the actual work. How I shaped Paybit's crypto payments API on the pattern Stripe proved.
Exact-match blocklists are blind to a bot farm that mutates its fingerprint by one field per request. This is how MinHash and locality-sensitive hashing turn "have I seen this exact client" into "have I seen something shaped like this", and catch the farm in sublinear time.
Object storage is the cheapest durable bytes you can buy and the worst place to search a million vectors. I forked VecPuff and taught its index to keep one bit per dimension resident, so a query pulls a full vector from S3 only when the cheap guess is too close to call.