Sealed is a Stellar Testnet marketplace for encrypted digital drops. Creators publish sealed content, buyers purchase contract-native Access Keys, and each key can reveal content or move through resale when the creator allows it.
- Soroban marketplace contract deployed on Stellar Testnet
- Freighter browser signing for create, buy, multi-buy, reveal, resale, offers, votes, pause, and resume flows
- Real XLM Testnet payment settlement for create fees, primary sales, resale, and escrowed offers
- One-use secrets consume the selected key after reveal
- Reusable digital assets can be opened and later resold
- Encrypted IPFS storage through Pinata when configured, with local encrypted fallback
- Pinata-backed public drop index for fresh-browser marketplace loading
- Server-side content key release after Stellar reveal verification
Sealed now generates a Noir Sealed Access Proof before reveal.
Implemented:
- Noir circuit at
circuits/sealed_access - Noir Pedersen hash constraints for the access commitment and nullifier relation
- Barretenberg UltraHonk proof generation in the browser
- browser-side proof verification before
reveal_key - deterministic proof-derived reveal nullifier
- proof hash shown in Judge Demo after browser reveal
- nullifier anchored on Stellar through the existing Soroban reveal transaction
Boundary:
- the deployed Soroban contract does not verify the full Noir proof bytes on-chain
- the contract receives the proof-derived nullifier, enforces ownership and key state, then blocks nullifier replay
- a dedicated Soroban verifier contract is a future hardening step, not simulated here
Private input:
- local access witness for the wallet, drop, key, and action
Public inputs:
- contract id commitment
- network commitment
- action commitment
- action
- drop id
- key id
- content commitment
- access commitment
- nullifier
The witness is not sent to Stellar. Stellar stores the nullifier and contract state.
- Connect Freighter on Testnet.
- Create or open a sealed drop.
- Buy an Access Key.
- Reveal the key.
- Open Judge Demo and review the ZK Evidence section.
- Confirm the Noir proof stack, proof hash, public input summary, nullifier, and Stellar transaction link.
Public files:
- cover artwork
- public listing images
- public listing PDFs
These may be uploaded as normal public IPFS files because they are visible before purchase.
Private files and hidden content:
- private article or guide text
- one-use secret codes
- private links
- private image attachments
- private PDF attachments
These are encrypted in the browser with AES-GCM before IPFS upload. Opening the raw IPFS CID should show encrypted JSON or encrypted bytes, not the readable hidden content. Buyer reveal decrypts content in the app only after access is verified.
Published drops are indexed through a Pinata-backed public manifest index.
GET /api/drop-indexreturns public listing manifests for the configured Testnet contract.POST /api/publish-drop-manifestpins a public manifest, stores the server-side release record, and republishes the shared index.POST /api/release-content-keyverifies Stellar Testnet key ownership and reveal state before returning decrypt material.GET /api/healthchecks server Pinata config, index reachability, Stellar RPC, and contract config.
The public manifest contains public listing metadata, public artwork/file references, encrypted payload references, hashes, and Stellar transaction references. It does not contain hidden plaintext, private links, one-use codes, private file plaintext, decrypt keys, wallet secrets, or PINATA_JWT.
localStorage is used as optimistic cache and wallet-scoped UI state. It is not the production source of truth for published listings.
Use this for plain Vite development:
npm run dev.env.local:
VITE_STORAGE_MODE=localPrivate content uses the local encrypted fallback. Public and private files are stored in IndexedDB, not localStorage.
Pinata mode uses a serverless endpoint so the Pinata JWT is never bundled into frontend code.
.env.local:
VITE_STORAGE_MODE=pinata
VITE_IPFS_GATEWAY=https://gateway.pinata.cloud/ipfs/
PINATA_JWT=your_server_side_pinata_jwt
VITE_SEALED_CONTRACT_ID=your_testnet_contract_id
VITE_STELLAR_RPC_URL=https://soroban-testnet.stellar.orgPINATA_JWT must be server-side only. Do not prefix it with VITE_.
Run Pinata mode with Vercel's local runtime:
vercel devPlain npm run dev does not provide the /api/ipfs-upload serverless endpoint, so the app falls back to local encrypted storage.
- Create a drop with a private article plus a private image or PDF.
- Use Pinata mode with
vercel dev. - Confirm Create shows encrypted IPFS ready.
- Publish the drop.
- Open an incognito browser and confirm the drop appears from
/api/drop-index. - Buy an Access Key with a buyer wallet.
- Reveal the key and confirm the Noir proof is generated and verified.
- Confirm Stellar stores the reveal nullifier.
- Confirm
/api/release-content-keysucceeds only after reveal verification. - Confirm the app decrypts and displays hidden content only after reveal.
- Open the raw IPFS CID or gateway URL for the private payload and confirm it exposes ciphertext, not the readable secret.
npm run build
npm run contract:test
npm run contract:buildDeploy command when the contract source changes:
powershell -ExecutionPolicy Bypass -File scripts/deploy-testnet.ps1 -Source sealed-admin -Network testnet