The ERC-721 Metadata Standard, Readable End to End

The most demoralizing bug I hit early on wasn't in the smart contract — it was a blank card on a marketplace. The token existed, the contract was correct, and yet nothing rendered. The problem was metadata: one malformed JSON field and the whole display chain silently breaks. That's why I care about this standard more than the hype around it.

An ERC-721 token is, at its core, just a number owned by an address. Everything people actually see — name, image, description — lives in a separate metadata file that the contract points to. This post is a working reference for getting that metadata right, from required fields to the display types that control how marketplaces render your traits. I'll use our own Quantum Genesis metadata as the concrete example throughout.

How ERC-721 metadata works

The token is just a number (token ID) owned by an address. By itself it has no image, no name, no description. All of that lives in metadata — a JSON file the contract references through its tokenURI function. When a marketplace displays your NFT it:

  1. Calls tokenURI(tokenId) on your contract
  2. Gets back a URL (usually IPFS)
  3. Fetches the JSON at that URL
  4. Reads the fields and renders the card

If any step fails — wrong URL, malformed JSON, missing fields — you get a blank card. Getting metadata right isn't optional polish; it's the difference between a visible piece and an invisible one.

The JSON structure

Here's a complete example with all the major fields:

{
  "name": "Quantum Genesis #42",
  "description": "A unique piece of generative art created from measurements on IBM Quantum's ibm_fez processor. The quantum circuit used Hadamard gates and CNOT entanglement to produce genuinely random bits that drive the color palette, geometry, and composition.",
  "image": "ipfs://bafybeifges7tei5x7drj37f34yhzqofwlz2icbo7z67isg6g446k65yw3a/42.png",
  "external_url": "https://opensea.io/collection/quantum-genesis",
  "background_color": "0a0a2e",
  "attributes": [
    {
      "trait_type": "Quantum Source",
      "value": "IBM Quantum ibm_fez"
    },
    {
      "trait_type": "Color Harmony",
      "value": "Triadic"
    },
    {
      "trait_type": "Qubit Count",
      "display_type": "number",
      "value": 8
    },
    {
      "trait_type": "Entropy Score",
      "display_type": "number",
      "value": 94
    },
    {
      "display_type": "date",
      "trait_type": "Generation Date",
      "value": 1710892800
    }
  ]
}

Quantum Genesis NFT #42

Required fields

Strictly, the ERC-721 standard only requires tokenURI to return a valid URI. But for marketplaces to render anything, three fields are effectively required:

name (string)

The token's title on marketplace cards. Convention: collection name + # + token ID, kept under 50 characters for clean display.

"name": "Quantum Genesis #42"

description (string)

The token's description, shown in the NFT detail page. OpenSea supports basic Markdown here. Mention what makes the piece unique; keep it under 500 characters for best readability.

"description": "A unique piece of generative art created from real quantum computer measurements..."

image (string — URI)

URL to the image. Supported formats: PNG, JPG, GIF, SVG, WebP. OpenSea's limit is about 40MB.

"image": "ipfs://bafybeifges7tei5x7drj37f34yhzqofwlz2icbo7z67isg6g446k65yw3a/42.png"

Use the ipfs:// prefix — marketplaces resolve it through their own IPFS gateways. Avoid pinning a specific gateway URL (like https://gateway.pinata.cloud/ipfs/...); if that gateway dies, your image breaks. And note the image field is what appears everywhere — wallets, marketplaces, social embeds — so get it right.

Optional fields

external_url (string — URI). A clickable link on the NFT's page under the token name. Point it at your project site or a detail page for the piece.

"external_url": "https://yourproject.com/nft/42"

animation_url (string — URI). For multimedia NFTs: HTML, GLB/GLTF 3D models, MP4/WebM video, MP3/WAV audio, interactive content. When present it takes display priority over image, which becomes the thumbnail.

"animation_url": "ipfs://Qm.../interactive.html"

background_color (string — hex without #). A six-character hex color for the NFT card background on OpenSea. No # prefix. It fills space when the image doesn't match card dimensions.

"background_color": "0a0a2e"

The attributes array

Attributes are the most powerful part of the schema. They render in the "Properties," "Stats," and "Levels" sections, and they power filtering and rarity calculations. Each attribute is an object:

{
  "trait_type": "Color Harmony",   // The trait category
  "value": "Triadic"               // The trait value
}

String traits show as rectangular badges under "Properties".

Numeric attributes appear under "Stats":

{
  "trait_type": "Entropy Score",
  "display_type": "number",
  "value": 94
}

OpenSea auto-calculates the range across your collection. Adding a max_value renders a progress bar; without it OpenSea infers the max from the highest value present:

{
  "trait_type": "Entropy Score",
  "display_type": "number",
  "value": 94,
  "max_value": 100
}

Display types deep dive

OpenSea supports several display_type values that change how traits render:

display_typeRenders asValue typeSection
(omitted)Text badgestringProperties
"number"Plain numberinteger/floatStats
"boost_number"Number with + prefixinteger/floatBoosts
"boost_percentage"Circular progress + %integer (0–100)Boosts
"date"Formatted dateUnix timestampProperties
{
  "display_type": "boost_number",
  "trait_type": "Quantum Advantage",
  "value": 12
}
// Renders as: "+12" in a circular badge
{
  "display_type": "boost_percentage",
  "trait_type": "Randomness Quality",
  "value": 95
}
// Renders as: circular progress bar showing 95%
{
  "display_type": "date",
  "trait_type": "Generation Date",
  "value": 1710892800
}
// Renders as: "March 20, 2024" (human-readable)
// Value MUST be Unix timestamp (seconds since epoch)

Quantum Genesis NFT #19

OpenSea-specific conventions

Beyond the base standard, OpenSea adds a few conventions:

  • Collection-level metadata — set via the OpenSea UI or API, not per-token JSON: collection name, banner, description, royalty percentage, social links.
  • Trait rarity — computed automatically from how traits are distributed across the collection. If only 5 of 100 pieces carry "Quantum Source: Origin Quantum WK_C180," that trait shows "5% have this trait." You don't specify rarity; it's derived.
  • Refresh metadata — after updating the JSON on IPFS (re-upload and update tokenURI), click "Refresh metadata" on the NFT page or call the API to force a re-fetch.

Our metadata: nine attributes in practice

Each Quantum Genesis piece carries nine attributes:

{
  "name": "Quantum Genesis #42",
  "description": "Generative art from real quantum computer measurements...",
  "image": "ipfs://bafybeifges7tei5x7drj37f34yhzqofwlz2icbo7z67isg6g446k65yw3a/42.png",
  "external_url": "https://opensea.io/collection/quantum-genesis",
  "attributes": [
    { "trait_type": "Quantum Source", "value": "IBM Quantum ibm_fez" },
    { "trait_type": "Color Harmony", "value": "Triadic" },
    { "trait_type": "Visual Style", "value": "Orbital" },
    { "trait_type": "Background Tone", "value": "Dark" },
    { "trait_type": "Complexity", "value": "High" },
    { "trait_type": "Qubit Count", "display_type": "number", "value": 8 },
    { "trait_type": "Entropy Score", "display_type": "number", "value": 94 },
    { "trait_type": "Gate Depth", "display_type": "number", "value": 12 },
    { "display_type": "date", "trait_type": "Generation Date", "value": 1710892800 }
  ]
}

The string traits create filterable categories; the numeric traits render as stats; the date records when the measurement happened. Why these particular nine, briefly:

  • Quantum Source — provenance: which quantum computer generated the data
  • Color Harmony — the palette algorithm (triadic, complementary, ...)
  • Visual Style — the geometric pattern type
  • Background Tone — dark or light
  • Complexity — visual complexity level
  • Qubit Count — parameter of the quantum circuit
  • Entropy Score — quality measure of the quantum randomness (0–100)
  • Gate Depth — circuit depth (more gates = more entanglement)
  • Generation Date — exact timestamp for provenance

IPFS vs. centralized hosting

IPFS (recommended). Content is addressed by hash — the URL derives from the content itself. If anyone on Earth has a copy, it's reachable. Pros: decentralized, content-addressed (tamper-proof), permanent if pinned. Cons: slower initial load, needs a pinning service to stay available. Services: Pinata (what we use), NFT.Storage, Infura IPFS, Filebase. We pin all 100 art PNGs in one IPFS directory and all 100 metadata JSONs in another; the directory CID becomes our baseURI.

Centralized (not recommended). You can host metadata on any web server — S3, your own domain. But if that server dies, every NFT in the collection goes blank. That defeats the permanence the blockchain promises. In general, keep NFT metadata and images on IPFS. Centralized storage means the art can be changed or deleted later, and buyers reasonably distrust that.

tokenURI and the baseURI pattern

Your contract has to return the right metadata URL per token. Two common patterns:

Pattern 1 — individual tokenURI. Store each full URI in the contract. Flexible but expensive: one storage write per mint.

// Solidity
mapping(uint256 => string) private _tokenURIs;

function tokenURI(uint256 tokenId) public view returns (string memory) {
    return _tokenURIs[tokenId];
}

Pattern 2 — baseURI + tokenId (recommended). Store one base URI; each token's URI is baseURI + tokenId. Much cheaper for large collections.

// Solidity (OpenZeppelin pattern)
string private _baseTokenURI;

function _baseURI() internal view override returns (string memory) {
    return _baseTokenURI;
}

// tokenURI(42) returns: "ipfs://QmBaseHash/42"
// The JSON file at that IPFS path contains the metadata for token 42

This is what we use. Our baseURI points at an IPFS directory containing files named 1 through 100 (no extension), each a JSON metadata document:

ipfs://QmMetadataCID/
├── 1       ← JSON metadata for token #1
├── 2       ← JSON metadata for token #2
├── ...
└── 100     ← JSON metadata for token #100

And in each JSON, the image field points at the art directory:

"image": "ipfs://bafybeifges7tei5x7drj37f34yhzqofwlz2icbo7z67isg6g446k65yw3a/42.png"

Quantum Genesis NFT #60

Getting the metadata schema right is the layer that turns a URL into something a marketplace can render — and in our case, into a card that carries its quantum provenance along with the pixels. If you want to know where that provenance comes from in the first place, the SHA-256 seed post explains how a measurement becomes a reproducible seed.

Comments

Popular posts from this blog

Getting Your Collection Visible on OpenSea, Step by Step

Polygon versus Ethereum for an NFT contract, from the gas bills up

Quantum Error Correction, or Why Your Qubits Forget What They Were Doing