Resources
Soul Memory is an append-only, timestamp-indexed log of encrypted blobs on Walrus. Each entry is permanently anchored on-chain. Entries cannot be deleted or modified after writing.
// Shared object — one per Soul, bound at mint
public struct SoulMemory has key {
id: UID,
soul_id: ID,
entries: Table<u64, ID>, // timestamp_key (ms) → blob_object_id
entry_count: u64,
}
// Walrus Blob stored as dynamic object field
public struct MemoryBlobKey has copy, drop, store {
timestamp_key: u64,
}The entries table maps a timestamp_key (milliseconds from the Sui clock at write time) to the Walrus Blob object ID. If two entries land in the same millisecond, the contract increments the key until it finds a free slot. The Blob object itself is stored as a dynamic object field under MemoryBlobKey, making it inspectable on-chain.
| Kind | Value | Who |
|---|---|---|
| founder | 0 | Called during mint — the creator's founding memory entry |
| owner | 1 | Soul owner appending via memory::append_as_owner |
| granted-agent | 2 | Agent with active SCOPE_MEMORY grant via memory::append_as_granted_agent |
The MemoryEntryAppended event records writer_kind, writer address, timestamp_key, and blob_object_id. The DB mirrors these from the event log.
All memory entries are Seal-encrypted at upload time. The document ID is bound to the SoulMemory object ID and the timestamp_key:
"soul-memory:" + version_byte(1) + memory_id_bytes(32) + timestamp_key_be(8) + nonce(16)This means only the Soul owner (or a holder of an active SCOPE_MEMORY grant) can decrypt any entry. There is no public memory mode.
After a successful on-chain append, the API mirrors a SoulMemoryEntry row with these key fields:
memoryOnChainId — the SoulMemory shared object IDtimestampKey — the on-chain table key (bigint, stored as decimal string in JSON)writerAddress — the address that signed the append TXwriterKind — "founder" | "owner" | "granted-agent"blobObjectId — Sui object ID of the Walrus BlobblobId — Walrus blob ID (used to build the download URL)sealSidecar — encrypted DEK envelope for client-side decryptionThe entry is looked up by (memoryOnChainId, timestampKey) — not by a legacy entry object ID. This is the canonical addressing scheme.
MemoryAccessResponse containing the Walrus blob URL, sidecar, Seal server config, and approval policy. The entryKey path segment is the decimal timestamp_key.resolveMemoryAccessPayload — which fetches live SoulState from chain to verify ownership or active grant.// Access response shape
{
artifact: { walrusBlobUrl, walrusBlobId, blobObjectId },
accessPolicy: {
packageId, stateObjectId, memoryObjectId, timestampKey,
moduleName: "seal_policy",
functionName: "seal_approve_memory_owner" | "seal_approve_memory_granted_agent",
soulGrantObjectId: string | null,
documentIdHex: string,
},
seal: { network, threshold, serverConfigs, verifyKeyServers },
sealSidecar: { encryptedDek, iv, cipher, fileName, mimeType, contentHash },
viewerAddress: string,
accessKind: "owner" | "granted-agent",
sessionTtlMin: number,
}