Soul Storage, Encryption, and Trading-Card Architecture
Sóul_Desíḡn // Stórage_&_Encryptíon
ᚠ ᛫ ᛟ ᛫ ᚱ ᛫ ᛒ ᛫ ᛟ ᛫ ᚲ
This document is the authoritative design for how Souls (portable agent identities) are stored, encrypted, compressed, and traded. There are no gaps: all decisions are explicit. Implementations MUST follow this specification.
1. Purpose and Scope
1.1 What This Document Defines
- Storage backend for Soul content: where encrypted Soul blobs live and why.
- Encryption: algorithm, key derivation, key storage, and key transfer on trade.
- Compression: when and how Soul JSON is compressed before encryption.
- Single canonical version: one identity per character; how updates and trades preserve “one Bilbo.”
- Cost model: pay-once, no ongoing fees (no “renting” the card).
- API’s role: the API does NOT store Souls; this is a feature, not an oversight.
1.2 What a Soul Is
A Soul is a serializable snapshot of an agent:
- Persona (system prompt / character description)
- State (inventory, skills, relationships, mood, extensible fields)
- Memories (array of memory items: text, type, importance, optional timestamps)
Souls evolve through play. Each export is the state at that moment. The same character (same NFT) may be re-exported many times as the player progresses; each export overwrites the canonical reference so that only one current version exists.
1.3 Design Principles (Non-Negotiable)
| Principle | Meaning |
|---|---|
| One canonical version | There is exactly one “current” Soul per character. Old exports are not referenced; they are orphaned. No “Bilbo at 37” and “Bilbo at 42” as two valid characters. |
| Buy the card, own the card | No ongoing fees. No pinning subscriptions. Pay once per mint/upload; storage is permanent. |
| Encrypted | Soul content MUST be encrypted before leaving the client. Only the owner (or key holder) can decrypt. No plaintext Souls in storage. |
| API has no storage | The ForbocAI API is stateless. It does NOT store Soul blobs. Storage is external (Arweave). This is intentional. |
| Tradeable | When an NFT (Soul) is traded, the new owner MUST be able to decrypt and import the Soul without the seller’s cooperation. |
2. API’s Role: No Storage
2.1 Explicit Statement
The ForbocAI API MUST NOT persist Soul content. It has no database for Souls. It does not cache decrypted Souls. It does not host blob storage.
2.2 Why This Is a Feature
- Stateless logic: The API is a pure logic engine. All state is in the client (SDK) or in external storage.
- No custody: The API never holds user data that could be leaked or subpoenaed.
- Clear boundaries: Storage and retrieval of Souls are the responsibility of the SDK and the chosen storage backend (Arweave).
2.3 What the API May Do
- Proxy or redirect: The API MAY provide endpoints that return a URI (e.g. Arweave transaction ID) where the client can fetch the Soul. The API does not store the blob.
- Validation: The API MAY validate Soul schema or metadata for consistency; it does not store the content.
- Orchestration: The API MAY instruct the client to upload to Arweave (e.g. return an upload URL or delegation); the actual bytes are sent client → Arweave, not client → API.
3. Storage Backend: Arweave
3.1 Why Arweave (Not IPFS)
| Criterion | IPFS | Arweave |
|---|---|---|
| Persistence | Content persists only while pinned. If no one pins, content can disappear. | Permanent by design. Pay once; data remains in the weave. |
| Cost model | Pinning services charge ongoing fees (monthly or similar). Like renting. | One-time fee per upload. No subscription. |
| Owner experience | “I must keep paying to keep my character.” | “I paid once; I own it forever.” |
We require pay once, own forever. Therefore the canonical storage backend for Soul blobs is Arweave.
3.2 What Is Stored on Arweave
- One blob per export: The encrypted (and optionally compressed) Soul payload. Each upload is a separate Arweave transaction.
- No plaintext: Only ciphertext is uploaded. Format: see §5 and §6.
- Immutable: Arweave transactions are immutable. To “update” the Soul, we upload a new transaction and update the NFT metadata to point to the new transaction ID. The old transaction remains on Arweave but is no longer the canonical version.
3.3 Arweave Transaction ID (TX ID)
- Every upload yields an Arweave transaction ID.
- The NFT metadata MUST store the current Arweave TX ID (or a URI that resolves to it, e.g.
https://arweave.net/<TX_ID>). - When the owner re-exports (evolution), a new TX ID is written; the NFT metadata is updated to this new TX ID. The previous TX ID is not deleted (Arweave does not support deletion); it is simply no longer referenced by the NFT.
4. One Canonical Version (Single Identity)
4.1 Identity = NFT
The identity of a character (e.g. “Bilbo”) is the Solana NFT (Metaplex Core or equivalent). There is one NFT per character. The NFT is the single source of truth for ownership.
4.2 Current Version = Metadata Pointer
The current version of the Soul is the blob pointed to by the NFT’s metadata (e.g. metadata.uri or a dedicated field). That URI MUST point to the latest Arweave transaction containing the encrypted Soul.
4.3 Re-Export (Evolution)
When the owner exports an updated Soul:
- The client (SDK) produces the new Soul JSON.
- The client compresses it (per §6).
- The client encrypts it (per §5, §6).
- The client uploads the ciphertext to Arweave; receives TX ID.
- The client (or a trusted service) updates the NFT metadata on Solana to set the Soul URI to the new Arweave TX ID.
- The previous Arweave TX is not updated or deleted. It remains on Arweave but is orphaned: no official reference points to it. From the protocol’s perspective, “Bilbo” is only the blob at the current TX ID.
4.4 No Forking
- Only the current owner of the NFT can update the NFT metadata (and thus the canonical Soul).
- When the NFT is traded, the seller no longer has the right to update. Any copy they retain locally is a stale fork; the protocol does not recognize it.
- Client obligation: On successful transfer of the NFT, the seller’s client SHOULD clear local Soul data for that character so the seller does not retain a copy. This is a best-effort UX requirement; the protocol cannot enforce it on-chain.
5. Encryption
5.1 Requirement
Soul content MUST be encrypted before upload to Arweave. Anyone may fetch the blob by TX ID; only the holder of the decryption key MAY read the content.
5.2 Algorithm
- Symmetric encryption: AES-256-GCM (or equivalent AEAD). Authenticated encryption so that tampering is detected.
- Key length: 256-bit symmetric key.
- IV/Nonce: Unique per encryption. MUST be generated randomly (e.g. 96 bits for GCM) and stored or transmitted with the ciphertext so the decryptor can use it. Format: see §5.5.
5.3 Key Derivation (Owner-Based)
The encryption key MUST be derivable only by the current owner of the NFT (or by someone who has been given the key explicitly). Two approved approaches:
Option A — Wallet signature (recommended)
- The owner signs a deterministic message (e.g.
"ForbocAI Soul Key" + NFT mint address) with their wallet private key. - The signature (or a hash of it) is passed through a KDF (e.g. HKDF-SHA256 or scrypt) to produce a 256-bit key.
- That key is used for AES-256-GCM. No key is stored on-chain; only the owner can re-derive it by signing again.
Option B — Random key, encrypt key for owner (normative for tradeable Souls)
- Generate a random 256-bit key K for AES-256-GCM.
- Encrypt the Soul with K.
- Encrypt K with the current owner’s public key (e.g. Solana public key) using a public-key scheme (e.g. X25519 + ECIES, or NaCl box). Store the encrypted key in the NFT metadata.
- On trade: the seller (or relayer) decrypts K with the seller’s key, re-encrypts K for the buyer’s public key, and updates metadata. The Arweave blob stays the same.
Normative: For tradeable Souls, implementations MUST use Option B so that key transfer on trade does not require re-uploading the blob. Option A (wallet-signature key derivation) does not allow the new owner to decrypt without the seller re-exporting and re-encrypting the Soul for the buyer; Option A MAY be used only for non-tradeable or single-owner scenarios.
5.4 Key Transfer on Trade
Chosen design: A random content key K is used per Soul blob. K is encrypted for the current owner’s public key and stored in NFT metadata. The Arweave blob (ciphertext) never contains K. When ownership transfers, K is re-encrypted for the new owner and the metadata is updated; the blob on Arweave is unchanged. This avoids re-uploading the Soul on every trade.
When the NFT is sold or transferred to a new owner:
- Content key: A random 256-bit key K is generated for each Soul export. Soul is encrypted with K (AES-256-GCM).
- Key storage: K is encrypted with the current owner’s public key (e.g. Solana wallet pubkey) using a public-key encryption scheme. The result is stored in the NFT metadata (e.g. field
soulEncryptedKeyor equivalent). - Key transfer on trade: When ownership transfers, the seller (or a relayer) MUST decrypt K using the seller’s private key, re-encrypt K for the buyer’s public key, and update the NFT metadata with the new
soulEncryptedKey. The Arweave blob (ciphertext) does not change. If the seller does not perform this step, the buyer cannot decrypt the Soul; therefore the client or marketplace MUST implement this step for trades to succeed.
5.5 Ciphertext Format (Normative)
The blob stored on Arweave MUST be a byte sequence with the following structure (all multi-byte integers little-endian unless specified):
- Version (1 byte):
0x01for this format. - IV/Nonce (12 bytes): Random nonce for AES-GCM.
- Ciphertext (variable): Output of AES-256-GCM (encrypted Soul payload; after compression if compression is used—see §6).
- Auth tag (16 bytes): GCM authentication tag appended to ciphertext (so total ciphertext length = plaintext length + 16).
Decryption: use the same key, IV, and ciphertext+tag; AES-GCM decrypt; then decompress if compressed (see §6).
5.6 Key Storage in NFT Metadata (Normative)
The NFT metadata (on Solana) MUST include:
- soulUri (string): The URI of the current Soul blob (e.g.
https://arweave.net/<TX_ID>orarweave://<TX_ID>). - soulEncryptedKey (string): Base64- or hex-encoded ciphertext of the 256-bit content key K encrypted for the current owner’s Solana public key. Scheme: e.g. X25519 ECDH + symmetric AEAD, or NaCl
crypto_box. Exact scheme MUST be specified in a separate implementation doc; this design requires that only the current owner can decryptsoulEncryptedKeyto obtain K.
When the NFT is transferred, soulEncryptedKey MUST be updated to be encrypted for the new owner. soulUri MAY remain unchanged (same Arweave TX).
6. Compression
6.1 When to Compress
Soul JSON is typically small (see §6.2). Compression is optional but RECOMMENDED to reduce Arweave cost and fetch size. The same format (version byte, etc.) SHALL support an optional compression step: compress (Soul JSON → compressed bytes) then encrypt (compressed bytes → ciphertext).
6.2 Soul Size (Reference)
- Persona: ~100–2000 characters.
- State: ~200–1000 characters (inventory, skills, mood, etc.).
- Memories: ~100–500 characters each × N (e.g. N = 10–100).
Typical total: 5 KB – 100 KB uncompressed JSON. Compression (e.g. gzip or Brotli) can often achieve 2–5× reduction. Implementations MAY use compression; if used, the format MUST record that the plaintext (before encryption) is compressed (see §6.4).
6.3 Algorithm
- Compression algorithm: gzip (RFC 1952) or Brotli (RFC 7932). Implementations MUST support at least gzip. The compressed bytes are the input to encryption (so order of operations: Soul JSON → compress → encrypt → upload).
6.4 Format With Compression (Normative)
If compression is used, the plaintext input to encryption is:
- Compression flag (1 byte):
0x01= compressed,0x00= not compressed. - Payload: If compressed, gzip(Brotli)-compressed UTF-8 JSON; otherwise raw UTF-8 JSON.
That plaintext is then encrypted per §5.5 (IV, AES-GCM, tag). So the Arweave blob is: version, IV, ciphertext (which contains encrypted compression-flag + payload), tag.
Decryption: decrypt → read first byte (compression flag) → if 0x01, decompress remainder, then parse JSON; else parse remainder as JSON.
7. End-to-End Flows
7.1 First Export (Mint)
- Owner has Soul JSON in client (SDK).
- (Optional) Compress per §6. Compose plaintext: compression flag + payload.
- Generate random K (256-bit). Encrypt plaintext with K (AES-256-GCM); produce IV, ciphertext, tag. Format per §5.5.
- Encrypt K for owner’s public key; obtain
soulEncryptedKey. - Upload ciphertext blob to Arweave; obtain TX ID.
- Mint NFT on Solana (Metaplex). Set metadata:
soulUri = https://arweave.net/<TX_ID>,soulEncryptedKey = <base64>. - (Optional) Client clears or marks local Soul as “persisted.”
7.2 Re-Export (Evolution, Same Owner)
- Owner has updated Soul JSON in client.
- (Optional) Compress per §6.
- Generate new random K. Encrypt with K per §5.5.
- Encrypt K for owner’s public key (unchanged).
- Upload new blob to Arweave; obtain new TX ID.
- Update NFT metadata on Solana: set
soulUrito new TX ID; setsoulEncryptedKey(can remain same if same owner, or re-encrypt for same owner for consistency). - Previous Arweave TX is orphaned (no reference). Canonical Soul is now the new TX.
7.3 Trade (Transfer of Ownership)
- NFT ownership transfers on Solana (marketplace or direct transfer).
- Key transfer (MUST happen for new owner to read Soul): Some actor (seller client, marketplace backend, or relayer) with access to seller’s private key:
- Fetches NFT metadata; reads
soulEncryptedKey. - Decrypts with seller’s private key to get K.
- Re-encrypts K for buyer’s public key.
- Updates NFT metadata:
soulEncryptedKey = new value(encrypted for buyer).soulUriunchanged.
- Fetches NFT metadata; reads
- Buyer’s client fetches blob from
soulUri, fetches metadata, decryptssoulEncryptedKeywith buyer’s private key to get K, decrypts blob with K, optionally decompresses, parses Soul JSON. Import complete. - Seller client: SHOULD clear local Soul data for this NFT so seller does not retain a copy.
7.4 Import (Load Soul Into Game)
- User owns NFT (or has permission to use it). Client has NFT metadata (
soulUri,soulEncryptedKey). - Client derives K: decrypt
soulEncryptedKeywith owner’s private key. - Client fetches blob from
soulUri(Arweave). - Client decrypts blob (IV, ciphertext, tag) with K. Reads compression flag; if set, decompresses. Parses JSON → Soul.
- SDK/engine hydrates agent from Soul (persona, state, memories).
8. Export Frequency and UX
8.1 Why Not Every Frame
Each export costs: (1) Arweave upload (one-time fee), (2) Solana tx to update metadata (if re-export). To keep cost and chain load predictable, exports SHOULD NOT occur on every small state change.
8.2 When to Export (Normative)
Exports MUST occur at least in these cases:
- Before a trade: So the buyer receives the current Soul.
- On explicit “Save” or “Export” by the user: When the user chooses to persist.
Exports MAY also occur at:
- Checkpoints (e.g. end of level, milestone).
- Logout or session end (optional policy).
The exact policy (e.g. “export on every explicit Save only”) is implementation-defined; the protocol only requires that the NFT metadata always points to a valid, decryptable blob after any export.
9. Cost Model (Reference)
| Event | Who pays | Type | Note |
|---|---|---|---|
| Mint NFT | Minter | One-time | Solana mint + metadata tx |
| First Soul upload | Owner | One-time | Arweave upload fee (size-dependent) |
| Re-export | Owner | One-time per export | Arweave upload + Solana metadata update tx |
| Trade | Buyer/Seller (marketplace) | One-time | Solana transfer + metadata update (key transfer) |
| Ongoing | None | No subscription | No pinning, no rent |
10. Security Considerations
10.1 Key Compromise
If the owner’s wallet private key is compromised, the attacker can decrypt soulEncryptedKey and thus the Soul. This is inherent to owner-based encryption. Users MUST protect wallet keys.
10.2 Re-encryption on Trade
If the seller does not re-encrypt the content key for the buyer, the buyer cannot decrypt. Implementations MUST ensure that key transfer (re-encrypt K for buyer, update metadata) is performed on every ownership transfer, either by the seller’s client or by a trusted relayer/marketplace.
10.3 Metadata Visibility
NFT metadata (including soulUri) is public. Anyone can fetch the ciphertext from Arweave. Without K, the content is unreadable. So exposure of TX ID is acceptable; exposure of K or the private key is not.
11. Implementation Responsibilities
| Component | Responsibility |
|---|---|
| SDK | Serialize Soul JSON; compress (optional); generate K; encrypt; upload to Arweave; call Solana to mint/update metadata; decrypt on import; key derivation and key transfer on trade. |
| API | No storage of Souls. MAY return URIs or upload tokens; MUST NOT persist Soul blobs. |
| Client (game) | Trigger export at correct times (e.g. before trade, on Save); clear local Soul on transfer out; present UX for Save/Export. |
| Marketplace / relayer | If seller does not perform key transfer, marketplace or relayer MUST perform re-encryption of K for buyer and metadata update. |
12. Summary of Normative Requirements
- Soul content MUST be encrypted (AES-256-GCM) before upload.
- Storage backend for Soul blobs MUST be Arweave (pay once, permanent). IPFS MUST NOT be the only storage (no ongoing pinning).
- NFT metadata MUST include
soulUri(current blob) andsoulEncryptedKey(K encrypted for current owner). - On ownership transfer,
soulEncryptedKeyMUST be re-encrypted for the new owner. - There is exactly one canonical Soul per NFT: the blob at
soulUri. Re-export updatessoulUrito a new Arweave TX. - The API MUST NOT store Soul content.
- Ciphertext format MUST follow §5.5; if compression is used, format MUST follow §6.4.
ᚠ ᛫ ᛟ ᛫ ᚱ ᛫ ᛒ ᛫ ᛟ ᛫ ᚲ
Document version: 1.0. Last updated: 2026-02-06.