music on atproto
plyr.fm
1# ATProto labeler service
2
3technical documentation for the moderation service's ATProto labeling capabilities.
4
5## overview
6
7the moderation service (`moderation.plyr.fm`) acts as an ATProto labeler - a service that produces signed labels about content. labels are metadata objects that follow the `com.atproto.label.defs#label` schema and can be queried by any ATProto-compatible app.
8
9key distinction: **labels are signed data objects, not repository records**. they don't live in a user's repo - they're served directly by the labeler via XRPC endpoints.
10
11## why labels?
12
13from [Bluesky's labeling architecture](https://docs.bsky.app/docs/advanced-guides/moderation):
14
15> "Labels are assertions made about content or accounts. They don't enforce anything on their own - clients decide how to interpret them."
16
17this enables **stackable moderation**: multiple labelers can label the same content, and clients can choose which labelers to trust and how to handle different label values.
18
19for plyr.fm, this means:
20- we produce `copyright-violation` labels when tracks are flagged
21- other ATProto apps can query our labels and apply their own policies
22- users/apps can choose to subscribe to our labeler or ignore it
23- we can revoke labels by emitting negations (`neg: true`)
24
25## architecture
26
27```
28┌─────────────────────────────────────────────────────────────────┐
29│ moderation service │
30│ (moderation.plyr.fm) │
31├─────────────────────────────────────────────────────────────────┤
32│ │
33│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
34│ │ /scan │ │ /emit-label │ │ /xrpc/com.atproto. │ │
35│ │ endpoint │ │ endpoint │ │ label.queryLabels │ │
36│ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘ │
37│ │ │ │ │
38│ ▼ ▼ ▼ │
39│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
40│ │ AuDD │ │ sign │ │ query labels │ │
41│ │ client │ │ label │ │ from postgres │ │
42│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
43│ │ │
44│ ▼ │
45│ ┌─────────────┐ │
46│ │ labels │ │
47│ │ table │ │
48│ └─────────────┘ │
49│ │
50└─────────────────────────────────────────────────────────────────┘
51```
52
53## endpoints
54
55### POST /scan
56
57scans audio for copyright matches via AuDD.
58
59```bash
60curl -X POST https://moderation.plyr.fm/scan \
61 -H "X-Moderation-Key: $MODERATION_AUTH_TOKEN" \
62 -H "Content-Type: application/json" \
63 -d '{"audio_url": "https://r2.plyr.fm/audio/abc123.mp3"}'
64```
65
66response:
67
68```json
69{
70 "matches": [
71 {
72 "artist": "Taylor Swift",
73 "title": "Love Story",
74 "score": 95,
75 "isrc": "USRC10701234"
76 }
77 ],
78 "is_flagged": true,
79 "highest_score": 95,
80 "raw_response": { ... }
81}
82```
83
84### POST /emit-label
85
86creates a signed ATProto label.
87
88```bash
89curl -X POST https://moderation.plyr.fm/emit-label \
90 -H "X-Moderation-Key: $MODERATION_AUTH_TOKEN" \
91 -H "Content-Type: application/json" \
92 -d '{
93 "uri": "at://did:plc:abc123/fm.plyr.track/xyz789",
94 "val": "copyright-violation",
95 "cid": "bafyreiabc123"
96 }'
97```
98
99the service:
1001. creates label with current timestamp
1012. signs with labeler's secp256k1 private key (DAG-CBOR encoded)
1023. stores in `labels` table with monotonic sequence number
103
104### GET /xrpc/com.atproto.label.queryLabels
105
106standard ATProto XRPC endpoint for querying labels.
107
108```bash
109# query by URI pattern
110curl "https://moderation.plyr.fm/xrpc/com.atproto.label.queryLabels?uriPatterns=at://did:plc:*"
111
112# query by source (labeler DID)
113curl "https://moderation.plyr.fm/xrpc/com.atproto.label.queryLabels?sources=did:plc:plyr-labeler"
114
115# query by cursor (pagination)
116curl "https://moderation.plyr.fm/xrpc/com.atproto.label.queryLabels?cursor=123&limit=50"
117```
118
119response:
120
121```json
122{
123 "cursor": "456",
124 "labels": [
125 {
126 "ver": 1,
127 "src": "did:plc:plyr-labeler",
128 "uri": "at://did:plc:abc123/fm.plyr.track/xyz789",
129 "cid": "bafyreiabc123",
130 "val": "copyright-violation",
131 "neg": false,
132 "cts": "2025-11-30T12:00:00.000Z",
133 "sig": "base64-encoded-secp256k1-signature"
134 }
135 ]
136}
137```
138
139## label signing
140
141labels are signed using DAG-CBOR serialization with secp256k1 keys (same as ATProto repo commits).
142
143signing process:
1441. construct label object without `sig` field
1452. encode as DAG-CBOR (deterministic CBOR)
1463. compute SHA-256 hash of encoded bytes
1474. sign hash with labeler's secp256k1 private key
1485. attach signature as `sig` field
149
150this allows any client to verify labels came from our labeler by checking the signature against our public key (in our DID document).
151
152## label values
153
154current supported values:
155
156| val | meaning | when emitted |
157|-----|---------|--------------|
158| `copyright-violation` | track flagged for potential copyright infringement | scan returns matches |
159
160future values could include:
161- `explicit` - explicit content marker
162- `spam` - suspected spam upload
163- `dmca-takedown` - formal DMCA notice received
164
165## negation labels
166
167to revoke a label, emit the same label with `neg: true`:
168
169```json
170{
171 "uri": "at://did:plc:abc123/fm.plyr.track/xyz789",
172 "val": "copyright-violation",
173 "neg": true
174}
175```
176
177use cases:
178- false positive resolved after manual review
179- artist provided proof of licensing
180- DMCA counter-notice accepted
181
182## database schema
183
184```sql
185CREATE TABLE labels (
186 id BIGSERIAL PRIMARY KEY,
187 seq BIGSERIAL UNIQUE NOT NULL, -- monotonic for subscribeLabels cursor
188 src TEXT NOT NULL, -- labeler DID
189 uri TEXT NOT NULL, -- target AT URI
190 cid TEXT, -- optional target CID
191 val TEXT NOT NULL, -- label value
192 neg BOOLEAN NOT NULL DEFAULT FALSE, -- negation flag
193 cts TIMESTAMPTZ NOT NULL, -- creation timestamp
194 exp TIMESTAMPTZ, -- optional expiration
195 sig BYTEA NOT NULL, -- signature bytes
196 created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
197);
198
199CREATE INDEX idx_labels_uri ON labels(uri);
200CREATE INDEX idx_labels_src ON labels(src);
201CREATE INDEX idx_labels_seq ON labels(seq);
202CREATE INDEX idx_labels_val ON labels(val);
203```
204
205## deployment
206
207the moderation service runs on Fly.io:
208
209```bash
210# deploy
211cd moderation && fly deploy
212
213# check logs
214fly logs -a plyr-moderation
215
216# secrets
217fly secrets set -a plyr-moderation \
218 LABELER_DID=did:plc:xxx \
219 LABELER_SIGNING_KEY=hex-private-key \
220 DATABASE_URL=postgres://... \
221 AUDD_API_KEY=xxx \
222 MODERATION_AUTH_TOKEN=xxx
223```
224
225## integration with backend
226
227the backend calls the moderation service in two places:
228
2291. **scan on upload** (`_internal/moderation.py:scan_track_for_copyright`)
230 - POST to `/scan` with R2 URL
231 - store result in `copyright_scans` table
232
2332. **emit label on flag** (`_internal/moderation.py:_store_scan_result`)
234 - if `is_flagged` and track has `atproto_record_uri`
235 - POST to `/emit-label` with track's AT URI and CID
236
237```python
238async def _emit_copyright_label(uri: str, cid: str | None) -> None:
239 async with httpx.AsyncClient(timeout=10.0) as client:
240 await client.post(
241 f"{settings.moderation.labeler_url}/emit-label",
242 json={"uri": uri, "val": "copyright-violation", "cid": cid},
243 headers={"X-Moderation-Key": settings.moderation.auth_token},
244 )
245```
246
247## troubleshooting
248
249### label not appearing in queries
250
2511. check moderation service logs for emit errors
2522. verify track has `atproto_record_uri` set
2533. query labels table directly:
254 ```sql
255 SELECT * FROM labels WHERE uri LIKE '%track_rkey%';
256 ```
257
258### signature verification failing
259
2601. ensure `LABELER_SIGNING_KEY` matches DID document's public key
2612. check DAG-CBOR encoding is deterministic
2623. verify hash algorithm is SHA-256
263
264### scan returning empty matches
265
266AuDD requires actual audio fingerprints. common issues:
267- audio too short (< 3 seconds usable)
268- microphone recordings don't match source audio
269- very low bitrate or corrupted files
270
271## references
272
273- [ATProto Labeling Spec](https://atproto.com/specs/label)
274- [Bluesky Moderation Guide](https://docs.bsky.app/docs/advanced-guides/moderation)
275- [DAG-CBOR Spec](https://ipld.io/specs/codecs/dag-cbor/spec/)
276- [AuDD API Docs](https://docs.audd.io/)