Collections and contracts
How an NFT collection is created, approved and deployed — the PENDING gate, the symbol uniqueness rule, the royalty clamp, what deploying an ERC-721 contract actually costs, and why an undeployed collection blocks nearly everything.
A collection is two things at once: a database row that groups tokens for browsing, and — after a separate deployment step — a real ERC-721 or ERC-1155 contract on a chain. Almost every complaint an operator receives about this product comes from the gap between those two states.
The lifecycle
DRAFT ──▶ PENDING ──▶ ACTIVE ──▶ (deployed) ──▶ mintable
│ │
│ └──▶ SUSPENDED (admin disabled it)
└──▶ INACTIVEnft_collection.status is an enum of DRAFT, PENDING, ACTIVE, INACTIVE
and SUSPENDED. A user-created collection is always written PENDING — that
is hardcoded in the create route, not a setting — and only an admin can move it
on.
Creating a collection
Users go to /nft/collection/create. The form is gated by the create_nft KYC
feature, so if you have bound that feature to a KYC level an unverified user
sees the gate rather than the form.
The required fields are name, symbol, categoryId, chain, network and
standard. Everything else — description, logo, banner, max supply, mint price,
currency, royalty, public flag — is optional.
The validation rules that surprise people
Names only have to be unique per creator — two different creators can both have a collection called "Genesis". Symbols are checked across the entire marketplace and a clash returns 409 with the message "Token symbols must be unique across the marketplace".
The format is also strict: 1–10 characters, uppercase alphanumerics plus $.
Lowercase input is upper-cased for you; anything else is rejected.
Creation looks up nft_marketplace for an ACTIVE row matching both chain and
network. No row, no collection — the error names the chain and network it
could not find.
This is why deploying a marketplace contract is the first real step of an
install. It is also why a chain silently disappearing from the create form
usually means somebody redeployed with force: true and the old row went
DEPRECATED.
The request schema advertises royaltyPercentage up to 50. The handler ignores
that and clamps to nftMaxRoyaltyPercentage (default 10), rejecting
anything above it.
That is deliberate. When the two disagreed, a collection created above the platform maximum showed one royalty to buyers and was silently clamped to a different one at settlement, so the creator was quoted earnings they would never receive.
Letters, digits, spaces, hyphens, underscores, apostrophes and full stops, up to
255 characters. Emoji and most punctuation are rejected outright. The value is
then HTML-escaped before it is stored, so &, < and / come back as
entities in the database.
logoImage and bannerImage must match https://… or /uploads/…. A bare
filename fails validation.
The creator profile is created for you
If the user has no nft_creator row, one is created during collection creation
using their real name from the user table, or creator_<first 8 chars of id>
when they have no name set. It starts isVerified: false and profilePublic: true.
Note that nft_collection.creatorId points at nft_creator.id, not at
user.id. Anything that needs to pay the creator — the royalty leg of a
settlement, for instance — has to resolve through nft_creator.userId to reach
a real account.
Approving a collection
Nobody can mint into a PENDING collection. The mint route checks status !== "ACTIVE" and refuses with the current status in the message.
Go to Admin → NFT → Content → Collections (/admin/nft/collection).
The endpoint takes a boolean, not a status string: true sets ACTIVE,
false sets SUSPENDED. There is no way from this control to reach DRAFT,
PENDING or INACTIVE again.
The admin Collections table is deliberately read-mostly — create and delete are both switched off in the UI, because collections belong to their creators. Editing metadata and flipping status are the two things an admin does here.
Verification is separate from approval. nft_collection.isVerified is what the
onboarding checklist counts as "featured content", and it is what puts the badge
on a collection in the marketplace.
Deploying the contract
Approval makes a collection browsable. It does not put anything on a chain. A
separate deployment writes contractAddress onto the row and flips its status
to ACTIVE.
This is gated by the deploy_nft_contract KYC feature.
What it does, in order:
-
Resolves the caller's creator profile and confirms the collection belongs to it. A collection you do not own returns 404, not 403.
-
Refuses a second deployment. If
contractAddressis already set you get 400 — there is no redeploy path for a collection. -
Loads the Ecosystem master wallet for the chain, with
statusenabled. No wallet, no deployment. -
Signs and broadcasts with the master wallet's decrypted private key, using the
ERC721NFTorERC1155NFTartifact. -
Writes the result back:
contractAddress,chain,standard,maxSupply,royaltyPercentage,mintPrice,deployedAt, andstatus: "ACTIVE". -
Records an activity row of type
COLLECTION_CREATEDwith aCONTRACT_DEPLOYMENTaction in its metadata, carrying the gas used and the deployment cost.
Who owns the deployed contract
The contract's owner and its royalty recipient are both set to the
creator's linked wallet address. If the creator has no walletAddress on
their user row, both fall back to the master wallet — which means the
platform, not the creator, owns the contract and collects its on-chain
royalties, permanently, with no way to change it afterwards.
There is no check for this. A creator with no linked address gets a successful deployment whose royalties are hard-wired to your master wallet. The only fix is a new collection with a new contract.
Linking is also harder than it looks — see the wallet-address entry in Troubleshooting.
Gas comes out of your master wallet, per collection
Every collection contract is a separate deployment paid by the platform's master wallet. On a busy marketplace this is a real recurring cost that scales with the number of creators, and nothing in the product bills it back to them. Watch the balance on the Marketplace screen; a drained master wallet fails deployments with an RPC error rather than a clear message.
Recording an externally deployed contract
If a contract was deployed outside the platform there is a route to attach it:
What an undeployed collection cannot do
This is the list worth pinning up, because each of these is a support ticket:
| Action | Behaviour without contractAddress |
|---|---|
| Mint a token | 400 — "Collection contract not deployed" |
| List a token at a fixed price | Allowed, but the purchase will fail later |
| List a token as an auction | 400 — an auction could never be settled, so it is refused up front |
| Deploy an auction contract | 400 |
| Buy a listed token | 400 — "Collection contract must be deployed to execute blockchain transfer" |
The auction guard is the important one. Auction escrow lives inside a per-listing
NFTAuction contract, and that contract can only be deployed against a real
collection contract and a real on-chain token id. Before the guard existed, an
auction on an undeployed collection was accepted, ran to completion, took bids,
and then sat unsettleable forever. Fixed-price listings still allow this shape,
which is why the failure surfaces at purchase time instead.
Collection statistics
Note that volumeTraded, totalSales and floorPrice are not columns on
nft_collection. They are computed from nft_sale and nft_listing rows when
asked for. A collection with no sales reports zeroes rather than nulls, and
there is no cached figure to go stale.
Next
- Minting — what happens after the contract exists.
- Trading — listings, auctions and offers.
- Fees and royalties — where the collection's royalty percentage is actually applied.