Skip to content

Latest commit

 

History

History
477 lines (369 loc) · 12.7 KB

File metadata and controls

477 lines (369 loc) · 12.7 KB

Nostr Integration Plan for Podcast Ad-Blocking

This document outlines the integration points for replacing the POC fixture-based system with real Nostr relays, payments, and caching.

Current POC Architecture

The POC uses a provider interface pattern that allows swapping implementations without refactoring:

interface AdSegmentProvider {
  fetchAdSegments(episodeKey: string): Promise<AdSegment[]>
  fetchTaggerProfile(pubkey: string): Promise<TaggerProfile | null>
  fetchEpisodeAdData?(episodeKey: string): Promise<EpisodeAdData | null>
}

// POC: FixtureAdSegmentProvider (reads local JSON)
// Future: NostrAdSegmentProvider (queries Nostr relays)

Integration Points

1. Nostr Ad Segment Provider

File: src/services/adSegments/adSegmentNostrProvider.ts (to be created)

Implementation:

import { AdSegmentProvider, AdSegment, TaggerProfile } from './adSegmentTypes'
import { relayPool } from '~/lib/nostr/relayPool' // To be implemented

export class NostrAdSegmentProvider implements AdSegmentProvider {
  private relayUrls: string[]
  private cache: Map<string, AdSegment[]>

  constructor(relayUrls: string[]) {
    this.relayUrls = relayUrls
    this.cache = new Map()
  }

  async fetchAdSegments(episodeKey: string): Promise<AdSegment[]> {
    // Query Nostr relays for ad segment events
    // Filter by episodeKey
    // Parse events into AdSegment objects
    // Cache results
  }

  async fetchTaggerProfile(pubkey: string): Promise<TaggerProfile | null> {
    // Query Nostr for NIP-01 kind 0 metadata event
    // Parse into TaggerProfile
    // Cache results
  }
}

Nostr Event Schema (Custom NIP):

Define a custom Nostr event kind for ad segments:

{
  "kind": 30XX, // TBD: Choose a custom kind number
  "pubkey": "<tagger's nostr pubkey>",
  "created_at": <unix timestamp>,
  "tags": [
    ["e", "<episodeKey>"],
    ["podcast", "<podcast feed URL>"],
    ["episode", "<episode enclosure URL>"],
    ["start", "<startTime in seconds>"],
    ["end", "<endTime in seconds>"],
    ["type", "ad"],
    ["description", "Pre-roll sponsor read"]
  ],
  "content": "Optional human-readable description or notes",
  "sig": "<signature>"
}

Relay Queries:

// Fetch all ad segments for an episode
const filter = {
  kinds: [30XX],
  "#e": [episodeKey],
  limit: 100
}

// Fetch tagger profile
const profileFilter = {
  kinds: [0], // NIP-01 metadata
  authors: [pubkey],
  limit: 1
}

2. Relay Connection & Pooling

File: src/lib/nostr/relayPool.ts (to be created)

Functionality:

  • Connect to multiple Nostr relays
  • Pool connections for efficiency
  • Handle WebSocket reconnection logic
  • Implement subscription management
  • Aggregate results from multiple relays

Libraries:

  • nostr-tools (official Nostr TypeScript library)
  • nostr-relay-pool (community relay pool implementation)

Example:

import { SimplePool } from 'nostr-tools/pool'

const pool = new SimplePool()
const relays = [
  'wss://relay.damus.io',
  'wss://relay.nostr.band',
  'wss://nos.lol',
]

export async function fetchAdSegmentEvents(episodeKey: string) {
  const events = await pool.list(relays, [{
    kinds: [30XX],
    "#e": [episodeKey]
  }])

  return events
}

3. Real-Time Updates via WebSocket Subscriptions

File: src/services/adSegments/adSegmentRealtimeUpdates.ts (to be created)

Functionality:

  • Subscribe to ad segment events for current episode
  • Update player state when new ad segments are published
  • Re-merge segments when updates arrive
  • Gracefully handle subscription errors

Example:

export function subscribeToAdSegmentUpdates(
  episodeKey: string,
  onUpdate: (newSegments: AdSegment[]) => void
) {
  const sub = pool.sub(relays, [{
    kinds: [30XX],
    "#e": [episodeKey]
  }])

  sub.on('event', (event) => {
    const newSegment = parseAdSegmentEvent(event)
    // Trigger re-fetch and re-merge
    onUpdate([...existingSegments, newSegment])
  })

  return () => sub.unsub() // Cleanup function
}

