Concepts

NFT Commerce

Every store is an ERC721A-C contract. Every purchase mints a unique receipt. Burn the receipt to confirm shipping.

Model

  • Ownership on chain — buyers get an ERC721 token, not a DB row.
  • Inventory on chain — max supply enforced by the contract.
  • Provenance on chain — every sale, royalty and resale is auditable on the store's chain explorer.
  • Burn on claim — receipt is consumed when the buyer confirms shipping.

Chains

CC0Store deploys through CC0CollectionFactory.deployStore(). The factory is live on two chains; the deploy hook resolves the target from its chain parameter ("base" / "ethereum").

ChainFactoryStatus
Base (8453)0xB9585C09B6A78a16Bfb18D5b49D7F43431623065VerifiedLive — the target the creator wizard signs against
Ethereum (1)0x343d77D94A119D5cEA495aeE8336A3a7Aa5CD385VerifiedDeployed — same bytecode, reachable via the deploy hook
Robinhood Chain (4663)Not deployed. The NFT stack is Base + Ethereum only.

Reference CC0Store (Base): 0xe82D55a89C8954Ca84307e01ea3699296fE9a8D3Verified. New stores share its bytecode, so they inherit the explorer's "Similar Match" verification automatically.

The store contract and the store currency can live on different chains

The CC0Store itself is Base or Ethereum. Its store currency — the ERC-20 you price products in — can be a launchpad token on Base, Ethereum or Robinhood Chain (4663). See Token Launch.

Royalty enforcement

CC0Store is ERC721-C (Limit Break Creator Token Standards). Its constructor points the token at the transfer validator 0x721C008fdff27BF06E7E123956E2Fe03B63342e3 (confirmed by getTransferValidator() on the reference store), and the OpenSea Conduit 0x1E0049783F008A0085193E00003D00cd54003c71 is baked into the creation bytecode so OpenSea order flow settles without extra configuration.

Onchain types

solidity
struct ProductType {
  string  name;
  string  description;
  string  metadataURI;   // ipfs://... OpenSea-compliant JSON
  uint256 price;         // base units of paymentToken
  address paymentToken;  // address(0) = ETH, else ERC20
  uint256 maxSupply;     // 0 = open edition
  uint256 minted;
  bool    isActive;
}

struct SalePhase {
  bytes32 merkleRoot;    // 0x0 = public phase
  uint256 maxPerWallet;
  uint256 price;
  uint256 endPrice;      // > 0 enables Dutch auction
  address paymentToken;
  uint256 startTime;
  uint256 endTime;
  uint256 maxSupply;
  uint256 minted;
  bool    isActive;
}

Deploy a store

Humans — wizard at /deploy/nft-store. It is a chained flow: it deploys the CC0Store contract and creates (or links) the store currency in a single run.

  1. Store contract. Name, symbol, description, logo, banner, royalty BPS (default 500 = 5%; the wizard caps input at 1000 = 10%). You sign factory.deployStore(...) yourself — the platform never deploys on your behalf.
  2. Store currency. Either link an ERC-20 you already own, or create one in-flow through a third-party provider:
    • Clanker (recommended) — auto-LP on Uniswap; 60% of pool fees to the creator, 20% to the cc0company treasury, 20% to the Clanker protocol.
    • Zora Coins — content-coin model, 50/50 split with the platform, Zora marketplace integration.
    • Bankrcoming soon; the tile is present in the wizard but disabled.
    These providers are not the cc0strategy launchpad — for the 75/15/10 (or paired 80/20) fee split, launch on the launchpad first and link the token as "existing". See Token Launch.
  3. Registration. The store record is written to the backend and the storefront goes live at /s/{store-slug}.

Agents — 3-step prepare / sign / confirm flow. The platform builds the calldata, your wallet signs it, you report the hash back:

bash
# 1. Prepare
curl -X POST https://cc0.company/api/store/agents/me/cc0store/prepare-deploy \
  -H "X-Owner-Address: $AGENT_WALLET" \
  -H "X-Owner-Message: $MSG" -H "X-Owner-Signature: $SIG" \
  -d '{ "store_id": "mstore_xxx", "name": "My Store", "symbol": "MYSTORE", "royalty_bps": 500 }'
# → { transaction: { to, data, value, chainId } }

# 2. Sign + submit with your own wallet (raw calldata — CDP, viem, Bankr /agent/submit, ...)

# 3. Confirm
curl -X POST https://cc0.company/api/store/agents/me/cc0store/mstore_xxx/confirm-deploy \
  -H "X-Owner-Address: $AGENT_WALLET" \
  -H "X-Owner-Message: $MSG" -H "X-Owner-Signature: $SIG" \
  -d '{ "tx_hash": "0x..." }'

Agent CC0Store deploy is not GA yet

The agent skill bundle at cc0.company/skill.md currently marks the CC0Store prepare-deploy / confirm-deploy / product-type endpoints as not yet live in production. What is live for agents today: the store record check (/agents/me/deploy-store) and orders + fulfillment (/agents/me/cc0store/:storeId/orders). Agents shipping digital art today should use the collection paths instead.

Add product types

Onchain call: addProductType(name, description, metadataURI, price, paymentToken, maxSupply). Every token of a product type shares one metadataURI; variants live in that JSON's attributes.

  1. Pin metadata JSON to IPFS via /api/upload/metadata (Pinata).
  2. Submit addProductType with the resulting ipfs:// URI.
  3. Product is live at /s/{store-slug}/{product-id}.

Sale phases

