July 29, 2026
Minting NFTs without charging users gas
Layer-2 minting, IPFS metadata, and an event-driven mint pipeline
The problem gas fees create for a consumer product
Minting an NFT on Ethereum mainnet means paying gas — a transaction fee denominated in ETH, priced by network demand, that can swing from negligible to significant depending on when you happen to mint. For a consumer-facing feature where a creator wants to mint a limited item for a subscriber, asking that subscriber to hold ETH, understand gas, and pay a fee that might exceed the value of the item itself is a dead end for adoption. The fix isn’t avoiding NFTs, it’s avoiding mainnet gas economics for this specific use case: minting on Immutable X, a layer-2 built for exactly this, where minting is gasless for the end user.
What layer-2 minting actually changes
Immutable X batches and settles transactions off of Ethereum mainnet while still anchoring ownership and finality back to it, which is what lets it offer gas-free minting without giving up the security properties that make an NFT meaningful in the first place — verifiable ownership, a real record of who minted and holds what. The platform’s own contracts and mint calls target Immutable X’s API and settlement layer rather than submitting a raw mainnet transaction for every mint:
async function mintNft(request: MintRequest) {
const mint = await immutableXClient.mints.mintTokens({
starkPublicKey: request.recipientStarkKey,
mints: [{
contractAddress: NFT_CONTRACT_ADDRESS,
royalties: [{ recipient: creatorAddress(request.creatorId), percentage: request.royaltyPercent }],
id: request.tokenId,
blueprint: request.metadataUri, // points at IPFS
}],
});
return mint;
}
The user pays nothing to receive the mint; the cost of interacting with the underlying settlement layer is absorbed by the platform, which is economically viable precisely because layer-2 minting on Immutable X is priced for this pattern rather than priced like a mainnet transaction.
Metadata lives on IPFS, not in the contract
An NFT’s on-chain record is deliberately small — an owner, a token ID, and a pointer. The actual metadata that makes a token meaningful — the image or media, a name, attributes — is stored on IPFS and referenced by the contract as a content-addressed URI:
{
"name": "Creator Drop #142",
"description": "Limited item minted for subscribers of...",
"image": "ipfs://bafybeigd.../artwork.png",
"attributes": [
{ "trait_type": "edition", "value": "142/500" },
{ "trait_type": "creator", "value": "..." }
]
}
Content-addressing is the property that matters here: the IPFS hash is a
function of the content itself, so ipfs://bafybeigd.../artwork.png
cannot silently point at different content later the way a mutable URL
could. Once metadata is pinned and its hash is written into the mint, what
the token refers to is fixed, independent of whether the platform’s own
servers are still running years from now — the content is addressable by
anyone who can reach the IPFS network, not solely by fetching it from
platform infrastructure.
An event-driven mint pipeline, not a synchronous call
Minting isn’t triggered synchronously inside the request that creates the eligibility for it — a creator publishing a drop, or a subscriber’s payment confirming. Instead, the eligibility event goes into the same event-driven architecture the rest of the platform uses, and a dedicated mint pipeline consumes it:
payment.confirmed (16 confirmations reached)
↓
mint-eligibility handler
↓
outbox: nft.mint_requested
↓
mint pipeline: upload metadata to IPFS → call Immutable X → record token id
↓
outbox: nft.minted
↓
notify user, update entitlement records
Decoupling the mint from the triggering event through the same
transactional outbox pattern used elsewhere in the system means a slow or
temporarily unavailable call to Immutable X doesn’t block payment
confirmation, and a failed mint attempt can be retried from the
nft.mint_requested event without needing to re-derive eligibility from
scratch. The mint pipeline is also where idempotency matters most acutely:
retrying a mint request for a token ID that already minted must be a no-op,
not a duplicate token.
async function handleMintRequested(event: MintRequestedEvent) {
const existing = await getMintRecord(event.tokenId);
if (existing?.status === "minted") return; // already done, retry is a no-op
const metadataUri = await uploadToIpfs(buildMetadata(event));
const mint = await mintNft({ ...event, metadataUri });
await recordMint(event.tokenId, mint.transactionId);
}
Royalties and ownership are enforced at the contract level
Because the platform’s own ERC-721 contracts are written in-house on OpenZeppelin with Hardhat, royalty percentages and transfer rules are encoded directly into the contract rather than left to a marketplace’s convention, which means a secondary sale anywhere that respects the standard also respects the creator’s royalty — a property that depends on the contract, not on which marketplace the resale happens through.
What this means for you
Gasless minting for the end user is a layer-2 and UX decision, not a change to what an NFT fundamentally is: ownership and provenance still need to be real and verifiable, which is why metadata belongs on content-addressed storage like IPFS rather than a mutable URL, and why the mint itself should flow through the same durable, retryable event pipeline as every other consequential action in the system rather than happening as a fragile synchronous call in the request path.
30 minutes with a senior engineer.
Tell us what you're building. You'll leave with an honest opinion, even if it's "you don't need us."
Reference calls with past clients are available under NDA during evaluation.