Rising Tide Developers
Concepts

Revenue Composition (developer)

Technical reference for ContributionRole and ProductContribution - schema, validation, position freezing, and substrate-derived creator slots.

ContributionRole

type ContributionRole =
  | "CREATOR"
  | "COLLABORATOR"
  | "TEMPLATE_ARTIST"
  | "STICKER_ARTIST"
  | "FUNDING_GROUP"
  | "PLATFORM"
  | "CONTRACT_TEMPLATE_DEVELOPER"  // supply-chain cap table (mint axis)
  | "APP_HOST";                    // supply-chain cap table (deploy axis)

Two tiers:

  • Creator-allocated (CREATOR, COLLABORATOR): the creator sets the recipient address and percentage directly.
  • Protocol-routed (TEMPLATE_ARTIST, STICKER_ARTIST, FUNDING_GROUP, PLATFORM, CONTRACT_TEMPLATE_DEVELOPER, APP_HOST): the recipient and rate are derived by the server from the picked asset (StyleSet, Sticker, FundingGroup), the contract template, or the source app record. The two supply-chain roles are capture-now-pay-later - they derive at royalty 0 by default.

ProductContribution schema

interface ProductContribution {
  id: number;
  smartProductId: number;
  role: ContributionRole;
  percentage: number;                    // 0-100
  position: number | null;               // 1-10, assigned by freezePositions()
  address: string;                       // denormalized on-chain payout address
  displayName: string;
  userId: number | null;                 // recipient FK (CREATOR/COLLABORATOR/artist)
  fundingGroupId: number | null;         // FUNDING_GROUP source + recipient FK
  sourceStyleSetId: number | null;       // TEMPLATE_ARTIST source FK
  sourceStickerId: number | null;        // STICKER_ARTIST source FK
  sourceContractTemplateId: number | null; // CONTRACT_TEMPLATE_DEVELOPER source FK
}

Validation rules

  • At least one CREATOR row is required. Multiple CREATOR rows are allowed - the substrate enforces the role budget, not the row count.
  • Total percentages must equal the domain-specific target sum (see below).
  • Max 10 contributions total (matches the Clarity template's creator-1..creator-10 slot count).
  • FUNDING_GROUP must reference a FundingGroup in APPROVED or VERIFIED status.
  • COLLABORATOR requires recipientAddress.
  • TEMPLATE_ARTIST requires styleSetId; STICKER_ARTIST requires stickerId.

Target sum per domain

contributionService.targetSumForProduct(product) returns:

DomainTarget sumWhy
PAGE100All revenue routes through distribute-fees on every collect.
EVENT100Same. Ticket fees flow through the same on-chain path.
PREDICTION100 - poolPercentageThe pool share is paid by tournament settlement, not by distribute-fees. On-chain creator-N slots only need to cover the remainder.
GAME100 - poolPctThe pool share of every buy-in stays in the contract as the match kitty. Contributions cover the remainder.

A bracket tournament with poolPercentage: 80 has ProductContribution rows summing to 20. A page with no pool concept has rows summing to 100.

Substrate-derived creator slots

The canonical create flow does not hand-roll a creators[] array. Instead, picker payloads feed into ContributionPickInput on the create body. The server derives ProductContribution rows from the picks inside the create transaction:

await rtClient.smartProducts.createPage({
  // ... required fields
  contributions: {
    styleSetId: 7,            // server derives TEMPLATE_ARTIST row at StyleSet.royaltyPercentage
    stickers: [               // server derives one STICKER_ARTIST row per sticker
      { stickerId: 12, x: 100, y: 200, scale: 1, rotation: 0 },
    ],
    collaborators: [          // free-form, creator sets address + percentage
      { address: "SP123...", displayName: "Collaborator", percentage: 10 },
    ],
    fundingGroups: [          // registry-backed pick
      { id: 3, percentage: 5 },
    ],
  },
});

The CREATOR row is auto-derived as the remainder after protocol-routed rows are allocated. No client-side rebalancing.

Position freezing

contributionService.freezePositions(productId) runs inside /api/contracts/generate before template evaluation. It assigns position values 1..N to all non-PLATFORM rows in insertion order, inside a single transaction. Idempotent - already-frozen rows are skipped.

Positions are passed to the deployed contract as creator-1..creator-N. At mint time distribute-fees reads each slot and pays out the configured percentage. The Chainhook position-verifier later matches on-chain transfer events back to ProductContribution.position to prove the split executed correctly.

Reading contributions

// Frozen rows in deploy order (position ASC), or insertion order if not yet frozen.
const { data: contributions } = await rtClient.smartProducts.getContributions(productId);

For mutations the SDK does not expose a single-row addContribution() - contributions are always created together with the SmartProduct draft via the contributions payload above. Sticker placements are the one exception: they have rtClient.smartProducts.addSticker() / updateStickerPlacement() / removeSticker(), which atomically maintain the ProductSticker row and its matching STICKER_ARTIST ProductContribution row. Mutating any contributions after position has been frozen is rejected (409) because the on-chain creator-info map would diverge.