Skip to main content
This flow outlines how royalties are typically reported, paid, and claimed for Licensed Creative Assets within the Cultura ecosystem, utilizing the reference RoyaltyClient and associated contracts like RightsBoundAccount and VerifierModule. The v0.2.0 SDK introduces enhanced sponsorship capabilities and streamlined off-chain payment flows.

Prerequisites

  • A Licensed Creative Asset exists (Token ID: licensedAssetId, Contract: assetContractAddress), minted with parent asset information (including royaltySplit).
  • The asset has an associated Attestation and a deployed Rights Bound Account (RBA) (Address: rbaAddress).
  • The Licensee (owner/user of the Licensed Creative Asset) has generated revenue or usage that triggers a royalty payment obligation (totalRoyaltyAmount).
  • The Parent Asset Owner(s) and Verifiers (who endorsed the Licensed Creative Asset’s attestation) have wallets ready to claim.
  • The designated Bond/Payment Token contract address is known and configured in the SDK.
  • SDK instances are initialized for the Licensee, Parent Owner(s), Verifiers, and optionally a Sponsor for gas-free operations.

Payment Methods

The Cultura ecosystem supports two primary royalty payment methods:

1. On-Chain Payment Flow

Direct blockchain-based payments where tokens are transferred through smart contracts.

2. Off-Chain Payment Flow (v0.2.0+)

Traditional payment methods (bank transfers, credit cards) with on-chain acceptance/denial by rights holders.

On-Chain Payment Flow Diagram

Off-Chain Payment Flow Diagram

On-Chain Payment Flow Explanation

1. Register Royalty Due (Optional)

  • The Licensee reports usage/revenue via the dApp for a specific period
  • Can be done directly or through a sponsor using delegatedRegisterRoyaltyDue()
  • Emits a RoyaltyDueRegistered event for tracking

2. Sponsored Payment Execution

  • v0.2.0 Enhancement: Sponsors can cover transaction costs and execute payments
  • Sponsor approves the VR (Verified Rights) RBA to spend payment tokens
  • Licensee signs payment authorization with sponsor address
  • Sponsor executes setPaymentInfo() on the VR RBA, transferring funds and recording splits

3. Parent Claims from Child RBA

  • New Multi-Level Architecture: Parent RBAs claim their allocated shares from the child VR RBA
  • Each parent RBA calls claimFromChild() to pull their portion from the VR RBA
  • This creates a new payment period in each parent RBA

4. Parent Owner Claims

  • Parent owners claim their shares from their respective parent RBAs
  • Uses the standard claim() method on each parent RBA
  • Parent owners receive their allocated royalty shares

5. Verifier Rewards Distribution

  • Two-Step Process:
    • First, trigger distribution from each parent RBA to the VerifierModule
    • Then, individual verifiers claim from the VerifierModule
  • Verifiers earn rewards based on their proportional bond contributions
  • Rewards are distributed separately for each parent asset

Off-Chain Payment Flow Explanation

1. Register Off-Chain Royalty Period

  • Delegated Registration: Sponsor can register royalty periods on behalf of licensee
  • Licensee signs authorization, sponsor executes transaction
  • Establishes the payment period and expected amounts

2. Report Off-Chain Payment

  • Traditional Payment Integration: Support for bank transfers, credit cards, etc.
  • Licensee reports payment with metadata (reference numbers, dates, notes)
  • Sponsor can execute the reporting transaction

3. Parent Acceptance/Denial

  • Democratic Process: Each parent owner must individually accept or deny the payment
  • Sponsored Fees: Sponsors can cover the blockchain fees for acceptance
  • Flexible Responses: Parents can accept their portion or deny if payment is disputed
  • Metadata Tracking: Full audit trail of off-chain payment details

SDK Snippets

On-Chain Payment Flow

import { CulturaSDK, ParentInfo } from "@cultura/sdk";
import { parseEther, getAddress } from "viem";

