Telegram Bots skill for Codex

Webhooks, keyboards and broadcasts that stay unbanned. Codex builds Telegram bots with one update source, a webhook guarded by secret_token, and callback queries that are always answered so buttons never spin. It escapes user text for HTML or MarkdownV2, keeps conversation state in persistent storage, throttles sends under Telegram's per-chat and global limits, and uses Telegram Stars for digital goods.

  • Integrations
  • $telegram-bots
  • 172 lines
  • 2 files
  • ~1,900 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- telegram-bots

Installs to ~/.agents/skills/telegram-bots/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $telegram-bots, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Builds Telegram bots on the Bot API with grammY or python-telegram-bot - BotFather setup, long polling versus webhooks with secret_token, commands, inline keyboards and callback queries, conversation state, MarkdownV2 and HTML formatting, rate limits and Telegram Stars payments. Use when creating a Telegram bot, switching from getUpdates to a webhook, buttons that keep spinning, Bad Request can't parse entities errors, 429 Too Many Requests, or broadcasting to many users.

Use it when

  • Creating a Telegram bot or adding commands and inline keyboards
  • Switching between getUpdates polling and webhooks
  • Sends fail with 429 Too Many Requests or parse entity errors
  • Selling digital goods in a bot with Telegram Stars

Use something else when

How to check the result

getWebhookInfo shows no pending backlog or errors, requests without the secret header are rejected, and user text with HTML or Markdown symbols renders without parse errors.

The mistake it prevents

Running a local polling copy with the production token, which deletes or competes with the production webhook and silently drops users' updates.

~/.agents/skills/telegram-bots/SKILL.md

--- name: telegram-bots description: Builds Telegram bots on the Bot API with grammY or python-telegram-bot - BotFather setup, long polling versus webhooks with secret_token, commands,… license: MIT ---

Done well means: one update source (polling or webhook, never both), webhook requests checked against secret_token, every callback query answered, user text escaped for the chosen parse mode, sends throttled below Telegram’s limits, and broadcasts only sent after the user confirms the audience and message.

Before writing code

  1. Read AGENTS.md and the dependency file. Pick or confirm the library:
StackDefault library
TypeScript or NodegrammY
Python (async)python-telegram-bot v21+ (or aiogram 3 if already used)
Raw HTTPhttps://api.telegram.org/bot<TOKEN>/<method> with JSON bodies
  1. The token from @BotFather goes in TELEGRAM_BOT_TOKEN. Anyone with it controls the bot; if it leaked, the user must revoke it in BotFather. Never put the token in logs or URLs you print.
  2. Choose update delivery:
SituationChoose
Local development, single long-running processLong polling (getUpdates)
Serverless or behind a load balancer, productionWebhook over HTTPS on port 443, 80, 88 or 8443
  1. Messages to real users, group broadcasts and payments are outward-facing: confirm first. Bots cannot message a user who has not started the bot.

Calls to the Bot API need network access; in a Codex sandbox, ask for approval, or test handlers with the library’s test utilities or stubbed contexts.

grammY skeleton

import { Bot, InlineKeyboard, webhookCallback } from "grammy";

const bot = new Bot(process.env.TELEGRAM_BOT_TOKEN!);

bot.command("start", async (ctx) => {
  const payload = ctx.match;                      // deep link: t.me/yourbot?start=<payload>
  await ctx.reply("Welcome. Choose a plan:", {
    reply_markup: new InlineKeyboard().text("Free", "plan:free").text("Pro", "plan:pro"),
  });
});

bot.callbackQuery(/^plan:(free|pro)$/, async (ctx) => {
  await ctx.answerCallbackQuery();                 // stops the loading spinner; do it first
  await ctx.editMessageText(`You chose ${ctx.match[1]}.`);
});

bot.catch((err) => console.error("update failed", err.ctx.update.update_id, err.error));

if (process.env.WEBHOOK) {
  app.post("/telegram", webhookCallback(bot, "express", { secretToken: process.env.TELEGRAM_SECRET }));
} else {
  bot.start();                                     // long polling
}

python-telegram-bot skeleton

from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.ext import Application, CallbackQueryHandler, CommandHandler, ContextTypes

async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    kb = InlineKeyboardMarkup([[InlineKeyboardButton("Free", callback_data="plan:free"),
                                InlineKeyboardButton("Pro", callback_data="plan:pro")]])
    await update.message.reply_text("Choose a plan:", reply_markup=kb)

async def on_plan(update: Update, context: ContextTypes.DEFAULT_TYPE):
    q = update.callback_query
    await q.answer()
    await q.edit_message_text(f"You chose {q.data.split(':', 1)[1]}.")

