Skip to main content

Payments & Monetization

The platform monetizes through per‑story purchases, subscriptions, and transparent revenue sharing, encoded directly in Sui state so that attribution is durable and independently verifiable. This section details the economic objects, flows, and protections that make payments seamless for readers and predictable for journalists.

Tokens and units

  • Gas and base currency: SUI for gas. End‑user payments can be denominated in SUI or supported stablecoins on Sui (e.g., USD‑pegged tokens issued on Sui).
  • Prices and royalties: expressed in token units and stored in policy metadata so that UIs can quote, convert, and validate consistently.

Economic objects

Kiosk Policy Rules Implementation

module pml::economics {
use sui::kiosk::{Self, Kiosk};
use sui::transfer_policy::{Self, TransferPolicy, TransferPolicyCap};
use kiosk::royalty_rule;
use kiosk::kiosk_lock_rule;
use sui::balance::{Self, Balance};
use sui::sui::SUI;

/// Content pricing configuration
struct PricingPolicy has store {
floor_price: u64,
royalty_bps: u16, // Basis points (e.g., 1000 = 10%)
creator_share: u16, // Creator's portion of royalties
platform_share: u16, // Platform's portion
contributor_shares: vector<ContributorShare>
}

struct ContributorShare has store {
address: address,
share_bps: u16 // Share in basis points
}

/// Create comprehensive transfer policy
public fun create_content_policy(
publisher: &Publisher,
floor_price: u64,
royalty_bps: u16,
contributors: vector<address>,
shares: vector<u16>,
ctx: &mut TxContext
): (TransferPolicy<ContentNFT>, TransferPolicyCap<ContentNFT>) {
let (policy, cap) = transfer_policy::new<ContentNFT>(publisher, ctx);

// Add floor price rule
kiosk_lock_rule::add(&mut policy, &cap);

// Add royalty rule with custom distribution
royalty_rule::add(&mut policy, &cap, royalty_bps, floor_price);

// Store custom distribution via dynamic field
let pricing = PricingPolicy {
floor_price,
royalty_bps,
creator_share: 6000, // 60% to creator
platform_share: 1000, // 10% to platform
contributor_shares: create_contributor_shares(contributors, shares)
};

df::add(&mut policy.id, b"pricing", pricing);

(policy, cap)
}
}

Entitlement Objects

module pml::entitlements {
/// Per-article entitlement
struct ArticleEntitlement has key {
id: UID,
content_id: ID,
reader: address,
purchased_at: u64,
perpetual: bool
}

/// Subscription entitlement
struct SubscriptionEntitlement has key {
id: UID,
tier: u8, // 0: basic, 1: premium, 2: unlimited
reader: address,
valid_from: u64,
valid_until: u64,
auto_renew: bool,
renewal_price: u64
}

/// Bundle entitlement for collections
struct BundleEntitlement has key {
id: UID,
bundle_id: ID,
content_ids: vector<ID>,
reader: address,
valid_from: u64,
valid_until: Option<u64>
}
}
  • Kiosk policy rules
    • Floor price: minimum per‑story purchase amount.
    • Royalty rules: perpetual revenue splits to creators and contributors.
    • Licensing constraints: transferability, display scope.
  • Entitlement
    • Owned access object. Variants: per‑article; subscription window (time‑boxed, renewable).
    • Contains valid_from and valid_until for subscriptions; per‑article may be perpetual.

Per‑story purchase

Implementation of Article Purchase

module pml::purchases {
use pml::entitlements::{Self, ArticleEntitlement};
use pml::economics::PricingPolicy;
use sui::coin::{Self, Coin};
use sui::sui::SUI;
use sui::kiosk::{Self, Kiosk};

/// Purchase article access
public entry fun purchase_article(
kiosk: &Kiosk,
content_id: ID,
payment: Coin<SUI>,
clock: &Clock,
ctx: &mut TxContext
) {
let reader = tx_context::sender(ctx);

// Get content price from kiosk listing
let (has_listing, price) = kiosk::is_listed<ContentNFT>(kiosk, content_id);
assert!(has_listing, E_NOT_LISTED);
assert!(coin::value(&payment) >= price, E_INSUFFICIENT_PAYMENT);

// Create entitlement
let entitlement = ArticleEntitlement {
id: object::new(ctx),
content_id,
reader,
purchased_at: clock::timestamp_ms(clock),
perpetual: true
};

// Distribute payment according to policy
distribute_revenue(kiosk, payment, content_id, ctx);

// Transfer entitlement to reader
transfer::transfer(entitlement, reader);

// Emit purchase event
event::emit(ArticlePurchased {
entitlement_id: object::id(&entitlement),
content_id,
reader,
price,
timestamp: clock::timestamp_ms(clock)
});
}

/// Distribute revenue according to policy
fun distribute_revenue(
kiosk: &Kiosk,
payment: Coin<SUI>,
content_id: ID,
ctx: &mut TxContext
) {
// Get pricing policy from kiosk's transfer policy
let policy = df::borrow<vector<u8>, PricingPolicy>(
&kiosk.id,
b"pricing"
);

let total = coin::value(&payment);
let mut remaining = coin::into_balance(payment);

// Calculate distributions
let royalty_amount = (total * (policy.royalty_bps as u64)) / 10000;
let creator_amount = (royalty_amount * (policy.creator_share as u64)) / 10000;
let platform_amount = (royalty_amount * (policy.platform_share as u64)) / 10000;

// Pay creator
let creator_payment = balance::split(&mut remaining, creator_amount);
transfer::public_transfer(
coin::from_balance(creator_payment, ctx),
get_content_creator(kiosk, content_id)
);

// Pay platform
let platform_payment = balance::split(&mut remaining, platform_amount);
transfer::public_transfer(
coin::from_balance(platform_payment, ctx),
@platform_treasury
);

// Distribute to contributors
let i = 0;
while (i < vector::length(&policy.contributor_shares)) {
let share = vector::borrow(&policy.contributor_shares, i);
let amount = (royalty_amount * (share.share_bps as u64)) / 10000;

let contributor_payment = balance::split(&mut remaining, amount);
transfer::public_transfer(
coin::from_balance(contributor_payment, ctx),
share.address
);

i = i + 1;
};

// Any remaining goes to treasury
if (balance::value(&remaining) > 0) {
transfer::public_transfer(
coin::from_balance(remaining, ctx),
@treasury
);
}
}
}