Phases are per product type: addSalePhase(productTypeId, merkleRoot, maxPerWallet, price, endPrice, paymentToken, startTime, endTime, maxSupply).

  • Allowlist → Public: a Merkle-root phase followed by a phase with merkleRoot == 0x0.
  • Dutch auction: set endPrice below price for linear decay.
  • Phases can be rewritten (updateSalePhase) or switched off (toggleSalePhase).

Allowlists + Merkle proofs

The merkleProof argument the mint call takes is served by the platform — you never build the tree yourself.

  • /api/store/nft-store/{storeSlug}/phases/{productTypeId}/allowlist — manage the entries backing a phase's root. storeSlug accepts a slug or a contract address.
  • POST /api/store/nft-store/{storeSlug}/phases/{productTypeId}/proof with { phaseIndex, walletAddress } { proof, maxMint }.

Leaf format is keccak256(abi.encodePacked(address, maxMint)) with sorted pairs (OpenZeppelin MerkleProof). A wallet that is not on the list gets a 404, not an empty proof.

Buy flow

Buyers land on the product page and see the price in the configured payment token plus a USD equivalent. Payment options:

  • ETH — native, payable mint.
  • USDC — approve + mint.
  • Store token — your ERC-20 if linked.
  • Card — Stripe onramp to USDC, then a standard USDC mint.

Three mint entrypoints, all payable:

FunctionUse
mint(productTypeId, phaseIndex, merkleProof)One unit to msg.sender.
mintBatch(productTypeId, quantity, phaseIndex, merkleProof)Several units in one transaction; returns the first token id.
mintTo(productTypeId, phaseIndex, merkleProof, to)Mint to a different recipient. This is the relay entrypoint — the x402 / fiat paths pay and mint to the buyer.

The contract validates the phase, the proof and the caps, splits 95 / 5 in-line, mints, and emits Minted (or MintedTo for the relay path). The storefront then reports the mint to the backend via POST /api/store/nft-store/report-mint so the purchase shows up under the buyer's orders immediately, without waiting on the indexer.

No backend role on the store

Since v9 CC0Store has no uploader / backend co-owner. Both mint() and mintTo() are public and payable: anyone can pay and mint to any recipient, so a relay is a convenience, never a gate.

Claim flow (burn-on-claim)

Claiming is two transactions, wrapped by a quote and a confirmation. Shipping is paid in USDC on Base (0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913, 6 decimals) and goes to the treasury address returned in the quote.

  1. Buyer taps "Confirm order" on /my-collection and enters variant + shipping address.
  2. POST /api/store/nft-store/claim/{tokenId}/quote → zone, tier, delivery window, shipping_cost_usdc,treasury_wallet, expiry.
  3. Buyer confirms the quote.
  4. Tx 1 — shipping. USDC transfer(treasury_wallet, shipping_cost). Skipped entirely when shipping_cost_usdc === 0.
  5. Tx 2 — claim. claimProduct(uint256 tokenId, bytes32 variantHash), where variantHash = keccak256(variantData). This burns the receipt token.
  6. POST /api/store/nft-store/claim/{tokenId}/confirm carrying both transaction hashes. Free shipping sends an empty shipping_tx_hash.

The order is sequenced strictly — shipping tx, one confirmation, then the burn. If the USDC transfer fails, claimProduct is never called and no token is destroyed.

Burn is one-way

Claiming burns the receipt. The buyer can no longer resell it afterwards. Buyers can hold indefinitely without claiming.

Digital delivery

Product types that ship a file rather than a parcel use POST /api/store/nft-store/claim-download. The route reads the store contract directly — ownerOf(tokenId) must equal the caller and getClaimData(tokenId).claimed must be true — then issues a download_token. The asset is gated on the receipt, not on a session.

Secondary market

Receipts are ordinary ERC721 tokens until they are claimed, so they trade. On the chains where our Seaport primitives run — Base and Ethereum — a viewer can buy a listed item, or list / cancel one they own, inline on the store page. On any other chain the grid still renders and each item degrades to an OpenSea link. Listings read the ERC-2981 royalty, which the ERC721-C transfer validator enforces on settlement.

Indexing

The store-nft-indexer module follows the store's own chain and writes store_nft_order rows mirroring onchain state. Off-chain fields (shipping address, tracking number) live alongside the mirror. Events it consumes:

EventMeaning
ProductTypeAddedNew SKU registered on the store.
SalePhaseAdded / SalePhaseUpdated / SalePhaseToggledPhase lifecycle for a product type.
MintedDirect purchase — tokenId, productTypeId, buyer, price, paymentToken.
MintedToRelay purchase (x402 / fiat) — tokenId, productTypeId, recipient, minter.
ProductClaimedtokenId, claimer, productTypeId, variantHash, claimedAt.
TransferResales, and to == 0x0 for the claim burn.

Order status enum: minted → claimed → order_created → processing → fulfilled → shipped → delivered → completed. Merchants advance it from fulfilled onward by posting a tracking number and carrier (ups | fedex | dhl | usps | colissimo | mondialrelay | other).

Fees + royalties

The 5% platform fee is split in-line on every mint. The recipient is not hardcoded in the bytecode — it is the platformFeeRecipient constructor argument. Every store the wizard deploys passes the platform wallet 0xAabEc077428420333c45b6D84455d4EAE8Ee0625, which platformFeeRecipient() returns on both the factory and the reference store.

Royalty BPS is set at deploy (default 500; the wizard caps input at 1000 = 10%) and is enforced by the ERC721-C transfer validator on every secondary sale. See Platform Fees.