💙 A custom feed platform for Bluesky bsky.app/profile/cuducos.me/feed/aaahlw3uvkhgq
feed bluesky atproto
README.md

Custom Bluesky Feed #

Create Bluesky feeds about ANY topic using:

  1. Simple rules for capturing posts (regular expressions, accounts to include or ignore)
  2. Label posts directly from Bluesky
  3. Simple (and optional) random forest classifier to score posts automatically
  4. One server, multiple feeds

This is serving a triathlon feed, live from an old PC at home!

Requirements #

  • Go 1.27
  • uv (for generating the ONNX model)

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] — unique name, did and rkey identifying the feed, plus two optional fields:
    • service_did (the feed generator service DID from the generator records, defaults to did)
    • admins (extra DIDs allowed to label posts)
  • [filter] — trusted_accounts (DIDs always included), ignored_accounts (DIDs always ignored), patterns (Go regexps), and exclude (substrings to reject)
  • [ranker] — scoring weights, candidate pool size, cutoff window, and a disabled flag 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 hideFromAlgorithmicRecommendations on their app.bsky.actor.contentVisibilityDeclaration record 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 import to 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