research: atprotofans paywall integration#

date: 2025-12-20 question: how should plyr.fm integrate with atprotofans to enable supporter-gated content?

summary#

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.

current integration#

from PR #562, plyr.fm has:

  • support link mode selector in portal: none / atprotofans / custom
  • eligibility check queries user's PDS for com.atprotofans.profile/self record
  • profile page shows support button linking to atprotofans.com/u/{did}

code locations:

  • frontend/src/routes/portal/+page.svelte:137-166 - eligibility check
  • frontend/src/routes/u/[handle]/+page.svelte:38-44 - support URL derivation
  • backend/src/backend/api/preferences.py - support_url validation

atprotofans API#

validated endpoints#

GET /xrpc/com.atprotofans.validateSupporter

validates if a user supports an artist.

params:
  supporter: did (the visitor)
  subject: did (the artist)
  signer: did (the broker/platform that signed the support template)

response (not a supporter):
  {"valid": false}

response (is a supporter):
  {
    "valid": true,
    "profile": {
      "did": "did:plc:...",
      "handle": "supporter.bsky.social",
      "displayName": "Supporter Name",
      ...metadata from support template
    }
  }

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.

platform integration flow#

from issue #564:

1. plyr.fm registers as platform with did:web:plyr.fm

2. artist creates support template from portal:
   POST /xrpc/com.atprotofans.proposeSupportTemplate
   {
     "platform": "did:web:plyr.fm",
     "beneficiary": "{artist_did}",
     "billingCycle": "monthly",
     "minAmount": 1000,  // cents
     "fees": {"platform": "5percent"},
     "metadata": {"package": "early-access", "source": "plyr.fm"}
   }
   → returns template_id

3. artist approves template on atprotofans.com

4. supporter visits atprotofans.com/support/{template_id}
   → pays, support record created with metadata

5. plyr.fm calls validateSupporter, gets metadata back
   → unlocks features based on package

proposed tier system#

package price what supporter gets
supporter $5 one-time badge on profile, listed in supporters
early-access $10/mo new releases 1 week early
lossless $15/mo access to FLAC/WAV downloads
superfan $25/mo all above + exclusive tracks

artists would choose which tiers to offer. supporters select tier on atprotofans. plyr.fm validates and gates accordingly.

implementation phases#

phase 1: read-only validation (week 1) - IMPLEMENTED#

goal: show supporter badges, no platform registration required

status: completed 2025-12-20

  1. add validateSupporter calls to artist page

    // when viewing artist page, if viewer is logged in:
    const ATPROTOFANS_BROKER_DID = 'did:plc:7ewx3bksukdk6a4vycoykhhw';
    const url = new URL('https://atprotofans.com/xrpc/com.atprotofans.validateSupporter');
    url.searchParams.set('supporter', auth.user.did);
    url.searchParams.set('subject', artist.did);
    url.searchParams.set('signer', ATPROTOFANS_BROKER_DID);
    
    const response = await fetch(url.toString());
    if (response.ok) {
      const data = await response.json();
      isSupporter = data.valid === true;
    }
    
  2. cache validation results - deferred

    • frontend calls atprotofans directly (no backend cache needed initially)
    • can add redis cache later if rate limiting becomes an issue
  3. display supporter badge on profile

    • heart icon with "supporter" label
    • tooltip: "you support this artist via atprotofans"
    • only shown when logged-in viewer is a supporter

files changed:

  • frontend/src/routes/u/[handle]/+page.svelte - added validation logic
  • frontend/src/lib/components/SupporterBadge.svelte - new component

implementation notes:

  • calls atprotofans directly from frontend (public endpoint, no auth needed)
  • uses broker DID did:plc:7ewx3bksukdk6a4vycoykhhw as signer
  • only checks if artist has support_url: 'atprotofans'
  • doesn't show on your own profile

phase 2: platform registration (week 2)#

goal: let artists create plyr.fm-specific support tiers

  1. register plyr.fm as platform

    • obtain did:web:plyr.fm (may already have)
    • register with atprotofans (talk to nick)
  2. add tier configuration to portal

    // portal settings
    let supportTiers = $state([
      { package: 'supporter', enabled: true, minAmount: 500 },
      { package: 'early-access', enabled: false, minAmount: 1000 },
    ]);
    
  3. create support templates on save

    • call proposeSupportTemplate for each enabled tier
    • store template_ids in artist preferences
  4. link to support page

    • instead of atprotofans.com/u/{did}
    • link to atprotofans.com/support/{template_id}

backend changes:

  • new table: support_templates (artist_id, package, template_id, created_at)
  • new endpoint: POST /artists/me/support-templates
  • atprotofans API client

phase 3: content gating (week 3+)#

goal: restrict content access based on support tier

  1. track-level gating

    • new field: required_support_tier on tracks
    • values: null (public), 'supporter', 'early-access', 'lossless', 'superfan'
  2. validation on play/download

    async def check_access(track: Track, viewer_did: str) -> bool:
        if not track.required_support_tier:
            return True  # public
    
        validation = await atprotofans.validate_supporter(
            supporter=viewer_did,
            subject=track.artist_did,
            signer="did:web:plyr.fm"
        )
    
        if not validation.valid:
            return False
    
        viewer_tier = validation.profile.get("metadata", {}).get("package")
        return tier_includes(viewer_tier, track.required_support_tier)
    
  3. early access scheduling

    • new fields: public_at timestamp, early_access_at timestamp
    • track visible to early-access supporters before public
  4. lossless file serving

    • store both lossy (mp3) and lossless (flac/wav) versions
    • check tier before serving lossless

database changes:

  • add required_support_tier to tracks table
  • add public_at, early_access_at timestamps

frontend changes:

  • track upload: tier selector
  • track detail: locked state for non-supporters
  • "become a supporter" CTA with link to atprotofans

open questions#

  1. what is the signer for existing atprotofans supporters?

    • when artist just has support_url: 'atprotofans' without platform registration
    • likely signer = artist's own DID?
  2. how do we handle expired monthly subscriptions?

    • atprotofans likely returns valid: false for expired
    • need to handle grace period for cached access?
  3. should lossless files be separate uploads or auto-transcoded?

    • current: only one audio file per track
    • lossless requires either: dual upload or transcoding service
  4. what happens to gated content if artist disables tier?

    • option A: content becomes public
    • option B: content stays gated, just no new supporters
    • option C: error state
  5. how do we display "this content is supporter-only" without revealing what's behind it?

    • show track title/artwork but blur?
    • completely hide until authenticated?

code references#

current integration:

  • frontend/src/routes/portal/+page.svelte:137-166 - atprotofans eligibility check
  • frontend/src/routes/u/[handle]/+page.svelte:38-44 - support URL handling
  • backend/src/backend/api/preferences.py - support_url validation

external references#

  • atprotofans.com - the platform
  • issue #564 - platform integration proposal
  • issue #562 - basic support link (merged)
  • StreamPlace integration example (from nick's description in #564)

next steps#

  1. test validateSupporter with real data

    • find an artist who has atprotofans supporters
    • verify response format and metadata structure
  2. talk to nick about platform registration

    • requirements for did:web:plyr.fm
    • API authentication for proposeSupportTemplate
    • fee structure options
  3. prototype phase 1 (badges)

    • start with frontend-only validation calls
    • no backend changes needed initially