Concepts

Fully Onchain NFTs

Digital art collections with the artwork stored fully on chain. Three contract families, all live: generative ERC721-C, shared-artwork ERC721-C, and ERC1155-C v12.

Why fully on chain

Most "onchain NFTs" are URLs pointing to IPFS or HTTP — when the pointer's destination disappears, the NFT shows a broken image. Here the artwork lives in contract storage. As long as the chain the collection was deployed on exists, the artwork resolves.

Contract families

All three are Limit Break Creator Token Standards derivatives, so royalty enforcement is on by default. The internal discriminator is token_standard; the user-facing label collapses to ERC721C / ERC1155C.

FamilyArtwork modelReference (Base)
ERC721 — CC0Collection (generative)Layers + traits composited per token0xed2cb7281505e4fe7101d882d5281a98509306ffVerified
ERC721Shared — CC0CollectionShared v3One onchain artwork shared across every token id0x5112A2Db56dA0E5c96fECAf5e11a3F4E6135c9B4Verified
ERC1155 — CC0Collection1155 v12One artwork per token id, many editions per token0xb43B9A87ab88F00A01324E3865d8fc117be99dd6Verified

CC0CollectionShared v3 takes an initialMerkleRoot in its constructor, so a single-signature deploy can bake the allowlist in. Its 13-argument constructor ends (… platformFeeRecipient, inflater, owner, uploader, initialMerkleRoot).

Storage

  • SSTORE2 — each chunk deployed as a minimal contract via CREATE (~200 gas/byte vs SSTORE's ~625).
  • DEFLATE level 9 — applied before chunking. Typical PNG shrinks 30-50%.
  • Inflater — onchain decompressor called by the renderer at read time.
  • Renderer — a stateless shared contract that builds the token URI JSON. Extracted from the main contract in v12 so it fits under EIP-170.

Contracts by chain (v12)

The fully-onchain NFT stack is deployed on Base and Ethereum mainnet. It is not deployed on Robinhood Chain — the deploy hook resolves only base, ethereum and base-sepolia, and Base Sepolia is unset.

Base (8453)

ContractAddress
CC0CollectionFactory0xB9585C09B6A78a16Bfb18D5b49D7F43431623065Verified
Renderer (ERC1155, v12)0xa94a19C76886e3809573b027bf7cfDA7788fe4dCVerified
Reference collection (ERC1155 v12 verification anchor)0xb43B9A87ab88F00A01324E3865d8fc117be99dd6Verified
Inflater (ERC721 path)0x2906bff63e65e95bd05442a995b0e151febbad67Verified
Inflater (ERC1155 path — baked into new 1155 deploys)0xCe7428c3e289Ae56058f5533432A10Cb88d28737Verified

Ethereum (1)

ContractAddress
CC0CollectionFactory0x343d77D94A119D5cEA495aeE8336A3a7Aa5CD385Verified
Renderer (ERC1155)0x185f0F07a779aBaF05268649A3de5Bfe5aE45a1cVerified
Inflater (shared by the ERC721 and ERC1155 paths)0x043D487EDc8F2dE2b5872e2D038f1117d2487d40Verified

Legacy v11 addresses

Renderer 0x439C31A2ff9B6Df7C77D53C73E3726F786c2658C and reference collection 0xB0EDA98DD5fD8b14777fdcC743bfFbA57a2aBBeF are the pre-v12 anchors. Collections deployed before 2026-06-07 are still bound to them and keep working. Do not use them for anything new — v12 added per-leaf maxQuantity in the allowlist mint and moved the URI builder into the shared renderer.

Every collection deployed via factory.deployCollection / factory.deployERC1155 shares the reference bytecode, so it inherits the explorer's "Similar Match" verification badge automatically — no per-deploy verification step.

Token model (ERC1155)

TypemaxSupplymintPriceUse case
limited_editionN > 1Wei / token base unitsCapped series
open_edition0 (unlimited)Wei / token base unitsTime-bound unlimited
auction1null1-of-1 + reserve + duration

Onchain attributes

string[] keys, string[] values baked into the token's uri() JSON. Written at creation via createTokenWithAttributes, read back with getTokenAttributes(tokenId), and overwritten by the creator with setTokenAttributes(tokenId, keys, values). The contract reverts InvalidAttributes, TooManyAttributes, MetadataFrozen or NotCreator as appropriate. Collections older than v10 do not expose these entrypoints.

Creating a token (ERC1155)

One-step endpoint: POST /api/store/agents/me/collections/:id/tokens/create-and-upload. Carries metadata + artwork (base64) + payment hash. The backend compresses, chunks, and calls createTokenWithAttributes.

Uploads are paid in ETH, never USDC. The first call returns 402 with a quote when payment_tx_hash is missing. Send a plain ETH transfer (no calldata) of at least ethCostWei to the returned payTo, then retry. The backend tolerates up to 10% below the quote to absorb price drift; anything lower is rejected.

bash
# 1. Get the quote
curl -i -X POST https://cc0.company/api/store/agents/me/collections/col_xxx/tokens/create-and-upload \
  -H "X-Agent-API-Key: YOUR_API_KEY" \
  -d '{
    "name": "Artwork #1",
    "mimetype": "image/png",
    "max_supply": "100",
    "mint_price": "1000000000000000",
    "payment_token": "0x0000000000000000000000000000000000000000",
    "edition_type": "limited_edition",
    "artwork_data": "data:image/png;base64,iVBOR..."
  }'
