Launching an NFT Collection (ERC-721)

Why NFTs Matter

Non-Fungible Tokens (NFTs) revolutionized digital ownership. Unlike ERC-20 tokens where every unit is identical, each ERC-721 token is completely unique — like a baseball card, a concert ticket, or a deed to a house.

The difference is simple:

  • ERC-20 is like cash: every $1 bill is worth exactly the same as every other $1 bill
  • ERC-721 is like collectible cards: each card has a unique ID, rarity, artwork, and value

Why this matters for your career:

  • NFTs have created a multi-billion dollar market for digital art, gaming items, and virtual real estate
  • ERC-721 is the foundation for tokenizing real-world assets: real estate deeds, concert tickets, diplomas, supply chain records
  • Major brands (Nike, Adidas, Starbucks, Gucci) have launched NFT collections — the demand for NFT developers is massive
  • Understanding ERC-721 teaches you the concept of "unique asset tokenization," which applies far beyond JPEGs

What Is the ERC-721 Standard?

ERC-721 defines a standard interface for non-fungible tokens. Every NFT — from Bored Apes to CryptoPunks — implements these same functions:

interface IERC721 {
    function balanceOf(address owner) external view returns (uint256);
    function ownerOf(uint256 tokenId) external view returns (address);
    function safeTransferFrom(address from, address to, uint256 tokenId) external;
    function transferFrom(address from, address to, uint256 tokenId) external;
    function approve(address to, uint256 tokenId) external;
    function getApproved(uint256 tokenId) external view returns (address);
    function setApprovalForAll(address operator, bool approved) external;
    function isApprovedForAll(address owner, address operator) external view returns (bool);
}

ERC-20 vs ERC-721: A Side-by-Side Comparison

| Feature | ERC-20 (Fungible) | ERC-721 (Non-Fungible) | |:--------|:-----------------|:----------------------| | Identity | All tokens are identical | Each token has a unique tokenId | | Transfer | transfer(to, amount) — send any number | safeTransferFrom(from, to, tokenId) — send one specific token | | Balance | How many tokens you own | How many distinct NFTs you own | | Metadata | No metadata per token | Each token has its own URI (image, attributes) | | Use case | Currency, governance, staking | Art, gaming items, real estate, identity | | Marketplace | Uniswap, Binance | OpenSea, Blur, Rarible | | Royalty | Not standard | EIP-2981 standard for secondary sale royalties |

Complete NFT Contract

// contracts/VibeNFT.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Royalty.sol";

contract VibeNFT is ERC721Enumerable, ERC721URIStorage, Ownable, ERC721Royalty {
    // === Core Configuration ===
    uint256 public constant MAX_SUPPLY = 10000;      // Maximum number of NFTs
    uint256 public constant MINT_PRICE = 0.01 ether; // Mint cost in ETH
    uint256 public constant MAX_PER_WALLET = 5;      // Max per wallet during public mint
    
    uint256 public totalMinted;
    bool public revealed;
    string public revealedURI;        // Metadata URI after reveal
    string public unrevealedURI;      // Placeholder URI before reveal
    
    // === Events ===
    event NFTMinted(address indexed minter, uint256 indexed tokenId);
    event NFTsRevealed(string revealedURI);
    
    constructor(
        string memory _unrevealedURI,
        string memory _revealedURI
    ) ERC721("Vibe NFT Collection", "VIBNFT") Ownable(msg.sender) {
        unrevealedURI = _unrevealedURI;
        revealedURI = _revealedURI;
        
        // Set 5% royalty on secondary sales (EIP-2981 standard)
        _setDefaultRoyalty(msg.sender, 500); // 500 basis points = 5%
    }
    
    // === Minting ===
    function mintNFT(uint256 _quantity) public payable {
        require(_quantity > 0, "Quantity must be greater than 0");
        require(totalMinted + _quantity <= MAX_SUPPLY, "Max supply reached");
        require(balanceOf(msg.sender) + _quantity <= MAX_PER_WALLET, "Max per wallet exceeded");
        require(msg.value >= MINT_PRICE * _quantity, "Insufficient ETH");
        
        for (uint256 i = 0; i < _quantity; i++) {
            totalMinted++;
            _safeMint(msg.sender, totalMinted);
            emit NFTMinted(msg.sender, totalMinted);
        }
    }
    
    // === Reveal (show the actual artwork) ===
    function reveal(string memory _revealedURI) public onlyOwner {
        revealed = true;
        revealedURI = _revealedURI;
        emit NFTsRevealed(_revealedURI);
    }
    
    // === Metadata ===
    function tokenURI(
        uint256 tokenId
    ) public view override(ERC721, ERC721URIStorage) returns (string memory) {
        require(ownerOf(tokenId) != address(0), "Token does not exist");
        
        if (!revealed) {
            return unrevealedURI;
        }
        
        // After reveal, return the full metadata (in production, each token has unique URI)
        return revealedURI;
    }
    
    // === Withdraw contract balance ===
    function withdraw() public onlyOwner {
        uint256 balance = address(this).balance;
        require(balance > 0, "No ETH to withdraw");
        
        // 70% to owner, 30% to treasury
        uint256 ownerShare = (balance * 70) / 100;
        uint256 treasuryShare = balance - ownerShare;
        
        payable(owner()).transfer(ownerShare);
        payable(treasuryAddress).transfer(treasuryShare);
    }
    
    // === Required Overrides (Solidity Multiple Inheritance) ===
    function _increaseBalance(
        address account,
        uint128 value
    ) internal override(ERC721, ERC721Enumerable) {
        super._increaseBalance(account, value);
    }
    
    function _update(
        address to,
        uint256 tokenId,
        address auth
    ) internal override(ERC721, ERC721Enumerable) returns (address) {
        return super._update(to, tokenId, auth);
    }
    
    function supportsInterface(
        bytes4 interfaceId
    ) public view override(ERC721, ERC721Enumerable, ERC721URIStorage, ERC721Royalty) returns (bool) {
        return super.supportsInterface(interfaceId);
    }
}

