NFT Minting Page Development
Typical situation: 10,000 token collection, launch in 48 hours. Contract ready, marketing done. Minting page written overnight — "Mint" button, MetaMask connection, done. At launch — 500 people simultaneously, MetaMask fails, transactions hang, whitelist not verifying, progress bar shows 0 even after 200 minted NFTs. Half the audience leaves. This isn't hype problem — it's frontend architecture problem under load.
Key Minting Page Components
Wallet Connection and Chain Management
wagmi v2 + viem — current standard for Web3 React apps. Connection via WalletConnect v2 (supports 300+ wallets), MetaMask, Coinbase Wallet. useConnect, useAccount, useNetwork hooks cover basic scenarios.
Critical moment: check and switch network. User connected to Ethereum, minting on Base. Need useSwitchChain with automatic switch request. If user declined — show blocking warning, don't let them try minting.
const { switchChain } = useSwitchChain()
const { chain } = useAccount()
if (chain?.id !== TARGET_CHAIN_ID) {
return <SwitchNetworkPrompt onSwitch={() => switchChain({ chainId: TARGET_CHAIN_ID })} />
}
Whitelist Verification
Two approaches: on-chain mapping and Merkle proof.
On-chain mapping (mapping(address => bool) public whitelist) — expensive. Adding 5000 addresses to whitelist = 5000 transactions or one batch via multicall. Gas on mainnet can exceed 1 ETH just for setup.
Merkle proof — standard for large whitelists. Root stored on-chain (one bytes32), proof for each address — off-chain, passed at minting. Frontend gets proof via API or stores in public JSON.
Frontend verification before sending transaction:
import { MerkleTree } from 'merkletreejs'
import { keccak256 } from 'viem'
const proof = merkleTree.getHexProof(keccak256(address))
const isValid = merkleTree.verify(proof, keccak256(address), merkleRoot)
Important: frontend verification — only for UX (don't show button if address not in whitelist). Final check — in smart contract. Never trust frontend verification.
Progress Bar and Realtime Data
Stale data problem. totalSupply() changes with each minted NFT. If reading via useReadContract with default polling — data stale by 1–3 blocks. On hot drop means progress bar lies.
Solution: WebSocket subscription to Transfer events of contract. On each Transfer(address(0), to, tokenId) (mint) — increment counter locally. useWatchContractEvent from wagmi:
useWatchContractEvent({
address: CONTRACT_ADDRESS,
abi: NFT_ABI,
eventName: 'Transfer',
onLogs: (logs) => {
const mints = logs.filter(log => log.args.from === zeroAddress)
setMintedCount(prev => prev + mints.length)
}
})
This gives realtime updates without polling.
Transaction States
User clicked "Mint" — what next? Minimum 4 states to show explicitly:
- Awaiting signature — MetaMask opened, waiting confirmation
- Transaction pending — transaction in mempool, show hash with Etherscan link
- Confirmed — transaction in block, show NFT or OpenSea link
- Failed — revert with understandable error message
useWriteContract + useWaitForTransactionReceipt from wagmi cover all states via isPending, isLoading, isSuccess, isError.
Typical mistake: show spinner without hash. User doesn't know if transaction passed, closes tab, thinks nothing happened — and mints again. Always show transaction hash as soon as it exists.
Handling Contract Errors
Revert messages from Solidity custom errors need decoding. viem does this automatically if ABI has error definitions. But you can't show user technical message like ERC721: transfer to non ERC721Receiver implementer.
Mapping contract errors to user-friendly text — separate task. Typical cases:
-
MaxSupplyReached→ "All NFTs already minted" -
NotWhitelisted→ "Your address not in whitelist" -
MintingPaused→ "Minting temporarily paused" -
InsufficientFunds→ "Insufficient funds for minting"
Performance Under Load
At drop hundreds of users simultaneously hit RPC provider. Public RPC (Infura free tier) have rate limits, easily exceeded. Solution: Alchemy or QuickNode with paid plan + cache static data (totalSupply, mintPrice, whitelistRoot) in own backend with TTL 2–5 seconds.
Merkle proofs for whitelist — serve via CDN (Cloudflare), not backend. This removes load and gives sub-50ms response.
Development Process
Development (3–4 days). Next.js + wagmi + viem. Components: wallet connector, mint button with all states, progress bar with websocket updates, whitelist checker.
Contract Integration (1 day). ABI connection, testnet testing (Sepolia), edge case checks: wrong network, not whitelisted, sold out, paused.
Optimization (1 day). RPC caching, CDN for proofs, gas estimate before transaction.
Timeline Estimates
Standard minting page with whitelist and progress bar — 3–5 days. Complex with mint phases (presale, public), multiple wallet types and custom design — up to 2 weeks.
Cost calculated after clarifying functional requirements and design.







