docs: add end-of-year sprint planning and research (#626)

- STATUS.md: add sprint section (Dec 20-31) with two tracks:
- moderation architecture overhaul (Osprey/Ozone patterns)
- atprotofans paywall integration (supporter gating)
- research docs for both tracks with implementation phases
- updated immediate priorities to reflect sprint focus

tracking: #625

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>

authored by zzstoatzz.io Claude and committed by GitHub c232a6e8 f27af224

+35 -8
STATUS.md
··· 47 47 48 48 ### December 2025 49 49 50 + #### end-of-year sprint (Dec 20-31) 51 + 52 + **focus**: two foundational systems need solid experimental implementations by 2026. 53 + 54 + **track 1: moderation architecture overhaul** 55 + - consolidate sensitive images into moderation service 56 + - add event-sourced audit trail 57 + - implement configurable rules (replace hard-coded thresholds) 58 + - informed by [Roost Osprey](https://github.com/roostorg/osprey) patterns and [Bluesky Ozone](https://github.com/bluesky-social/ozone) workflows 59 + 60 + **track 2: atprotofans paywall integration** 61 + - phase 1: read-only supporter validation (show badges) 62 + - phase 2: platform registration (artists create support tiers) 63 + - phase 3: content gating (track-level access control) 64 + 65 + **research docs**: 66 + - [moderation architecture overhaul](docs/research/2025-12-20-moderation-architecture-overhaul.md) 67 + - [atprotofans paywall integration](docs/research/2025-12-20-atprotofans-paywall-integration.md) 68 + 69 + **tracking**: issue #625 70 + 71 + --- 72 + 50 73 #### beartype + moderation cleanup (PRs #617-619, Dec 19) 51 74 52 75 **runtime type checking** (PR #619): ··· 347 370 348 371 ## immediate priorities 349 372 373 + ### end-of-year sprint (Dec 20-31) 374 + 375 + see [sprint tracking issue #625](https://github.com/zzstoatzz/plyr.fm/issues/625) for details. 376 + 377 + | track | focus | status | 378 + |-------|-------|--------| 379 + | moderation | consolidate architecture, add rules engine | planning | 380 + | atprotofans | supporter validation, content gating | planning | 381 + 350 382 ### known issues 351 383 - playback auto-start on refresh (#225) 352 384 - iOS PWA audio may hang on first play after backgrounding 353 385 354 - ### immediate focus 355 - - **moderation cleanup**: consolidate copyright detection, reduce AudD API costs, streamline labeler integration (issues #541-544) 356 - 357 - ### feature ideas 358 - - issue #334: add 'share to bluesky' option for tracks 359 - - issue #373: lyrics field and Genius-style annotations 360 - 361 386 ### backlog 362 387 - audio transcoding pipeline integration (#153) - transcoder service deployed, integration deferred 388 + - share to bluesky (#334) 389 + - lyrics and annotations (#373) 363 390 364 391 ## technical state 365 392 ··· 513 540 514 541 --- 515 542 516 - this is a living document. last updated 2025-12-19. 543 + this is a living document. last updated 2025-12-20.
+250
docs/research/2025-12-20-atprotofans-paywall-integration.md
··· 1 + # research: atprotofans paywall integration 2 + 3 + **date**: 2025-12-20 4 + **question**: how should plyr.fm integrate with atprotofans to enable supporter-gated content? 5 + 6 + ## summary 7 + 8 + atprotofans provides a creator support platform on ATProto. plyr.fm currently has basic support link integration (#562). The full platform integration model allows defining support tiers with metadata that round-trips through validation, enabling feature gating. Implementation should proceed in phases: read-only badge display first, then platform registration, then content gating. 9 + 10 + ## current integration 11 + 12 + from PR #562, plyr.fm has: 13 + - support link mode selector in portal: none / atprotofans / custom 14 + - eligibility check queries user's PDS for `com.atprotofans.profile/self` record 15 + - profile page shows support button linking to `atprotofans.com/u/{did}` 16 + 17 + **code locations:** 18 + - `frontend/src/routes/portal/+page.svelte:137-166` - eligibility check 19 + - `frontend/src/routes/u/[handle]/+page.svelte:38-44` - support URL derivation 20 + - `backend/src/backend/api/preferences.py` - support_url validation 21 + 22 + ## atprotofans API 23 + 24 + ### validated endpoints 25 + 26 + **GET `/xrpc/com.atprotofans.validateSupporter`** 27 + 28 + validates if a user supports an artist. 29 + 30 + ``` 31 + params: 32 + supporter: did (the visitor) 33 + subject: did (the artist) 34 + signer: did (the broker/platform that signed the support template) 35 + 36 + response (not a supporter): 37 + {"valid": false} 38 + 39 + response (is a supporter): 40 + { 41 + "valid": true, 42 + "profile": { 43 + "did": "did:plc:...", 44 + "handle": "supporter.bsky.social", 45 + "displayName": "Supporter Name", 46 + ...metadata from support template 47 + } 48 + } 49 + ``` 50 + 51 + **key insight**: the `metadata` field from the support template is returned in the validation response. this enables plyr.fm to define packages and check them at runtime. 52 + 53 + ### platform integration flow 54 + 55 + from issue #564: 56 + 57 + ``` 58 + 1. plyr.fm registers as platform with did:web:plyr.fm 59 + 60 + 2. artist creates support template from portal: 61 + POST /xrpc/com.atprotofans.proposeSupportTemplate 62 + { 63 + "platform": "did:web:plyr.fm", 64 + "beneficiary": "{artist_did}", 65 + "billingCycle": "monthly", 66 + "minAmount": 1000, // cents 67 + "fees": {"platform": "5percent"}, 68 + "metadata": {"package": "early-access", "source": "plyr.fm"} 69 + } 70 + → returns template_id 71 + 72 + 3. artist approves template on atprotofans.com 73 + 74 + 4. supporter visits atprotofans.com/support/{template_id} 75 + → pays, support record created with metadata 76 + 77 + 5. plyr.fm calls validateSupporter, gets metadata back 78 + → unlocks features based on package 79 + ``` 80 + 81 + ## proposed tier system 82 + 83 + | package | price | what supporter gets | 84 + |---------|-------|---------------------| 85 + | `supporter` | $5 one-time | badge on profile, listed in supporters | 86 + | `early-access` | $10/mo | new releases 1 week early | 87 + | `lossless` | $15/mo | access to FLAC/WAV downloads | 88 + | `superfan` | $25/mo | all above + exclusive tracks | 89 + 90 + artists would choose which tiers to offer. supporters select tier on atprotofans. plyr.fm validates and gates accordingly. 91 + 92 + ## implementation phases 93 + 94 + ### phase 1: read-only validation (week 1) 95 + 96 + **goal**: show supporter badges, no platform registration required 97 + 98 + 1. **add validateSupporter calls to artist page** 99 + ```typescript 100 + // when viewing artist page, if viewer is logged in: 101 + const validation = await fetch( 102 + `https://atprotofans.com/xrpc/com.atprotofans.validateSupporter` + 103 + `?supporter=${viewer.did}&subject=${artist.did}&signer=${artist.did}` 104 + ); 105 + if (validation.valid) { 106 + // show "supporter" badge 107 + } 108 + ``` 109 + 110 + 2. **cache validation results** 111 + - redis cache with 5-minute TTL 112 + - key: `atprotofans:supporter:{viewer_did}:{artist_did}` 113 + 114 + 3. **display supporter badge on profile** 115 + - similar to verified badge styling 116 + - tooltip: "supports this artist via atprotofans" 117 + 118 + **frontend changes:** 119 + - `+page.svelte` (artist): call validation on mount if viewer logged in 120 + - new `SupporterBadge.svelte` component 121 + 122 + **backend changes:** 123 + - new endpoint: `GET /artists/{did}/supporter-status?viewer_did={did}` 124 + - or: call atprotofans directly from frontend (simpler, public endpoint) 125 + 126 + ### phase 2: platform registration (week 2) 127 + 128 + **goal**: let artists create plyr.fm-specific support tiers 129 + 130 + 1. **register plyr.fm as platform** 131 + - obtain `did:web:plyr.fm` (may already have) 132 + - register with atprotofans (talk to nick) 133 + 134 + 2. **add tier configuration to portal** 135 + ```typescript 136 + // portal settings 137 + let supportTiers = $state([ 138 + { package: 'supporter', enabled: true, minAmount: 500 }, 139 + { package: 'early-access', enabled: false, minAmount: 1000 }, 140 + ]); 141 + ``` 142 + 143 + 3. **create support templates on save** 144 + - call `proposeSupportTemplate` for each enabled tier 145 + - store template_ids in artist preferences 146 + 147 + 4. **link to support page** 148 + - instead of `atprotofans.com/u/{did}` 149 + - link to `atprotofans.com/support/{template_id}` 150 + 151 + **backend changes:** 152 + - new table: `support_templates` (artist_id, package, template_id, created_at) 153 + - new endpoint: `POST /artists/me/support-templates` 154 + - atprotofans API client 155 + 156 + ### phase 3: content gating (week 3+) 157 + 158 + **goal**: restrict content access based on support tier 159 + 160 + 1. **track-level gating** 161 + - new field: `required_support_tier` on tracks 162 + - values: null (public), 'supporter', 'early-access', 'lossless', 'superfan' 163 + 164 + 2. **validation on play/download** 165 + ```python 166 + async def check_access(track: Track, viewer_did: str) -> bool: 167 + if not track.required_support_tier: 168 + return True # public 169 + 170 + validation = await atprotofans.validate_supporter( 171 + supporter=viewer_did, 172 + subject=track.artist_did, 173 + signer="did:web:plyr.fm" 174 + ) 175 + 176 + if not validation.valid: 177 + return False 178 + 179 + viewer_tier = validation.profile.get("metadata", {}).get("package") 180 + return tier_includes(viewer_tier, track.required_support_tier) 181 + ``` 182 + 183 + 3. **early access scheduling** 184 + - new fields: `public_at` timestamp, `early_access_at` timestamp 185 + - track visible to early-access supporters before public 186 + 187 + 4. **lossless file serving** 188 + - store both lossy (mp3) and lossless (flac/wav) versions 189 + - check tier before serving lossless 190 + 191 + **database changes:** 192 + - add `required_support_tier` to tracks table 193 + - add `public_at`, `early_access_at` timestamps 194 + 195 + **frontend changes:** 196 + - track upload: tier selector 197 + - track detail: locked state for non-supporters 198 + - "become a supporter" CTA with link to atprotofans 199 + 200 + ## open questions 201 + 202 + 1. **what is the signer for existing atprotofans supporters?** 203 + - when artist just has `support_url: 'atprotofans'` without platform registration 204 + - likely `signer` = artist's own DID? 205 + 206 + 2. **how do we handle expired monthly subscriptions?** 207 + - atprotofans likely returns `valid: false` for expired 208 + - need to handle grace period for cached access? 209 + 210 + 3. **should lossless files be separate uploads or auto-transcoded?** 211 + - current: only one audio file per track 212 + - lossless requires either: dual upload or transcoding service 213 + 214 + 4. **what happens to gated content if artist disables tier?** 215 + - option A: content becomes public 216 + - option B: content stays gated, just no new supporters 217 + - option C: error state 218 + 219 + 5. **how do we display "this content is supporter-only" without revealing what's behind it?** 220 + - show track title/artwork but blur? 221 + - completely hide until authenticated? 222 + 223 + ## code references 224 + 225 + current integration: 226 + - `frontend/src/routes/portal/+page.svelte:137-166` - atprotofans eligibility check 227 + - `frontend/src/routes/u/[handle]/+page.svelte:38-44` - support URL handling 228 + - `backend/src/backend/api/preferences.py` - support_url validation 229 + 230 + ## external references 231 + 232 + - [atprotofans.com](https://atprotofans.com) - the platform 233 + - issue #564 - platform integration proposal 234 + - issue #562 - basic support link (merged) 235 + - StreamPlace integration example (from nick's description in #564) 236 + 237 + ## next steps 238 + 239 + 1. **test validateSupporter with real data** 240 + - find an artist who has atprotofans supporters 241 + - verify response format and metadata structure 242 + 243 + 2. **talk to nick about platform registration** 244 + - requirements for `did:web:plyr.fm` 245 + - API authentication for `proposeSupportTemplate` 246 + - fee structure options 247 + 248 + 3. **prototype phase 1 (badges)** 249 + - start with frontend-only validation calls 250 + - no backend changes needed initially
+239
docs/research/2025-12-20-moderation-architecture-overhaul.md
··· 1 + # research: moderation architecture overhaul 2 + 3 + **date**: 2025-12-20 4 + **question**: how should plyr.fm evolve its moderation architecture based on Roost Osprey and Bluesky Ozone patterns? 5 + 6 + ## summary 7 + 8 + plyr.fm has a functional but minimal moderation system: AuDD copyright scanning + ATProto label emission. Osprey (Roost) provides a powerful rules engine for complex detection patterns, while Ozone (Bluesky) offers a mature moderation workflow UI. The recommendation is a phased approach: first consolidate the existing Rust labeler with Python moderation logic, then selectively adopt patterns from both projects. 9 + 10 + ## current plyr.fm architecture 11 + 12 + ### components 13 + 14 + | layer | location | purpose | 15 + |-------|----------|---------| 16 + | moderation service | `moderation/` (Rust) | AuDD scanning, label signing, XRPC endpoints | 17 + | backend integration | `backend/src/backend/_internal/moderation.py` | orchestrates scans, stores results, emits labels | 18 + | moderation client | `backend/src/backend/_internal/moderation_client.py` | HTTP client with redis caching | 19 + | background tasks | `backend/src/backend/_internal/background_tasks.py` | `sync_copyright_resolutions()` perpetual task | 20 + | frontend | `frontend/src/lib/moderation.svelte.ts` | sensitive image state management | 21 + 22 + ### data flow 23 + 24 + ``` 25 + upload → schedule_copyright_scan() → docket task 26 + 27 + moderation service /scan 28 + 29 + AuDD API 30 + 31 + store in copyright_scans table 32 + 33 + if flagged → emit_label() → labels table (signed) 34 + 35 + frontend checks labels via redis-cached API 36 + ``` 37 + 38 + ### limitations 39 + 40 + 1. **single detection type**: only copyright via AuDD fingerprinting 41 + 2. **no rules engine**: hard-coded threshold (score >= X = flagged) 42 + 3. **manual admin ui**: htmx-based but limited (no queues, no workflow states) 43 + 4. **split architecture**: sensitive images in backend, copyright labels in moderation service 44 + 5. **no audit trail**: resolutions tracked but no event sourcing 45 + 46 + ## osprey architecture (roost) 47 + 48 + ### key concepts 49 + 50 + Osprey is a **rules engine** for real-time event processing, not just a labeler. 51 + 52 + **core components:** 53 + 54 + 1. **SML rules language** - declarative Python subset for signal combining 55 + ```python 56 + Spam_Rule = Rule( 57 + when_all=[ 58 + HasLabel(entity=UserId, label='new_account'), 59 + PostFrequency(user=UserId, window=TimeDelta(hours=1)) > 10, 60 + ], 61 + description="High-frequency posting from new account" 62 + ) 63 + ``` 64 + 65 + 2. **UDF plugin system** - extensible signals and effects 66 + ```python 67 + @hookimpl_osprey 68 + def register_udfs() -> Sequence[Type[UDFBase]]: 69 + return [TextContains, AudioFingerprint, BanUser] 70 + ``` 71 + 72 + 3. **stateful labels** - labels persist and are queryable in future rules 73 + 4. **batched async execution** - gevent greenlets with automatic batching 74 + 5. **output sinks** - kafka, postgres, webhooks for result distribution 75 + 76 + ### what osprey provides that plyr.fm lacks 77 + 78 + | capability | plyr.fm | osprey | 79 + |------------|---------|--------| 80 + | multi-signal rules | no | yes (combine 10+ signals) | 81 + | label persistence | yes (basic) | yes (with TTL, query) | 82 + | rule composition | no | yes (import, require) | 83 + | batched execution | no | yes (auto-batching UDFs) | 84 + | investigation UI | minimal | full query interface | 85 + | operator visibility | limited | full rule tracing | 86 + 87 + ### adoption considerations 88 + 89 + **pros:** 90 + - could replace hard-coded copyright threshold with configurable rules 91 + - would enable combining signals (e.g., new account + flagged audio + no bio) 92 + - plugin architecture aligns with plyr.fm's need for multiple moderation types 93 + 94 + **cons:** 95 + - heavy infrastructure (kafka, druid, postgres, redis) 96 + - python-based (plyr.fm moderation service is Rust) 97 + - overkill for current scale 98 + 99 + ## ozone architecture (bluesky) 100 + 101 + ### key concepts 102 + 103 + Ozone is a **moderation workflow UI** with queue management and team coordination. 104 + 105 + **review workflow:** 106 + ``` 107 + report received → reviewOpen → (escalate?) → reviewClosed 108 + 109 + muted / appealed / takendown 110 + ``` 111 + 112 + **action types:** 113 + - acknowledge, label, tag, mute, comment 114 + - escalate, appeal, reverse takedown 115 + - email (template-based) 116 + - takedown (PDS or AppView target) 117 + - strike (graduated enforcement) 118 + 119 + ### patterns applicable to plyr.fm 120 + 121 + 1. **queue-based review** - flagged content enters queue, moderators triage 122 + 2. **event-sourced audit trail** - every action is immutable event 123 + 3. **internal tags** - team metadata not exposed to users 124 + 4. **policy-linked actions** - associate decisions with documented policies 125 + 5. **bulk CSV import/export** - batch artist verification, label claims 126 + 6. **graduated enforcement (strikes)** - automatic actions at thresholds 127 + 7. **email templates** - DMCA notices, policy violations 128 + 129 + ### recent ozone updates (dec 2025) 130 + 131 + from commits: 132 + - `ae7c30b`: default to appview takedowns 133 + - `858b6dc`: fix bulk tag operations 134 + - `8a1f333`: age assurance events with access property 135 + 136 + haley's team focus: making takedowns and policy association more robust. 137 + 138 + ## recommendation: phased approach 139 + 140 + ### phase 1: consolidate (week 1) 141 + 142 + **goal**: unify moderation into single service, adopt patterns 143 + 144 + 1. **move sensitive images to moderation service** (issue #544) 145 + - add `sensitive_images` table to moderation postgres 146 + - add `/sensitive-images` endpoint 147 + - update frontend to fetch from moderation service 148 + 149 + 2. **add event sourcing for audit trail** 150 + - new `moderation_events` table: action, subject, actor, timestamp, details 151 + - log: scans, label emissions, resolutions, sensitive flags 152 + 153 + 3. **implement negation labels on track deletion** (issue #571) 154 + - emit `neg: true` when tracks with labels are deleted 155 + - cleaner label state 156 + 157 + ### phase 2: rules engine (week 2) 158 + 159 + **goal**: replace hard-coded thresholds with configurable rules 160 + 161 + 1. **add rule configuration** (can be simple JSON/YAML to start) 162 + ```yaml 163 + rules: 164 + copyright_violation: 165 + when: 166 + - audd_score >= 85 167 + actions: 168 + - emit_label: copyright-violation 169 + 170 + suspicious_upload: 171 + when: 172 + - audd_score >= 60 173 + - account_age_days < 7 174 + actions: 175 + - emit_label: needs-review 176 + ``` 177 + 178 + 2. **extract UDF-like abstractions** for signals: 179 + - `AuddScore(track_id)` 180 + - `AccountAge(did)` 181 + - `HasPreviousFlag(did)` 182 + 183 + 3. **add admin review queue** (borrowing from ozone patterns) 184 + - list items by state: pending, reviewed, dismissed 185 + - bulk actions 186 + 187 + ### phase 3: polish (week 3 if time) 188 + 189 + **goal**: robustness and UX 190 + 191 + 1. **graduated enforcement** - track repeat offenders, auto-escalate 192 + 2. **policy association** - link decisions to documented policies 193 + 3. **email templates** - DMCA notices, takedown confirmations 194 + 195 + ## code references 196 + 197 + current moderation code: 198 + - `moderation/src/main.rs:70-101` - router setup 199 + - `moderation/src/db.rs` - label storage 200 + - `moderation/src/labels.rs` - secp256k1 signing 201 + - `backend/src/backend/_internal/moderation.py` - scan orchestration 202 + - `backend/src/backend/_internal/moderation_client.py` - HTTP client 203 + - `backend/src/backend/_internal/background_tasks.py:180-220` - sync task 204 + 205 + osprey patterns to adopt: 206 + - `osprey_worker/src/osprey/engine/executor/executor.py` - batched execution model 207 + - `osprey_worker/src/osprey/worker/adaptor/plugin_manager.py` - plugin hooks 208 + - `example_plugins/register_plugins.py` - UDF registration pattern 209 + 210 + ozone patterns to adopt: 211 + - event-sourced moderation actions 212 + - review state machine (open → escalated → closed) 213 + - bulk workspace operations 214 + 215 + ## open questions 216 + 217 + 1. **should we rewrite moderation service in python?** 218 + - pro: unified stack, easier to add rules engine 219 + - con: rust is working, label signing is performance-sensitive 220 + 221 + 2. **how much of osprey do we actually need?** 222 + - full osprey: kafka + druid + postgres + complex infra 223 + - minimal: just the rule evaluation pattern with simple config 224 + 225 + 3. **do we need real-time event processing?** 226 + - current: batch via docket (5-min perpetual task) 227 + - osprey: real-time kafka streams 228 + - likely overkill for plyr.fm scale 229 + 230 + 4. **should admin UI move to moderation service?** 231 + - currently: htmx in rust service 232 + - alternative: next.js like ozone, or svelte in frontend 233 + 234 + ## external references 235 + 236 + - [Roost Osprey](https://github.com/roostorg/osprey) - rules engine 237 + - [Bluesky Ozone](https://github.com/bluesky-social/ozone) - moderation UI 238 + - [Roost roadmap](https://github.com/roostorg/community/blob/main/roadmap.md) 239 + - [ATProto Label Spec](https://atproto.com/specs/label)