Preparing NFT Metadata

NFT metadata follows a standardized JSON format stored on decentralized storage (IPFS):

Unrevealed Placeholder Metadata

// metadata/unrevealed.json
{
    "name": "Vibe NFT Collection",
    "description": "This NFT has not been revealed yet... Stay tuned!",
    "image": "ipfs://QmPlaceholder/placeholder.png",
    "attributes": []
}

Revealed Metadata (Each NFT Gets Its Own)

// metadata/1.json
{
    "name": "Vibe NFT #1",
    "description": "The very first Vibe Tutor NFT! Celebrating our entry into the Web3 world!",
    "image": "ipfs://QmYourHash/images/1.png",
    "attributes": [
        {
            "trait_type": "Rarity",
            "value": "Legendary"
        },
        {
            "trait_type": "Type",
            "value": "Genesis"
        },
        {
            "trait_type": "Background",
            "value": "Cosmic"
        }
    ]
}

Uploading to IPFS

IPFS (InterPlanetary File System) is the standard storage layer for NFTs. Here is how to upload your metadata and images:

# Install IPFS CLI (if not already installed)
brew install ipfs

# Initialize and start your IPFS node
ipfs init
ipfs daemon &

# Add your metadata and images directories
ipfs add -r ./metadata
ipfs add -r ./images

# You will see output like:
# added QmXyz123 metadata
# added QmAbc456 images

For a simpler alternative, use Pinata (https://pinata.cloud) — a pinning service that provides a drag-and-drop UI:

  1. Create a free account
  2. Upload your metadata/ and images/ folders
  3. Copy the returned IPFS CID (content identifier)
  4. Use it in your contract's revealedURI

Deploying and Interacting with the Frontend

// scripts/mint_interact.js
const hre = require("hardhat");

async function main() {
  // Load the deployed contract
  const contractAddress = "0x...YOUR_CONTRACT_ADDRESS...";
  const VibeNFT = await hre.ethers.getContractFactory("VibeNFT");
  const nft = VibeNFT.attach(contractAddress);
  
  const [owner, user1] = await hre.ethers.getSigners();
  
  // Mint NFTs
  console.log("\n=== Minting NFTs ===");
  const mintTx = await nft.connect(user1).mintNFT(2, {
    value: hre.ethers.parseEther("0.02")  // 2 NFTs at 0.01 ETH each
  });
  await mintTx.wait();
  
  console.log(`User ${user1.address} minted 2 NFTs`);
  
  // Check the user's NFT balance
  const balance = await nft.balanceOf(user1.address);
  console.log(`User owns ${balance.toString()} NFTs`);
  
  // Query Token URIs
  for (let i = 1; i <= 2; i++) {
    const uri = await nft.tokenURI(i);
    console.log(`Token ${i} URI:`, uri);
  }
  
  // Reveal the NFTs
  console.log("\n=== Revealing NFTs ===");
  const revealTx = await nft.connect(owner).reveal(
    "ipfs://QmRevealedURI/revealed.json"
  );
  await revealTx.wait();
  console.log("NFTs have been revealed!");
}

main().catch(console.error);

How to Use Vibe Coding to Launch an NFT Collection

Instead of writing all the Solidity yourself, describe your requirements to AI. Here is a battle-tested prompt:

🔥 Vibe Coding Prompt for NFT Creation "Create an ERC-721 NFT contract with the following specifications:

  1. Total supply: 5,000, max 3 per wallet
  2. Mint price: 0.005 ETH
  3. Reveal mechanism: placeholder image before reveal, actual artwork after
  4. Royalty: 7% on secondary sales (EIP-2981 standard)
  5. Metadata stored on IPFS
  6. Owner can withdraw accumulated ETH"

Summary

In this chapter, you have built a complete, production-ready NFT collection:

  • What you built: An ERC-721 contract with minting, supply limits, reveal mechanics, royalties, and ETH withdrawal
  • Why it matters: NFTs represent the first widespread application of unique digital asset ownership — from art and gaming to real estate and identity
  • How it works: OpenZeppelin's ERC-721 contracts provide the standard interface; you extend them with supply caps, pricing, reveal logic, and royalty support

Key takeaways:

  • ERC-721 is the standard for unique, non-fungible tokens — each token has a distinct tokenId and metadata URI
  • Supply caps and per-wallet limits prevent bots from sweeping the entire collection
  • Reveal mechanics create excitement: minters commit before knowing what they get
  • EIP-2981 royalties ensure creators earn from secondary market sales automatically
  • IPFS provides decentralized metadata storage — no central server can take down your NFT images
  • Always verify your contract on Etherscan so buyers can see the source code

What Is Next: Decentralized Exchange (DEX) and Automated Market Maker (AMM)

Now that you can create both fungible tokens (ERC-20) and non-fungible tokens (ERC-721), the next chapter teaches you how to build a Decentralized Exchange (DEX) using the Automated Market Maker (AMM) model — the same technology that powers Uniswap. You will learn how liquidity pools work, how to implement the constant product formula ($x \times y = k$), and how users can swap tokens without a central order book.

Unlock Full Tutorial

This chapter is paid content. Join the project to unlock over 5000 words of deep analysis, including 10+ god-tier Prompts and real Source Code examples!