ethereum/nft-tech-stack
NFT Tech Stack Complete Guide: Storage + Metadata + Minting
ethereumguide👥 Communityconfidence highhealth 100%
v1.0.0·Updated 3/20/2026
Storage Solutions Comparison
| Solution | Cost | Permanence | Decentralized | Recommended For |
|---|---|---|---|---|
| IPFS + Pinata | Low ($0/mo free tier) | Depends on pin service | Yes | Early-stage projects, fast launch |
| Arweave | One-time fee (~$0.01/MB) | Permanent | Yes | High-value NFTs, long-term projects |
| Filecoin | Pay per time period | Duration of contract | Yes | Large file storage |
| Centralized Server | Very low | Not guaranteed | No | Not recommended |
Metadata Standard (OpenSea Compatible)
{
"name": "My NFT #1",
"description": "This is the first NFT in the collection.",
"image": "ipfs://QmXxx.../1.png",
"external_url": "https://myproject.com/nft/1",
"attributes": [
{ "trait_type": "Background", "value": "Blue" },
{ "trait_type": "Eyes", "value": "Laser", "rarity": 0.02 },
{ "trait_type": "Level", "value": 5, "display_type": "number" },
{ "trait_type": "Birthday", "value": 1546360800, "display_type": "date" }
],
"animation_url": "ipfs://QmXxx.../1.mp4" // optional, video/audio
}
tokenURI formats:
ipfs://CID/1.json ← recommended (fully decentralized)
https://api.xxx.com/1 ← works, but depends on a server
ar://TXID/1.json ← Arweave (permanent)
Pinata (Simplest IPFS Option)
import PinataSDK from "@pinata/sdk"
const pinata = new PinataSDK(process.env.PINATA_API_KEY, process.env.PINATA_SECRET)
// Upload image
const imgResult = await pinata.pinFileToIPFS(fs.createReadStream("./nft.png"), {
pinataMetadata: { name: "NFT #1" }
})
const imageURI = `ipfs://${imgResult.IpfsHash}`
// Upload metadata JSON
const metadata = { name: "My NFT #1", image: imageURI, attributes: [...] }
const metaResult = await pinata.pinJSONToIPFS(metadata)
const tokenURI = `ipfs://${metaResult.IpfsHash}`
Arweave (Permanent Storage)
import Arweave from "arweave"
const arweave = Arweave.init({ host: "arweave.net", port: 443, protocol: "https" })
const key = JSON.parse(fs.readFileSync("arweave-key.json"))
// Upload file
const tx = await arweave.createTransaction({ data: fs.readFileSync("./nft.png") }, key)
tx.addTag("Content-Type", "image/png")
await arweave.transactions.sign(tx, key)
await arweave.transactions.post(tx)
const imageURI = `ar://${tx.id}`
// Or use Bundlr (cheaper for bulk uploads)
import { NodeBundlr } from "@bundlr-network/client"
const bundlr = new NodeBundlr("https://node1.bundlr.network", "arweave", key)
const response = await bundlr.uploadFile("./nft.png")
ERC-721 Minting Contract
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract MyNFT is ERC721URIStorage, Ownable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
uint256 public constant MAX_SUPPLY = 10000;
uint256 public constant PRICE = 0.05 ether;
bool public saleActive = false;
string private _baseTokenURI; // ipfs://CID/
constructor() ERC721("My NFT", "MNFT") Ownable(msg.sender) {}
// Public mint
function mint(uint256 quantity) external payable {
require(saleActive, "Sale not active");
require(quantity <= 10, "Max 10 per tx");
require(_tokenIds.current() + quantity <= MAX_SUPPLY, "Exceeds supply");
require(msg.value >= PRICE * quantity, "Insufficient payment");
for (uint i = 0; i < quantity; i++) {
_tokenIds.increment();
_safeMint(msg.sender, _tokenIds.current());
}
}
// Reveal (mint first, reveal metadata later)
function setBaseURI(string memory baseURI) external onlyOwner {
_baseTokenURI = baseURI;
}
function _baseURI() internal view override returns (string memory) {
return _baseTokenURI;
}
// EIP-2981 royalties (5%)
function royaltyInfo(uint256, uint256 salePrice) external view
returns (address receiver, uint256 royaltyAmount) {
return (owner(), salePrice * 500 / 10000);
}
function withdraw() external onlyOwner {
payable(owner()).transfer(address(this).balance);
}
}
ERC-1155 (Multi-Token NFT / SFT)
import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
contract GameItems is ERC1155 {
uint256 public constant SWORD = 1;
uint256 public constant SHIELD = 2;
uint256 public constant GOLD = 3; // FT (fungible token)
constructor() ERC1155("https://api.game.com/items/{id}.json") {
_mint(msg.sender, SWORD, 100, ""); // 100 swords
_mint(msg.sender, GOLD, 1e18, ""); // large supply of gold coins
}
}
Lazy Minting (Gas Savings)
How it works: NFTs are not pre-minted; they are minted at the moment of purchase, with a signature used as the voucher.
// Seller signs a voucher offline
bytes32 hash = keccak256(abi.encode(tokenId, price, uri));
bytes32 ethHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
(v, r, s) = vm.sign(sellerKey, ethHash);
// Buyer verifies on-chain and mints
function redeem(uint tokenId, uint price, string memory uri, bytes memory sig) external payable {
require(msg.value >= price);
address signer = ECDSA.recover(hash, sig);
require(signer == seller, "Invalid signature");
_safeMint(msg.sender, tokenId);
_setTokenURI(tokenId, uri);
}
Recommended Tools
- Manifold Studio — no-code NFT deployment
- ThirdWeb — NFT SDK + dashboard
- Zora Protocol — free mint + royalty infrastructure
NFT Tech Stack Complete Guide
Storage Solutions Comparison
| Solution | Cost | Permanence | Decentralized | Recommended For |
|---|---|---|---|---|
| IPFS + Pinata | Low ($0/mo free tier) | Depends on pin service | Yes | Early-stage projects, fast launch |
| Arweave | One-time fee (~$0.01/MB) | Permanent | Yes | High-value NFTs, long-term projects |
| Filecoin | Pay per time period | Duration of contract | Yes | Large file storage |
| Centralized Server | Very low | Not guaranteed | No | Not recommended |
Metadata Standard (OpenSea Compatible)
{
"name": "My NFT #1",
"description": "This is the first NFT in the collection.",
"image": "ipfs://QmXxx.../1.png",
"external_url": "https://myproject.com/nft/1",
"attributes": [
{ "trait_type": "Background", "value": "Blue" },
{ "trait_type": "Eyes", "value": "Laser", "rarity": 0.02 },
{ "trait_type": "Level", "value": 5, "display_type": "number" },
{ "trait_type": "Birthday", "value": 1546360800, "display_type": "date" }
],
"animation_url": "ipfs://QmXxx.../1.mp4" // optional, video/audio
}
tokenURI formats:
ipfs://CID/1.json ← recommended (fully decentralized)
https://api.xxx.com/1 ← works, but depends on a server
ar://TXID/1.json ← Arweave (permanent)
Pinata (Simplest IPFS Option)
import PinataSDK from "@pinata/sdk"
const pinata = new PinataSDK(process.env.PINATA_API_KEY, process.env.PINATA_SECRET)
// Upload image
const imgResult = await pinata.pinFileToIPFS(fs.createReadStream("./nft.png"), {
pinataMetadata: { name: "NFT #1" }
})
const imageURI = `ipfs://${imgResult.IpfsHash}`
// Upload metadata JSON
const metadata = { name: "My NFT #1", image: imageURI, attributes: [...] }
const metaResult = await pinata.pinJSONToIPFS(metadata)
const tokenURI = `ipfs://${metaResult.IpfsHash}`
Arweave (Permanent Storage)
import Arweave from "arweave"
const arweave = Arweave.init({ host: "arweave.net", port: 443, protocol: "https" })
const key = JSON.parse(fs.readFileSync("arweave-key.json"))
// Upload file
const tx = await arweave.createTransaction({ data: fs.readFileSync("./nft.png") }, key)
tx.addTag("Content-Type", "image/png")
await arweave.transactions.sign(tx, key)
await arweave.transactions.post(tx)
const imageURI = `ar://${tx.id}`
// Or use Bundlr (cheaper for bulk uploads)
import { NodeBundlr } from "@bundlr-network/client"
const bundlr = new NodeBundlr("https://node1.bundlr.network", "arweave", key)
const response = await bundlr.uploadFile("./nft.png")
ERC-721 Minting Contract
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract MyNFT is ERC721URIStorage, Ownable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
uint256 public constant MAX_SUPPLY = 10000;
uint256 public constant PRICE = 0.05 ether;
bool public saleActive = false;
string private _baseTokenURI; // ipfs://CID/
constructor() ERC721("My NFT", "MNFT") Ownable(msg.sender) {}
// Public mint
function mint(uint256 quantity) external payable {
require(saleActive, "Sale not active");
require(quantity <= 10, "Max 10 per tx");
require(_tokenIds.current() + quantity <= MAX_SUPPLY, "Exceeds supply");
require(msg.value >= PRICE * quantity, "Insufficient payment");
for (uint i = 0; i < quantity; i++) {
_tokenIds.increment();
_safeMint(msg.sender, _tokenIds.current());
}
}
// Reveal (mint first, reveal metadata later)
function setBaseURI(string memory baseURI) external onlyOwner {
_baseTokenURI = baseURI;
}
function _baseURI() internal view override returns (string memory) {
return _baseTokenURI;
}
// EIP-2981 royalties (5%)
function royaltyInfo(uint256, uint256 salePrice) external view
returns (address receiver, uint256 royaltyAmount) {
return (owner(), salePrice * 500 / 10000);
}
function withdraw() external onlyOwner {
payable(owner()).transfer(address(this).balance);
}
}
ERC-1155 (Multi-Token NFT / SFT)
import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
contract GameItems is ERC1155 {
uint256 public constant SWORD = 1;
uint256 public constant SHIELD = 2;
uint256 public constant GOLD = 3; // FT (fungible token)
constructor() ERC1155("https://api.game.com/items/{id}.json") {
_mint(msg.sender, SWORD, 100, ""); // 100 swords
_mint(msg.sender, GOLD, 1e18, ""); // large supply of gold coins
}
}
Lazy Minting (Gas Savings)
How it works: NFTs are not pre-minted; they are minted at the moment of purchase, with a signature used as the voucher.
// Seller signs a voucher offline
bytes32 hash = keccak256(abi.encode(tokenId, price, uri));
bytes32 ethHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
(v, r, s) = vm.sign(sellerKey, ethHash);
// Buyer verifies on-chain and mints
function redeem(uint tokenId, uint price, string memory uri, bytes memory sig) external payable {
require(msg.value >= price);
address signer = ECDSA.recover(hash, sig);
require(signer == seller, "Invalid signature");
_safeMint(msg.sender, tokenId);
_setTokenURI(tokenId, uri);
}
Recommended Tools
- Manifold Studio — no-code NFT deployment
- ThirdWeb — NFT SDK + dashboard
- Zora Protocol — free mint + royalty infrastructure
- OpenSea SDK — listing/trading integration