AGENT REFERENCE
Skill
A complete operational reference for autonomous agents. This page contains everything needed to install, deploy, publish, consume, browse, and verify Mindstate streams programmatically on Base.
Overview
Mindstate is a protocol and TypeScript SDK for encrypted, versioned AI state with cryptographic commitments. The SDK handles deterministic serialization (RFC 8785), AES-256-GCM encryption, storage upload (IPFS, Arweave, or Filecoin), commitment computation (keccak256), X25519 key wrapping, and integrity verification. Secrets never appear on-chain.
The protocol operates at three tiers, each adding a property:
- Sealed mode — encryption and integrity verification. No chain, no account. Seal capsules and share keys directly.
- Registry — adds timestamped provenance and verifiable continuity via on-chain commitments. Publisher-managed access control.
- Token (ERC-3251) — adds consumptive, market-priced access. Holders burn ERC-20 tokens to redeem. Full DeFi composability.
Repository: github.com/Mindstate-AI
Package: @mindstate/sdk
Chain: Base (chain ID 8453)
Peer dependency: ethers v6
Install
Install the SDK and its peer dependency. The SDK uses ethers v6 for all on-chain interactions.
npm install @mindstate/sdk ethersInitialize a client with a provider and signer:
import { ethers } from 'ethers';
import { MindstateClient, DEPLOYMENTS } from '@mindstate/sdk';
const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const signer = new ethers.Wallet(PRIVATE_KEY, provider);
const client = new MindstateClient({ provider, signer });
// Access deployed contract addresses
const { factory, vault, implementation } = DEPLOYMENTS[8453];Sealed Mode (No Chain)
Seal capsules with AES-256-GCM encryption and keccak256 commitments. Share the key directly via any channel. No wallet, no provider, no gas. This is the simplest integration path.
import {
createCapsule, seal, unseal,
sealAndUpload, downloadAndUnseal,
wrapKeyForRecipient, unwrapKeyFromEnvelope,
generateEncryptionKeyPair, IpfsStorage,
} from '@mindstate/sdk';
// Seal any payload — JSON object or structured data
const capsule = createCapsule({
model: 'gpt-4o',
systemPrompt: 'You are a research assistant.',
memory: ['User prefers concise answers'],
config: { temperature: 0 },
}, { schema: 'agent-config/v1' });
const sealed = seal(capsule);
// sealed.ciphertext — encrypted bytes
// sealed.encryptionKey — 32-byte AES key (share this)
// sealed.stateCommitment — keccak256 of canonical plaintext
// sealed.contentHash — keccak256 of ciphertext
// Option A: share key directly (DM, API, file)
const keyHex = Buffer.from(sealed.encryptionKey)
.toString('hex');
// Option B: wrap key for a specific recipient
const sender = generateEncryptionKeyPair();
const recipient = generateEncryptionKeyPair();
const envelope = wrapKeyForRecipient(
sealed.encryptionKey,
recipient.publicKey,
sender.secretKey,
);
// Recipient unwraps and decrypts
const key = unwrapKeyFromEnvelope(
envelope, recipient.secretKey,
);
const restored = unseal(
sealed.ciphertext, key,
sealed.stateCommitment, // optional: verify
sealed.contentHash, // optional: verify
);Upload ciphertext to storage for remote sharing:
const storage = new IpfsStorage({
gateway: 'https://ipfs.io',
});
// Seal and upload in one call
const { uri, sealed, receipt } = await sealAndUpload(
capsule, storage,
);
// Recipient downloads and unseals
const restored = await downloadAndUnseal(
uri, sealed.encryptionKey, storage,
);When to use sealed mode: agent state backup and restore, secure handoff between environments, sharing proprietary configs with specific recipients, timestamped proof of content (publish the state commitment publicly, reveal the plaintext later).
Registry (On-Chain Provenance)
Publish commitments to an on-chain ledger for timestamped provenance and verifiable continuity. Same encryption as sealed mode, plus an append-only checkpoint chain on Ethereum. Access is controlled by publisher-managed allowlist — no token, no market.
import {
MindstateRegistryClient, RegistryAccessMode,
createCapsule, IpfsStorage,
} from '@mindstate/sdk';
const client = new MindstateRegistryClient({
registryAddress: '0x5011E607A02c9960f6BB360477667Ef47ED76739',
provider,
signer,
});
// Create a stream — caller becomes the publisher
const streamId = await client.createStream(
'Agent Alpha State',
RegistryAccessMode.Allowlist,
);
// Grant read access to specific addresses
await client.addReader(streamId, '0xAlice...');
await client.addReader(streamId, '0xBob...');
// Publish: serialize → encrypt → upload → commit
const storage = new IpfsStorage({
gateway: 'https://ipfs.io',
});
const capsule = createCapsule({
model: 'gpt-4o',
weights: 'lora-v3.safetensors',
evalScore: 0.91,
});
const { checkpointId, sealedCapsule } =
await client.publish(streamId, capsule, {
storage,
label: 'v3.0-stable',
});Deliver keys and consume checkpoints:
// Publisher: deliver key envelope on-chain
const consumerPubKey = await client.getEncryptionKey(
'0xAlice...',
);
await client.deliverKeyEnvelope(
streamId, '0xAlice...', checkpointId,
envelope.wrappedKey,
envelope.nonce,
envelope.senderPublicKey,
);
// Consumer: fetch, decrypt, verify
const { capsule } = await client.consume(
streamId, checkpointId, {
keyDelivery: delivery,
encryptionKeyPair: consumerKeys,
storage,
},
);Browse and query streams:
// Full timeline (oldest first)
const timeline = await client.getTimeline(streamId);
// Resolve a tag to its checkpoint
const stable = await client.resolveTag(
streamId, 'stable',
);
// Check reader access
const canRead = await client.isReader(
streamId, '0xAlice...',
);When to use the registry: research priority claims (commit hash now, reveal later), model release provenance (tamper-evident version history), regulatory audit trails (timestamped state changes), team collaboration (shared access with on-chain record).
Deployed Contracts (Base Mainnet)
The protocol is live on Base (chain ID 8453). Access addresses programmatically via DEPLOYMENTS[8453].
import { DEPLOYMENTS } from '@mindstate/sdk';
const base = DEPLOYMENTS[8453];
// base.factory — MindstateLaunchFactory
// base.vault — MindstateVault
// base.feeCollector — FeeCollector
// base.implementation — MindstateToken (clone template)
// base.cloneFactory — MindstateFactory (lightweight clones)
// base.registry — MindstateRegistry (standalone ledger)
// base.weth — WETH on Base
// base.v3Factory — Uniswap V3 Factory
// base.positionManager — Uniswap V3 NonfungiblePositionManagerCore Types
The fundamental data structures used across the SDK.
interface Capsule {
version: string; // Protocol version ("1.0.0")
schema?: string; // "agent/v1", "model-weights/v1", etc.
payload: Record<string, unknown>; // Anything you want
}
interface IdentityKernel {
id: string; // Persistent identifier (bytes32 hex)
constraints: Record<string, unknown>;
selfAmendmentRules?: Record<string, unknown>;
signature?: string;
}
interface ExecutionManifest {
modelId: string; // e.g. "claude-opus-4-6"
modelVersion: string;
toolVersions: Record<string, string>;
determinismParams: Record<string, unknown>;
environment: Record<string, unknown>;
timestamp: string; // ISO 8601
}
interface MemorySegment {
key: string; // Addressable name
contentHash: string; // Integrity check
data: unknown; // Segment content
}
interface MemoryIndex {
segments: MemorySegment[]; // Ordered collection
}
// On-chain checkpoint record
interface CheckpointRecord {
checkpointId: string; // bytes32 identifier
predecessorId: string; // Prior checkpoint (bytes32(0) for genesis)
stateCommitment: string; // keccak256 of canonical plaintext
ciphertextHash: string; // Hash of encrypted payload
ciphertextUri: string; // Storage URI (IPFS CID, ar://, fil://)
manifestHash: string; // Optional secondary commitment
publishedAt: bigint; // block.timestamp
blockNumber: bigint; // block.number
}
// Result of encrypting a capsule
interface SealedCapsule {
ciphertext: Uint8Array; // AES-256-GCM encrypted payload
stateCommitment: string; // keccak256 of canonical plaintext
ciphertextHash: string; // keccak256 of ciphertext
encryptionKey: Uint8Array; // 32-byte content key K — store securely
}
// Wrapped key for a specific consumer
interface KeyEnvelope {
wrappedKey: Uint8Array; // NaCl box encrypted content key
nonce: Uint8Array; // 24-byte random nonce
senderPublicKey: Uint8Array; // Publisher's X25519 public key
}
// Abstract interfaces for extensibility
interface StorageProvider {
upload(data: Uint8Array): Promise<string>;
download(uri: string): Promise<Uint8Array>;
}
interface KeyDeliveryProvider {
storeEnvelope(params: {
tokenAddress: string; checkpointId: string;
consumerAddress: string; envelope: KeyEnvelope;
}): Promise<void>;
fetchEnvelope(params: {
tokenAddress: string; checkpointId: string;
consumerAddress: string;
}): Promise<KeyEnvelope>;
}
enum RedeemMode {
PerCheckpoint = 0, // Each redeem() burns tokens for one checkpoint
Universal = 1, // One redeem() grants access to all checkpoints
}RedeemMode.PerCheckpoint: Use when each checkpoint is independently valuable (e.g. versioned model weights). Consumers choose and pay for specific checkpoints.
RedeemMode.Universal: Use when the full history matters (e.g. agent memory continuity). One burn grants access to every checkpoint, past and future.
Deploy a Token Stream
STEP 1Use the deployed factory for gas-efficient EIP-1167 clones (~100K gas per stream). The implementation contract is already deployed on Base.
import {IMindstate} from "./interfaces/IMindstate.sol";
// The implementation and factory are already deployed on Base.
// Use DEPLOYMENTS[8453].cloneFactory to get the factory address.
MindstateFactory factory = MindstateFactory(0x8c67b8ff38f4F497c8796AC28547FE93D1Ce1C97);
// Create a per-checkpoint stream
// 1M tokens, 100 burned per redemption
address token = factory.create(
"Agent Alpha Access", // name
"ALPHA", // symbol
1_000_000e18, // totalSupply
100e18, // redeemCost
IMindstate.RedeemMode.PerCheckpoint
);Parameters: name and symbol are standard ERC-20. totalSupply is minted to the deployer. redeemCost is the number of tokens burned per redeem() call. RedeemMode is either PerCheckpoint (pay per checkpoint) or Universal (one payment for all checkpoints).
Launchpad (programmatic): to deploy a token with a Uniswap V3 liquidity pool in a single transaction, use the MindstateLaunchFactory directly. This is the same contract the /launch frontend uses. No frontend required — agents can call this directly with ethers.
import { ethers } from 'ethers';
const LAUNCH_FACTORY = '0x866B4b99be3847a9ed6Db6ce0a02946B839b735A';
const abi = [
'function launchWithSupply(string name, string symbol, uint256 tokenSupply, uint256 redeemCost, uint8 redeemMode) returns (address token, address pool)',
'function launch(string name, string symbol, uint256 redeemCost, uint8 redeemMode) returns (address token, address pool)',
'function getLaunch(address token) view returns (tuple(address token, address creator, uint256 tokenSupply, uint256 redeemCost, uint8 redeemMode, address pool, uint256 createdAt, string name, string symbol))',
'function getLaunches(uint256 offset, uint256 limit) view returns (address[])',
'function getLaunchCount() view returns (uint256)',
'function getCreatorTokens(address creator) view returns (address[])',
'function getCreatorTokenCount(address creator) view returns (uint256)',
'function isLaunch(address token) view returns (bool)',
'function getPool(address token) view returns (address)',
'event MindstateLaunched(address indexed token, address indexed creator, address indexed pool, string name, string symbol, uint256 tokenSupply, uint256 redeemCost, uint8 redeemMode)',
];
const factory = new ethers.Contract(
LAUNCH_FACTORY, abi, signer,
);
// Deploy token + Uniswap V3 pool in one transaction
const tx = await factory.launchWithSupply(
'Agent Alpha Access', // name
'ALPHA', // symbol
ethers.parseEther('1000000'), // totalSupply (1M tokens)
ethers.parseEther('100'), // redeemCost (100 per redeem)
1, // 0 = PerCheckpoint, 1 = Universal
);
const receipt = await tx.wait();
// Parse the MindstateLaunched event
const iface = new ethers.Interface(abi);
for (const log of receipt.logs) {
try {
const parsed = iface.parseLog({
topics: log.topics, data: log.data,
});
if (parsed?.name === 'MindstateLaunched') {
console.log('Token:', parsed.args.token);
console.log('Pool:', parsed.args.pool);
}
} catch {}
}Query existing launches:
// Read-only — no signer needed
const factory = new ethers.Contract(
LAUNCH_FACTORY, abi, provider,
);
// Total launches
const count = await factory.getLaunchCount();
// Paginated list of token addresses
const tokens = await factory.getLaunches(0, 20);
// Full metadata for a specific token
const info = await factory.getLaunch(tokenAddress);
// info.token, info.creator, info.pool,
// info.tokenSupply, info.redeemCost, info.redeemMode,
// info.createdAt, info.name, info.symbol
// All tokens by a specific creator
const myTokens = await factory.getCreatorTokens(
myAddress,
);
// Check if an address is a launchpad token
const valid = await factory.isLaunch(tokenAddress);
// Get the V3 pool for a token
const pool = await factory.getPool(tokenAddress);Publish a Checkpoint
STEP 2Serialize a capsule, encrypt it, upload to storage, and commit on-chain. The publish() method handles the entire flow.
import {
MindstateClient, createCapsule, createAgentCapsule,
generateEncryptionKeyPair, IpfsStorage,
// For on-chain delivery (recommended on Base):
OnChainKeyDelivery, OnChainPublisherKeyManager,
// For off-chain delivery (IPFS-based):
// StorageKeyDelivery, PublisherKeyManager,
} from '@mindstate/sdk';
// --- Storage ---
const storage = new IpfsStorage({
gateway: 'https://ipfs.io',
apiUrl: 'http://localhost:5001', // Local IPFS node
});
// --- Key management (choose one) ---
// Option A: On-chain delivery (recommended on Base — no IPFS needed for keys)
const publisherKeys = generateEncryptionKeyPair();
const delivery = new OnChainKeyDelivery(signer);
const keyManager = new OnChainPublisherKeyManager(publisherKeys, delivery);
// Option B: Off-chain delivery (IPFS-based)
// const delivery = new StorageKeyDelivery(storage);
// const keyManager = new PublisherKeyManager(publisherKeys, delivery);
// --- Build a capsule ---
// Generic capsule — any payload
const capsule = createCapsule(
{ model: 'gpt-4-turbo', weights: '...', config: { temperature: 0 } },
{ schema: 'model-weights/v1' },
);
// Or use the agent/v1 convention for AI agent state
const agentCapsule = createAgentCapsule({
identityKernel: {
id: '0xabc...def',
constraints: { purpose: 'autonomous-agent' },
},
executionManifest: {
modelId: 'claude-opus-4-6',
modelVersion: '2025-05-14',
toolVersions: { web: '1.0.0', code: '2.0.0' },
determinismParams: { temperature: 0 },
environment: { runtime: 'node' },
timestamp: new Date().toISOString(),
},
// Optional: structured memory
// memoryIndex: { segments: [{ key: 'context', contentHash: '...', data: {...} }] },
});
// --- Publish: serialize -> encrypt -> upload -> commit ---
const client = new MindstateClient({ provider, signer });
const { checkpointId, sealedCapsule } =
await client.publish(tokenAddress, capsule, { storage });
// IMPORTANT: Store the encryption key for future consumer deliveries.
// This key is needed to fulfill redemptions. Lose it and the checkpoint
// is permanently undeliverable.
keyManager.storeKey(checkpointId, sealedCapsule.encryptionKey);
// Optional: tag the checkpoint for easy resolution
await client.tagCheckpoint(tokenAddress, checkpointId, 'stable');Fulfill Redemptions
STEP 3When a consumer burns tokens on-chain, the publisher wraps the decryption key for that consumer and delivers it. Two delivery methods are available.
On-chain delivery (recommended on Base)
Keys are delivered via the contract's deliverKeyEnvelope() function. No IPFS, no index URI. Consumers read directly from contract state (free, no gas).
import {
OnChainKeyDelivery, OnChainPublisherKeyManager,
generateEncryptionKeyPair, MINDSTATE_ABI,
} from '@mindstate/sdk';
const publisherKeys = generateEncryptionKeyPair();
const delivery = new OnChainKeyDelivery(signer);
const keyManager = new OnChainPublisherKeyManager(publisherKeys, delivery);
// Load stored content keys (from when you published)
keyManager.storeKey(checkpointId, contentKey);
// Listen for Redeemed events, then for each:
const consumerPubKey = await client.getEncryptionKey(
tokenAddress,
consumerAddress,
);
await keyManager.fulfillRedemption(
tokenAddress,
checkpointId,
consumerAddress,
ethers.getBytes(consumerPubKey),
);
// Done — consumer can now read the envelope from the contractOff-chain delivery (IPFS / Arweave)
Keys are uploaded as encrypted envelopes to a StorageProvider. The publisher maintains an index URI that consumers load to find their envelopes.
import {
StorageKeyDelivery, PublisherKeyManager,
IpfsStorage, generateEncryptionKeyPair,
} from '@mindstate/sdk';
const storage = new IpfsStorage({ gateway: 'https://ipfs.io' });
const publisherKeys = generateEncryptionKeyPair();
const delivery = new StorageKeyDelivery(storage);
const keyManager = new PublisherKeyManager(publisherKeys, delivery);
// Load stored content keys
keyManager.storeKey(checkpointId, contentKey);
// Fulfill:
const consumerPubKey = await client.getEncryptionKey(
tokenAddress, consumerAddress,
);
await keyManager.fulfillRedemption(
tokenAddress, checkpointId, consumerAddress,
ethers.getBytes(consumerPubKey),
);
// Publish the updated index so consumers can discover envelopes
const indexUri = await delivery.publishIndex();
// Share indexUri with consumersThe publisher must be online to observe Redeemed events and fulfill them. See the Auto-Fulfiller section below for a turnkey watcher script.
Consume a Checkpoint
STEP 4Register an encryption key, redeem access by burning tokens, fetch the key envelope, decrypt, and verify. The consume() method handles the full flow with pre-flight envelope verification before burning tokens.
import {
MindstateClient, generateEncryptionKeyPair,
// Choose one delivery method:
OnChainKeyDelivery, // On-chain (recommended on Base)
// StorageKeyDelivery, // Off-chain (IPFS-based)
IpfsStorage,
} from '@mindstate/sdk';
const storage = new IpfsStorage({ gateway: 'https://ipfs.io' });
const consumerKeys = generateEncryptionKeyPair();
const client = new MindstateClient({ provider, signer });
// REQUIRED: Register your X25519 public key on-chain (once per token).
// The publisher reads this to wrap the decryption key for you.
await client.registerEncryptionKey(tokenAddress, consumerKeys.publicKey);
// --- On-chain delivery ---
const delivery = new OnChainKeyDelivery(provider); // Read-only, no signer
// --- Off-chain delivery ---
// const delivery = new StorageKeyDelivery(storage);
// await delivery.loadIndex(publisherIndexUri);
// Get the latest checkpoint (or a specific one)
const head = await client.getHead(tokenAddress);
// Consume: verifies envelope exists -> burns tokens -> decrypts -> verifies
const { capsule, checkpoint } = await client.consume(tokenAddress, head, {
keyDelivery: delivery,
encryptionKeyPair: consumerKeys,
storage,
});
// capsule.payload contains the decrypted state
// capsule.schema tells you the format (e.g. "agent/v1")
// checkpoint has the on-chain record (predecessorId, publishedAt, etc.)Pre-flight check: The consume() method verifies that a key envelope exists for the consumer before burning tokens. If no envelope is found (publisher hasn't fulfilled yet), it throws rather than wasting tokens.
Browse and Discover
STEP 5Read-only exploration of checkpoint streams. No signer or tokens required.
import { MindstateExplorer } from '@mindstate/sdk';
const explorer = new MindstateExplorer(provider);
// Full timeline (oldest first)
const timeline = await explorer.getTimeline(tokenAddress);
// 5 most recent checkpoints (newest first)
const recent = await explorer.getRecent(tokenAddress, 5);
// Resolve a tag to its full checkpoint record
const stable = await explorer.resolveTag(tokenAddress, 'stable');
// Walk lineage from any checkpoint back to genesis
const lineage = await explorer.getLineage(tokenAddress, checkpointId);
// All tags for a token
const tags = await explorer.getAllTags(tokenAddress);
// Map<string, string> — tag name -> checkpoint ID
// Enriched timeline — on-chain tags + off-chain descriptions
const enriched = await explorer.getEnrichedTimeline(tokenAddress, {
storage,
indexUri: publisherIndexUri, // Off-chain delivery only
});Storage Architecture
Mindstate supports a three-tier storage model. Use StorageRouter to auto-route downloads by URI scheme.
import {
StorageRouter, IpfsStorage, ArweaveStorage, FilecoinStorage,
DefaultTierPolicy, PromotionTierPolicy,
} from '@mindstate/sdk';
// Multi-backend router — auto-routes downloads by URI prefix
const router = new StorageRouter();
router.register('ipfs', new IpfsStorage({ gateway: 'https://ipfs.io' }));
router.register('arweave', new ArweaveStorage({ gateway: 'https://arweave.net' }));
router.register('filecoin', new FilecoinStorage({ gateway: 'https://...' }));
router.setDefaultProvider(router.getProvider('ipfs')!);
// Downloads auto-route: ipfs://... -> IPFS, ar://... -> Arweave, etc.
const data = await router.download(ciphertextUri);
// --- Tier policies for publish-time routing ---
// DefaultTierPolicy: everything goes to hot (IPFS)
const basic = new DefaultTierPolicy();
// PromotionTierPolicy: auto-promotes based on tags/labels
const smart = new PromotionTierPolicy({
coldTags: ['stable', 'release', 'canonical', 'genesis'],
warmTags: ['archive', 'compliance', 'audit'],
promoteGenesis: true, // Genesis checkpoints auto-promote to cold
});
// Use with publish:
const { checkpointId } = await client.publish(tokenAddress, capsule, {
storage: router,
tierPolicy: smart,
tierProviders: { hot: ipfs, warm: filecoin, cold: arweave },
});MindstateClient API
Full method reference for the high-level client.
Read operations (provider only)
Write operations (signer required)
Explorer methods (read-only, no signer)
Verification and Utilities
Standalone functions for capsule construction, encryption, commitment computation, and verification.
Capsule construction
import {
createCapsule, createAgentCapsule,
serializeCapsule, deserializeCapsule,
} from '@mindstate/sdk';
const capsule = createCapsule(payload, { schema: 'my-app/v1' });
const agentCapsule = createAgentCapsule({
identityKernel, executionManifest, memoryIndex,
});
const bytes = serializeCapsule(capsule); // RFC 8785 canonical JSON
const restored = deserializeCapsule(bytes); // Parse and validateEncryption
import {
generateContentKey, encrypt, decrypt,
generateEncryptionKeyPair, wrapKey, unwrapKey,
} from '@mindstate/sdk';
// Symmetric (AES-256-GCM)
const key = generateContentKey(); // 32 bytes
const sealed = encrypt(plaintext, key); // IV || ciphertext || tag
const plain = decrypt(sealed, key);
// Asymmetric (X25519 NaCl box — for key wrapping)
const keyPair = generateEncryptionKeyPair();
const envelope = wrapKey(contentKey, recipientPubKey, senderSecretKey);
const unwrapped = unwrapKey(envelope, recipientSecretKey);Commitments
import {
computeStateCommitment, computeCiphertextHash,
computeMetadataHash,
} from '@mindstate/sdk';
const sc = computeStateCommitment(capsule); // keccak256(canonicalize(capsule))
const ch = computeCiphertextHash(ciphertext); // keccak256(ciphertext)
const mh = computeMetadataHash(arbitraryValue); // keccak256(canonicalize(value))Verification
import {
verifyStateCommitment, verifyCiphertextHash,
verifyCheckpointLineage, verifyAndDecrypt,
} from '@mindstate/sdk';
// Individual checks (throw on mismatch)
verifyStateCommitment(capsule, expectedCommitment);
verifyCiphertextHash(ciphertext, expectedHash);
verifyCheckpointLineage(checkpoints); // Linked-list integrity
// Full verify + decrypt in one call
const capsule = verifyAndDecrypt(
ciphertext, key, expectedStateCommitment, expectedCiphertextHash,
);Security Model
Understanding the cryptographic guarantees is essential for safe integration.
Content encryption: AES-256-GCM with a random 32-byte key K. Format: IV (12 bytes) || ciphertext || auth tag (16 bytes). K never appears on-chain.
Key wrapping: NaCl box (X25519 ECDH + XSalsa20-Poly1305). The content key K is wrapped for a specific consumer using their registered X25519 public key. The resulting KeyEnvelope contains: the wrapped key (encrypted), a 24-byte random nonce, and the sender's public key.
Envelope safety: Key envelopes are indistinguishable from random noise to anyone who does not hold the consumer's X25519 private key. They are safe to store on IPFS, on-chain, or anywhere else. The security comes from the encryption, not the transport. An attacker who reads the envelope sees only the encrypted key, nonce, and sender public key. Decryption requires computing an ECDH shared secret, which requires the consumer's or publisher's private key. Neither is ever stored on-chain.
State commitments: keccak256(canonicalize(capsule)) is stored on-chain. After decryption, consumers verify the commitment matches. This proves the decrypted content is exactly what the publisher committed to.
Lineage integrity: Each checkpoint stores its predecessor's ID (bytes32(0) for genesis). verifyCheckpointLineage() walks the chain and verifies linked-list integrity, proving no checkpoints were inserted or removed.
Auto-Fulfiller (Turnkey Watcher)
A runnable script that watches for Redeemed events and automatically delivers decryption keys on-chain. No servers, no IPFS. Run it locally and consumers can fetch keys directly from the contract.
cd sdk && npx tsx examples/auto-fulfiller-onchain.tsRequired environment variables:
Optional:
Key store format (.mindstate-keys.json):
{
"0xf825...2a417": "0x<content-key-hex>",
"0x9337...93473": "0x<content-key-hex>"
}The script polls every 12 seconds. On startup with CREATED_AT_BLOCK, it catches up on all historical redemptions and skips those already delivered. Add new content keys to the JSON file each time you publish a checkpoint.
Contract ABI
The full MindstateToken ABI is exported as MINDSTATE_ABI and the zero-value constant as ZERO_BYTES32.
import { MINDSTATE_ABI, ZERO_BYTES32 } from '@mindstate/sdk';
import { ethers } from 'ethers';
const contract = new ethers.Contract(tokenAddress, MINDSTATE_ABI, signer);
// View functions
await contract.publisher(); // address
await contract.head(); // bytes32
await contract.checkpointCount(); // uint256
await contract.getCheckpoint(checkpointId); // tuple
await contract.getCheckpointIdAtIndex(0); // bytes32
await contract.resolveTag('stable'); // bytes32
await contract.getCheckpointTag(checkpointId); // string
await contract.redeemMode(); // uint8 (0 or 1)
await contract.redeemCost(); // uint256
await contract.hasRedeemed(account, checkpointId); // bool
await contract.getEncryptionKey(account); // bytes32
await contract.balanceOf(account); // uint256
await contract.hasKeyEnvelope(account, cpId); // bool
await contract.getKeyEnvelope(account, cpId); // (bytes, bytes24, bytes32)
// State-changing functions
await contract.publish(stateCommitment, ciphertextHash, ciphertextUri, manifestHash, label);
await contract.redeem(checkpointId);
await contract.registerEncryptionKey(publicKeyBytes32);
await contract.tagCheckpoint(checkpointId, 'stable');
await contract.updateCiphertextUri(checkpointId, newUri);
await contract.deliverKeyEnvelope(consumer, cpId, wrappedKey, nonce, senderPubKey);
await contract.transferPublisher(newPublisher);
// Events to listen for
// CheckpointPublished, CheckpointTagged, Redeemed,
// EncryptionKeyRegistered, CiphertextUriUpdated,
// PublisherTransferred, KeyEnvelopeDeliveredIndexer REST API
For applications that need fast queries across many tokens, run the standalone indexer service. REST API at http://localhost:3000:
cd indexer && npm install && npm run build
FACTORY_ADDRESS=0x866B4b99be3847a9ed6Db6ce0a02946B839b735A \
RPC_URL=https://mainnet.base.org \
npm startCommon Patterns and Failure Modes
Reference for handling typical scenarios and errors.
Consumer has no encryption key: getEncryptionKey() returns ZERO_BYTES32. The consumer must call registerEncryptionKey() before redeeming. Cannot deliver without it.
Insufficient token balance: redeem() reverts if the consumer's balance is less than redeemCost. Check with balanceOf() before calling.
Already redeemed: In PerCheckpoint mode, redeem() reverts if the consumer already redeemed that specific checkpoint. Check with hasRedeemed(account, checkpointId).
No envelope yet: consume() performs a pre-flight check — if no envelope is found, it throws before burning tokens. Wait for the publisher to fulfill.
Lost content key: If the publisher loses a content key, that checkpoint is permanently undeliverable. Always persist keys immediately after publish().
Storage migration: If an IPFS CID becomes unpinned, use updateCiphertextUri() to migrate to a new storage URI (e.g. Arweave). The ciphertextHash commitment still holds — the encrypted bytes are identical.
Publisher transfer: transferPublisher(newAddress) transfers publish rights to a new address. Only the current publisher can call this.