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
Forked Aeon? Change
aeonfun/aeonin the button URL above toyour-username/your-forkso 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'sapps/webhook/wrangler.tomlfirst.
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)
| Secret | Required | Notes |
|---|---|---|
TELEGRAM_BOT_TOKEN | yes | From @BotFather. |
TELEGRAM_CHAT_ID | yes | Only messages from this chat are relayed; everything else is dropped. |
TELEGRAM_ALLOWED_USER_ID | for groups | The 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_SECRET | yes | Random string; pass the same value to setWebhook as secret_token. The Worker rejects every update with 403 until it's set. |
GITHUB_REPO | yes | owner/repo of your Aeon fork, e.g. aeonfun/aeon — not the worker repo the deploy button creates. |
GITHUB_TOKEN | yes | Fine-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.