Skip to content

Instantly share code, notes, and snippets.

@jimpick
Last active January 8, 2025 20:25
Show Gist options
  • Save jimpick/33f273e92a5c6513155a4c4039d83ffb to your computer and use it in GitHub Desktop.
Save jimpick/33f273e92a5c6513155a4c4039d83ffb to your computer and use it in GitHub Desktop.

Fireproof + AT Proto Labeler mashup project

I'm doing some small investigations to see if I can find some fun "mashup" opportunities for demos that mix Fireproof and AT Proto (the protocol for Bluesky) together.

Bluesky has the concept of "bring your own algorithm". An obvious project to work on would be a feed generator.

But another interesting opportunity is providing some tools for building labelers, which can be used to moderate accounts and content.

Having a labeler with some integrated Fireproof ledgers would make for an interesting demo:

  • Ledger-driven labels could be applied to accounts and content
  • The list of supported labels could be set via ledger updates
  • External UI could enable multiple users to control the labeler
  • AI tools could also update the ledger and create labels using rich context

Experimentation so far...

There's a well-done starter project for labelers here:

If you have a Bluesky account, you can try out a labeler by the same author that uses the pattern:

I followed the instructions, and created a new labeler for experimentation:

To make a labeler, the first step is to create a new Bluesky account manually using the website just like any other account. Then you can run the code in the repo to convert it to be a labeler account.

Here's my labeler:

Labeler screenshot

Feel free to try it out. It's just for development, so I won't keep it running long term.

You can see 5 "label definitions" (earth, fire, air, water, love), which have been stored in the data repository (PDS) for the labeler account. These can be browsed here:

You can see the code in the demo that sets the labels here:

At the top level of the repo for the account, there is also a record that points at the HTTPS server that gets called by the "AppView" that provides the backend for the Bluesky UI:

For the labeler to work, that service needs to be running. When accounts subscribe to a labeler from the Bluesky website or apps, the backend "AppView" service (run by Bluesky) will connect to the labeler and subscribe to any records that are published. It caches very aggressively, so only new records will be retrieved if it already has old records.

The hardest task when setting up a labeler is arranging for a hosting environment. I'm using my home development machine, with a Caddy webserver in front for SSL (plus some additional stuff to get past my firewall). A production deploy could go on any cloud web service hosting platform.

The demo labeler uses a very simple SQLite3 database to store the labels. It only has one table:

$ sqlite3 labels.db 'select id, src, uri, val, neg, cts from labels;'
1|did:plc:ybpucsbaoq5ntkio2wuaoz5l|did:plc:iztv76aq7fbpxmyjwqv5vrhz|fire|0|2025-01-06T22:03:21.787Z
2|did:plc:ybpucsbaoq5ntkio2wuaoz5l|did:plc:horhobvzn3dl423deeh6orwj|earth|0|2025-01-06T22:15:20.158Z
3|did:plc:ybpucsbaoq5ntkio2wuaoz5l|did:plc:ybpucsbaoq5ntkio2wuaoz5l|fire|0|2025-01-06T22:17:16.528Z
4|did:plc:ybpucsbaoq5ntkio2wuaoz5l|did:plc:iztv76aq7fbpxmyjwqv5vrhz|fire|1|2025-01-06T22:20:51.254Z
5|did:plc:ybpucsbaoq5ntkio2wuaoz5l|did:plc:iztv76aq7fbpxmyjwqv5vrhz|fire|0|2025-01-06T22:20:51.261Z
6|did:plc:ybpucsbaoq5ntkio2wuaoz5l|did:plc:iztv76aq7fbpxmyjwqv5vrhz|fire|1|2025-01-06T22:21:03.236Z
7|did:plc:ybpucsbaoq5ntkio2wuaoz5l|did:plc:iztv76aq7fbpxmyjwqv5vrhz|air|0|2025-01-06T22:21:03.244Z

Here's an API request that retrieves the labels. The "AppView" will make similar requests for the newest labels:

$ curl -s https://jimpick-labeler1.v6z.me/xrpc/com.atproto.label.queryLabels | jq
{
  "cursor": "7",
  "labels": [
    {
      "id": 1,
      "src": "did:plc:ybpucsbaoq5ntkio2wuaoz5l",
      "uri": "did:plc:iztv76aq7fbpxmyjwqv5vrhz",
      "val": "fire",
      "neg": false,
      "cts": "2025-01-06T22:03:21.787Z",
      "sig": {
        "$bytes": "xhG403X+M3Fx9WWaaU1SdYJTnbPr2Vc0IDZnOgp1Ejp0ve9ZqTzK4gUderjya87BEdDO8fA/QkqA2Hj99B6Y8g"
      },
      "ver": 1
    },
    {
      "id": 2,
      "src": "did:plc:ybpucsbaoq5ntkio2wuaoz5l",
      "uri": "did:plc:horhobvzn3dl423deeh6orwj",
      "val": "earth",
      "neg": false,
      "cts": "2025-01-06T22:15:20.158Z",
      "sig": {
        "$bytes": "n7BdOKM7kX6McHtZoqqmgf2h/Oqck5gaXsz2jeV4XhR4WeMvXeSbMXH/ctOs/HgJhkrtE2VS/CCR1f6iuqAK9g"
      },
      "ver": 1
    },
    {
      "id": 3,
      "src": "did:plc:ybpucsbaoq5ntkio2wuaoz5l",
      "uri": "did:plc:ybpucsbaoq5ntkio2wuaoz5l",
      "val": "fire",
      "neg": false,
      "cts": "2025-01-06T22:17:16.528Z",
      "sig": {
        "$bytes": "OFO61fILCIMik7/QqLS4s/QyXadUGNgQfDGCrZC1PE03gYzQzNOgX7TJIAfJte0g9Tm+bfiMhTJR2qTpp0qZVg"
      },
      "ver": 1
    },
    {
      "id": 4,
      "src": "did:plc:ybpucsbaoq5ntkio2wuaoz5l",
      "uri": "did:plc:iztv76aq7fbpxmyjwqv5vrhz",
      "val": "fire",
      "neg": true,
      "cts": "2025-01-06T22:20:51.254Z",
      "sig": {
        "$bytes": "OQI5SnH1YXBmEhZx5yv3mV7NXR1iv+xgIPWtsglx9k9aGQ36cYwajldPyplBDQc4+dP0hnoyWspdRaOG0GuqVw"
      },
      "ver": 1
    },
    {
      "id": 5,
      "src": "did:plc:ybpucsbaoq5ntkio2wuaoz5l",
      "uri": "did:plc:iztv76aq7fbpxmyjwqv5vrhz",
      "val": "fire",
      "neg": false,
      "cts": "2025-01-06T22:20:51.261Z",
      "sig": {
        "$bytes": "+ZBb5N3VV1uTsfdKK9/L0nIuaNjTLpOv9Er2RyFPmr9lixHeOC9BKD+x7GlWYWnU0nW5Y8sdBQrkm5YfX7HEEQ"
      },
      "ver": 1
    },
    {
      "id": 6,
      "src": "did:plc:ybpucsbaoq5ntkio2wuaoz5l",
      "uri": "did:plc:iztv76aq7fbpxmyjwqv5vrhz",
      "val": "fire",
      "neg": true,
      "cts": "2025-01-06T22:21:03.236Z",
      "sig": {
        "$bytes": "vgUEYTTBl+u9Uj8Dpbaz0LNceBRH0PDEX5IjTqurFEARcV/w0CUpXHJXMOyinmUZAY9ZaKD1by+3n1G8NICuhA"
      },
      "ver": 1
    },
    {
      "id": 7,
      "src": "did:plc:ybpucsbaoq5ntkio2wuaoz5l",
      "uri": "did:plc:iztv76aq7fbpxmyjwqv5vrhz",
      "val": "air",
      "neg": false,
      "cts": "2025-01-06T22:21:03.244Z",
      "sig": {
        "$bytes": "Wx66h9ZAf14Cj/mOYGGCi0vKxx92kCh5pC8WEPDIyX0LLbHpmhRoheLc9j9LMfP3+yU13djGCrOWdPIuimPTVQ"
      },
      "ver": 1
    }
  ]
}

You can see that the output is a cumulative list of transactions (instead of the final state). The "id" is very important.

