Skip to main content

Architecture

SealedIP has four layers. Each layer talks only to its neighbors. The contracts are the source of truth.

Layer 1 — User-facing apps

Web app (web/)

Next.js 15 App Router + RainbowKit + wagmi + viem. The reference UI for bidders and sellers. Lives at sealedip.com.

Key responsibilities:

  • Browsing, filtering, and sorting auctions
  • Mint-and-list flow for new IPs
  • List-existing-IP flow with Story v4 owner lookup
  • Sealed-bid placement (delegates to SDK)
  • Reserve sealing for sellers (delegates to SDK)
  • Watchlist (localStorage per wallet)
  • My bids dashboard

TDH2 encryption requires a WASM module (@piplabs/cdr-crypto) that does not load in browser or edge runtimes. The web app therefore proxies all encryption calls through Next.js API routes running in Node.

TypeScript SDK (sdk/)

A viem-compatible wrapper around:

  • ABIs for SealedAuction and AuctionRevealCondition
  • placeBid — the bidder's two-call flow (allocate slot, sign, encrypt, submit ciphertext)
  • sealReserve — the seller's flow (read reserveUuid, sign, encrypt, submitEncryptedReserve)
  • settleAuction — orchestrator: wait, trigger, decrypt bids, decrypt reserve, settle
  • getAuction / getBid / getAllBids — read helpers (return reserveUuid and reserveHasCiphertext, not reservePrice)
  • encodePayload / decodePayload / signBidDigest / encryptBid / decryptBid

Subpath exports: @sealedip/sdk/constants, @sealedip/sdk/abi/sealed-auction, @sealedip/sdk/encrypt, @sealedip/sdk/payload, etc.

Layer 2 — Off-chain services

Encryption API

A Node.js server that runs TDH2 encryption against the active CDR public key. The caller signs a bid payload off-chain; the encryption API encrypts the signed payload under the threshold key and returns ciphertext bytes.

The same API handles reserve encryption for sellers. The plaintext format is identical: a 149-byte BidPayload where the signer field is the seller address instead of a bidder address.

Why a separate server: TDH2 encryption uses @piplabs/cdr-crypto WASM, which does not load in browser or Cloudflare Workers environments.

Pinata / IPFS

When a seller mints a new IP through /create, the IP metadata JSON and the NFT metadata JSON are pinned to IPFS via Pinata. The on-chain tokenURI ends up as ipfs://<cid> so any NFT viewer can dereference it.

If Pinata is unavailable, the SDK falls back to embedding the metadata as a data:application/json;base64,... URI so the mint never blocks on IPFS infra.

Story v4 API

Story Protocol's official REST API for asset discovery. SealedIP's web app proxies POST /assets (filtered by ownerAddress) to populate the IP picker for the list-existing-IP flow.

Read-only. Server-side key only. See the reference notes for API quirks worth knowing.

Layer 3 — On-chain contracts

SealedAuction.sol

The core state machine. Tracks every auction, every bid slot, every CDR vault uuid, and the sealed reserve vault per auction. Handles createAuction, submitEncryptedReserve, allocateBidSlot, submitEncryptedBid, trigger, and settle. Coordinates with the other on-chain pieces.

The auction storage tuple is: (address seller, address ipId, uint256 licenseTermsId, uint32 reserveUuid, bool reserveHasCiphertext, uint64 deadline, uint8 state, uint16 bidCount). There is no plaintext reservePrice field.

Full reference →

BidPayload library

Pure-Solidity library for the 149-byte payload format: encoding, decoding, and signature recovery. The same format is used for both bids (signer = bidder) and the reserve (signer = seller). Uses the eth-signed-message prefix so browser personal_sign and SDK private-key signing produce identical signatures. EIP-2 low-s is enforced.

Reference →

AuctionRevealCondition.sol