// --- Step 1: Register Royalty Due (Optional) ---
async function registerDue(sdk: CulturaSDK) {
  try {
    const startDate = BigInt(Math.floor(Date.now() / 1000));
    const endDate = startDate + 30n * 24n * 60n * 60n; // 30 days later
    console.log(`Registering royalty due for period ${periodIndex}...`);
    const txHash = await sdk.royalty.registerRoyaltyDue(
      licensedAssetId,
      totalRoyaltyAmount,
      startDate,
      endDate,
      periodIndex
    );
    await sdk.publicClient.waitForTransactionReceipt({ hash: txHash });
    console.log("Royalty due registered:", txHash);
  } catch (e) {
    console.error("Error registering royalty due:", e);
  }
}

// --- Step 2: Sponsored Payment Flow ---
async function sponsoredPaymentFlow(
  licenseeSdk: CulturaSDK,
  sponsorSdk: CulturaSDK,
  sponsorAddress: `0x${string}`,
  licenseeAddress: `0x${string}`
) {
  try {
    // 2a: Sponsor approves VR RBA to spend tokens
    console.log(
      `Sponsor approving VR RBA ${vrRbaAddress} to spend ${totalRoyaltyAmount}...`
    );
    const approveTx = await sponsorSdk.bondToken.approve(
      vrRbaAddress,
      totalRoyaltyAmount
    );
    await sponsorSdk.publicClient.waitForTransactionReceipt({
      hash: approveTx,
    });
    console.log("VR RBA approved by sponsor.");

    // 2b: Licensee signs payment authorization with sponsor
    console.log("Licensee signing payment authorization...");
    licenseeSdk.setRightsBoundAccount(vrRbaAddress);
    const paymentSignature =
      await licenseeSdk.rightsBoundAccount.signForSetPaymentInfo(
        periodIndex,
        totalRoyaltyAmount,
        sponsorAddress
      );
    console.log("Payment signature obtained from licensee.");

    // 2c: Sponsor executes payment
    console.log("Sponsor executing payment to VR RBA...");
    sponsorSdk.setRightsBoundAccount(vrRbaAddress);
    await sponsorSdk.rightsBoundAccount.setPaymentInfo({
      totalAmount: totalRoyaltyAmount,
      sponsor: sponsorAddress,
      signature: paymentSignature,
      signer: licenseeAddress,
    });
    console.log("Payment executed successfully by sponsor.");
  } catch (e) {
    console.error("Error in sponsored payment flow:", e);
    throw e;
  }
}

// --- Step 3: Parent Claims from Child (VR RBA) ---
async function parentClaimsFromChild(
  sdk: CulturaSDK,
  parentRbaAddress: `0x${string}`
) {
  try {
    sdk.setRightsBoundAccount(parentRbaAddress);
    console.log(
      `Parent claiming from child VR RBA for period ${periodIndex}...`
    );
    const claimTx = await sdk.rightsBoundAccount.claimFromChild(
      vrRbaAddress,
      periodIndex
    );
    await sdk.publicClient.waitForTransactionReceipt({ hash: claimTx });
    console.log("Parent successfully claimed from child VR RBA.");
  } catch (e) {
    console.error("Error claiming from child:", e);
    throw e;
  }
}

// --- Step 4: Parent Owner Claims from Parent RBA ---
async function claimFromParentRba(
  sdk: CulturaSDK,
  parentRbaAddress: `0x${string}`
) {
  try {
    sdk.setRightsBoundAccount(parentRbaAddress);
    console.log(`Parent owner claiming from parent RBA for period 0...`);
    // Parent RBA period will be 0 after claiming from child
    const claimTx = await sdk.rightsBoundAccount.claim(0n);
    await sdk.publicClient.waitForTransactionReceipt({ hash: claimTx });
    console.log("Parent owner claimed from parent RBA.");
  } catch (e) {
    console.error("Error claiming from parent RBA:", e);
    if (e.message.includes("NothingToClaim")) {
      console.warn("Parent share already claimed or not available.");
    } else {
      throw e;
    }
  }
}

