Create the bot with @BotFather
Open a chat with @BotFather (check the blue verification mark), send
/newbot, choose a display name and a username that ends inbot. BotFather replies with a token.Figure 2. Creating the bot with @BotFather (illustration). The last message contains the token: treat it like a password. Create the project
mkdir mi-bot && cd mi-bot npm init -y npm pkg set type=module npm install grammytype=modulelets you useimportand top-levelawait.Store the token outside the code
printf 'BOT_TOKEN=123456789:replace-me\n' > .env printf '.env\nnode_modules\n' > .gitignorePaste your real token in
.env. If it ever leaks, use/revokein BotFather to get a new one.Write the bot
bot.jsimport { Bot } from "grammy"; const token = process.env.BOT_TOKEN; if (!token) throw new Error("BOT_TOKEN is missing (see .env)"); const bot = new Bot(token); bot.command("start", (ctx) => ctx.reply(`Hola, ${ctx.from?.first_name ?? "!"} Send /help.`)); bot.command("help", (ctx) => ctx.reply("/start - greet\n/help - this list\n/ping - check the bot")); bot.command("ping", (ctx) => ctx.reply("pong")); // Echo any other text message bot.on("message:text", (ctx) => ctx.reply(`You said: ${ctx.message.text}`)); // Never crash on a handler error; never log the token bot.catch((err) => console.error("handler error:", err.error?.message ?? err.message)); await bot.api.setMyCommands([ { command: "start", description: "Start the bot" }, { command: "help", description: "Show commands" }, { command: "ping", description: "Check the bot is alive" }, ]); bot.start({ onStart: (me) => console.log(`@${me.username} running with long polling`) }); process.once("SIGINT", () => bot.stop()); process.once("SIGTERM", () => bot.stop());Handlers run in order: commands first, then the generic text handler.
bot.catchkeeps one bad update from stopping the bot.Run it and test from your phone
node --env-file=.env bot.jsFigure 3. Expected output after starting the bot (illustration). Open your bot by username, press Start and try
/ping. Stop it with Ctrl+C.Keep it running
Your laptop will not stay on forever. Deploy the same code to a host that runs 24/7: follow the Railway tutorial (no server to manage) or the Vultr VPS tutorial (full control). Compare options in the hosting comparator.
Troubleshooting
- 401 Unauthorized: the token is wrong or was revoked. Copy it again from BotFather.
- 409 Conflict: another process is polling with the same token, or a webhook is set. Stop the other copy; grammY's start() removes a webhook only if you call deleteWebhook first.
- Bot does not answer in groups: by default privacy mode only delivers commands and replies to the bot. Change it with /setprivacy in BotFather if you really need all messages.
Official docs: grammY getting started, Telegram Bot API.