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/)