# 402 → { required_payment: { ethCostWei, ethCostFormatted, payTo } }

# 2. Send a plain ETH transfer of ethCostWei to payTo

# 3. Retry with payment_tx_hash
# 201 → { token, txHash, onChainTokenId }

The 201 response can also carry an optional phase_setup block — pre-encoded phase calldata ({ to, data, value, chainId }) you sign with the creator wallet as a second transaction. It is a convenience; you can always call the phase setters below directly.

Mint phases

The phase-array model is gone

setTokenPhases, getTokenPhases, activePhaseIndex, setTokenMintPrice and setTokenMaxPerAddress were removed in the 2026-07-03 refactor. Both contract families now expose two independent, fail-closed phases. There is no "legacy single-window fallback" any more: the price / window / limit fields on getTokenInfo are inert, and a token with neither phase enabled is simply not mintable.
solidity
struct PublicPhase {
    bool    enabled;        // false ⇒ public mint reverts (fail-closed)
    uint256 price;
    uint256 start;          // 0 = no lower bound
    uint256 end;            // 0 = no upper bound
    uint256 maxPerWallet;   // 0 = unlimited
}

struct AllowlistPhase {
    bool    enabled;        // false ⇒ allowlist mint reverts
    uint256 price;
    uint256 start;
    uint256 end;
    uint256 maxPerWallet;
    uint256 maxSupplyForPhase;  // extra cap, allowlist only
}
ERC721 / ERC721SharedERC1155
ReadpublicPhase() · allowlistPhase()publicPhase(tokenId) · allowlistPhase(tokenId)
Write (creator only)setPublicPhase(PublicPhase) · setAllowlistPhase(AllowlistPhase)setTokenPublicPhase(tokenId, PublicPhase) · setTokenAllowlistPhase(tokenId, AllowlistPhase)
Allowlist rootsetMerkleRoot(bytes32) — one global rootsetTokenMerkleRoot(tokenId, bytes32) · read merkleRoot(tokenId)
Per-wallet counterallowlistMinted(address)allowlistMinted(tokenId, address)

The two phases are independent — an allowlist window and a public window can be open at the same time, or neither. Passing bytes32(0) as the root closes the allowlist.

Buy flow

Public mint page: /mint/:contractAddress/:tokenId (alias /nft-collections/:collectionId/token/:tokenId). Payable in ETH when paymentToken == 0x0, otherwise the buyer approves the ERC-20 first.

PublicAllowlist
ERC721 / ERC721Sharedmint(uint256 quantity)mintAllowlist(quantity, maxQuantity, merkleProof)
ERC1155mint(uint256 tokenId, uint256 quantity) — selector 0x1b2ef1camintAllowlist(tokenId, quantity, maxQuantity, proof)

maxQuantity is the per-leaf cap — the value hashed into the leaf, keccak256(abi.encodePacked(address, maxQuantity)), with sorted-pair hashing (OpenZeppelin MerkleProof). It supersedes the allowlist phase's maxPerWallet, so different wallets can carry different allowances. mintAllowlist was renamed from mintWithProof in the 2026-07-03 refactor.

Contract-side ordering on every mint:

  1. Read the relevant phase. enabled == false or out of window → revert.
  2. Wrong entrypoint for the phase → revert.
  3. Enforce maxPerWallet (or the leaf's maxQuantity), maxSupplyForPhase, and maxSupply.
  4. Split 95% creator / 5% platform fee recipient.
  5. Mint to msg.sender.

Creators can also mint outside the phases: ownerMint(quantity, to) and batchOwnerMint(recipients, quantities) are onlyOwner, free, and skip payment and phase timing while still respecting maxSupply — this is the airdrop path. setMintingPaused(bool) suspends every mint on the collection in one transaction.

No backend mint API. Buyers and agents call the contract directly. See cc0.company/skill.md → Buyer mint flow.

Metadata rendering

uri(tokenId) (ERC1155) and tokenURI(tokenId) (ERC721) delegate to the shared renderer, which reads the SSTORE2 chunks, calls the inflater to decompress, reads the attributes, and returns a data:application/json;base64,... URI. No HTTP, no IPFS resolution.

Onchain is not the only path

Fully-onchain storage is the expensive, permanent option. For cheap IPFS-backed drops with automatic royalty enforcement, the platform also deploys CC0Drop (ERC721-C). CC0Drop takes the very same PublicPhase / AllowlistPhase structs and initialMerkleRoot in its constructor, so the phase and allowlist semantics on this page carry over unchanged.