HAX
← Back

Steps for Creating a 3D NFT Marketplace on Stacks #01

2025-04-08Fabo Hax

4V4 NFT Marketplace on Stacks

This is a proof of concept for an idea that's been on my mind for a while:

What if we could own 3D models as NFT avatars or accessories usable across different games or metaverses?

I play a lot of Fortnite and RPGs. While I love these games, I often wish I could use my avatars across various platforms or even integrate them with my website as a unique authentication method or historical proof.

A significant challenge here is interoperability. Different platforms impose different standards, polygon limits, animation parameters, and compatibility requirements. For example, Sandbox uses voxel-based models, while Decentraland opts for low-poly designs. Each platform also has its own policies regarding external avatar imports. Still, I think there's plenty playground yet for exploring this kind of digital craftmanships.

To push this idea further, I chose Stacks, a fit Bitcoin Layer-2 chain for deploying contracts using the predictable language called Clarity.

Prerequisites

  • Basic understanding of Clarity
  • Familiarity with the Stacks blockchain

Step 01 — Creating the NFT Minting Contract

One of the main differences is that the avatar has different properties at the metadata level. Something like this:

{
  "name": "4V4 Avatar #001",
  "description": "A fully rigged and interoperable 3D avatar, part of the 4V4 collection on the Stacks blockchain.",
  "image": "ipfs://QmAvatarPreviewHash/preview.png",
  "model": {
    "uri": "ipfs://QmAvatarModelHash/model.glb",
    "format": "glb",
    "rigged": true,
    "polycount": 8500,
    "animation": ["idle", "walk", "jump"]
  },
  "attributes": [
    {
      "trait_type": "Faction",
      "value": "Cyber Nomad"
    },
    {
      "trait_type": "Eyes",
      "value": "Neon Blue"
    },
    {
      "trait_type": "Armor",
      "value": "Carbon-X Tactical"
    },
    {
      "trait_type": "Rarity",
      "value": "Epic"
    }
  ],
  "creator": "Fabo Hax",
  "royalty_percent": 7,
  "external_url": "https://4v4.world/avatar/001",
  "collection": "4V4"
}

So here we go to design the contract that defines a SIP-009 compliant NFT collection using Clarity.

Traits and Token Definition

(impl-trait '...nft-trait.nft-trait)
(define-non-fungible-token avatar uint)

Implements the SIP-009 NFT trait and defines a new NFT type called avatar.


Constants

(define-constant COLLECTION_LIMIT u0)
(define-constant CONTRACT_OWNER tx-sender)
  • COLLECTION_LIMIT: Limits the total supply of NFTs (set to 0 initially).
  • CONTRACT_OWNER: Sets the contract owner to the deployer.

Errors

(define-constant ERR_UNAUTHORIZED (err u401))
(define-constant ERR_SOLD_OUT (err u402))

Defines errors for unauthorized access and sold out collection.


Storage

(define-data-var last-token-id uint u0)
(define-data-var base-uri (string-ascii 256) "ipfs://ipfs.io/")
  • last-token-id: Tracks the last minted token ID.
  • base-uri: Points to the metadata location (e.g., IPFS).

Royalty Info

(define-data-var royalty-percent uint u7)
(define-data-var royalty-recipient principal CONTRACT_OWNER)

Used to define royalty details per secondary sale.


Admin Function

(define-public (set-base-uri (new-uri (string-ascii 256)))
  ...)

Allows the contract owner to update the base URI for metadata.


Minting

(define-public (mint-public)
  ...)

This function allows any user to mint a new NFT if the collection isn't sold out.


Transfer

(define-public (transfer (id uint) (sender principal) (recipient principal))
  ...)

Allows token owners to transfer their NFTs.


SIP-009 Read Functions

(define-read-only (get-last-token-id) ...)
(define-read-only (get-token-uri (id uint)) ...)
(define-read-only (get-owner (id uint)) ...)

Required getters to be compliant with the SIP-009 standard.


Royalty Info (Read-Only)

(define-read-only (get-royalty-info (sale-price uint))
  ...)

Returns the recipient and amount of royalties to pay on secondary sales.


Full Contract Code

;; 4V4 SIP-009 NFT Contract

(impl-trait 
'...nft-trait.nft-trait)

(define-non-fungible-token avatar uint)

(define-constant COLLECTION_LIMIT u0)
(define-constant CONTRACT_OWNER tx-sender)

(define-constant ERR_UNAUTHORIZED (err u401))
(define-constant ERR_SOLD_OUT (err u402))

(define-data-var last-token-id uint u0)
(define-data-var base-uri (string-ascii 256) "ipfs://ipfs.io/")

(define-data-var royalty-percent uint u7)
(define-data-var royalty-recipient principal CONTRACT_OWNER)

(define-public (set-base-uri (new-uri (string-ascii 256)))
  (begin
    (asserts! (is-eq tx-sender CONTRACT_OWNER) ERR_UNAUTHORIZED)
    (var-set base-uri new-uri)
    (ok true)
  ))

(define-public (mint-public)
  (let ((token-id (+ (var-get last-token-id) u1)))
    (begin
      (asserts! (<= token-id COLLECTION_LIMIT) ERR_SOLD_OUT)
      (try! (nft-mint? avatar token-id tx-sender))
      (var-set last-token-id token-id)
      (ok token-id))))

(define-public (transfer (id uint) 
  (sender principal) (recipient principal))
    (begin
      (asserts! (is-eq sender contract-caller) ERR_UNAUTHORIZED)
      (try! (nft-transfer? avatar id sender recipient))
      (ok true)))

(define-read-only (get-last-token-id)
  (ok (var-get last-token-id)))

(define-read-only (get-token-uri (id uint))
  (ok (some (var-get base-uri))))

(define-read-only (get-owner (id uint))
  (ok (nft-get-owner? avatar id)))

(define-read-only (get-royalty-info (sale-price uint))
  (ok {
    recipient: (var-get royalty-recipient), 
    amount: (/ (* sale-price (var-get royalty-percent)) u100)})
  )

Next Steps

  • Create the Marketplace contract.
  • Test contracts locally using Clarinet.
  • Deploy to the Stacks Testnet.
  • Integrate the NFT minting functionality into a frontend (e.g., using React or Next.js).
  • Add features like auctions or secondary sales.

Stay tuned for future parts of this tutorial series, where we'll tackle frontend integration, marketplace functionalities, and more advanced interoperability solutions.

Go to Step #02

Sign Up to Stay Sync:

menu