Resources
Paid access lets viewers buy time-bound (or lifetime) USDC access to specific Soul content kinds. This page documents the on-chain model and the owner / buyer / cleanup paths.
The Soul owner may revoke a buyer's access at any time by calling paid_access::revoke_access. No on-chain refund is issued. Entries also auto-invalidate when the Soul changes hands. Any UI taking payment for a kind must disclose this revocability and non-refundability — see Terms.
Every Soul minted on phase 2 has exactly one SoulPaidAccessList shared object, created at mint and bound to the Soul via SoulState.access_list_id. It holds:
kind_configs: Table<u32, KindPaidConfig> — one config per kind the owner has opened up to paid access. Configs are added with configure_paid_access_kind and removed with delete_paid_access_kind.entries: Table<address, Table<u32, KindPaidEntry>> — buyer rows, keyed by address then by kind. Inner rows are created lazily on first purchase and reaped when emptied.public struct KindPaidConfig has copy, drop, store {
version: u64,
price_atomic: u64,
scope_mask: u64, // pinned to kind's default_grant_scope_mask
duration_ms: Option<u64>, // None = lifetime
ownership_epoch_snapshot: u64,
}
public struct KindPaidEntry has copy, drop, store {
version: u64,
scope_mask: u64,
expires_at_ms: Option<u64>,
ownership_epoch_snapshot: u64,
}Paid access only applies to kinds whose read_mode_mask includes READ_PAID. Among the five built-in kinds:
| Kind | READ_PAID | Scope on purchase |
|---|---|---|
| soul_doc | ❌ | — |
| memory | ❌ | — |
| skill | ❌ | — |
| sprite | ✅ | SCOPE_ASSETS (8) |
| audio | ✅ | SCOPE_ASSETS (8) |
Custom kinds registered through kind_registry::register_kind may opt into READ_PAID (admin-only registration). See Kind Registry.
configure_paid_access_kind: price in atomic USDC, scope mask (must equal the kind's default_grant_scope_mask), and an optional duration_ms. The config snapshots the current ownership epoch.paid_access::record_purchase internally.record_purchase asserts the config's epoch matches the current epoch (rejecting purchases against a stale config from a previous owner), computes the new expires_at_ms from the renewal base, and writes a fresh KindPaidEntry under the buyer's row.SoulPaidAccessEntry for the My Souls UI and any indexers.Free access can be granted by the owner via paid_access::add_access, which writes the same entry shape with price_paid_atomic = 0 and a SoulPaidAccessGranted event.
duration_ms is Some(d), the entry's expires_at_ms = renewal_base + d. The renewal base is max(now, previous_expires_at_ms) — re-purchasing before expiry extends from the existing end, not from now.duration_ms is None, the entry has no expiry and a re-purchase aborts with EAlreadyHasAccess — there is nothing to renew.paid_access::revoke_access(grantee, kind) removes the buyer's entry and emits SoulPaidAccessRevoked. Subsequent seal_approve_content_paid_access calls for that buyer fail until they re-purchase. The owner may also content::delete_* or purge_* the underlying slot, which makes the entry useless even without explicit revoke.
No on-chain refund rail exists. Any refund or credit policy must be handled off-chain. If you intend to offer guaranteed-term access, build a slot-level delete lock and an explicit refund path into your front-end — the protocol does not enforce one.
Every KindPaidEntry snapshots ownership_epoch at write time. has_access and seal_approve_content_paid_access require the snapshot to equal the Soul's current ownership_epoch, so all entries auto-invalidate when the Soul changes hands. The new owner inherits an effectively-empty list. Buyers can re-purchase under the new owner, which overwrites the stale row.
Invalidated entries hold no value; reaping them reclaims storage rebate. paid_access::cleanup_stale_entries is callable by anyone and takes parallel vector<address> + vector<u32> arguments. It removes entries whose snapshot is stale, and drops the outer buyer row when its inner table is empty. Indexers and bots typically run a periodic cleanup pass after high-volume ownership rotations.
| SoulGrant | Paid access | |
|---|---|---|
| Issued by | Soul owner | Anyone buying (with config in place); owner via add_access |
| Payment | No (free delegation) | Yes (atomic USDC, split by fees) |
| Storage | Per-grantee object | Per-(buyer, kind) row in shared list |
| Scope | Multi-bit (seal | memory | skills | assets) | Single bit, pinned to kind's default_grant_scope_mask |
| Owner revoke | Yes | Yes (no refund) |
| Auto-invalidation | Ownership epoch | Ownership epoch + expiry |
action: "revoke", txDigest, buyerAddress, and kind. The route is idempotent on digest.