Readers purchase an entitlement for a single content object. Clients enforce access by verifying ownership.

Properties

  • Atomicity: mint entitlement and distribute revenue in one transaction block.
  • Durability: entitlement and revenue records live on‑chain; clients can prove attribution.

Subscriptions

Subscriptions are represented as time‑boxed entitlements and validated client‑side during reads. Renewal extends validity.

Design choices

  • Client‑side validation: avoids server session checks; the chain is the source of truth.
  • Grace windows: policy‑driven grace periods absorb clock skew and UX hiccups without compromising controls.

Revenue sharing and payouts

Revenue Tracking and Withdrawal

module pml::revenue {
use sui::balance::{Self, Balance};
use sui::coin::{Self, Coin};
use sui::sui::SUI;
use sui::table::{Self, Table};

/// Revenue accumulator for creators
struct RevenueVault has key {
id: UID,
creator: address,
balance: Balance<SUI>,
total_earned: u64,
total_withdrawn: u64,
pending_royalties: Table<ID, PendingRoyalty>
}

struct PendingRoyalty has store {
content_id: ID,
amount: u64,
timestamp: u64,
claimed: bool
}

/// Accumulate royalties from sales
public fun accumulate_royalty(
vault: &mut RevenueVault,
content_id: ID,
payment: Coin<SUI>,
clock: &Clock,
ctx: &mut TxContext
) {
let amount = coin::value(&payment);

// Add to balance
balance::join(&mut vault.balance, coin::into_balance(payment));
vault.total_earned = vault.total_earned + amount;

// Record pending royalty
let royalty = PendingRoyalty {
content_id,
amount,
timestamp: clock::timestamp_ms(clock),
claimed: false
};

table::add(&mut vault.pending_royalties, content_id, royalty);

// Emit event for tracking
event::emit(RoyaltyAccumulated {
vault_id: object::id(vault),
content_id,
amount,
total_balance: balance::value(&vault.balance)
});
}

/// Withdraw accumulated earnings
public entry fun withdraw_earnings(
vault: &mut RevenueVault,
amount: u64,
ctx: &mut TxContext
) {
assert!(tx_context::sender(ctx) == vault.creator, E_UNAUTHORIZED);
assert!(balance::value(&vault.balance) >= amount, E_INSUFFICIENT_BALANCE);

// Split and transfer
let withdrawal = balance::split(&mut vault.balance, amount);
transfer::public_transfer(
coin::from_balance(withdrawal, ctx),
vault.creator
);

vault.total_withdrawn = vault.total_withdrawn + amount;

// Emit withdrawal event
event::emit(EarningsWithdrawn {
vault_id: object::id(vault),
creator: vault.creator,
amount,
remaining_balance: balance::value(&vault.balance)
});
}

/// Batch claim multiple royalties
public entry fun batch_claim_royalties(
vault: &mut RevenueVault,
content_ids: vector<ID>,
ctx: &mut TxContext
) {
assert!(tx_context::sender(ctx) == vault.creator, E_UNAUTHORIZED);

let i = 0;
let total_claimed = 0u64;

while (i < vector::length(&content_ids)) {
let content_id = *vector::borrow(&content_ids, i);

if (table::contains(&vault.pending_royalties, content_id)) {
let royalty = table::borrow_mut(&mut vault.pending_royalties, content_id);

if (!royalty.claimed) {
royalty.claimed = true;
total_claimed = total_claimed + royalty.amount;
}
};

i = i + 1;
};

// Process batch withdrawal if any
if (total_claimed > 0) {
withdraw_earnings(vault, total_claimed, ctx);
}
}
}
  • Royalty accounting: Kiosk policy emits events containing split proportions and destinations; UIs compute running totals by summing events.
  • Payouts: creators withdraw accrued balances via Move entrypoints; fees and splits are computed deterministically from events.
  • New users can purchase with sponsored transactions when they lack SUI for gas; co‑signature pattern ensures user intent plus sponsor payment.
  • Sponsors limit exposure via quotas and minimal proof‑of‑personness (see Identity & Auth).

Anti‑abuse and refunds

  • Chargebacks do not exist on chain; however, moderation or fraud outcomes can be paired with discretionary refunds by emitting compensating events to affected addresses.
  • Rate limits on sponsorship and per‑principal spend caps constrain abuse amplitude.

Pricing and currency conversion

  • UI presents local currency estimates client‑side; on‑chain state remains in token units.
  • Where necessary, a price oracle (attested or zk) can anchor fiat reference rates; conversion does not affect on‑chain contract correctness.