NFT Marketplace
Build a complete NFT marketplace with minting, trading, and auction functionality.
Overview
This example demonstrates how to create a full-featured NFT marketplace using ao-forge, including:
- NFT Minting - Create and mint new NFTs
- Trading System - Buy and sell NFTs
- Auction System - Bid-based auctions
- Collection Management - Organize NFTs into collections
- User Profiles - User accounts and portfolios
- Search and Discovery - Find NFTs by various criteria
Project Setup
Create the Project
# Create new NFT marketplace project
ao-forge init my-nft-marketplace --template nft
# Or create manually
ao-forge init my-nft-marketplace --framework nextjs
cd my-nft-marketplace
Install Dependencies
# Install additional dependencies
npm install @solana/web3.js @solana/wallet-adapter-react
npm install @headlessui/react @heroicons/react
npm install ipfs-http-client
npm install date-fns
AO Process Implementation
NFT Contract (nft.lua)
-- NFT Contract with minting and trading functionality
local nft = {}
-- State management
local function getNFT(id)
return json.decode(AO.load("nft:" .. id) or "{}")
end
local function setNFT(id, nft)
AO.store("nft:" .. id, json.encode(nft))
end
local function getCollection(id)
return json.decode(AO.load("collection:" .. id) or "{}")
end
local function setCollection(id, collection)
AO.store("collection:" .. id, json.encode(collection))
end
-- Mint NFT
Handlers.add("mint_nft", function(msg)
local nftId = msg.NFTId or tostring(AO.load("nft_counter") or "0")
local nft = {
id = nftId,
name = msg.Name,
description = msg.Description,
image = msg.Image,
creator = msg.From,
owner = msg.From,
created = msg.Timestamp,
collection = msg.Collection,
attributes = msg.Attributes or {},
price = msg.Price or 0,
forSale = false,
auction = nil
}
setNFT(nftId, nft)
-- Update NFT counter
AO.store("nft_counter", tostring(tonumber(nftId) + 1))
return { success = true, nft = nft }
end)
-- List NFT for sale
Handlers.add("list_nft", function(msg)
local nftId = msg.NFTId
local price = tonumber(msg.Price)
local nft = getNFT(nftId)
if not nft then
return { error = "NFT not found" }
end
if nft.owner ~= msg.From then
return { error = "Not the owner" }
end
nft.price = price
nft.forSale = true
nft.seller = msg.From
setNFT(nftId, nft)
return { success = true, nft = nft }
end)
-- Buy NFT
Handlers.add("buy_nft", function(msg)
local nftId = msg.NFTId
local buyer = msg.From
local nft = getNFT(nftId)
if not nft then
return { error = "NFT not found" }
end
if not nft.forSale then
return { error = "NFT not for sale" }
end
-- Transfer ownership
nft.owner = buyer
nft.forSale = false
nft.seller = nil
nft.price = 0
setNFT(nftId, nft)
return { success = true, nft = nft }
end)
-- Create auction
Handlers.add("create_auction", function(msg)
local nftId = msg.NFTId
local startPrice = tonumber(msg.StartPrice)
local endTime = tonumber(msg.EndTime)
local nft = getNFT(nftId)
if not nft then
return { error = "NFT not found" }
end
if nft.owner ~= msg.From then
return { error = "Not the owner" }
end
local auction = {
nftId = nftId,
startPrice = startPrice,
currentBid = startPrice,
highestBidder = nil,
endTime = endTime,
created = msg.Timestamp,
bids = {}
}
nft.auction = auction
nft.forSale = false
setNFT(nftId, nft)
return { success = true, auction = auction }
end)
-- Place bid
Handlers.add("place_bid", function(msg)
local nftId = msg.NFTId
local bidAmount = tonumber(msg.BidAmount)
local bidder = msg.From
local nft = getNFT(nftId)
if not nft or not nft.auction then
return { error = "Auction not found" }
end
if msg.Timestamp > nft.auction.endTime then
return { error = "Auction ended" }
end
if bidAmount <= nft.auction.currentBid then
return { error = "Bid too low" }
end
-- Record bid
nft.auction.bids[bidder] = {
amount = bidAmount,
timestamp = msg.Timestamp
}
nft.auction.currentBid = bidAmount
nft.auction.highestBidder = bidder
setNFT(nftId, nft)
return { success = true, bid = nft.auction.bids[bidder] }
end)
-- End auction
Handlers.add("end_auction", function(msg)
local nftId = msg.NFTId
local nft = getNFT(nftId)
if not nft or not nft.auction then
return { error = "Auction not found" }
end
if msg.Timestamp < nft.auction.endTime then
return { error = "Auction not ended" }
end
-- Transfer to highest bidder
if nft.auction.highestBidder then
nft.owner = nft.auction.highestBidder
end
nft.auction = nil
setNFT(nftId, nft)
return { success = true, nft = nft }
end)
-- Get NFT
Handlers.add("get_nft", function(msg)
local nftId = msg.NFTId
local nft = getNFT(nftId)
if not nft then
return { error = "NFT not found" }
end
return { success = true, nft = nft }
end)
-- List NFTs
Handlers.add("list_nfts", function(msg)
local limit = tonumber(msg.Limit) or 10
local offset = tonumber(msg.Offset) or 0
local collection = msg.Collection
local forSale = msg.ForSale
-- This is a simplified implementation
-- In practice, you'd need to maintain indexes
return { success = true, nfts = [] }
end)
Frontend Implementation
NFT Marketplace Component
// components/NFTMarketplace.tsx
import React, { useState, useEffect } from 'react'
import { useWallet } from '@solana/wallet-adapter-react'
interface NFT {
id: string
name: string
description: string
image: string
creator: string
owner: string
price: number
forSale: boolean
auction?: any
}
export default function NFTMarketplace() {
const { publicKey, connected } = useWallet()
const [nfts, setNFTs] = useState<NFT[]>([])
const [loading, setLoading] = useState(true)
const [selectedNFT, setSelectedNFT] = useState<NFT | null>(null)
useEffect(() => {
loadNFTs()
}, [])
const loadNFTs = async () => {
try {
const response = await fetch('/api/nft/list')
const data = await response.json()
setNFTs(data.nfts || [])
} catch (error) {
console.error('Error loading NFTs:', error)
} finally {
setLoading(false)
}
}
const mintNFT = async (name: string, description: string, image: string) => {
if (!connected || !publicKey) return
try {
const response = await fetch('/api/nft/mint', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name,
description,
image,
creator: publicKey.toString()
})
})
if (response.ok) {
loadNFTs()
}
} catch (error) {
console.error('Error minting NFT:', error)
}
}
const buyNFT = async (nftId: string, price: number) => {
if (!connected || !publicKey) return
try {
const response = await fetch('/api/nft/buy', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
nftId,
buyer: publicKey.toString(),
price
})
})
if (response.ok) {
loadNFTs()
}
} catch (error) {
console.error('Error buying NFT:', error)
}
}
if (loading) {
return <div>Loading NFT marketplace...</div>
}
return (
<div className="max-w-7xl mx-auto p-6">
<h1 className="text-3xl font-bold mb-8">NFT Marketplace</h1>
{/* Mint NFT Form */}
<div className="bg-white rounded-lg shadow p-6 mb-8">
<h2 className="text-xl font-semibold mb-4">Mint New NFT</h2>
<MintNFTForm onSubmit={mintNFT} />
</div>
{/* NFTs Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{nfts.map((nft) => (
<NFTCard
key={nft.id}
nft={nft}
onBuy={buyNFT}
onSelect={setSelectedNFT}
userAddress={publicKey?.toString()}
/>
))}
</div>
{/* NFT Detail Modal */}
{selectedNFT && (
<NFTDetailModal
nft={selectedNFT}
onClose={() => setSelectedNFT(null)}
onBuy={buyNFT}
userAddress={publicKey?.toString()}
/>
)}
</div>
)
}
NFT Card Component
// components/NFTCard.tsx
import React from 'react'
import Image from 'next/image'
interface NFTCardProps {
nft: NFT
onBuy: (nftId: string, price: number) => void
onSelect: (nft: NFT) => void
userAddress?: string
}
export default function NFTCard({ nft, onBuy, onSelect, userAddress }: NFTCardProps) {
const isOwner = userAddress === nft.owner
const canBuy = nft.forSale && !isOwner && userAddress
return (
<div
className="bg-white rounded-lg shadow-md overflow-hidden cursor-pointer hover:shadow-lg transition-shadow"
onClick={() => onSelect(nft)}
>
<div className="aspect-square relative">
<Image
src={nft.image}
alt={nft.name}
fill
className="object-cover"
/>
{nft.forSale && (
<div className="absolute top-2 right-2 bg-green-500 text-white px-2 py-1 rounded text-sm">
For Sale
</div>
)}
{nft.auction && (
<div className="absolute top-2 right-2 bg-blue-500 text-white px-2 py-1 rounded text-sm">
Auction
</div>
)}
</div>
<div className="p-4">
<h3 className="font-semibold text-lg mb-2">{nft.name}</h3>
<p className="text-gray-600 text-sm mb-3 line-clamp-2">{nft.description}</p>
<div className="flex justify-between items-center">
<div>
<p className="text-sm text-gray-500">Creator</p>
<p className="text-sm font-medium">
{nft.creator.slice(0, 8)}...{nft.creator.slice(-8)}
</p>
</div>
{nft.forSale && (
<div className="text-right">
<p className="text-sm text-gray-500">Price</p>
<p className="text-lg font-bold">{nft.price} SOL</p>
</div>
)}
</div>
{canBuy && (
<button
onClick={(e) => {
e.stopPropagation()
onBuy(nft.id, nft.price)
}}
className="w-full mt-3 bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600"
>
Buy Now
</button>
)}
{isOwner && (
<div className="mt-3 text-center">
<span className="text-sm text-gray-500">You own this NFT</span>
</div>
)}
</div>
</div>
)
}
API Routes
NFT API
// pages/api/nft/list.ts
import { NextApiRequest, NextApiResponse } from 'next'
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === 'GET') {
try {
const { limit = 10, offset = 0, collection, forSale } = req.query
const response = await fetch(`${process.env.AO_PROCESS_URL}/list_nfts`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
Limit: limit,
Offset: offset,
Collection: collection,
ForSale: forSale === 'true'
})
})
const data = await response.json()
res.json(data)
} catch (error) {
res.status(500).json({ error: 'Failed to fetch NFTs' })
}
} else {
res.setHeader('Allow', ['GET'])
res.status(405).end(`Method ${req.method} Not Allowed`)
}
}
Mint NFT API
// pages/api/nft/mint.ts
import { NextApiRequest, NextApiResponse } from 'next'
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === 'POST') {
try {
const { name, description, image, creator } = req.body
const response = await fetch(`${process.env.AO_PROCESS_URL}/mint_nft`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
Name: name,
Description: description,
Image: image,
From: creator,
Timestamp: Date.now()
})
})
const data = await response.json()
res.json(data)
} catch (error) {
res.status(500).json({ error: 'Failed to mint NFT' })
}
} else {
res.setHeader('Allow', ['POST'])
res.status(405).end(`Method ${req.method} Not Allowed`)
}
}
Configuration
ao.config.yml
name: 'nft-marketplace'
framework: 'nextjs'
packageManager: 'pnpm'
luaFiles: ['nft.lua']
autoStart: true
processName: 'nft-process'
ports:
dev: 3000
ao: 8080
Features
NFT Minting
- Custom Metadata - Name, description, and attributes
- Image Upload - Support for various image formats
- Collection Support - Organize NFTs into collections
- Batch Minting - Mint multiple NFTs at once
Trading System
- Fixed Price Sales - Set and buy at fixed prices
- Auction System - Bid-based auctions with time limits
- Royalty Support - Creator royalties on secondary sales
- Escrow System - Secure transaction handling
User Experience
- Search and Filter - Find NFTs by various criteria
- User Profiles - View user collections and activity
- Favorites - Save favorite NFTs
- Activity Feed - Track marketplace activity
Best Practices
- Metadata Standards - Use standard metadata formats
- Image Optimization - Optimize images for performance
- Security - Implement proper access controls
- Scalability - Design for high transaction volume
- User Experience - Make trading intuitive and fast
Next Steps
- Explore DeFi Protocol example
- Learn about Token Contract implementation
- Check out DAO Application example
Table of Contents