The CDR read condition for every vault in a SealedIP auction: both bid vaults and the reserve vault. Returns true only when BOTH gates pass:

  1. block.timestamp >= deadline — the deadline has elapsed.
  2. SealedAuction.isTriggered(auctionId) == truetrigger() has been called.

SealedAuction registers each vault (bid or reserve) on AuctionRevealCondition at allocation time via register(uint32 uuid, uint256 auctionId, uint256 deadline). Registration is one-shot per uuid; re-registration and zero-deadline revert. CDR validators call checkReadCondition(uint32 uuid, bytes, bytes, address) before contributing any partial decryption share.

This replaced the old TimeBasedReadCondition (time-gate only, deleted), which allowed decryption as soon as the clock passed. The trigger gate prevents premature decryption even if the clock has elapsed.

Reference →

Write conditions (important nuance)

SealedAuction passes writeConditionAddr = address(this) to CDR.allocate. CDR does not block writes in this setup. Write-access rules (caller is the slot's allocator or the seller; before deadline; single write) are enforced in SealedAuction's Solidity, not via a custom CDR write condition. A custom BidWriteCondition was investigated in an on-chain spike and deliberately not shipped: no functional gain, regression risk. Do not document a custom write condition as existing.

CDR threshold network

The off-chain validator set plus the on-chain CDR contract at 0xCCCcCC0000000000000000000000000000000005. Holds the threshold key, processes encryption requests, and publishes partial decryption shares.

Operated by piplabs. SealedIP delegates confidentiality to CDR entirely. The CDR contract is not verified on Storyscan; the marketplace reads NEXT_PUBLIC_CDR_THRESHOLD for display only. Real threshold parameters live in CDR governance.

CDR network details →

Story Periphery (SPG) + LicensingModule

Story Protocol's official contracts for minting NFTs, registering IPs, attaching license terms, and issuing license tokens.

  • RegistrationWorkflows.mintAndRegisterIp — mint-and-list flow for new IPs
  • LicensingModule.attachLicenseTerms — attach PIL terms at listing
  • LicensingModule.mintLicenseTokens — mint the PIL license token to the auction winner at settle

Payment currency is WIP. Royalty policies LAP (Liquid Absolute Percentage) and LRP (Liquid Relative Percentage) are used depending on the preset chosen by the seller. Story's canonical "Non-Commercial Social Remixing" PIL terms id is 1.

Deployed addresses →

Layer 4 — Settlement transport

WIP (Wrapped IP) is the ERC-20 token used for bid deposits and seller payouts. All transfers happen via standard transfer/transferFrom calls from the SealedAuction contract.

When settle() runs, the contract makes one transfer per payout: to the seller, to the winner (as overpayment refund), and to each losing bidder. Because every transfer reverts on failure, the whole settlement is atomic. See Atomic settlement.

Trustlessness argument

The central trust claim: a malicious orchestrator can only censor, never forge a winner or pay a non-winner.

What makes this true:

  • Every bid signature is re-verified on-chain at settle via ecrecover.
  • The deposit cap is enforced on-chain: a bid cannot reveal an amount larger than the escrowed deposit.
  • The reserve signature is verified on-chain: the signer must recover to the seller's address; otherwise the entire settle reverts.
  • Threshold cryptography means no single party (including the orchestrator) holds the decryption key. t-of-n validators must cooperate.

If the orchestrator censors (refuses to call settle()), any other party that obtains the decrypted reveals can call settle() themselves. Censorship auto-refunds all bidders because the state stays Triggered indefinitely and anyone can retry.

Why this layering matters

  • Off-chain services are replaceable. The Encryption API and Pinata could be swapped for any equivalent without touching contracts.
  • Contracts are not. Once deployed and adopted, the contract surface is the API. Changes require deploying new versions.
  • CDR is a dependency, not a fork. SealedIP does not run validators. If the CDR network changes, we adapt.

For the protocol's per-state behavior, see Lifecycle and the State machine.