Content Paywall

Complete examples for implementing paywalled content with BSV microtransactions, including pay-per-view, subscriptions, and metered access.

Overview

BSV's low transaction fees make microtransactions economically viable, enabling new monetization models for digital content. This guide covers pay-per-view content, token-based access, time-limited access, metered consumption, and subscription tiers. These patterns work for articles, videos, APIs, and any digital resource.

Related SDK Components:

Pay-Per-View Content

import { Transaction, PrivateKey, P2PKH, Script, OP, Hash } from '@bsv/sdk'

/**
 * Pay-Per-View Content Manager
 *
 * Manage single payment access to content
 */
class PayPerViewManager {
  private contentPublisher: PrivateKey
  private accessTokens: Map<string, AccessToken> = new Map()

  constructor(publisherKey: PrivateKey) {
    this.contentPublisher = publisherKey
  }

  /**
   * Purchase content access
   */
  async purchaseAccess(params: {
    userKey: PrivateKey
    contentId: string
    price: number
    utxo: {
      txid: string
      vout: number
      satoshis: number
      script: Script
    }
  }): Promise<{
    tx: Transaction
    accessToken: AccessToken
  }> {
    try {
      console.log('Processing content purchase')
      console.log('Content ID:', params.contentId)
      console.log('Price:', params.price, 'satoshis')

      const tx = new Transaction()

      // Add user's payment input
      tx.addInput({
        sourceTXID: params.utxo.txid,
        sourceOutputIndex: params.utxo.vout,
        unlockingScriptTemplate: new P2PKH().unlock(params.userKey),
        sequence: 0xffffffff
      })

      // Payment output to publisher
      tx.addOutput({
        satoshis: params.price,
        lockingScript: new P2PKH().lock(this.contentPublisher.toPublicKey().toHash())
      })

      // OP_RETURN with access proof
      const accessProof = this.createAccessProof(
        params.contentId,
        params.userKey.toPublicKey().toAddress()
      )

      const opReturnScript = new Script()
      opReturnScript.writeOpCode(OP.OP_FALSE)
      opReturnScript.writeOpCode(OP.OP_RETURN)
      opReturnScript.writeBin(Buffer.from('CONTENT_ACCESS', 'utf8'))
      opReturnScript.writeBin(Buffer.from(params.contentId, 'utf8'))
      opReturnScript.writeBin(Buffer.from(accessProof, 'hex'))

      tx.addOutput({
        satoshis: 0,
        lockingScript: opReturnScript
      })

      // Change output
      const fee = 500
      const change = params.utxo.satoshis - params.price - fee

      if (change >= 546) {
        tx.addOutput({
          satoshis: change,
          lockingScript: new P2PKH().lock(params.userKey.toPublicKey().toHash())
        })
      }

      await tx.sign()

      // Generate access token
      const accessToken: AccessToken = {
        tokenId: this.generateTokenId(),
        contentId: params.contentId,
        userAddress: params.userKey.toPublicKey().toAddress(),
        paymentTxid: tx.id('hex'),
        accessProof,
        createdAt: Date.now(),
        expiresAt: Date.now() + 30 * 24 * 60 * 60 * 1000, // 30 days
        status: 'active'
      }

      this.accessTokens.set(accessToken.tokenId, accessToken)

      console.log('Access purchased successfully')
      console.log('Access token:', accessToken.tokenId)

      return { tx, accessToken }
    } catch (error) {
      throw new Error(`Purchase failed: ${error.message}`)
    }
  }

  /**
   * Verify access token
   */
  verifyAccess(tokenId: string, contentId: string): AccessVerification {
    const token = this.accessTokens.get(tokenId)

    if (!token) {
      return {
        valid: false,
        reason: 'Token not found'
      }
    }

    if (token.contentId !== contentId) {
      return {
        valid: false,
        reason: 'Token not valid for this content'
      }
    }

    if (token.status !== 'active') {
      return {
        valid: false,
        reason: `Token status: ${token.status}`
      }
    }

    if (Date.now() > token.expiresAt) {
      token.status = 'expired'
      return {
        valid: false,
        reason: 'Token expired'
      }
    }

    return {
      valid: true,
      token
    }
  }

  /**
   * Revoke access
   */
  revokeAccess(tokenId: string): void {
    const token = this.accessTokens.get(tokenId)
    if (token) {
      token.status = 'revoked'
      console.log('Access revoked:', tokenId)
    }
  }

  /**
   * Create access proof
   */
  private createAccessProof(contentId: string, userAddress: string): string {
    const data = `${contentId}:${userAddress}:${Date.now()}`
    return Hash.sha256(Buffer.from(data, 'utf8')).toString('hex')
  }

  /**
   * Generate token ID
   */
  private generateTokenId(): string {
    return `TOKEN-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
  }
}

interface AccessToken {
  tokenId: string
  contentId: string
  userAddress: string
  paymentTxid: string
  accessProof: string
  createdAt: number
  expiresAt: number
  status: 'active' | 'expired' | 'revoked'
}

interface AccessVerification {
  valid: boolean
  token?: AccessToken
  reason?: string
}

/**
 * Usage Example
 */
async function payPerViewExample() {
  const publisher = PrivateKey.fromRandom()
  const manager = new PayPerViewManager(publisher)

  const user = PrivateKey.fromRandom()

  // User purchases access
  const { accessToken } = await manager.purchaseAccess({
    userKey: user,
    contentId: 'ARTICLE-001',
    price: 1000, // 1000 satoshis
    utxo: {
      txid: 'user-utxo...',
      vout: 0,
      satoshis: 10000,
      script: new P2PKH().lock(user.toPublicKey().toHash())
    }
  })

  console.log('Access token:', accessToken)

  // Verify access
  const verification = manager.verifyAccess(accessToken.tokenId, 'ARTICLE-001')

  if (verification.valid) {
    console.log('Access granted!')
    // Deliver content...
  } else {
    console.log('Access denied:', verification.reason)
  }
}

Metered Content Access

Time-Limited Access

See Also

SDK Components:

Learning Paths:

Last updated