// --- Step 5: Verifier Rewards Distribution ---
async function distributeVerifierRewards(
  sdk: CulturaSDK,
  parentAssetId: bigint,
  parentRbaAddress: `0x${string}`
) {
  try {
    console.log(`Distributing verifier rewards for parent ${parentAssetId}...`);
    const distributeTx = await sdk.verifierModule.distributeRewards(
      assetContractAddress,
      parentAssetId,
      parentRbaAddress,
      0n // Parent RBA period after claiming from child
    );
    await sdk.publicClient.waitForTransactionReceipt({ hash: distributeTx });
    console.log("Verifier rewards distributed to VerifierModule.");
  } catch (e) {
    console.error("Error distributing verifier rewards:", e);
    throw e;
  }
}

// --- Step 6: Individual Verifier Claims ---
async function claimVerifierReward(sdk: CulturaSDK, parentAssetId: bigint) {
  try {
    const verifierAddress = sdk.walletClient!.account!.address;
    const claimable = await sdk.verifierModule.calculateClaimableRewards(
      assetContractAddress,
      parentAssetId,
      verifierAddress
    );
    console.log(
      `Verifier claimable amount for parent ${parentAssetId}: ${claimable}`
    );

    if (claimable > 0n) {
      console.log("Verifier claiming rewards...");
      const claimTx = await sdk.verifierModule.claimRewards(
        assetContractAddress,
        parentAssetId
      );
      await sdk.publicClient.waitForTransactionReceipt({ hash: claimTx });
      console.log("Verifier rewards claimed.");
    } else {
      console.log("No rewards available to claim.");
    }
  } catch (e) {
    console.error("Error claiming verifier reward:", e);
    throw e;
  }
}

Off-Chain Payment Flow

// --- Off-Chain Payment Flow ---
async function offChainPaymentFlow(
  licenseeSdk: CulturaSDK,
  parentOwnerSdk: CulturaSDK,
  sponsorSdk: CulturaSDK,
  sponsorAddress: `0x${string}`,
  licenseeAddress: `0x${string}`,
  parentOwnerAddress: `0x${string}`
) {
  try {
    // Step 1: Register off-chain royalty period (delegated)
    console.log("Registering off-chain royalty period...");
    const offchainRoyaltyDue = parseEther("5");
    const startDate = BigInt(Math.floor(Date.now() / 1000));
    const endDate = startDate + BigInt(365 * 24 * 60 * 60);
    const offchainPeriodIndex =
      await licenseeSdk.royalty.getRoyaltyInfoCount(licensedAssetId);

    const registrationSignature =
      await licenseeSdk.royalty.signForDelegatedRegisterRoyaltyDue(
        licensedAssetId,
        offchainRoyaltyDue,
        startDate,
        endDate,
        offchainPeriodIndex
      );

    await sponsorSdk.royalty.delegatedRegisterRoyaltyDue(
      licensedAssetId,
      offchainRoyaltyDue,
      startDate,
      endDate,
      offchainPeriodIndex,
      registrationSignature
    );
    console.log("Off-chain royalty period registered.");

    // Step 2: Report off-chain payment (delegated)
    console.log("Reporting off-chain payment...");
    const paymentAmount = parseEther("1");
    const paymentMetadata = JSON.stringify({
      paymentMethod: "Bank Transfer",
      referenceNumber: "BT-2023-12345",
      paymentDate: new Date().toISOString(),
      notes: "Payment for Q3 licensing",
    });

    const reportSignature =
      await licenseeSdk.royalty.signForDelegatedOffchainReportRoyalty(
        licensedAssetId,
        offchainPeriodIndex,
        paymentAmount
      );

    await sponsorSdk.royalty.delegatedReportOffChainPayment(
      licensedAssetId,
      offchainPeriodIndex,
      paymentAmount,
      paymentMetadata,
      reportSignature
    );
    console.log("Off-chain payment reported.");

    // Step 3: Parent acceptance (sponsored fees)
    console.log("Parent accepting off-chain payment...");

    // Get parent's allocated amount
    const parentAcceptanceInfo =
      await licenseeSdk.royalty.getParentAcceptanceInfo(
        licensedAssetId,
        offchainPeriodIndex,
        parent1RbaAddress
      );
    const allocatedAmount = parentAcceptanceInfo.allocatedAmount;

    if (allocatedAmount > 0n) {
      // Parent owner signs acceptance
      const acceptSignature =
        await parentOwnerSdk.royalty.signForAcceptOffChainPaymentByParent(
          parent1RbaAddress,
          allocatedAmount,
          offchainPeriodIndex,
          sponsorAddress
        );

      // Sponsor executes acceptance (paying fees)
      await sponsorSdk.royalty.acceptOffChainPaymentByParent(
        licensedAssetId,
        offchainPeriodIndex,
        parent1RbaAddress,
        acceptSignature,
        parentOwnerAddress,
        sponsorAddress
      );
      console.log("Off-chain payment accepted by parent.");
    }

    // Step 4: Check acceptance status
    const allParentInfo = await licenseeSdk.royalty.getAllParentAcceptanceInfo(
      licensedAssetId,
      offchainPeriodIndex,
      assetContractAddress
    );

    console.log("Parent acceptance statuses:");
    for (const parentInfo of allParentInfo) {
      console.log(`- Parent RBA: ${parentInfo.parentBoundAccount}`);
      console.log(`  Status: ${parentInfo.acceptanceInfo.status}`);
      console.log(`  Amount: ${parentInfo.acceptanceInfo.allocatedAmount}`);
    }
  } catch (e) {
    console.error("Error in off-chain payment flow:", e);
    throw e;
  }
}

