Aeon Telegram webhook

August 23, 2026 · View on GitHub

Default polling checks Telegram every 5 minutes. Deploy this Cloudflare Worker as a Telegram webhook to drop that to ~1 second: the Worker classifies each update and relays it to your Aeon fork via a GitHub repository_dispatch, which fires the Messages workflow immediately.

It routes the full inbound feature set — slash commands, inline-button taps, and reply follow-ups — not just plain messages (see docs/telegram-commands.md). Redeploy the Worker (npx wrangler deploy) after updating src/worker.js to pick up changes.

Each user deploys it into their own Cloudflare account. There's no shared relay and no credential custody — your bot token and GitHub PAT live only in your Worker's secrets. The replay guard also needs one small piece of Cloudflare infrastructure: a Workers KV namespace in that account.

Deploy

Deploy to Cloudflare

Forked Aeon? Change aeonfun/aeon in the button URL above to your-username/your-fork so it deploys from your repo. (The button requires a public source repo.) The button is no longer deploy-and-done by itself: create the required KV namespace and put its ID in your fork's apps/webhook/wrangler.toml first.

Or from a clone:

cd apps/webhook
npm install
npx wrangler kv namespace create REPLAY_GUARD
npx wrangler kv namespace create REPLAY_GUARD --preview  # for local wrangler dev
# Paste the returned ids into [[kv_namespaces]] in wrangler.toml.
npx wrangler deploy

The KV namespace is required for replay protection on the normal *.workers.dev deployment target. Keep the production id and local preview_id separate; do not use a preview namespace for a deployed Worker.

Configure

The deploy button prompts for all five values during the wizard (declared in .dev.vars.example) and stores them as encrypted Worker secrets — the Worker comes out configured. Deploying from a clone instead? Set them via the CLI:

npx wrangler secret put TELEGRAM_BOT_TOKEN        # bot token from @BotFather
npx wrangler secret put TELEGRAM_CHAT_ID          # your chat id (only this chat is allowed)
npx wrangler secret put TELEGRAM_ALLOWED_USER_ID  # optional; your user id (required for group chats — see table)
npx wrangler secret put TELEGRAM_WEBHOOK_SECRET   # shared secret for webhook verification (required)
npx wrangler secret put GITHUB_REPO               # owner/repo of your Aeon fork
npx wrangler secret put GITHUB_TOKEN              # GitHub PAT (see scopes below)
SecretRequiredNotes
TELEGRAM_BOT_TOKENyesFrom @BotFather.
TELEGRAM_CHAT_IDyesOnly messages from this chat are relayed; everything else is dropped.
TELEGRAM_ALLOWED_USER_IDfor groupsThe only user allowed to command the bot. Defaults to TELEGRAM_CHAT_ID, which is correct for a 1:1 DM (there chat.id == user.id). If TELEGRAM_CHAT_ID is a group, set this to your numeric user id — otherwise any group member can drive the bot by tapping a posted button. Left unset in a group, buttons fail closed. Get your id from @userinfobot.
TELEGRAM_WEBHOOK_SECRETyesRandom string; pass the same value to setWebhook as secret_token. The Worker rejects every update with 403 until it's set.
GITHUB_REPOyesowner/repo of your Aeon fork, e.g. aeonfun/aeon — not the worker repo the deploy button creates.
GITHUB_TOKENyesFine-grained PAT scoped to your fork with Contents: read/write and Actions: read/write, or a classic token with repo.

To edit secret values later: Cloudflare dashboard → Workers & Pages → your worker → Settings → Variables and secrets. To manage the replay namespace later, use Workers & Pages → KV or the wrangler kv namespace commands.

Point Telegram at the Worker

Register your Worker URL as the bot's webhook:

curl "https://api.telegram.org/bot<YOUR_BOT_TOKEN>/setWebhook?url=https://aeon-telegram-webhook.<your-subdomain>.workers.dev&secret_token=<YOUR_WEBHOOK_SECRET>"

Verify it took:

curl "https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getWebhookInfo"

Messages now arrive in ~1s. To go back to polling, clear the webhook:

curl "https://api.telegram.org/bot<YOUR_BOT_TOKEN>/deleteWebhook"

How it coexists with polling

A webhook and getUpdates polling are mutually exclusive — once a webhook is set, getUpdates returns 409 Conflict. The Messages workflow's poller calls getWebhookInfo first and skips the Telegram branch when a webhook is active, so the two never fight. Delivery then runs entirely through this Worker → repository_dispatch.

Dedupe in webhook mode is by the update_id carried in the dispatch payload and stored in KV for five minutes. The Worker returns 200 once GitHub accepts the dispatch (so Telegram normally never redelivers) and a non-2xx only when the dispatch genuinely failed (so Telegram retries). KV has no atomic check-and-set, so two simultaneous redeliveries can still both dispatch; the guard is bounded deduplication, not an exactly-once guarantee.

What it does

Telegram → POST update → Worker
  ├─ verify method + secret token
  ├─ callback_query (button tap) → answerCallbackQuery → dispatch telegram-callback
  ├─ ignore (200) anything not from the owner chat AND owner user (a stranger's private DM gets "This bot is private.")
  ├─ reply to a [skill::intent] prompt   → dispatch telegram-reply
  ├─ /slash command or /start deep link  → dispatch telegram-command
  └─ plain text                          → dispatch telegram-message
       → GitHub Actions: `route` job (commands/callbacks/replies, no LLM) or
         `run` job (plain text → Claude) acts on it (~1s)

The Worker source is src/worker.js — small, no build step.