app = Application.builder().token(os.environ["TELEGRAM_BOT_TOKEN"]).build()
app.add_handler(CommandHandler("start", start))
app.add_handler(CallbackQueryHandler(on_plan, pattern=r"^plan:"))
app.run_polling()   # or app.run_webhook(listen="0.0.0.0", port=8443, url_path="telegram",
                    #   webhook_url="https://bot.example.com/telegram", secret_token=os.environ["TELEGRAM_SECRET"])

Webhook setup

curl -s "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/setWebhook" \
  -d url=https://bot.example.com/telegram \
  -d secret_token="$TELEGRAM_SECRET" \
  -d drop_pending_updates=true \
  --data-urlencode 'allowed_updates=["message","callback_query"]'
curl -s "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/getWebhookInfo"
  • Telegram sends the secret in the X-Telegram-Bot-Api-Secret-Token header; reject requests where it does not match. secret_token allows 1-256 characters of A-Z a-z 0-9 _ -.
  • Return 200 quickly. Non-2xx responses make Telegram retry and back up the queue; getWebhookInfo shows pending_update_count and last_error_message.
  • Setting a webhook disables getUpdates and the reverse (deleteWebhook). Running a polling dev copy against the production token steals or breaks production updates: use a separate dev bot.
  • Dedupe on update_id if your handler is not idempotent.

Formatting

Parse modeEscaping
HTML (recommended)Escape <, >, & in user text; tags b, i, u, s, code, pre, a, blockquote, tg-spoiler
MarkdownV2Backslash-escape all of _*[]()~>#+-={}.!, the pipe and the backtick outside entities

“Bad Request: can’t parse entities” almost always means unescaped user text. Text messages are capped at 4,096 characters, media captions at 1,024; split longer content.

Keyboards and state

  • callback_data is 1-64 bytes. Encode action:id; keep anything larger in storage.
  • Inline keyboards attach to a message; reply keyboards replace the user’s keyboard. Remove reply keyboards with remove_keyboard: true when done.
  • Multi-step flows: grammY conversations plugin or sessions with persistent storage; python-telegram-bot ConversationHandler with a persistence backend. In-memory state is lost on deploy and breaks with more than one instance.

Rate limits and broadcasts

  • Roughly 1 message per second per chat, 20 messages per minute per group, and about 30 messages per second overall for bulk sends. On 429 the response includes parameters.retry_after; wait that long.
  • Use grammY’s auto-retry and transformer-throttler plugins, or python-telegram-bot’s AIORateLimiter, rather than hand-written sleeps.
  • Broadcast: confirm audience and text with the user, send in throttled batches, record per-user results, and stop on users who blocked the bot (403 Forbidden) by marking them inactive.

Payments

Digital goods and services sold inside Telegram must use Telegram Stars: sendInvoice with currency XTR and an empty provider_token. Answer pre_checkout_query within 10 seconds, deliver on successful_payment, and store telegram_payment_charge_id for refunds (refundStarPayment). Physical goods can use a payment provider token from BotFather. Confirm before any live refund.

Gotchas

  • Buttons spin forever: the callback query was never answered.
  • Group bots in privacy mode only see commands, replies to them and mentions. Disable privacy in BotFather only when needed, then re-add the bot to the group.
  • chat_id for supergroups and channels is negative (-100...); store it as a 64-bit integer.
  • Editing a message to identical content returns “message is not modified”; ignore that error.
  • Files: getFile gives a path valid for at least one hour; bot downloads are limited to 20 MB on the cloud Bot API.

Verification

  • getWebhookInfo shows the right URL, pending_update_count near 0, no last_error_message.
  • A request without the secret header gets 401 or 403 from your endpoint.
  • Send user text containing <b> and _*[ through every reply path: no parse errors.
  • Each button press stops spinning and updates the message.

Report template

**Library:** grammY 1.x, webhook at /telegram (secret_token checked), allowed_updates: message, callback_query
**Commands:** /start (deep link payload), /plan; inline keyboard plan:<id>
**State:** sessions in Redis; conversations plugin for onboarding
**Limits:** auto-retry + throttler; broadcast job batches 25/s, marks 403 users inactive
**Tested:** dev bot token only; production webhook switch awaiting approval

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Telegram Bots"
  short_description: "Webhooks, keyboards and broadcasts that stay unbanned"
  default_prompt: "$telegram-bots Build a Telegram bot with grammY that offers plans through inline keyboard buttons"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem