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

  1. Metadata Standards - Use standard metadata formats
  2. Image Optimization - Optimize images for performance
  3. Security - Implement proper access controls
  4. Scalability - Design for high transaction volume
  5. User Experience - Make trading intuitive and fast

Next Steps