Documentation

How CryptoProto encrypts, stores, and gates content.

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.

encryptuploadmintregisterAUTHORchallengeverifydecryptVIEWERon-chain state

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:

AUTHOR FLOW
// Plaintext → NFT-gated permanence
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:

VIEWER FLOW
challenge(wallet) → nonce
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:

CREATE FLOW
// Plaintext → NFT-gated permanence, all in the tab
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.

INSTALL · ANY MCP CLIENT
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:

ENVIRONMENT · DEFAULTS TARGET MAINNET
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).

EXAMPLE
{
  "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.

EXAMPLE
{
  "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.

EXAMPLE
{
  "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.

EXAMPLE
{
  "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.

EXAMPLE
{
  "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.

EXAMPLE
{
  "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.

EXAMPLE
{
  "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.

REQUEST · RESPONSE
→ GET /health
← { "status": "ok" }

POST /register

Escrow a key for a mint. The signature proves the caller holds the registering wallet.

REQUEST · RESPONSE
→ {
    "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.

REQUEST · RESPONSE
→ 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.

REQUEST · RESPONSE
→ 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.

⚠ SIGNING STRINGS
The two signed messages are exact: register:<mint> and the raw nonce string.
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.

REQUEST · RESPONSE
→ { "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.

REQUEST · RESPONSE
→ 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 SHAPE
{ "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.

METADATA
{
  "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.