Custom Bluesky Feed #
Create Bluesky feeds about ANY topic using:
- Simple rules for capturing posts (regular expressions, accounts to include or ignore)
- Label posts directly from Bluesky
- Simple (and optional) random forest classifier to score posts automatically
- One server, multiple feeds
This is serving a triathlon feed, live from an old PC at home!
Requirements #
just is optional, but handy.
Usage #
Settings #
| Environment variable | Required | Description | Example |
|---|---|---|---|
TRI_BSKY_FEED_CONFIG |
Yes | Comma-separated paths or URLs to config files (details below) | feed.toml or triathlon.toml,maracatu.toml |
TRI_BSKY_FEED_DB_URL |
No | Path to the SQLite database URI | sqlite://feed.db |
TRI_BSKY_FEED_ORT_LIB_PATH |
No | Path to the ONNX runtime shared library (.so, or .dylib for macOS), required when using a trained model |
/usr/local/lib/libonnxruntime.so |
TRI_BSKY_FEED_MODEL_DIR |
No | Path to the model/ directory with the ONNX model (see below) |
Defaults to ./model/ relative to the CLI entrypoint |
TRI_BSKY_FEED_JETSTREAM |
No | Jetstream WebSocket URL (also available as --jetstream flag) |
Defaults to wss://jetstream1.us-east.bsky.network/subscribe |
Config file #
Feed identity, filter rules, and ranker parameters are controlled by a TOML config file. The file has three sections:
[feed]— uniquename,didandrkeyidentifying the feed, plus two optional fields:service_did(the feed generator service DID from the generator records, defaults todid)admins(extra DIDs allowed to label posts)
[filter]—trusted_accounts(DIDs always included),ignored_accounts(DIDs always ignored),patterns(Go regexps), andexclude(substrings to reject)[ranker]— scoring weights, candidate pool size, cutoff window, and adisabledflag for chronological order
See feed.toml in the repository for a fully annotated example (the triathlon feed with current values).
Compile #
$ just build
Database #
This project uses SQLite for storage. Set TRI_BSKY_FEED_DB_URL with a sqlite:// prefix (e.g. sqlite://feed.db). A single database hosts multiple feeds independently.
Running the feed #
$ tri-bsky-feed run
- Consumes from the jetstream, saving relevant posts to the database
- Without a trained model, serves all matching posts in reverse chronological order
- With a trained model, scores posts and serves those above the configured threshold, ranked by the configured weights
- Authors who set
hideFromAlgorithmicRecommendationson theirapp.bsky.actor.contentVisibilityDeclarationrecord are excluded from served feeds
Getting started #
Create a config file with your feed identity and filter rules, then run the feed. It will start capturing matching posts from the jetstream and serve them in reverse chronological order right away — no model needed.
When multiple feeds are configured, pass --feed <name> to any command to target a specific feed.
Labeling posts from Bluesky #
The feed owner (the account in did), the feed generator service account (service_did), and any admins you configure can label posts directly from Bluesky using the show more like this or show less like this options on posts in your feed. These labels are saved and take priority over any classifier score.
Interactions from any other account have no effect.
Training a classifier (optional) #
Once you have enough labeled posts (aim for at least one thousand), you can train a model to automatically score incoming posts. This requires the ONNX runtime and uv.
First, export the database as a CSV:
$ tri-bsky-feed export data.csv
You can use
tri-bsky-feed importto import data back to the database if you need.
Then, we use Python to train and export an ONNX model:
$ just train data.csv
You can compare different models with
just compare data.csv.
Once the model files are in the model directory, restart the feed. It will automatically pick up the model and start scoring posts, serving only those above the configured related_threshold.
You can check statistics from the database:
$ tri-bsky-feed stats
Developer tooling #
Check tri-bsky-feed --help as well. These commands accept both post URL or AT URI.
Saving a specific post #
$ tri-bsky-feed save https://bsky.app/profile/cuducos.bsky.social/post/3mejpnaf5ns2a
Fetches a post and saves it to the database. Use --related or --no-related to label it, or omit both to save as pending.
Backfilling historical posts #
$ tri-bsky-feed backfill
Replays past posts from the Bluesky network archive into the database, applying your filter rules. Also removes posts from the database that were deleted from Bluesky since then.
Requires the BSKY_NETWORK_API_KEY environment variable.
Testing the classification model for a specific post #
$ tri-bsky-feed classify at://did:plc:3272gdrjsuikiff7qsgokgas/app.bsky.feed.post/3lpr3nl6jtg2j
Checking a post status in the database #
$ tri-bsky-feed debug at://did:plc:3272gdrjsuikiff7qsgokgas/app.bsky.feed.post/3lpr3nl6jtg2j
Contributing #
Lint, format and tests:
$ just check