Command Palette

Search for a command to run...

988

Command Palette

Search for a command to run...

Blog

Launching a 10K NFT Collection: Gas-Optimised Minting at Scale

How we shipped a generative NFT minting platform that handled 2,400 mints in 90 minutes — Merkle tree allowlists, batch minting, and the gas savings that made it viable.

The brief was straightforward: launch a 10,000-piece generative art collection with an allowlist mint followed by a public mint. The constraint was less straightforward — the client's community was mostly first-time minters with small wallets. Every dollar of gas mattered.

We shipped it. 2,400 mints in the first 90 minutes. Per-token gas was roughly 28% lower than a naive ERC-721 implementation. Nobody got priced out of the mint they'd been waiting weeks for.

The Contract Architecture

Standard ERC-721 with a few critical optimisations:

Merkle tree allowlist instead of on-chain mapping. Storing 5,000 allowlisted addresses on-chain would have cost the client ~2 ETH in deployment gas alone. With a Merkle tree, the root is a single bytes32 storage slot. Each minter submits their proof, and verification is O(log n) — cheap and elegant.

function allowlistMint(uint256 quantity, bytes32[] calldata proof)
    external
    payable
{
    require(MerkleProof.verify(proof, _merkleRoot, keccak256(abi.encodePacked(msg.sender))));
    require(_numberMinted(msg.sender) + quantity <= MAX_PER_WALLET);
    _safeMint(msg.sender, quantity);
}

Batch minting was the bigger win. The naive approach mints one token per transaction, each writing a fresh storage slot for the new owner. We used a pattern where minting N tokens only writes the owner to the first token ID in the batch. Subsequent tokens in the batch are inferred during ownerOf lookups by walking backwards until a set owner is found.

This is what cut gas by 28%. Minting 3 tokens cost roughly the same as minting 1.5 tokens under the naive approach.

The Allowlist Problem

Generating the Merkle tree off-chain was simple — a Node.js script that takes a CSV of addresses, builds the tree with merkletreejs, and outputs the root plus individual proofs.

The hard part was distribution. We needed 5,000 community members to each have their unique proof ready at mint time. The solution was a Next.js frontend that:

  1. Connected the user's wallet
  2. Looked up their proof from a pre-computed JSON file hosted on IPFS
  3. Showed them their mint eligibility and remaining allocation
  4. Called allowlistMint with the proof pre-filled

No server. No database. The proof file was pinned to IPFS and served via a gateway. If our frontend went down, anyone could reconstruct the proofs from the published Merkle tree and mint directly on Etherscan.

Gas Estimation and UX

First-time minters don't understand gas. They see a MetaMask popup with a number and panic. We built a gas estimation display that showed:

  • Estimated gas cost in ETH and USD
  • A comparison: "This is about the cost of a coffee"
  • A warning if network congestion was unusually high, suggesting they wait

We also pre-checked every transaction client-side before sending it to MetaMask. If the user had already hit their per-wallet cap, or if the allowlist phase hadn't started, or if they weren't on the allowlist — they got a clear error before MetaMask opened. No wasted gas on reverted transactions.

Reveal and Metadata

The art was generative — 10,000 unique combinations of traits assembled from hand-drawn layers. The metadata was pre-generated but hidden behind a placeholder image until the reveal.

Pre-reveal, every token's tokenURI pointed to a single placeholder JSON on IPFS. Post-reveal, we updated a base URI in the contract, and each token resolved to its unique metadata and image.

The images and metadata were pinned on IPFS with Pinata. We also kept a backup on Arweave for permanence. The client's community cared about decentralisation — pointing metadata at an S3 bucket would have been a dealbreaker.

What I'd Change

ERC-721A. We built our own batch minting optimisation because ERC-721A wasn't battle-tested yet at the time. Today I'd use it directly — it's audited, widely adopted, and handles the same optimisations we built by hand.

Allowlist updates. Once the Merkle root was set, adding late entries meant recomputing the entire tree and updating the contract. A small Merkle tree update mechanism or a secondary allowlist for late additions would have saved us two contract interactions during the mint.

Better gas monitoring. We estimated gas at page load, but gas prices can swing 3x in the time it takes someone to read the mint page and click "Mint." A real-time gas price feed with auto-refresh would have prevented a few support tickets from users who saw a low estimate and then got a higher actual cost.

The mint sold out in under 3 hours. Zero failed transactions from our frontend. The client's community was happy, and the secondary market opened at 2x mint price. Not every project gets that outcome, but the engineering held up its end of the deal.