Minting NFTs
How minting works on this platform — the creator signs, the backend verifies — plus the six-step wizard, the IPFS requirement, the duplicate rules, the metadata endpoint, and why batch minting only creates drafts.
Minting on this platform is confirm-after-signature. The creator's browser wallet signs and broadcasts the mint transaction; the backend then fetches the receipt, checks it, and only then writes a token row. There is no custodial signer, so the platform can never mint on somebody's behalf.
Understanding that one sentence explains most of the product's behaviour: why the creator needs gas, why a "batch mint" cannot go on chain, and why a failed transaction leaves no token behind.
Before a creator can mint
Four things must be true, and each one produces a different error:
| Requirement | Failure |
|---|---|
| A collection they own | 404 — "Collection not found or access denied" |
That collection has a contractAddress |
400 — "Collection contract not deployed" |
That collection is ACTIVE |
400 — names the current status |
The create_nft KYC feature is satisfied |
The KYC gate replaces the form |
The creator also needs native currency in their own wallet for gas, and they need to be on the right network — the wizard checks and blocks on a network mismatch rather than letting the transaction fail.
The six-step wizard
/nft/create walks a creator through six steps and tracks completion of each:
-
Select collection — from the collections they own. Only deployed, active ones can be used.
-
Connect wallet and verify network — the wizard compares the connected chain against the collection's chain and refuses to continue on a mismatch.
-
Provide the IPFS image — pasted as a URL and validated before anything else. This is the step most creators get stuck on.
-
Basic information — name and description. Name is required and capped at 200 characters when metadata validation is on.
-
Properties — attributes, rarity tier, royalty percentage, unlockable content, and the explicit-content flag. All optional.
-
Preview and mint — the wallet signs; the wizard then posts the receipt to the backend.
IPFS is the creator's responsibility
There is no upload-to-IPFS pipeline in this product. Creators bring their own pinned content — Pinata, NFT.Storage, their own node, anything that serves a gateway URL.
Two URLs are involved and they are not the same thing:
image— required. The artwork itself.metadataUri— optional. A JSON document that becomes the token'stokenURI. If the creator supplies one it is used verbatim; if not, the platform builds the metadata object itself from the form fields.
The wizard refuses a URL that is not recognisably IPFS before it lets the wallet
sign, because a bad tokenURI is permanent once it is on chain.
When the platform generates metadata, it embeds a properties.creator block
with the creator's display name and linked wallet address, a properties.collection
block with the collection name and contract address, plus rarity, unlockable
content and the explicit flag.
Serving metadata from your own domain
If a collection's contract points its base URI at your site rather than at IPFS, there is a public ERC-721-compatible endpoint for it:
The tokenId in that path is the blockchain token id (1, 2, 3…), not the
row's UUID. MetaMask, OpenSea and every other wallet consume this shape
directly.
Duplicate protection
The same artwork cannot be minted twice into the same collection. Both the metadata URI and the image URL are normalised — trimmed, lower-cased, trailing slash removed — and checked within the collection.
The wizard calls this before asking the wallet to sign, so a creator finds out they are about to re-mint the same file while it is still free to fix. If they get past it anyway, the mint-recording route returns 409 naming the existing token — after the transaction has already been paid for and mined. The token exists on chain and the platform will not index it.
That asymmetry is worth telling creators about: on this platform, a duplicate is caught at the database, not at the contract.
What the backend verifies
Before writing anything, the handler pulls the transaction receipt from the chain and checks four things:
-
The receipt exists. Missing means the transaction is not confirmed yet — the error asks the creator to wait rather than failing permanently.
-
receipt.status === 1. A reverted transaction produces no token. -
receipt.tomatches the collection's contract address. A transaction sent somewhere else cannot be claimed as a mint into this collection. -
The block number matches the one submitted, when one is submitted.
It then refuses any transactionHash that already appears in nft_activity, so
the same mint cannot be recorded twice.
Only after all of that does it open a transaction and write the token row with
status: "MINTED", isMinted: true, the recipient address in
ownerWalletAddress, a computed rarity score, a MINT activity row carrying
the block number and gas used, and an increment to the collection's
totalSupply.
recipientAddress is taken from the request and stored as-is. A creator can
mint from a different wallet than the one linked to their account, and the
platform records where the token actually went. Ownership inside the platform
still tracks ownerId, the user.
Royalty on a token
royaltyPercentage can be set per token as well as per collection, and it is
validated against nftMaxRoyaltyPercentage at mint time. Anything above the
platform maximum is rejected with the ceiling named in the message.
Note that settlement reads the collection's royalty, not the token's. A per-token royalty is recorded but does not change what a sale pays out. See Fees and royalties.
Batch preparation
/nft/batch-mint looks like a bulk mint and is not one.
It creates the token rows with status: "DRAFT", isMinted: false and a
placeholder local tokenId of the form draft-<timestamp>-<index>. Nothing
goes on chain.
Passing mintToBlockchain: true is refused with an explanation rather than
being ignored:
Minting on chain cannot be batched: each token needs a signature from your own wallet. Create the batch first, then mint each token from your collection.
The limit is 50 tokens per call. Each entry needs a name; entries whose royalty exceeds the platform maximum are skipped individually with a per-row error rather than failing the whole batch.
A DRAFT older than 72 hours — the platform's approval SLA — is counted as a catalogue defect on the admin moderation dashboard, because it represents a drop that was prepared and never carried out. If you use batch preparation heavily, expect that counter to move.
Approving the marketplace to move a token
Before a token can be listed, the marketplace contract must be approved to
transfer it. This is an on-chain approve or setApprovalForAll that the owner
signs.
The listing route calls the approval check itself and refuses an unapproved token. For bundle listings it checks every token in the bundle and names the ones that are not approved.
Approval checks are made against the chain with a three-second timeout per RPC call, so a slow or flaky provider shows up here as listings that will not create.
Transferring outside a sale
Gated by the transfer_nft KYC feature. A token with no blockchainTokenId —
one that looks minted but was never actually put on chain — is rejected here,
by the approval routes, and by listing creation. That combination is the second
catalogue defect the admin dashboard counts.
Gas estimates
Readings are stored in gas_history, which is one of the two tables created by
the boot sync rather than shipped in initial.sql.
Next
- Trading — listing, selling, auctioning and offers.
- Collections — the contract deployment that has to happen first.