Skip to content
Guide · NFTs

Deploy an NFT collection with IPFS metadata

Pin artwork and JSON metadata to permanent storage, deploy an ERC721 with URIStorage, and enforce creator royalties via ERC2981.

NFTs
13 min read

How NFT metadata actually works

An ERC721 token is just an id and an owner. Wallets and marketplaces call tokenURI(tokenId) to fetch a JSON file describing the asset — name, description, image, attributes. That JSON points to media (PNG, GIF, video). If metadata lives on a centralized server, the art can disappear when you stop paying hosting. IPFS content-addressed storage fixes this: the URI embeds a cryptographic hash of the content.

  • tokenURI returns a string — typically ipfs://CID/tokenId.json
  • JSON schema follows OpenSea metadata standards for broad marketplace compatibility
  • image field points to ipfs://CID/filename.png or an IPFS gateway URL
  • attributes array powers rarity tools and on-chain filtering

Prepare and upload your assets

Before touching Solidity, finish your artwork and metadata. Each tokenId needs a matching JSON file (1.json, 2.json, …) and a media file referenced inside it. Ethereum Toolset's NFT Creator integrates with Irys (formerly Bundlr) for uploads — you pin images and metadata in the wizard, and it returns a base URI you paste into the deploy form.

  • Export images at final resolution — upscaling later does not change the on-chain URI
  • Name each JSON file to match tokenId: 0.json, 1.json, or 1.json if you start at id 1
  • Validate JSON with a linter — a trailing comma breaks parsing in wallets
  • Upload the full folder structure so the base URI + tokenId + ".json" resolves correctly
  • Keep a local backup of all files independent of any pinning service

Structure the metadata JSON

Each metadata file should be self-contained. Marketplaces read name, description, and image at minimum; attributes drive rarity rankings. Use IPFS URIs in the image field, not HTTP links, so the metadata remains decentralized.

  • Use consistent trait_type names across the collection for filter compatibility
  • Avoid embedding private or personal data — metadata is public forever
  • For animated pieces, set animation_url instead of or in addition to image
1.jsonsolidity
{
  "name": "Acme Genesis #1",
  "description": "First edition from the Acme Genesis collection.",
  "image": "ipfs://QmExampleImageHash/1.png",
  "attributes": [
    { "trait_type": "Background", "value": "Midnight" },
    { "trait_type": "Rarity", "value": "Legendary" }
  ]
}

Deploy with URIStorage

The NFT Creator generates an ERC721 that inherits OpenZeppelin's ERC721URIStorage, letting you set per-token URIs or a shared base URI. At deploy you set the collection name, symbol, max supply, and the base URI returned from your upload step. Minting assigns sequential token ids starting at zero unless you configure otherwise.

AcmeGenesis.solsolidity
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {ERC721URIStorage} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

contract AcmeGenesis is ERC721, ERC721URIStorage, Ownable {
    uint256 private _nextTokenId;

    constructor(address initialOwner)
        ERC721("Acme Genesis", "ACGEN")
        Ownable(initialOwner)
    {}

    function safeMint(address to, string memory uri) public onlyOwner {
        uint256 tokenId = _nextTokenId++;
        _safeMint(to, tokenId);
        _setTokenURI(tokenId, uri);
    }

    function tokenURI(uint256 tokenId)
        public
        view
        override(ERC721, ERC721URIStorage)
        returns (string memory)
    {
        return super.tokenURI(tokenId);
    }
}

Reveal mechanics and base URI

Many collections launch with a placeholder image before revealing final art. Set a single hidden.json as the URI for all tokens pre-reveal, then call setBaseURI with the real CID once reveal day arrives. If your contract uses per-token URIs set at mint, plan the reveal as an owner-only batch update.

  • Pre-reveal — point every tokenURI to ipfs://CID/hidden.json
  • Post-reveal — update base URI or batch-setTokenURI to the final metadata folder
  • Announce the reveal tx hash so collectors can verify the switch on-chain
  • Never reveal before all metadata is pinned — broken links destroy trust instantly

Enforce royalties with ERC2981

Secondary sales are where creators earn long-term. EIP-2981 defines a standard royaltyInfo(tokenId, salePrice) hook that marketplaces query. OpenZeppelin's ERC2981 mixin lets you set a default royalty receiver and basis points (100 = 1%). Enforcement depends on marketplace support — OpenSea, Blur, and others respect it to varying degrees, but the standard is the best available on-chain signal.

  • 500 basis points = 5% royalty on secondary sales
  • Set royalty receiver to a treasury or splitter contract, not a personal EOA
  • ERC2981 is a signal, not a legal contract — off-chain licensing still matters
AcmeGenesisRoyalties.solsolidity
import {ERC2981} from "@openzeppelin/contracts/token/common/ERC2981.sol";

contract AcmeGenesis is ERC721, ERC721URIStorage, ERC2981, Ownable {
    constructor(address initialOwner, address royaltyReceiver)
        ERC721("Acme Genesis", "ACGEN")
        Ownable(initialOwner)
    {
        _setDefaultRoyalty(royaltyReceiver, 500); // 5%
    }

    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(ERC721, ERC2981)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
}

Launch checklist

After deploy and verification, mint a test token to yourself, confirm tokenURI resolves in a wallet, and list on a marketplace that reads ERC2981. Use Ethereum Toolset's contract templates if you need a custom auction or marketplace on top of your collection.

  • Mint token #0 to your test wallet and open it in MetaMask or Rainbow
  • Confirm the IPFS gateway resolves image and JSON within 30 seconds
  • Verify the contract on Etherscan so collectors can read mint and royalty functions
  • Transfer Ownable to a multisig before public mint
  • Document the provenance hash of your metadata folder for permanence claims