Resources
SoulGrant is the on-chain access delegation system. It lets the Soul owner authorize AI agents or other wallets to decrypt the Soul bundle, read or append memory entries, publish skill versions, or manage private sprite / audio versions — without transferring ownership.
Every grant carries a scope_mask — a bitfield that determines which Soul data channels the grantee can access. Each bit maps to one or more content kinds via KindDescriptor.default_grant_scope_mask (single-bit). Scope bits are combinable; scope_mask = 0 and any unknown bits are rejected on issue.
| Constant | Value | Grants access to (kind → scope) |
|---|---|---|
| SCOPE_SEAL | 1 | KIND_SOUL_DOC — decrypt the immutable soul.md bundle |
| SCOPE_MEMORY | 2 | KIND_MEMORY — read memory entries and append new ones |
| SCOPE_SKILLS | 4 | KIND_SKILL — read private skill versions and publish new ones |
| SCOPE_ASSETS | 8 | KIND_SPRITE + KIND_AUDIO — read private persona versions and publish new ones |
To grant all four scopes, use scope_mask = 15. The Move module rejects a mask of 0 and any bits outside these four. Admin-registered custom kinds must pick exactly one of the four scopes for their default_grant_scope_mask; combined masks are rejected at registration time.
Each Soul has one grant slot per grantee. Issuing a second grant to the same grantee fully replaces the previous scope_mask — Soulidity does not union the two masks. If you intend to extend an existing grant, you must compute the merged mask yourself first.
grant::issue_to_grantee with SoulState, grantee, scope_mask, optional expires_at_ms. Emits SoulGrantIssued.SoulGrant object to them, emits SoulGrantSuperseded + new SoulGrantIssued, and leaves the prior grant object invalidated (epoch snapshot mismatch) for storage reclaim by any caller.grant::revoke_scope_to_grantee to strip specific scope bits and write a replacement grant in one atomic step.grant::revoke to remove a grantee's slot entirely. Emits SoulGrantRevoked.expires_at_ms is set, it must be in the future at issue time. Grants fail validation once the Sui clock reaches that timestamp.ownership_epoch_snapshot on the grant must equal the current SoulState.ownership_epoch; rotation bumps the epoch and lazily kills all grants. Reclaim storage rebate on dead grants via grant::destroy_invalidated_grant — any caller may invoke.Because supersede replaces (not unions) scope, you must look up the agent's current mask before issuing a fresh grant if your intent is to extend access. The pre-check endpoint computes existing | added in one round-trip and returns the on-chain object to supersede.
POST /api/souls/grant-merge-masks
{
"items": [
{
"soulOnChainId": "0x...",
"granteeAddress": "0x...",
"addedScopeMask": 4 // SCOPE_SKILLS
}
]
}
→ {
"items": [
{
"soulOnChainId": "0x...",
"granteeAddress": "0x...",
"addedScopeMask": 4,
"existingScopeMask": 2, // pre-existing SCOPE_MEMORY
"mergedScopeMask": 6, // memory | skills
"isNewGrantee": false,
"currentCapacity": 16,
"activeGrantCount": 3,
"requiredCapacity": 16
}
]
}Use the returned mergedScopeMask and capacity fields to build the grant PTB with buildIssueGrantTx or buildBatchIssueGrantsTx. The SDK does not run this pre-check implicitly, so callers must invoke the endpoint before signing when they want to preserve existing scopes.
When the Soul owner uploads a non-public version of any kind, Soulidity automatically issues scope-matched grants to every active agent on the owner's account that doesn't already cover the required scope. The merge is done with the same grant-merge-masks pre-check so existing scopes are preserved.
default_grant_scope_mask.merged = existing | needed and submit a supersede TX.Public slots are not auto-granted because they require no grant to read. See Agent Integration for the full rules.
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>,
}issue_to_grantee transfers the SoulGrant object to the grantee wallet. The grantee must pass it as an argument to any guarded Move entry — content reads, memory appends, skill publishes. The ownership_epoch_snapshot must equal the current SoulState.ownership_epoch; ownership rotation bumps the epoch and lazily kills the grant without per-grant events.
txDigest, action ("issue" | "revoke" | "revoke-scope"), and granteeAddress for revoke / revoke-scope. Idempotent on txDigest.SoulState.grant_capacity ceiling).existing | added across (soulOnChainId, granteeAddress) pairs. Returns merged masks and capacity planning fields.activeGrantCount from the DB mirror. For live on-chain grant state use the SDK queries.ts helpers.When a viewer calls GET /api/souls/[id]/content/[kind]/[name]/[versionIndex]/access, the server runs resolveContentAccessPayload for that exact slot. The legacy /api/souls/[id]/access route resolves only the canonical Soul document at (KIND_SOUL_DOC, "soul", 0).
SoulState from chain to get the current owner and the active grant table.seal_approve_content_owner approval params.read_mode_mask permits READ_PUBLIC and the slot's download_policy is public → return seal_approve_content_public params.scope_mask includes the slot's cached grant_scope_mask → return seal_approve_content_granted_agent params (with the grant object ID).KindPaidEntry for the viewer satisfying the same scope → return seal_approve_content_paid_access params.