Posts project activity to Telegram and Discord:
- GitHub (webhooks): published releases and pushes to the default branch
- X (polled every 5 min): new posts from each project's account (no replies or retweets)
- Daily digest to an internal Telegram chat: 24h activity per project + any delivery problems
Design: docs/superpowers/specs/2026-10-15-feedbot-design.md.
- Telegram bot: create it with @BotFather and put the token in
TELEGRAM_BOT_TOKEN. Add the bot as an admin of each announcement channel, and as a member of the internal digest chat. Public channels can be addressed as@name. For private chats, use the numeric id (-100…): send a message in the chat, then openhttps://api.telegram.org/bot<token>/getUpdatesand readchat.id. - Discord: in the channel go to Settings → Integrations → Webhooks → New Webhook → Copy URL.
Put the URL in
.env(e.g.DISCORD_OGEE_WEBHOOK=...) and reference it inprojects.yamlas${DISCORD_OGEE_WEBHOOK}. - X: create an app on developer.x.com with pay-per-use credits and copy the bearer token into
X_BEARER_TOKEN. Reading costs about $0.005 per post returned, so expect a few dollars a month for 10 accounts. If the credits run out, the digest chat gets one alert. - GitHub: add one webhook per project (repo or org settings → Webhooks):
- Payload URL
https://<domain>/github/<project-slug>, content typeapplication/json, and that project's secret (shown in the admin panel). Imported v1 projects keep the old shared secret until it is rotated. - Events: Releases and Pushes only
- GitHub sends a
ping, and the webhook page should show a green tick. - Put
[skip feed]in a commit message to keep that commit out of the channels.
- Payload URL
Configuration now lives in the database and is managed in the admin panel (Plan 2).
.env holds only FEEDBOT_MASTER_KEY (create it with python -m feedbot gen-key), DB_PATH and
LISTEN:
cp .env.example .envExisting v1 setups migrate once with python -m feedbot --env .env import-yaml projects.yaml.
projects.example.yaml documents that format, and the v1 variables (GITHUB_WEBHOOK_SECRET,
TELEGRAM_BOT_TOKEN, X_BEARER_TOKEN, DISCORD_*_WEBHOOK) go into .env just for that import.
Feedbot refuses to start on invalid config and says why.
python -m venv .venv && source .venv/Scripts/activate # Linux: .venv/bin/activate
pip install -e ".[dev]"
python -m pytest -q
python -m feedbot gen-key # put the key in .env as FEEDBOT_MASTER_KEY
# also in .env: DB_PATH=feedbot-dev.db
python -m feedbot --env .env import-yaml projects.yaml # optional: import a v1 projects.yaml
python -m feedbot --dry-run # prints messages instead of sending
# in a second terminal (the project's secret goes in GITHUB_WEBHOOK_SECRET):
python scripts/send_test_event.py release_published.json --url http://127.0.0.1:8081/github/<slug>.env.example sets DB_PATH=/var/lib/feedbot/feedbot.db (the VPS path), so for local runs set
DB_PATH=feedbot-dev.db in your .env. Use a separate dev database: --dry-run
marks outbox rows as sent in whichever database it uses. It still polls the real X API (billed) when a
project has an x handle.
python -m feedbot --digest-now sends the digest once and exits.
The admin panel is served at https://bot.bookrunner.tech/admin from the same VPS as Leveret
(148.113.237.230). Point a DNS record for bot.bookrunner.tech at it first. For the first install
and every update:
- Build the upload:
scripts/vps/pack.sh - Copy it over:
ssh <vps> mkdir -p feedbot-upload scp dist/feedbot.tgz scripts/vps/install.sh <vps>:feedbot-upload/
- Install:
ssh <vps> 'bash ~/feedbot-upload/install.sh'. On the first run this creates/opt/feedbot/.envwith a newly generated master key (plusDB_PATHandLISTEN) and never overwrites it afterwards. Back it up (sudo cat /opt/feedbot/.env): without the master key the stored secrets cannot be decrypted. - First time only, issue the certificate:
sudo certbot --nginx -d bot.bookrunner.tech - First time only, create the admin account with the command
install.shprints at the end (sudo -u feedbot bash -c 'set -a; . /opt/feedbot/.env; cd /opt/feedbot/app; /opt/feedbot/venv/bin/python -m feedbot --env /dev/null create-admin --email you@example.com'). - Open
https://bot.bookrunner.tech/adminand log in. Set the Telegram token, X bearer and digest in Settings. Create customers in Users, then projects. - Optional, for a v1 config: run
import-yaml projects.yamlonce as thefeedbotuser with the env loaded (samesudo -u feedbot bash -c '...'wrapper as above, withimport-yaml <path>instead ofcreate-admin).pack.shincludesprojects.yamlonly if the file exists; the service itself no longer reads it, since the database holds the config.
- Back up
/opt/feedbot/.envand the database (theDB_PATHfile it names) first. - Run
install.shas above. It appends a newFEEDBOT_MASTER_KEYto the existing.env(back up the file again afterwards). If/etc/nginx/sites-available/feedbot.confis the v1 site (no/adminlocation), it stops before changing anything and prints how to replace it: move the old file aside, re-runinstall.sh, then re-runcertbot --nginx -d bot.bookrunner.tech. - Right after, run
import-yamlwith the oldprojects.yaml(step 7). It links the queued v1 messages to the imported channels. Until then the service holds them back and rechecks hourly, for up to 24h; after that they are marked failed withwaiting for import-yaml.
pack.sh tars feedbot/, so the admin templates and static files are included. Check with
tar -tzf dist/feedbot.tgz | grep -c admin/templates.
install.sh also installs the login rate-limit zone (/etc/nginx/conf.d/30-feedbot-ratelimit.conf,
5 requests per minute per IP with a burst of 5) before adding the site. The nginx site does not
include snippets/security-headers.conf: that snippet is shared with another site on the VPS and may
set its own Content-Security-Policy, which would stack with the app's. The app already sends CSP,
X-Frame-Options, Referrer-Policy and nosniff for /admin and /static; nginx adds only
Strict-Transport-Security, which takes effect once certbot adds TLS.
- Roles: an
adminmanages everything: settings, users, all projects and the audit log. Acustomersees and changes only the projects they own; another customer's project returns 404 and admin-only pages return 403. - Customers manage their own projects' channels and repo, and see the webhook URL and secret on the project page (the secret is revealed by an explicit action, which is audited).
- To restrict
/adminto your own addresses, uncomment theallow/denylines in thelocation ^~ /adminblock ofscripts/vps/feedbot.conf(in/etc/nginx/sites-available/feedbot.confon the VPS, sinceinstall.shwrites the site only once), thensudo nginx -t && sudo systemctl reload nginx. - Locked-out admin:
python -m feedbot --env /dev/null reset-password --email you@example.com(run with the env loaded, as forcreate-admin) sets a one-time temporary password and clears the login lockout for that email. Reset password on the Users page does the same.
curl -s http://127.0.0.1:8081/healthzreturns the last X poll time and the outbox backlog. It answers 503 if the last X poll is more than 15 minutes old.journalctl -u feedbot -fshows the logs.python -m feedbot --env /opt/feedbot/.env rotate-keyre-encrypts all stored secrets under a new master key and prints it on the last line. Stop the service first, then put the new key in.envand start it again.- Failed deliveries are retried for 24h, then listed under
⚠️ in the next digest.