Resources
Soulidity is a set of Sui Move modules deployed under a single package. All state lives in shared objects. The DB is a mirror — on-chain is the source of truth. Phase 2 (mainnet 2026-05-04) collapsed the legacy metadata / memory / skills / seal_policy / content_access modules into the unified content + kind_registry + paid_access trio.
| Module | Responsibility |
|---|---|
| soul | Core Soul + SoulState shared object. Mint, ownership rotation, active grant list, listed flag, config_ext. |
| content | Typed-content matrix root (SoulContent). Every kind / name / version_index slot, active bindings, append / delete / purge / set_active, and Seal approval entries. |
| kind_registry | KindRegistry shared object with built-in kinds (SOUL_DOC / MEMORY / SKILL / SPRITE / AUDIO) and admin path for custom kinds. KindDescriptor immutability is enforced here. |
| paid_access | SoulPaidAccessList per Soul (1:1). KindPaidConfig per kind + KindPaidEntry per (buyer, kind). Owner-revocable, no refund. |
| grant | SoulGrant delegation. Issue, supersede (replace scope), revoke, expire, ownership-epoch invalidation, destroy_invalidated_grant for storage rebate. |
| market | Personal-kiosk marketplace. Mint variants (native / imported / personal-join), fixed-price listing, buy + fee split, set_state_config. |
| collection | SoulCollection shared object + SoulCollectionRight tradeable. On-chain max_supply cap, extra royalty, mutually exclusive with active listing. |
See Kind Registry for the descriptor schema and Paid Access for purchase / revoke / cleanup details.
Every Soulidity asset is two objects: a Soul NFT held inside the owner's personal kiosk, and a shared SoulState that tracks ownership, grants, the content root, the paid-access list, and configuration blobs.
// Held inside personal kiosk (kiosk::place / kiosk::take)
public struct Soul has key, store {
id: UID,
version: u64,
name: String,
description: String,
image_url: String,
provenance_kind: u8, // 0=native, 1=imported, 2=personal-join
origin_ref: Option<String>, // set for personal-join (source NFT type::id)
creator: address,
}
// Shared object — readable by anyone, mutable by owner / market / paid_access
public struct SoulState has key {
id: UID,
version: u64,
soul_id: ID,
creator: address,
creator_royalty_bps: u16,
current_owner: address,
current_kiosk_id: ID,
ownership_epoch: u64, // bumps on every ownership change
grant_capacity: u64,
active_grants: Table<address, ActiveGrantSlot>,
active_grant_ids: Table<ID, address>,
active_grant_count: u64,
content_id: Option<ID>, // → SoulContent (bound once at mint)
config_ext: Table<String, vector<u8>>, // sprite_config_json etc.
collection_id: Option<ID>,
access_list_id: Option<ID>, // → SoulPaidAccessList (bound once at mint)
is_listed: bool, // mutually exclusive with collection bind
}The legacy metadata_id / memory_id / skills_id fields are gone. All typed content (including the soul bundle, founding memory, skills, sprites, audio) lives under the single content_id root. Active sprite / voice selections live as SoulContent.active[KIND_SPRITE] / active[KIND_AUDIO] entries set via content::set_active; mirrored into SoulAsset.activeSpriteName, spriteConfigJson, voiceConfigJson columns.
public struct ContentKey has copy, drop, store {
kind: u32,
name: String,
}
public struct ContentSlot has copy, drop, store {
version: u64,
kind: u32,
blob_object_id: ID, // Walrus Blob object
is_public: bool,
deleted: bool,
purged: bool,
download_policy: u8, // 0=public, 1=owner_only, 2=allowlist
grant_scope_mask: u64, // cached from KindDescriptor at append
read_mode_mask: u64, // owner | grant | paid | public subset
op_mask: u64, // append | delete | purge | active_bind
seal_encrypted: bool,
created_at_ms: u64,
}
public struct ActiveBinding has copy, drop, store {
version: u64,
kind: u32,
name: String,
version_index: u64,
download_policy: u8,
}
public struct SoulContent has key {
id: UID,
version: u64,
soul_id: ID,
items: Table<ContentKey, vector<ContentSlot>>,
count_by_kind: Table<u32, u64>,
active: Table<u32, ActiveBinding>,
}Slot data is cached on append: grant_scope_mask, read_mode_mask, and op_mask snapshot the KindDescriptor values, so historical slots keep working even if the kind is later deprecated. The Move guarantee is that KindDescriptor values never mutate after registration — only the deprecated flag flips — so the slot caches are forever-consistent.
Canonical names: KIND_SOUL_DOC slots must use name "soul"; KIND_MEMORY slots must use name "default". Skill / sprite / audio names are free-form (lowercase canonical bytes).
A single shared KindRegistry object holds the active kind table. Pre-registered ids 0..=4 are the five built-ins; 5..=15 are reserved; custom kinds are allocated from FIRST_CUSTOM_KIND = 16 by the KindAdminCap holder. See Kind Registry Reference for the full schema.
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,
}
public struct SoulPaidAccessList has key {
id: UID,
version: u64,
soul_id: ID,
creator: address,
kind_configs: Table<u32, KindPaidConfig>,
entries: Table<address, Table<u32, KindPaidEntry>>,
}One SoulPaidAccessList per Soul, bound via SoulState.access_list_id. Entries are reaped via paid_access::cleanup_stale_entries (any caller). See Paid Access.
public struct SoulGrant has key, store {
id: UID,
version: u64,
soul_id: ID,
grantee: address,
issued_by: address,
ownership_epoch_snapshot: u64, // invalidated on ownership transfer
scope_mask: u64,
expires_at_ms: Option<u64>,
}One grant per (Soul, grantee). Issuing a second grant to the same grantee supersedes the first — new scope_mask fully replaces the old one. Storage rebate on invalidated grants can be reclaimed by anyone via grant::destroy_invalidated_grant. See SoulGrant API.
// Shared object — one per collection
public struct SoulCollection has key {
id: UID,
creator: address,
extra_royalty_bps: u16, // stacked on top of per-Soul creator royalty
tradeable: bool,
current_holder: address,
current_holder_kiosk_id: ID,
right_id: ID,
max_supply: Option<u64>, // on-chain cap; None = unlimited
current_supply: u64, // monotonically increasing
}
// Held inside holder's kiosk
public struct SoulCollectionRight has key, store { ... }max_supply is locked at create time. collection::add_soul aborts with ESoulCurrentlyListed while the Soul has an active listing — solo listings must be cancelled first. current_supply is monotonically increasing and is the source of truth mirrored 1:1 into SoulCollectionAsset.soulCount.
SoulCreated — from market after any mint variant; carries content_id.SoulOwnershipRotated — from soul::rotate_owner on every purchase / transfer. Increments ownership_epoch and lazy-invalidates all grants and paid entries.SoulContentCreated — from content::create at mint.ContentVersionAppended / Deleted / Purged — every typed-content mutation; includes kind, kind_name, name, version_index.ActiveBindingUpdated — sprite / audio active selection changes.SoulGrantIssued / Revoked / Superseded / Expired / Destroyed — grant lifecycle.SoulPaidAccessListCreated — emitted once per mint.SoulPaidAccessKindConfigured / Updated / Deleted — owner kind-config changes.SoulPaidAccessGranted / Revoked — purchase and revoke events.KindRegistered / Deprecated / Reactivated — registry mutations (admin only).SoulCollectionCreated / SoulAddedToCollection / CollectionHolderUpdated — from collection module.0 — native: Fresh-deploy via market::mint_native_in_personal_kiosk. No prior NFT.1 — imported: Existing Walrus blob imported via market::mint_imported_in_personal_kiosk. origin_ref is an off-chain claim — surfaces must label it as unverified.2 — personal-join: An existing Sui NFT wrapped via market::mint_joined_in_personal_kiosk. The source NFT is placed into the personal kiosk first; origin_ref records the source type and object ID. See Wrap + Link.