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 /tools 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.

Devnet beta

The flows run on Solana devnet today. Use test SOL only. Grab it free from the faucet and switch your wallet to devnet. Mainnet support lands next. The flows are identical.

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 KB; larger funds storage in SOL
sign #1 → mint the NFT + pay a flat 0.01 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.

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.

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. Names are stable; arguments may evolve before 1.0.

encrypt

encrypt(file: Buffer, key?: Uint8Array) → { ciphertext, key, nonce }Params
file
Plaintext content to encrypt.
key
Optional 32-byte AES key. Generated when omitted.

Returns. Ciphertext bytes, the encryption key used, and the GCM nonce.

EXAMPLE
{
  "ciphertext": "0x4a7f1c8e3b…",
  "key":        "0xb1e2f0a4d9…",
  "nonce":      "0x9f3a72c0b6…"
}

upload

upload(ciphertext: Buffer) → { txId, url }Params
ciphertext
The encrypted bytes to pin to Arweave.

Returns. Arweave transaction id and the gateway URL.

EXAMPLE
{
  "txId": "T9k…ZQ",
  "url":  "https://arweave.net/T9k…ZQ"
}

mint

mint(metadata: NFTMetadata, owner: PublicKey) → { mint, sig }Params
metadata
Name, image, attributes, and the cipher hash.
owner
Solana wallet that receives the minted NFT.

Returns. The mint address and the transaction signature.

EXAMPLE
{
  "mint": "Cp7…aR",
  "sig":  "5Yz…Ax"
}

register

register(key: Uint8Array, mint: PublicKey) → { keyId }Params
key
AES key produced by encrypt().
mint
NFT mint address that gates this key.

Returns. An opaque key id for later release calls.

EXAMPLE
{
  "keyId": "k_01HQX…"
}

challenge

challenge(wallet: PublicKey) → { nonce, expiresAt }Params
wallet
Solana public key the holder will sign with.

Returns. A single-use nonce and an expiration timestamp.

EXAMPLE
{
  "nonce":     "0x6f4c…",
  "expiresAt": "2026-04-27T17:30:00Z"
}

verify

verify(wallet, mint, signature, nonce) → { token }Params
wallet
Public key claiming ownership.
mint
NFT mint address being claimed.
signature
Signature of the issued nonce.
nonce
The nonce returned from challenge().

Returns. A short-lived bearer token usable with decrypt().

EXAMPLE
{
  "token": "vt_…"
}

decrypt

decrypt(ciphertext: Buffer, token: string) → BufferParams
ciphertext
Encrypted bytes fetched from Arweave.
token
Verification token from verify().

Returns. The original plaintext bytes.

EXAMPLE
{
  "plaintext": "<original file bytes>"
}

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.

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.

⚠ SIGNING STRINGS
The two signed messages are exact: register:<mint> and the raw nonce string.
Any deviation returns AUTH_ERROR.

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": "…",
  "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>"
    }
  }
}

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 — as a devnet beta. The browser flows run on Solana devnet today. Use test SOL only. Mainnet support lands next; the flows are identical.

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 KB). Minting is one Solana transaction plus a flat 0.01 SOL service fee per mint. Verification is free at the protocol level.