
How to mint NFTs with persistent metadata comes down to getting the pinning step right before you ever touch a smart contract, since the on-chain transaction only ever stores a pointer, not the actual file. This walks through the practical workflow on AIOZ Pin: preparing the asset and metadata, pinning both together, verifying the pin actually took, and pointing your contract's tokenURI at the result.
TL;DR:
pinNft({fileStream, metadata}) to pin both together in one operationtokenURI to ipfs://<CID>Start with the actual file, image, video, or audio, that the NFT represents. Nothing AIOZ-specific happens here, just have the file ready as a readable stream, since that's what the SDK's pinNft() call expects for the fileStream parameter.
import fs from 'fs'
const fileStream = fs.createReadStream('./artwork.png')
NFT metadata follows a standard shape, name, description, and a properties array of trait_type/value pairs, the format most marketplaces expect for displaying attributes:
const metadata = {
name: 'Artwork #001',
description: 'A generative piece from the Genesis collection',
properties: [
{ trait_type: 'Background', value: 'Deep Blue' },
{ trait_type: 'Rarity', value: 'Rare' }
]
}
Get this right before pinning, since changing metadata after the fact means a new CID, not an edit to the old one, content addressing makes metadata immutable once pinned, by design.
import AiozPinClient from '@aioznetwork/aioz-pin-sdk'
const client = new AiozPinClient('your-api-key', 'your-secret-key')
const result = await client.pinNft({ fileStream, metadata })
console.log(result)
This is the step that matters most for persistence: pinNft() pins the asset and metadata as one operation, covered in full in how AIOZ Pin's NFT tooling works, rather than two separate pin calls where one could succeed while the other fails or expires independently. The response includes the CID your metadata resolves to, which is what your contract will actually reference.
Before wiring anything into a contract, confirm the pin resolved successfully rather than assuming the call succeeded:
const pin = await client.getPinByCID(result.cid)
console.log(pin.status)
This costs one extra API call and catches a failed or still-processing pin before it becomes a broken tokenURI on-chain, which is much harder to fix after a mint than before one.
With a confirmed, resolving CID, point your contract's tokenURI at it using the standard ipfs:// scheme:
ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi
Most NFT contract standards (ERC-721, ERC-1155) expect this exact format, resolved by marketplaces and wallets through their own IPFS gateway or through the one your pinning service exposes. Because the CID is derived from the content itself, this reference can't be silently redirected to different content later, the same tamper-detection property that makes CID-based storage suitable for on-chain references in the first place.
Beyond confirming AIOZ Pin's own getPinByCID() reports a resolved status, it's worth checking the CID actually resolves through at least one gateway independent of AIOZ Pin before minting, a public IPFS gateway, or a second provider if you have access to one. getPinByCID() confirms AIOZ Pin's own infrastructure sees the pin as complete; a cross-check through an independent gateway confirms the content is actually reachable the way an eventual buyer's wallet or marketplace will reach it, not just present in AIOZ Pin's own records. This costs one extra HTTP request and catches the rare case where a pin reports success internally but something else, a gateway configuration issue, a DNS problem, stands between the CID and someone actually being able to view it.
Pinning before minting, not after, means your contract never points at a CID that doesn't exist yet. Verifying the pin before setting the tokenURI means you catch a failed pin while it's still cheap to fix, before it's a permanent on-chain reference. Skipping either step is how projects end up with a valid, minted token pointing at nothing, the exact failure mode a dedicated pinning service exists to prevent.
The five steps above are written for a single mint, but a collection launch means running this same sequence many times, and the ordering discipline matters more, not less, at that scale. Prepare and pin every asset-and-metadata pair first, verify every resulting CID resolves, and only then move to the on-chain minting step for the batch, rather than interleaving pin-then-mint-then-pin-then-mint one at a time. Catching a handful of failed pins before any minting transaction has gone out is a batch of retries; catching the same failures after some tokens are already minted against broken CIDs means a mix of working and broken tokens in the same collection, exactly the inconsistent, partially-broken result this workflow exists to prevent.
What's the correct order of steps to mint an NFT with persistent metadata? Prepare the asset and metadata, pin them together, verify the pin resolved, then set the contract's tokenURI to the resulting CID, in that order, before minting.
Should I pin the asset and metadata separately or together? Together, using pinNft({fileStream, metadata}). Pinning them as one operation avoids the case where one persists and the other doesn't.
What format should NFT metadata use? name, description, and a properties array of trait_type/value pairs, the standard shape most marketplaces expect for displaying attributes.
Can I edit NFT metadata after it's pinned? Not in place. Since a CID is derived from the content itself, any change produces a new CID, not an edit to the existing one. You'd need to re-pin and update the reference, which isn't possible once a tokenURI is set immutably on-chain.
How do I confirm a pin actually succeeded before minting? Call getPinByCID() with the CID returned from pinNft() and check its status before using that CID in a tokenURI, rather than assuming the pin call succeeding means the content is fully available.
pinNft()
IPFS defaults to 256 KiB fixed-size chunks, but also ships Rabin and Buzhash content-defined chunkers. Here is why the choice affects deduplication.

An IPFS CID is not a random string. It encodes a version, a codec, and a hash algorithm plus digest. Here is how to decode a real CID piece by piece.

IPFS does not use a B-tree for large directories. It uses a HAMT, a Hash Array Mapped Trie. Here is exactly how it shards a folder once it outgrows one block.

AIOZ Pin image resizing happens straight in the gateway URL, no upload step or separate service. Here is every img- parameter, with real srcset examples.

x402 payments let ERC-8004 weight a paid AI agent interaction more heavily than free work in its reputation score. Here is exactly how that link works.

AIOZ Pin NFT API calls pin an asset and its metadata as two tracked pins under one record. Here is how to automate it directly over REST, no SDK needed.