4. Zap-Based Validation & Reputation (NIP-57)

File: src/services/adSegments/adSegmentValidation.ts (to be created)

Concept:

  • Users can "zap" (send sats via Lightning) ad segments to vote on accuracy
  • Zaps increase confidence score and tagger reputation
  • High-zapped segments take precedence in merging conflicts
  • Low-zapped segments may be filtered out

Implementation:

// Fetch zaps for an ad segment event
async function fetchZapsForSegment(eventId: string): Promise<Zap[]> {
  const zapEvents = await pool.list(relays, [{
    kinds: [9735], // NIP-57 zap receipt
    "#e": [eventId]
  }])

  return zapEvents.map(parseZapEvent)
}

// Calculate confidence based on zaps
function calculateConfidence(segment: AdSegment, zaps: Zap[]): number {
  const totalSats = zaps.reduce((sum, zap) => sum + zap.amount, 0)
  // Map sats to confidence score (0-100)
  return Math.min(100, totalSats / 1000) // 1000 sats = 100% confidence
}

// Weighted merging: prefer segments with higher zap totals
function mergeWithWeights(segments: AdSegment[], zaps: Map<string, Zap[]>) {
  // Sort by confidence before merging
  const sorted = segments.sort((a, b) => {
    const aConfidence = calculateConfidence(a, zaps.get(a.id) || [])
    const bConfidence = calculateConfidence(b, zaps.get(b.id) || [])
    return bConfidence - aConfidence
  })

  return mergeAdSegments(sorted)
}

5. Caching Layer

File: src/services/adSegments/adSegmentCache.ts (to be created)

Strategies:

  1. In-Memory Cache (current player session):

    • Already implemented in FixtureAdSegmentProvider
    • Keep for active episode
  2. IndexedDB Cache (persistent browser storage):

    import { openDB } from 'idb'
    
    const db = await openDB('podverse-ad-segments', 1, {
      upgrade(db) {
        db.createObjectStore('adSegments', { keyPath: 'episodeKey' })
        db.createObjectStore('profiles', { keyPath: 'pubkey' })
      }
    })
    
    // Store ad segments
    await db.put('adSegments', {
      episodeKey,
      segments,
      cachedAt: Date.now()
    })
    
    // Retrieve with TTL check
    const cached = await db.get('adSegments', episodeKey)
    if (cached && Date.now() - cached.cachedAt < 3600000) { // 1 hour TTL
      return cached.segments
    }
  3. Server-Side Cache (optional, for scaling):

    • Cache popular episodes on Podverse API server
    • Serve from cache if available, fall back to relay query
    • Periodically refresh from relays

6. User-Generated Ad Tagging

File: src/components/AdSegments/AdSegmentTagger.tsx (to be created)

Functionality:

  • UI for users to mark ad start/end times during playback
  • Publish ad segment event to Nostr relays
  • Optionally: request zaps from other users for validation

Workflow:

  1. User presses "Mark Ad Start" button
  2. Current playback time recorded
  3. User presses "Mark Ad End" button
  4. Publish ad segment event to relays:
import { getEventHash, signEvent } from 'nostr-tools'

async function publishAdSegment(
  episodeKey: string,
  startTime: number,
  endTime: number,
  description: string,
  userKeys: { pubkey: string; privkey: string }
) {
  const event = {
    kind: 30XX,
    pubkey: userKeys.pubkey,
    created_at: Math.floor(Date.now() / 1000),
    tags: [
      ["e", episodeKey],
      ["start", startTime.toString()],
      ["end", endTime.toString()],
      ["type", "ad"],
      ["description", description]
    ],
    content: description
  }

  event.id = getEventHash(event)
  event.sig = signEvent(event, userKeys.privkey)

  // Publish to relays
  await pool.publish(relays, event)
}

7. Conflict Resolution

File: src/services/adSegments/adSegmentConflictResolution.ts (to be created)

Scenarios:

  1. Multiple taggers mark different times for same ad:

    • Use weighted average based on reputation/zaps
    • Or use majority vote (most common time range)
  2. User reports false positive:

    • Publish negative vote event (custom kind)
    • Decrease confidence score for that segment
  3. Spam/malicious tagging:

    • Filter out segments from low-reputation taggers
    • Require minimum zap threshold for segments to appear

