Payment Distribution

Complete examples for distributing payments to multiple recipients including revenue sharing, affiliate payouts, and royalty distribution.

Overview

Payment distribution allows a single transaction to split funds among multiple recipients according to predefined rules. This is essential for revenue sharing, affiliate programs, royalty payments, and collaborative applications. BSV's low fees make micro-splits economically viable.

Related SDK Components:

Simple Payment Splitting

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

/**
 * Payment Splitter
 *
 * Split a payment among multiple recipients
 */
class PaymentSplitter {
  /**
   * Split payment by percentages
   */
  async splitByPercentage(params: {
    senderKey: PrivateKey
    totalAmount: number
    splits: Array<{
      address: string
      percentage: number // 0-100
    }>
    utxo: {
      txid: string
      vout: number
      satoshis: number
      script: Script
    }
  }): Promise<{
    tx: Transaction
    distributions: Array<{
      address: string
      amount: number
      percentage: number
    }>
  }> {
    try {
      // Validate percentages
      const totalPercentage = params.splits.reduce((sum, s) => sum + s.percentage, 0)
      if (Math.abs(totalPercentage - 100) > 0.01) {
        throw new Error(`Percentages must sum to 100, got ${totalPercentage}`)
      }

      const tx = new Transaction()

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

      const distributions: Array<{
        address: string
        amount: number
        percentage: number
      }> = []

      // Calculate and add recipient outputs
      let totalDistributed = 0

      for (let i = 0; i < params.splits.length; i++) {
        const split = params.splits[i]
        let amount: number

        // Last recipient gets remainder to handle rounding
        if (i === params.splits.length - 1) {
          amount = params.totalAmount - totalDistributed
        } else {
          amount = Math.floor((params.totalAmount * split.percentage) / 100)
          totalDistributed += amount
        }

        // Check dust threshold
        if (amount < 546) {
          throw new Error(`Split amount ${amount} below dust threshold for ${split.address}`)
        }

        tx.addOutput({
          satoshis: amount,
          lockingScript: Script.fromAddress(split.address)
        })

        distributions.push({
          address: split.address,
          amount,
          percentage: split.percentage
        })
      }

      // Add change output
      const fee = 500 + (params.splits.length * 50)
      const change = params.utxo.satoshis - params.totalAmount - fee

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

      await tx.sign()

      console.log('Payment split completed')
      console.log('Recipients:', params.splits.length)
      console.log('Total distributed:', params.totalAmount)
      console.log('Transaction ID:', tx.id('hex'))

      return { tx, distributions }
    } catch (error) {
      throw new Error(`Payment split failed: ${error.message}`)
    }
  }

  /**
   * Split payment by fixed amounts
   */
  async splitByAmount(params: {
    senderKey: PrivateKey
    splits: Array<{
      address: string
      amount: number
    }>
    utxo: {
      txid: string
      vout: number
      satoshis: number
      script: Script
    }
  }): Promise<Transaction> {
    try {
      const totalAmount = params.splits.reduce((sum, s) => sum + s.amount, 0)
      const fee = 500 + (params.splits.length * 50)

      if (params.utxo.satoshis < totalAmount + fee) {
        throw new Error('Insufficient funds')
      }

      const tx = new Transaction()

      tx.addInput({
        sourceTXID: params.utxo.txid,
        sourceOutputIndex: params.utxo.vout,
        unlockingScriptTemplate: new P2PKH().unlock(params.senderKey),
        sequence: 0xffffffff
      })

      // Add recipient outputs
      for (const split of params.splits) {
        if (split.amount < 546) {
          throw new Error(`Amount ${split.amount} below dust threshold for ${split.address}`)
        }

        tx.addOutput({
          satoshis: split.amount,
          lockingScript: Script.fromAddress(split.address)
        })
      }

      // Add change
      const change = params.utxo.satoshis - totalAmount - fee

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

      await tx.sign()

      console.log('Fixed amount split completed')
      console.log('Recipients:', params.splits.length)
      console.log('Total:', totalAmount)

      return tx
    } catch (error) {
      throw new Error(`Fixed split failed: ${error.message}`)
    }
  }

  /**
   * Equal split among recipients
   */
  async splitEqually(params: {
    senderKey: PrivateKey
    totalAmount: number
    recipientAddresses: string[]
    utxo: {
      txid: string
      vout: number
      satoshis: number
      script: Script
    }
  }): Promise<Transaction> {
    try {
      const amountPerRecipient = Math.floor(
        params.totalAmount / params.recipientAddresses.length
      )

      if (amountPerRecipient < 546) {
        throw new Error('Split amount below dust threshold')
      }

      const splits = params.recipientAddresses.map(address => ({
        address,
        amount: amountPerRecipient
      }))

      return await this.splitByAmount({
        senderKey: params.senderKey,
        splits,
        utxo: params.utxo
      })
    } catch (error) {
      throw new Error(`Equal split failed: ${error.message}`)
    }
  }
}

/**
 * Usage Example
 */
async function paymentSplitterExample() {
  const splitter = new PaymentSplitter()
  const senderKey = PrivateKey.fromRandom()

  const utxo = {
    txid: 'funding-tx...',
    vout: 0,
    satoshis: 200000,
    script: new P2PKH().lock(senderKey.toPublicKey().toHash())
  }

  // Split by percentage
  const { distributions } = await splitter.splitByPercentage({
    senderKey,
    totalAmount: 100000,
    splits: [
      { address: '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa', percentage: 50 },
      { address: '1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2', percentage: 30 },
      { address: '1HLoD9E4SDFFPDiYfNYnkBLQ85Y51J3Zb1', percentage: 20 }
    ],
    utxo
  })

  console.log('Distributions:', distributions)

  // Equal split
  await splitter.splitEqually({
    senderKey,
    totalAmount: 90000,
    recipientAddresses: [
      '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa',
      '1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2',
      '1HLoD9E4SDFFPDiYfNYnkBLQ85Y51J3Zb1'
    ],
    utxo
  })
}

Revenue Sharing

Affiliate Payouts

See Also

SDK Components:

Learning Paths:

Last updated