What a buyer actually pays on a free mint: gas only, with the real number on Base and how it compares to buying the same token on the secondary market
My mint function takes no payment. The buyer pays only the Base transaction gas required to execute the mint.
There is no honest fixed ETH number for that gas cost. Base fees change with network conditions, and the final amount also depends on the contract execution path. The real number is the wallet’s gas estimate multiplied by the current gas price when the transaction is submitted.
I check it immediately before minting:
const request = { account: buyer, address: contractAddress, abi, functionName: "mint", }; const gas = await publicClient.estimateContractGas(request); const gasPrice = await publicClient.getGasPrice(); const buyerCost = gas * gasPrice; console.log(formatEther(buyerCost), "ETH"); That output is the number I show as the expected buyer cost. The transaction receipt is the source of truth after confirmation.
For a secondary-market purchase, the buyer pays the token’s sale price plus gas. The transaction may also execute marketplace transfers and fee logic. A free mint has no sale consideration, so its buyer-side comparison is straightforward:
| Path | Token payment | Buyer also pays |
|---|---|---|
| Free mint on Base | Free | Mint transaction gas |
| Secondary-market purchase | Seller’s asking price | Purchase transaction gas |
The decision rule is: never describe “free mint” as “free transaction.” It means the contract charges no mint price.
What the creator stores: preloading token URIs versus lazy minting, and the storage cost each one puts on chain for a small drop
I preload each token URI on chain before it can be minted. For my contract, that write costs about 0.0000008 ETH per piece on Base.
The drop contains 131 pieces generated and pinned. 60 are minted and 71 remain available to mint. Preloading makes the creator pay for URI registration before demand is known, including inventory that may remain unminted.
My accounting query is:
const preloadCost = parseEther("0.0000008"); const remaining = 71n; console.log(formatEther(preloadCost * remaining), "ETH"); Lazy minting moves that storage write to the mint path. The creator avoids preloading every available URI, but mint execution becomes more complex and the buyer may fund the extra gas needed to commit token data. It also requires a signed authorization or another rule proving that the requested metadata belongs to the collection.
My decision rule is based on inventory risk:
- Preload when the drop is small, metadata is final, and predictable mint behavior matters.
- Lazy mint when uncertain demand makes writing every token URI in advance wasteful.
- In either design, pin the metadata before exposing the mint. On-chain storage of a URI does not guarantee the referenced content exists.
The revert that catches people testing their own contract: the wallet limit fires for the deployer's address too, so simulate from a fresh address
The contract limit is 5 per wallet. The deployer is not exempt.
This matters during testing because deployment scripts, setup mints, and manual calls often reuse the deployer account. Once that address reaches the limit, another simulated mint reverts even when the mint logic is otherwise correct.
I test the public path from a fresh address:
await publicClient.simulateContract({ account: freshBuyer, address: contractAddress, abi, functionName: "mint", }); The checklist is short: use a fresh address, confirm its minted balance, simulate with that address, then submit from the same address. Do not diagnose a wallet-limit revert using only the deployer.
The collection is available at https://thunderous-sunflower-a910b4.netlify.app.
Practical takeaway: estimate the Base transaction from the buyer’s actual wallet, account separately for creator-side URI writes, and test wallet limits from a fresh address before investigating the contract.
