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").
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
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
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.
- 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. - 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.
- Bankr — coming soon; the tile is present in the wizard but disabled.
- 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:
# 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
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.
- Pin metadata JSON to IPFS via
/api/upload/metadata(Pinata). - Submit
addProductTypewith the resultingipfs://URI. - 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
endPricebelowpricefor 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.storeSlugaccepts a slug or a contract address.POST /api/store/nft-store/{storeSlug}/phases/{productTypeId}/proofwith{ 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:
| Function | Use |
|---|---|
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
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.
- Buyer taps "Confirm order" on
/my-collectionand enters variant + shipping address. POST /api/store/nft-store/claim/{tokenId}/quote→ zone, tier, delivery window,shipping_cost_usdc,treasury_wallet, expiry.- Buyer confirms the quote.
- Tx 1 — shipping. USDC
transfer(treasury_wallet, shipping_cost). Skipped entirely whenshipping_cost_usdc === 0. - Tx 2 — claim.
claimProduct(uint256 tokenId, bytes32 variantHash), wherevariantHash = keccak256(variantData). This burns the receipt token. POST /api/store/nft-store/claim/{tokenId}/confirmcarrying both transaction hashes. Free shipping sends an emptyshipping_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
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:
| Event | Meaning |
|---|---|
ProductTypeAdded | New SKU registered on the store. |
SalePhaseAdded / SalePhaseUpdated / SalePhaseToggled | Phase lifecycle for a product type. |
Minted | Direct purchase — tokenId, productTypeId, buyer, price, paymentToken. |
MintedTo | Relay purchase (x402 / fiat) — tokenId, productTypeId, recipient, minter. |
ProductClaimed | tokenId, claimer, productTypeId, variantHash, claimedAt. |
Transfer | Resales, 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.