// --- Alternative: Parent Denial Flow ---
async function denyOffChainPayment(
  parentOwnerSdk: CulturaSDK,
  sponsorSdk: CulturaSDK,
  parentOwnerAddress: `0x${string}`,
  parentRbaAddress: `0x${string}`,
  periodIndex: bigint
) {
  try {
    console.log("Parent denying off-chain payment...");

    const denySignature =
      await parentOwnerSdk.royalty.signForDenyOffChainPaymentByParent(
        licensedAssetId,
        periodIndex,
        parentRbaAddress
      );

    await sponsorSdk.royalty.denyOffChainPaymentByParent(
      licensedAssetId,
      periodIndex,
      parentRbaAddress,
      denySignature,
      parentOwnerAddress
    );
    console.log("Off-chain payment denied by parent.");
  } catch (e) {
    console.error("Error denying off-chain payment:", e);
    throw e;
  }
}

Key Features in v0.2.0

1. Sponsorship Support

  • Gas-Free User Experience: Sponsors can cover all transaction costs
  • Flexible Business Models: Enables subscription, freemium, and sponsored content models
  • Enterprise Workflows: Complex multi-party operations with clear cost attribution

2. Multi-Level RBA Architecture

  • Hierarchical Claims: Child RBAs (VR) distribute to Parent RBAs, then to owners
  • Proportional Distribution: Automatic calculation based on royalty splits
  • Separate Verifier Pools: Individual reward pools for each parent asset

3. Off-Chain Payment Integration

  • Traditional Payment Methods: Support for bank transfers, credit cards, etc.
  • Democratic Acceptance: Each parent can individually accept or deny payments
  • Audit Trail: Complete metadata tracking for compliance and transparency
  • Dispute Resolution: Built-in denial mechanism for contested payments

4. Enhanced Developer Experience

  • Direct Returns: Attestation functions return UIDs and addresses directly
  • Comprehensive Error Handling: Clear error messages and recovery patterns
  • Parallel Operations: Support for batch processing and optimization
This updated flow provides a robust foundation for both traditional blockchain payments and modern off-chain payment integration, making the Cultura ecosystem accessible to a broader range of users and business models.