You can see that in the last transaction, my jimpick.com Bluesky profile has added a label for "Air" to itself.

My DID for my account is did:plc:iztv76aq7fbpxmyjwqv5vrhz ... you can see the label events in the JSON above where the DID is in the "uri" field.

If I view it from another account that is also subscribed to the labeler, the label is shown:

Profile screenshot with label shown

Ideas for integrating Fireproof

Getting the demo labeler running was a lot of work but it's a good starting point, and shouldn't be too hard to extend with small amounts of JavaScript/TypeScript.

The next step would be to integrate Fireproof.

Right now, the demo labeler listens to the Bluesky firehose to see if any users have "liked" it's posts, and it reacts by labeling the accounts with the appropriate label.

An obvious modification would be to instead listen to a Fireproof ledger, and convert activity on the ledger into labeler transactions.

A simple standalone web UI for updating the Fireproof ledger could be built. It could simply have a list of documents with a target Bluesky handle (the DID could be resolved), and a desired label. For a non-demo, it would need to be access restricted to prevent abuse. The ledger would be synced between the standalone web UI and the labeler process.

If the labeler only checked the Fireproof ledger for updates, it wouldn't need to monitor the Bluesky firehose. The mechanism for adding labels by liking posts (and the posts themselves) would be removed.

The labeler process would likely be called infrequently by the AppView, so it could be hosted somewhere where it can go to sleep, possibly in a Cloudflare Durable Object (it still needs a consistent database). Since it wouldn't need to run constantly, just when the AppView wakes it up, it should be cheap to run.

The current demo only labels accounts, but a big use case for labelers is for labeling content (posts). Check the docs here for all the possibilities:

A standalone web UI could provide an interface for selecting posts and applying labels to them. There are a lot of label values that interact with the Bluesky UI to do things like hide content or add information labels that users can see.

In the current demo, the list of possible labels is stored in the code, and loaded one time using bun set-posts to call the code. We could instead have a ledger with documents for the possible labels, and when it changes, the labels could be updated in the labeler's repo. This would be nice because then we could ship a fairly generic labeler implementation, and the configuration could be done primarily via Fireproof ledgers instead of modifying code.

Labelers can also receive "reports" ... a user can select "Report post" and select which labeler to send it to.

The labeler could take the reports and write them into a ledger that could be synced elsewhere. The reporting UI is somewhat limited in that it only supports a preset number of categories:

Reporting dialog screenshot

Bluesky's main moderation service is called Ozone and has a lot of advanced features they need to do moderation for the entire service. It's definitely a bit overkill and hard to set up for smaller teams. With something like Fireproof, it should be possible to make simple moderation services quickly that small teams could use for simpler use cases.

Because a ledger can be updated in a separate process, and it would communicate with the labeler via the sync protocol, it becomes easy to build applications that could integrate AI (eg. Claude Desktop + MCP) to update a ledger. The labeler process would listen and convert updates into labels. This could be really useful for building strong moderation tools for communities.

@jimpick
Copy link
Author

jimpick commented Jan 7, 2025

Images:

Screenshot 2025-01-07 at 8 02 41 AM

@jimpick
Copy link
Author

jimpick commented Jan 7, 2025

Screenshot 2025-01-07 at 8 26 44 AM

@jimpick
Copy link
Author

jimpick commented Jan 7, 2025

Screenshot 2025-01-07 at 10 20 53 AM

@jchris
Copy link

jchris commented Jan 8, 2025

This is great. I wonder can ship a "no-backend" labeler that is easier to install, so people dont need a server to run it. One option would be to deploy a serverless function that people have to trust but that can interact with their database in a programmatic way.

Or we can add user JavaScript to the page we can bring in Fireproof and sync, or we could add links that eg open "https://fireproof-labeler-ui.netlify.app/label-this-post/" + currentPostId and have that UI open the Fireproof db.

@jimpick
Copy link
Author

jimpick commented Jan 8, 2025

Since I think Fireproof will run in a Cloudflare Worker / Durable Object, that might be a great place to run a production labeler service. And then we can try to make it easy to build standalone static HTML frontends that can be deployed anywhere.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment