An ATProto labeler that watches the Bluesky firehose for astrology content and annotates it with cryptographically signed labels — in real time.
Live visualizer: https://sandboxlabs.ai/celestial-signals
Built as a learning project + meetup demo to understand the labeler primitive and explore the two-pass NLP pattern at the core of Attie.
A labeler, not a feed generator. Key difference:
| Feed Generator | Labeler | |
|---|---|---|
| What it emits | Post URIs | Signed label objects |
| Protocol primitive | getFeedSkeleton |
com.atproto.label.* |
| Travels with content | ❌ app-local | ✅ cross-app, portable |
| User action | Subscribe to feed | Subscribe to labeler |
Labels are protocol-level metadata. When a user subscribes to Celestial Signals, posts about astrology get annotated inside any atproto client — not just Bluesky.
Jetstream (JSON WebSocket)
│
▼
Pass 1: Keyword filter ← cheap gate, ~O(n keywords)
│ match
▼
Pass 2: Claude Haiku classifier ← semantic verification
│ true
▼
server.createLabel() ← CBOR-signed, broadcast via
│ com.atproto.label.subscribeLabels
▼
Subscribers see labels in app
This two-pass pattern mirrors what Attie is doing at scale: cheap filter first, LLM understanding only where it counts.
| identifier | description |
|---|---|
astrology |
Post meaningfully engages with astrological concepts |
mercury-retrograde |
Specifically discusses Mercury retrograde |
natal-chart |
Birth charts, rising signs, house placements |
The raw com.atproto.sync.subscribeRepos stream emits CBOR-encoded MST blocks.
Jetstream proxies this to plain JSON WebSocket — same data, no binary decoding,
filterable by collection and DID. For labelers and feed generators that don't
need full sync semantics, Jetstream is the right layer.
- Runtime: Bun
- Labeler: @skyware/labeler — handles XRPC endpoints, CBOR signing, label distribution
- Firehose: @skyware/jetstream — typed Jetstream client
- Classifier: Claude Haiku (
claude-haiku-4-5-20251001) via Anthropic SDK
git clone https://github.com/YOUR_HANDLE/celestial-signals
cd celestial-signals
bun install
cp .env.example .env
# Convert a dedicated Bluesky account into a labeler
bunx @skyware/labeler setup
# → copies DID + SIGNING_KEY into .env
# Set SETUP_COMPLETE=true, then:
bun run devVerify:
curl http://localhost:4100/xrpc/com.atproto.label.queryLabels
# → {"cursor":"0","labels":[]}| var | description |
|---|---|
DID |
did:plc of your labeler account |
SIGNING_KEY |
secp256k1 private key for label signing |
BSKY_IDENTIFIER |
labeler account handle |
BSKY_PASSWORD |
app password |
USE_LLM_CLASSIFIER |
true = two-pass, false = keywords only |
ANTHROPIC_API_KEY |
required if USE_LLM_CLASSIFIER=true |
- Labelers require a DID document mutation to register
#atproto_labelsigning key and#atproto_labelerservice endpoint — this is the one-time ceremony that makes your server recognizable to the network - Label values must be published to the ATProto record before the server can
issue them —
bunx @skyware/labeler label add, not runtime registration - Jetstream cursors are unix microseconds — time-based, portable across instances, resumable without sequence coordination
- The
neg: truepattern for label retraction is elegant — negation is a first-class protocol concept, not an out-of-band delete
- Attie — natural language feed builder, the production version of the NLP-over-firehose pattern
- labeler-starter-kit-bsky — reference implementation this is based on
- ATmosphereConf 2026
Built for the ATProto SF meetup — Sandbox Labs AI