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:
- Create a free account
- Upload your
metadata/andimages/folders - Copy the returned IPFS CID (content identifier)
- 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:
- Total supply: 5,000, max 3 per wallet
- Mint price: 0.005 ETH
- Reveal mechanism: placeholder image before reveal, actual artwork after
- Royalty: 7% on secondary sales (EIP-2981 standard)
- Metadata stored on IPFS
- 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
tokenIdand 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.