Skip to main content

Overview

The bot uses Telegraf v4 as the framework layer over the Telegram Bot API. Telegraf handles command routing, context injection, and the long-polling connection.
  • Framework: Telegraf v4
  • Transport: Long polling (no webhook, no inbound port required)
  • Authentication: Bot token from @BotFather, provided via BOT_TOKEN environment variable
  • Message timeout: 15 seconds per send (telegramTimeoutMs)
Users must send /start before the bot can send them messages. Telegram blocks bots from initiating conversations with users who have never messaged them.

Bot instance patterns

Creating the bot

import { Telegraf } from 'telegraf';

const bot = new Telegraf(botToken);

Registering command handlers

// /start — subscribe user and send welcome message
bot.start(async (ctx) => {
  saveChatId(ctx.chat.id);
  await ctx.reply('...');
});

// /deals — fetch and display current deals
bot.command('deals', async (ctx) => {
  await ctx.sendChatAction('typing');
  // ...
  await ctx.reply(message, { parse_mode: 'HTML' });
});

Sending replies from command handlers

// Plain reply
await ctx.reply(text);

// HTML-formatted reply
await ctx.reply(text, { parse_mode: 'HTML' });

// Typing indicator (shown while fetching deals)
await ctx.sendChatAction('typing');
parse_mode: 'HTML' is used instead of Markdown throughout the codebase. Game titles frequently contain characters like _ and [ that silently break Telegram’s Markdown parser. HTML tags (<b>, <a>) are unambiguous and safe.

Programmatic send (notifier)

The daily broadcast uses Telegram directly instead of going through a bot context, since there is no incoming message to reply to:
import { Telegram } from 'telegraf';

const telegram = new Telegram(config.telegram.botToken);

await telegram.sendMessage(chatId, message, { parse_mode: 'HTML' });
Each send is wrapped in a Promise.race with a 15-second timeout to prevent a hung Telegram connection from blocking the entire broadcast loop.

Registered commands

CommandDescription
/startSubscribe to daily notifications. Saves chat ID and sends welcome message.
/dealsFetch and display the current curated deal list on demand. Rate-limited to once per 45 seconds per chat.
/stopUnsubscribe from daily notifications. Removes the chat ID from the subscriber list.
/helpDisplay the command list and bot description.

Broadcast error handling

During the daily broadcast, the notifier checks each send error to determine whether the chat ID should be permanently removed:
Error codeDescriptionAction
403Bot was blocked by the userRemove chat ID
400 + "chat not found"Chat was deleted or the ID is invalidRemove chat ID
Any other errorTransient failure (network, rate limit, etc.)Log and continue, keep chat ID
Chat IDs that trigger permanent errors are collected and removed in a single batch write after the broadcast completes.
function isPermanentError(err: unknown): boolean {
  const code = (err as any)?.response?.error_code;
  const desc: string = (err as any)?.response?.description ?? '';
  return code === 403 || (code === 400 && desc.toLowerCase().includes('chat not found'));
}

Chat ID persistence

Subscriber chat IDs are stored in data/chat_ids.json as a flat JSON array of integers. Reads and writes go through write-file-atomic to prevent corruption from concurrent writes or process crashes.
// Save on /start
saveChatId(ctx.chat.id);

// Remove on /stop or permanent error
removeChatId(chatId);

Build docs developers (and LLMs) love