Overview
CryptoProto is a protocol for encrypted, NFT-gated content ownership.
The internet's ownership model is a polite fiction: you buy access to something, and somewhere a server holds the right to take it away. CryptoProto removes that server from the loop. Content is encrypted client-side with AES-256-GCM, the ciphertext is pinned to Arweave forever, and the decryption key is gated by a Solana NFT.
Every viewer proves ownership the same way: sign a fresh challenge with the wallet that holds the NFT. The escrow verifies the signature, confirms ownership against live on-chain state, and releases the key. No accounts, no subscriptions, no platforms in the path.
Architecture
Seven composable steps, two flows. The author chain runs once when content is published; the viewer chain runs every time someone unlocks it.
The author flow ends at register — the key is now held by the escrow, bound to the mint address. The viewer flow begins independently any time someone with the NFT wants to read the content.
Getting started
Two paths
The browser is the zero-install path. Go to /create or /view, connect a Solana wallet, and you are done — no install, no SDK, no account. See Use it in the browser.
For agents and automation, CryptoProto also runs as an MCP server inside any MCP-compatible runtime (Claude Desktop, Cursor, Claude Code). See /agents for the per-runtime config.
First encryption (author)
From an agent prompt:
encrypt(file) → ciphertext, key
upload(ciphertext) → arweave txId
mint(metadata, owner) → mint address
register(key, mint) → keyId
Viewer flow
When a holder wants to access the content, the agent performs three calls — typically against the wallet the user is signed in with:
verify(wallet, mint, signature, nonce) → token
decrypt(ciphertext, token) → plaintext
Use it in the browser
CryptoProto runs end-to-end in the browser at /create and /view. No install, no SDK, no account. A Solana wallet — Phantom, Solflare, or Backpack — is the only requirement.
Live on mainnet
The flows run on Solana mainnet — real SOL, real permanence. Content minted here is pinned to Arweave forever and the NFT is a real asset in the connected wallet. See the warning below before you start: the encryption key can be lost if the tab closes at the wrong moment.
Create
Pick a file — image, video, or PDF, up to 100 MB. The steps run in order:
encrypt with AES-256-GCM in the browser tab
upload ciphertext to Arweave // free under 100 KiB; larger funds storage in SOL
sign #1 → mint a compressed NFT (Bubblegum, shared Merkle tree, ~0.0001 SOL network cost) + pay a flat 0.001 SOL service fee
sign #2 → escrow the key, bound to the mint
Two signatures total. The plaintext and the key never leave the tab. The key goes only to the escrow, bound to the mint. An itemized cost card — storage, mint, fee, total — shows before you sign.
View
Paste a link, or connect the wallet that holds the NFT. The page proves ownership by signing a single-use challenge — no transaction, no fee — fetches the key from escrow, decrypts in the browser, and renders the content. Sell or transfer the NFT and the new owner unlocks automatically. You no longer can. Fresh uploads can take a few minutes for Arweave gateways to index — until then, /view shows a "content propagating" state and rechecks automatically.
One warning. If the tab closes between minting and key escrow, the key is gone and that NFT can never be unlocked. The UI blocks and warns before letting this happen.
Tools
Seven MCP tools, published on npm as cryptoproto-mcp — one command in any MCP runtime, no clone, no build. Names are stable; arguments may evolve before 1.0.
npx -y cryptoproto-mcp # run directly, or: claude mcp add cryptoproto -- npx -y cryptoproto-mcp
Building your own service instead of using an MCP client? The same package works as a subprocess from any agent framework that speaks MCP over stdio — or skip it entirely and call the escrow REST API directly. Configuration is environment variables; every default targets the live mainnet stack, so the minimum setup is just a funded Solana keypair:
SOLANA_KEYPAIR_PATH ~/.config/solana/id.json signs mints + registrations, pays storage SOLANA_CLUSTER mainnet-beta or devnet ESCROW_URL (live escrow Worker) self-hosters override MERKLE_TREE (CryptoProto public tree) compressed-NFT mint target RPC_URL public cluster endpoint set a provider key for mainnet writes PLATFORM_WALLET (CryptoProto fee wallet) mint service-fee destination SERVICE_FEE_LAMPORTS 1000000 (0.001 SOL) "0" disables the fee
Costs per creation: ~0.0001 SOL mint + 0.001 SOL service fee; storage free under ~100 KiB, ~0.0004 SOL/MiB above — paid in SOL from the same keypair. No Arweave wallet needed.
encrypt_and_upload
encrypt_and_upload(content_base64) → { arweave_tx, iv, tag, key_hex }Params- content_base64
- Raw content to encrypt, base64-encoded.
Returns. Arweave tx id of the ciphertext, base64 IV and auth tag, and the AES-256 key as hex. Storage paid via Turbo in SOL (free under ~100 KiB).
{
"arweave_tx": "T9k…ZQ",
"iv": "4fJ…==",
"tag": "Qm3…==",
"key_hex": "b1e2f0a4d9…"
}mint_nft
mint_nft(name, metadata_uri, cluster?, merkle_tree?) → { asset_id, merkle_tree, solscan_url }Params- name
- NFT name.
- metadata_uri
- URI of the NFT metadata JSON (Arweave URI works).
- cluster
- Optional: devnet or mainnet-beta (default mainnet-beta).
- merkle_tree
- Optional: public Bubblegum tree (defaults to the CryptoProto production tree).
Returns. A compressed NFT: ~0.0001 SOL network fee + 0.001 SOL service fee in one transaction. The asset id works anywhere a mint address is expected. Requires SOLANA_KEYPAIR_PATH.
{
"asset_id": "J88gMLBLXKS1ELZncCudkmkcpTVfkCkPJM3aHyTvfLuz",
"merkle_tree": "9YrrBFbZ…KVwC",
"solscan_url": "https://solscan.io/token/J88g…fLuz"
}register_key
register_key(nft_mint, key_hex) → { success, nft_mint }Params- nft_mint
- NFT mint address or compressed-asset id (base58).
- key_hex
- AES-256 key from encrypt_and_upload.
Returns. Escrows the key, bound to the NFT. The local Solana keypair signs the registration.
{
"success": true,
"nft_mint": "J88g…fLuz"
}build_manifest
build_manifest(arweave_tx, iv, tag, nft_mint) → ManifestParams- arweave_tx
- Ciphertext location from encrypt_and_upload.
- iv
- Base64 12-byte IV.
- tag
- Base64 16-byte GCM auth tag.
- nft_mint
- The gating NFT.
Returns. The public manifest JSON — safe to publish; never contains the key.
{
"arweave_tx": "T9k…ZQ",
"iv": "4fJ…==", "tag": "Qm3…==",
"algo": "AES256GCM",
"nft_mint": "J88g…fLuz"
}create_locked_content
create_locked_content(content_base64, nft_mint) → { manifest, arweave_tx }Params- content_base64
- Raw content, base64-encoded.
- nft_mint
- Existing NFT (or compressed-asset id) to gate the content.
Returns. High-level: encrypt + upload + escrow-register + manifest in one call.
{
"manifest": { "algo": "AES256GCM", … },
"arweave_tx": "T9k…ZQ"
}unlock_content
unlock_content(manifest) → { content_base64, size_bytes }Params- manifest
- CryptoProto manifest (from NFT metadata or build_manifest).
Returns. High-level viewer flow: wallet-signature challenge, on-chain ownership check, download, decrypt. The local keypair must currently own the NFT.
{
"content_base64": "iVBORw0KGgo…",
"size_bytes": 65536
}check_escrow
check_escrow(escrow_url?) → { status, body }Params- escrow_url
- Optional override; defaults to the live CryptoProto escrow.
Returns. Escrow health-check response.
{
"status": 200,
"body": { "status": "ok" }
}Escrow API
The key escrow runs as a Cloudflare Worker. Most callers use the MCP tools above; the raw HTTP surface is documented here for non-MCP integrations. The full OpenAPI spec ships at /openapi.json. CORS is allowlisted to cryptoproto.com, www.cryptoproto.com, and localhost:3000.
GET /health
Liveness check. No auth, no rate limit.
→ GET /health
← { "status": "ok" }POST /register
Escrow a key for a mint. The signature proves the caller holds the registering wallet.
→ {
"nftMint": "<base58>",
"pubkey": "<base58>",
"keyHex": "<hex>",
"signature": "<hex>"
}
← { "success": true }signature is Ed25519 over the UTF-8 string register:<nftMint>, hex-encoded.
GET /challenge
Issue a single-use nonce for a wallet. Valid 60 seconds. Rate limit 30/min/IP.
→ GET /challenge?pubkey=<base58>
← { "nonce": "<hex string>", "expiresAt": <epoch ms> }GET /key
Release the escrowed key. The escrow verifies the signature and confirms live on-chain ownership before responding. Rate limit 10/min/IP.
→ GET /key?nftMint=<>&pubkey=<>&nonce=<>&signature=<>
← { "key": "<hex>" }signature is Ed25519 over the UTF-8 nonce string exactly as returned — do not hex-decode it — hex-encoded. Alternatively, skip the challenge/nonce round trip entirely and send an X-Agent-Token header from POST /agent-session — the read-scoped token authorizes the same request on its own.
Any deviation returns AUTH_ERROR.
POST /agent-session
Mint a short-lived, read-scoped token for agent callers — skips the per-call challenge/nonce dance on GET /key. Rate limit 10/min/IP. Token is valid 15 minutes and grants read access only.
→ { "pubkey": "<base58>", "signature": "<hex>" }
← { "agentToken": "<X-Agent-Token value>", "scope": "read", "expiresAt": <epoch seconds> }signature is Ed25519 over the UTF-8 string agent-session:<pubkey>, hex-encoded.
GET /tree-status
Capacity of the shared compressed-NFT Merkle tree — mint count vs. capacity, with a warning message at 80% full.
→ GET /tree-status
← { "merkleTree": "<base58>", "minted": 2, "capacity": 16384,
"usedPct": 0.01, "isPublic": true, "warning": null | "<message>" }Errors
Errors return JSON with a message and a stable code.
{ "error": "<message>", "code": "<CODE>" }Codes: BAD_REQUEST, AUTH_ERROR, OWNERSHIP_ERROR, KEY_NOT_FOUND, RATE_LIMIT.
On-chain manifest
The NFT metadata JSON, at the token's URI, carries everything a viewer needs under properties.cryptoproto.
{
"name": "…",
"description": "…",
"image": "https://arweave.net/qSFxp3Klc3gDqphNFTAbVCIy2EIXFEBKqtqC9VHzDLM",
"external_url": "https://cryptoproto.com/view",
"properties": {
"content_type": "image/png",
"cryptoproto": {
"arweave_tx": "<ciphertext data-item id>",
"iv": "<base64, 12 bytes>",
"tag": "<base64, 16-byte GCM auth tag>",
"algo": "AES256GCM",
"nft_mint": "<mint or empty>"
}
}
}Every mint carries the same image (a gold cover art) and external_url (pointing back to /view) at the top level, so the NFT renders sensibly in wallets and marketplaces. The encrypted manifest itself is unaffected — it still lives entirely under properties.cryptoproto.
One interop trap: the ciphertext on Arweave excludes the GCM tag; the tag travels separately in the manifest and is re-appended before decryption.
Security
- AES-256-GCM. Authenticated encryption. Tampering with the ciphertext fails the auth check before a single byte is decoded.
- Nonce policy. 96-bit random nonces. Never reused for a given key. Stored alongside the ciphertext.
- Key custody. The escrow holds keys encrypted at rest. Keys are released only after a signature + on-chain ownership proof.
- Threat model. Stolen ciphertext is useless without the NFT. Stolen NFT is useless without the wallet. Compromised escrow cannot mint or transfer NFTs. Replays are blocked by nonce expiry. Network MITM sees only ciphertext and signatures.
FAQ
Is this live?
Yes — live on Solana mainnet. The browser flows use real SOL and mint real, permanent assets.
What if I lose my wallet?
Access follows the NFT. If the wallet is lost, access is lost — same as losing the only key to a safe deposit box. Use a hardware wallet for content you can't afford to lose.
Can the encrypted file be deleted?
No. Arweave's incentive model is pay-once-store-forever. Content uploaded to the network is replicated and persists past any single node.
Why Solana and not Ethereum?
Verification needs to be fast and cheap to feel like a UI primitive, not a transaction. Solana's confirmation times and fees fit the access pattern.
Is the escrow trustless?
Trust-minimised. The escrow can refuse to release keys, but it cannot mint NFTs, transfer ownership, or read your plaintext. Multi-party escrow is on the roadmap.
How much does this cost?
Storage is paid once (Arweave bundle fee, scales with file size; free under 100 KiB). Minting is a compressed NFT (Bubblegum) into a shared Merkle tree — ~0.0001 SOL network cost — plus a flat 0.001 SOL service fee per mint. An itemized cost card shows the full breakdown before you sign. Verification is free at the protocol level.