Example:

function resolveConflicts(segments: AdSegment[], votes: Vote[]): AdSegment[] {
  // Group overlapping segments
  const groups = groupOverlapping(segments)

  // For each group, choose the best segment based on:
  // 1. Highest zap total
  // 2. Highest reputation
  // 3. Most votes
  return groups.map(group => {
    return group.sort((a, b) => {
      const scoreA = calculateScore(a, votes)
      const scoreB = calculateScore(b, votes)
      return scoreB - scoreA
    })[0]
  })
}

8. Nostr Authentication (NIP-07)

File: src/lib/nostr/auth.ts (to be created)

Functionality:

  • Integrate with browser Nostr extensions (Alby, nos2x, Flamingo)
  • Request user's public key
  • Sign events using extension

Example:

// Check if Nostr extension is available
if (window.nostr) {
  // Get user's public key
  const pubkey = await window.nostr.getPublicKey()

  // Sign ad segment event
  const signedEvent = await window.nostr.signEvent(unsignedEvent)

  // Publish to relays
  await pool.publish(relays, signedEvent)
}

Migration Path (POC → Production)

Step 1: Implement NostrAdSegmentProvider

  • Create NostrAdSegmentProvider class
  • Implement relay queries
  • Add caching layer

Step 2: Swap Providers

Replace in src/services/player/player.tsx:

// POC (current)
import { fixtureAdSegmentProvider } from '../adSegments/adSegmentFixtureProvider'
const provider = fixtureAdSegmentProvider

// Production (future)
import { nostrAdSegmentProvider } from '../adSegments/adSegmentNostrProvider'
const provider = nostrAdSegmentProvider

Or use environment variable:

const provider = process.env.NEXT_PUBLIC_USE_NOSTR === 'true'
  ? nostrAdSegmentProvider
  : fixtureAdSegmentProvider

Step 3: Add User Tagging UI

  • Implement "Mark Ad" button in player
  • Implement ad segment submission form
  • Integrate with Nostr signing

Step 4: Implement Zap Validation

  • Query zaps for ad segments
  • Calculate confidence scores
  • Display reputation in UI

Step 5: Enable Real-Time Updates

  • Subscribe to relay events during playback
  • Update ad segments dynamically
  • Handle WebSocket errors gracefully

Testing Strategy

Unit Tests

  • Mock Nostr relay responses
  • Test event parsing
  • Test conflict resolution logic

Integration Tests

  • Connect to test relay
  • Publish test events
  • Verify fetching and parsing

E2E Tests

  • Full user workflow: mark ad → publish → fetch → skip
  • Test with multiple users tagging same episode
  • Verify zap-based validation

Security Considerations

  1. Event Validation:

    • Verify event signatures
    • Check event timestamps (reject too old or future events)
    • Validate tag formats
  2. Spam Protection:

    • Rate limit queries to relays
    • Filter out segments from blocked users
    • Require minimum reputation for first-time taggers
  3. Privacy:

    • Ad segment events are public (by design)
    • Users can use pseudonymous Nostr keys
    • No personal data required

Relay Recommendations

Public Relays:

  • wss://relay.damus.io (popular, well-maintained)
  • wss://relay.nostr.band (indexing relay)
  • wss://nos.lol (reliable)
  • wss://relay.snort.social (high uptime)

Specialized Relays (Future):

  • Create dedicated relay for podcast ad segments
  • Better performance and lower latency
  • Custom filtering and indexing

Open Questions

  1. Custom NIP Number: Should we propose a new NIP for podcast ad segments, or use an existing kind?
  2. Zap Threshold: What minimum zap amount should be required for high confidence?
  3. Relay Selection: Should users choose their own relays, or use a default set?
  4. Offline Mode: How to handle when relays are unavailable (fallback to cached data)?

TODO Markers in Code

Search for // TODO: Future Nostr Integration Points in these files:

  • src/services/adSegments/adSegmentTypes.ts
  • src/services/adSegments/adSegmentIdentity.ts
  • src/services/adSegments/adSegmentMerge.ts
  • src/services/adSegments/adSegmentFixtureProvider.ts
  • src/services/player/playerAdSkip.ts

These markers indicate where Nostr-specific logic should be added.

Resources