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.