Celebrity Death Bot is a Cloudflare Worker that runs on a scheduled Cron trigger. It checks the latest Wikipedia page of notable deaths for the current year and notifies subscribed users when a new entry appears.
- Fetches Wikipedia: The worker retrieves the current month, and early in the month also the previous month, using
https://en.wikipedia.org/wiki/Deaths_in_<Month>_<Year>. - Parses entries: From the target month section, it extracts each person's name, Wikipedia path, age, description and cause of death.
- Stores in D1: Entries are stored in a D1 database. Items already in the database are ignored.
- LLM evaluation: Newly discovered entries are evaluated via OpenAI (default) or Replicate (optional). Replicate uses a webhook callback; OpenAI is evaluated inline.
- Telegram notifications: When the callback provides results, the worker sends a message via Telegram to subscribed chats.
- X (Twitter) posting: If X OAuth 2.0 is connected, each approved result is also posted to the timeline.
If OPENAI_WEBHOOK_SECRET is set, OpenAI requests are sent in background mode and results are processed via POST /openai/webhook. Configure the webhook in the OpenAI dashboard to subscribe to response.completed.
The worker expects the following bindings and environment variables:
DB– D1 database binding used to persist entries.OPENAI_API_KEY– OpenAI API key used for Responses API calls (default provider).OPENAI_WEBHOOK_SECRET– OpenAI webhook signing secret (optional; enables background Responses +/openai/webhookprocessing). The webhook route returns503when it is absent.LLM_PROVIDER– Optional provider override:openai(default) orreplicate.REPLICATE_API_TOKEN– API token for Replicate (required only whenLLM_PROVIDER=replicate).TELEGRAM_BOT_TOKEN– Telegram bot token used for sending messages.TELEGRAM_ALERT_CHAT_ID– Optional direct Telegram chat ID(s) for operational job alerts. Comma/space/semicolon delimited. Falls back toTELEGRAM_CHAT_IDSwhen present.BASE_URL– Public URL of the worker, used when building Replicate webhook URLs.REPLICATE_WEBHOOK_SECRET– Replicate webhook signing secret. Required when using Replicate; unsigned callbacks are never accepted.MANUAL_RUN_SECRET– Secret token required to call the manual/runendpoint.TELEGRAM_WEBHOOK_SECRET– Required when configuring the Telegram webhook; the route fails closed when it is absent.ALLOWED_GOOGLE_ACCOUNTS– Comma/space/semicolon-delimited administrator email allowlist. Store it as a secret rather than a committed Wrangler variable.GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET– Google OpenID Connect credentials for the administrator dashboard.SESSION_HMAC_KEY– At least 32 random bytes used to sign the 12-hour administrator session and OAuth state.OAUTH_CALLBACK_URL– Canonical Google callback URL, normallyhttps://celebritydeathbot.com/oauth/callback.JOB_ALERT_MIN_SCANNED– Optional minimum parsed rows before a completed cron run is considered suspicious. Defaults to1.JOB_ALERT_STALE_HOURS– Optional maximum age of the latest inserted death row before a completed cron run sends a stale-data alert. Defaults to24.JOB_ALERT_COOLDOWN_MINUTES– Optional duplicate-alert cooldown per alert type. Defaults to360.- X (Twitter) OAuth 2.0 (PKCE) configuration:
X_CLIENT_ID– OAuth 2.0 client ID for your X AppX_CLIENT_SECRET– (optional) client secret; included when presentX_ENC_KEY– base64 AES-256-GCM key to encrypt tokens in D1POST_TO_X– Set totrueto automatically post approved deaths to X. Missing or any other value disables automatic X posting.X_POST_INCLUDE_WIKIPEDIA_LINK– Set totrueto append Wikipedia links to automatic X posts. Missing or any other value omits the link.
When connected once via OAuth 2.0, the worker stores and refreshes tokens and posts via POST /2/tweets with a Bearer token.
Store secrets with Wrangler (once per environment):
wrangler secret put OPENAI_API_KEY
wrangler secret put OPENAI_WEBHOOK_SECRET # optional (OpenAI webhooks)
wrangler secret put REPLICATE_API_TOKEN # only if using Replicate
wrangler secret put TELEGRAM_BOT_TOKEN
wrangler secret put TELEGRAM_WEBHOOK_SECRET # required when using Telegram commands
wrangler secret put TELEGRAM_ALERT_CHAT_ID # optional direct ops alerts
wrangler secret put MANUAL_RUN_SECRET
wrangler secret put REPLICATE_WEBHOOK_SECRET # required for Replicate
wrangler secret put ALLOWED_GOOGLE_ACCOUNTS
wrangler secret put GOOGLE_CLIENT_ID
wrangler secret put GOOGLE_CLIENT_SECRET
wrangler secret put SESSION_HMAC_KEYThe default schedule runs hourly at minute 5 (see wrangler.jsonc). Adjust as needed.
Install dependencies and start the development server using Wrangler:
npm install
npm run devDeploy the worker with:
npm run deploy-
POST /run– Manually trigger the job. RequiresMANUAL_RUN_SECRET.-
Auth: Send the secret in the
Authorizationheader:Authorization: Bearer <MANUAL_RUN_SECRET> -
Full run (curl):
curl -X POST \ -H "Authorization: Bearer $MANUAL_RUN_SECRET" \ https://<your-worker>/run
-
Targeted reprocess by IDs: Re-evaluate specific
deaths.idrows via the configured LLM provider (OpenAI default). These IDs are explicitly flagged in the prompt as MUST INCLUDE so the model accepts them as notable.curl -X POST \ -H "Authorization: Bearer $MANUAL_RUN_SECRET" \ -H "Content-Type: application/json" \ -d '{"ids":[123,124,130]}' \ https://<your-worker>/run
Notes: The worker also avoids marking these IDs as
llm_result = 'no'if the model unexpectedly omits them; they'll remain pending so you can retry. -
Targeted reprocess by wiki_path(s): If you prefer specifying the Wikipedia ID(s) instead of database IDs, send
wiki_pathsor a singlewiki_path. Accepts raw IDs likeJane_Doe, full article paths like/wiki/Jane_Doe, or edit/redlink URLs like/w/index.php?title=Jane_Doe&action=edit&redlink=1.# Single curl -X POST \ -H "Authorization: Bearer $MANUAL_RUN_SECRET" \ -H "Content-Type: application/json" \ -d '{"wiki_path":"Jane_Doe"}' \ https://<your-worker>/run # Multiple curl -X POST \ -H "Authorization: Bearer $MANUAL_RUN_SECRET" \ -H "Content-Type: application/json" \ -d '{"wiki_paths":["Jane_Doe","/wiki/John_Smith","/w/index.php?title=Greg_O%2727Connell&action=edit&redlink=1"]}' \ https://<your-worker>/run
Behavior mirrors the ID-based mode: These paths are treated as MUST INCLUDE in the LLM prompt and won’t be auto-marked
noif omitted. -
Retry pending LLM batches: Re-evaluate rows stuck with
llm_result = 'pending'(useful after LLM outages). For OpenAI (default), omitpending_limitto drain all pending rows; addpending_limitto cap volume. For Replicate, the default remains 120 rows per call, split into batches of 30 for safer prompts.curl -X POST \ -H "Authorization: Bearer $MANUAL_RUN_SECRET" \ -H "Content-Type: application/json" \ -d '{"retry_pending":true}' \ https://<your-worker>/run
To cap the batch size, include
pending_limit:curl -X POST \ -H "Authorization: Bearer $MANUAL_RUN_SECRET" \ -H "Content-Type: application/json" \ -d '{"retry_pending":true,"pending_limit":150}' \ https://<your-worker>/run
-
Use a different provider/model (full run, retry, or targeted reprocess): Include
providerin the JSON body to switch betweenopenaiandreplicate. OpenAI defaults togpt-5.6-luna; use model IDs likegpt-5.6-lunafor explicit overrides. For Replicate, use model paths likeopenai/gpt-5-miniorgoogle/gemini-3-pro.curl -X POST \ -H "Authorization: Bearer $MANUAL_RUN_SECRET" \ -H "Content-Type: application/json" \ -d '{"retry_pending":true,"provider":"replicate","model":"google/gemini-3-pro"}' \ https://<your-worker>/run
-
-
POST /replicate/callback– Endpoint for Replicate webhook callbacks (only when usingLLM_PROVIDER=replicate; always verified via HMAC). -
POST /openai/webhook– Endpoint for OpenAI webhook callbacks (enable by settingOPENAI_WEBHOOK_SECRETand configuring the webhook in the OpenAI dashboard forresponse.completed,response.failed, andresponse.cancelled). -
POST /telegram/webhook– Telegram webhook endpoint for subscription commands.TELEGRAM_WEBHOOK_SECRETis mandatory and Telegram must send it inX-Telegram-Bot-Api-Secret-Token. -
GET /health– Simple health check returningok.
The POST /run endpoint is rate-limited to protect the worker from abuse and accidental overload.
- Default limits: 3 requests per 60 seconds and 20 requests per hour per client IP.
- Configuration: Override with the env var
RUN_RATE_LIMITSas a comma-separated list of<windowInSeconds>:<limit>pairs.- Example:
RUN_RATE_LIMITS="60:5,3600:50"sets 5/minute and 50/hour.
- Example:
- Behavior: If a limit is exceeded, the endpoint responds with
429 Too Many Requestsand includes aRetry-Afterheader (seconds). - Logging: Exceed events are logged with IP and window info for operational visibility.
Schema
- The limiter uses a small D1 table
rate_limitsfor counters. - Apply the migration:
wrangler d1 execute celebrity-death-bot --file=./migrations/002_create_rate_limits.sql
Scheduled jobs can send direct Telegram alerts when:
runJob(...)throws, including D1 startup/reset errors, Wikipedia fetch failures, or LLM enqueue failures.- A completed run parses fewer than
JOB_ALERT_MIN_SCANNEDentries, catching parser/page-shape regressions. - A completed run finds the latest inserted death row older than
JOB_ALERT_STALE_HOURS, catching silent stale-data failures.
Alerts are sent directly to TELEGRAM_ALERT_CHAT_ID using the bot token and do not read the subscriber table, so D1 outages can still be reported. Duplicate alerts are throttled with KV using JOB_ALERT_COOLDOWN_MINUTES.
To get your Telegram chat ID, send a message to the bot and inspect the webhook payload or query the subscribers table after /subscribe.
The worker stores parsed entries and subscriber/chat state in D1.
-
Death entries:
migrations/003_create_deaths.sqlwrangler d1 execute celebrity-death-bot --file=./migrations/003_create_deaths.sql
-
Telegram subscribers:
migrations/001_create_subscribers.sqlwrangler d1 execute celebrity-death-bot --file=./migrations/001_create_subscribers.sql
-
Security hardening (one-time X OAuth state ownership and webhook replay ledger):
migrations/006_security_hardening.sql. Apply migrations before deploying this code so webhook processing never runs against the old schema:wrangler d1 migrations apply celebrity-death-bot --remote
Configure your bot to send updates to the Worker and let users manage subscriptions via chat.
-
Set the Telegram webhook URL and provide a secret token that Telegram will send in header
X-Telegram-Bot-Api-Secret-Tokenwith every webhook request:export BASE_URL=<your-worker-url> export TELEGRAM_BOT_TOKEN=<your-token> export TELEGRAM_WEBHOOK_SECRET=<your-secret> # allowed chars: A-Z a-z 0-9 _ - # Store secret in Worker wrangler secret put TELEGRAM_WEBHOOK_SECRET <<< "$TELEGRAM_WEBHOOK_SECRET" # Configure webhook and secret_token on Telegram side curl -X POST \ "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/setWebhook" \ -d url="${BASE_URL}/telegram/webhook" \ -d secret_token="${TELEGRAM_WEBHOOK_SECRET}"
-
Supported commands (send in a DM to your bot):
/startor/subscribe– Subscribe this chat to alerts./stopor/unsubscribe– Unsubscribe this chat (we delete your chat ID)./status– Show current subscription status./commands– Show the list of available commands.
Notes
- Subscriptions are stored in the D1 table
subscriberswith fields:id,type,chat_id,enabled,created_at(unique on(type, chat_id)). - Only
type = 'telegram'is used currently; the schema allows future channels (SMS, Signal, etc.). - Schema is managed outside runtime. A sample migration exists at
migrations/001_create_subscribers.sql. - Apply schema (option A — migration file):
wrangler d1 execute celebrity-death-bot --file=./migrations/001_create_subscribers.sql
- Or create manually (option B):
CREATE TABLE IF NOT EXISTS subscribers ( id INTEGER PRIMARY KEY AUTOINCREMENT, type TEXT NOT NULL, chat_id TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE (type, chat_id) );
- Add secrets via Wrangler:
wrangler secret put TELEGRAM_WEBHOOK_SECRETand ensureTELEGRAM_BOT_TOKENis set.
The worker can post each LLM-approved death to X (Twitter) at x.com/CelebDeathBot.
- Automatic posting is disabled unless
POST_TO_X=true. - Automatic X posts omit Wikipedia links unless
X_POST_INCLUDE_WIKIPEDIA_LINK=true. - The
/llm-debugdashboard includes a manual Post to X button for approved rows. It opens X's browser composer with the Wikipedia link included. - When link inclusion is enabled, format matches Telegram, but the Wikipedia link is appended at the end (since X posts cannot embed clickable HTML links):
- Example:
🚨💀Jane Doe (88) : American actor and philanthropist - cancer 💀🚨\nhttps://en.wikipedia.org/wiki/Jane_Doe
- Example:
- Length is constrained to 280 characters with t.co URL weighting (23 chars). The body text is truncated with an ellipsis if necessary.
Setup (OAuth 2.0, PKCE)
- In your X developer app, enable OAuth 2.0 user auth with scopes:
tweet.read tweet.write users.read offline.access. - Store secrets (never commit these):
wrangler secret put X_CLIENT_ID wrangler secret put X_CLIENT_SECRET # optional; included when present wrangler secret put X_ENC_KEY # base64 32-byte key for AES-GCM
- Apply the migration for token storage:
wrangler d1 execute celebrity-death-bot --file=./migrations/004_create_x_oauth.sql wrangler d1 execute celebrity-death-bot --file=./migrations/006_security_hardening.sql
- Sign into
/llm-debug, choose Connect X, and confirm the CSRF-protected authorization step. The callback and status endpoints require the same administrator session.
Security notes
- Access and refresh tokens are encrypted at rest in D1 via AES-256-GCM using
X_ENC_KEY. - PKCE state is one-time, expires after ten minutes, and is bound to the administrator email and session that initiated it.
- Tokens are auto-refreshed as they near expiry; no interactive login is needed after the first connect.
Only required when using LLM_PROVIDER=replicate.
Replicate signs each webhook delivery. This worker verifies signatures to prevent spoofed or replayed requests.
- Headers used:
webhook-id,webhook-timestamp(seconds),webhook-signature. - Signed content:
${webhook-id}.${webhook-timestamp}.${rawBody}(raw, unmodified body string). - Algorithm: HMAC-SHA256 with your Replicate webhook signing key.
- Timestamp window: 5 minutes (requests older than this are rejected).
Setup
- Retrieve your signing key from Replicate (associated with your API token):
curl -s -H "Authorization: Bearer $REPLICATE_API_TOKEN" \ https://api.replicate.com/v1/webhooks/default/secret # { "key": "whsec_..." }
- Store the key as a Worker secret:
wrangler secret put REPLICATE_WEBHOOK_SECRET
Notes
- Do not append secrets to the webhook URL. This worker no longer uses
?secret=...for Replicate callbacks; it relies on HMAC verification only. - The secret format is
whsec_<base64>. Only the base64 part is used as the raw HMAC key. - The worker uses constant-time comparison and enforces a 5-minute timestamp tolerance to mitigate replay attacks.
- Successfully authenticated webhook deliveries are claimed in D1 before processing, so provider retries cannot send duplicate notifications.
When OPENAI_WEBHOOK_SECRET is set, OpenAI requests are sent in background mode and results are processed via POST /openai/webhook. Configure a webhook endpoint in the OpenAI dashboard and subscribe to response.completed, response.failed, and response.cancelled.
- Headers used:
webhook-id,webhook-timestamp,webhook-signature - Signed content:
${webhook-id}.${webhook-timestamp}.${rawBody} - Algorithm: HMAC-SHA256 with your OpenAI webhook signing secret (supports
whsec_...format)
This repo uses Node’s built-in node:test for focused pure/unit tests and Cloudflare’s Vitest integration for route tests inside the Workers runtime.
- Run all tests:
npm test
npm test runs both suites. Worker integration tests live in tests/worker/ and use vitest.config.ts, which loads the production Wrangler compatibility settings and bindings.
workers_dev and preview URLs are disabled in wrangler.jsonc; production traffic is served only on https://celebritydeathbot.com. All responses receive HSTS, clickjacking, MIME-sniffing, referrer, and permissions-policy headers. The administrator dashboard additionally uses a nonce-based content security policy.
- See
release-notes/for version-specific notes (e.g., v1.1, v1.2).
This project is licensed under the MIT License.
