NFT Metadata: ERC-721, ERC-1155, and What OpenSea Actually Reads
An NFT collection mints on mainnet, the contract is verified, the team Tweets the launch. Twenty minutes later the OpenSea listing shows blank images and a single trait labeled undefined. The contract is fine. The transaction is fine. The metadata file served at tokenURI has one field named image_url instead of image, and marketplaces silently drop everything that does not match their schema.
The first NFT collection I shipped had attributes scattered across two formats — half the team wrote trait_type, half wrote attribute. Both displayed fine in our preview script. Neither displayed on OpenSea. I learned that the standard is not the spec the dev wrote — it is whatever marketplace renders the NFT.
What marketplaces actually read
Every NFT marketplace ultimately follows the same flow. The contract returns a URI from tokenURI(tokenId) (ERC-721) or uri(id) (ERC-1155). The marketplace fetches that URI, parses the JSON, and looks for a fixed set of fields. Anything outside that set is ignored. Anything misnamed is treated as missing.
OpenSea's metadata schema reads these top-level fields:
{
"name": "Pixel Punk #1234",
"description": "On-chain pixel art, edition 1 of 1000.",
"image": "ipfs://Qm.../1234.png",
"external_url": "https://example.com/punks/1234",
"animation_url": "ipfs://Qm.../1234.mp4",
"background_color": "ffffff",
"attributes": [
{ "trait_type": "Background", "value": "Cosmic" },
{ "trait_type": "Eyes", "value": "Laser" },
{ "trait_type": "Rarity Score", "value": 87, "display_type": "number" }
]
}The fields that matter for the visual listing: name, description, image, attributes. Get those four right and the listing renders. Get them wrong and the NFT is invisible even though the on-chain state is correct.
ERC-721 vs ERC-1155 — where the JSON diverges
Both standards point at off-chain JSON, but the contract-side conventions differ in two places: how the URI is templated and what is on-chain by default.
URI templating
ERC-721 contracts usually concatenate the base URI with the token ID: tokenURI(7) → ipfs://Qm.../7.json. ERC-1155 uses a substitution token — uri(7) → ipfs://Qm.../{id}.json, where the literal {id} is replaced client-side by a 64-character zero-padded hex.
// ERC-1155 URI 치환 규칙
// uri()가 반환한 문자열에 "{id}"가 있으면
// 클라이언트가 0x로 시작하는 64자 hex로 치환
// 예: tokenId 7 → "0x0000000000000000000000000000000000000000000000000000000000000007"
// 예: tokenId 123 → "0x000000000000000000000000000000000000000000000000000000000000007b"
// 그래서 ERC-1155 메타데이터 파일 이름은 보통 0-padded 16진수
// metadata/0x0000...0007.jsonMarketplaces handle both. The bug is when a team builds the metadata file names in decimal (7.json) but returns the ERC-1155 templated URI — the lookup fails because the marketplace expanded the template to the hex form and the file does not exist at that path.
Fungible vs non-fungible in the same contract
ERC-1155 supports fungible (supply > 1) and non-fungible (supply == 1) tokens in the same contract. The metadata schema is the same for both, but marketplaces display fungible 1155 tokens differently — as editions with a remaining count, not as unique items. Plan attribute design around that. "Rarity rank" on an edition of 1000 is meaningless.
Attributes — the field that makes or breaks the listing
The attributes array is what drives the "Properties" sidebar, the rarity tools (rarity.tools, Trait Sniper), and most of the user's judgment about value. Three shapes show up in production:
// 1. 문자열 trait — 가장 흔함
{ "trait_type": "Background", "value": "Cosmic" }
// 2. 숫자 trait — display_type으로 표현 방식 지정
{ "trait_type": "Generation", "value": 1, "display_type": "number" }
{ "trait_type": "Aqua Power", "value": 50, "display_type": "boost_number" }
{ "trait_type": "Stamina", "value": 90, "display_type": "boost_percentage" }
// 3. 날짜 trait — Unix timestamp (초 단위)
{ "trait_type": "Birthday", "value": 1546360800, "display_type": "date" }
// max_value를 함께 보내면 OpenSea가 진행률 막대로 그려줌
{ "trait_type": "Speed", "value": 85, "max_value": 100 }display_type is the field that changes how the trait renders. Without it, every number becomes a string and rarity tools cannot sort numerically. With it set to boost_number, OpenSea draws the trait as a circular badge. With date, it formats as a human-readable date.
Three rules for designing attributes that age well:
- Consistency over completeness. Every NFT in the collection should have the same
trait_typekeys, even if the value is "None". Missing keys break rarity calculations. - One value type per trait. Do not mix string
"Common"and numeric1under the same trait_type. Rarity tools assume the type is uniform across the collection. - Avoid free-form text traits."Description" or "Story" as attributes do nothing for filtering and add noise to the properties sidebar.
Image, animation_url, and the IPFS gateway problem
The image field can be a centralized URL, an IPFS URI (ipfs://...), an Arweave URI, or a data URI. Marketplaces resolve each differently, and a wrong choice here is the single most common reason a collection "disappears" from marketplaces a year later.
- Centralized URL (
https://yoursite.com/...) — fastest, breaks the day your domain expires or the bucket goes private. - IPFS URI (
ipfs://Qm...) — marketplaces rewrite to their own gateway. Persistent only as long as someone pins the CID. Pinata, NFT.storage, and your own IPFS node all count as "someone." - Arweave URI (
ar://...) — pay once, store forever. The most expensive option upfront, the cheapest over a decade. - Data URI (
data:image/svg+xml;base64,...) — on-chain art. Image lives in the contract. Strongest persistence, hardest to update.
animation_url carries the same trade-offs but for video, audio, GLB models, or interactive HTML. OpenSea's viewer renders MP4, WebM, GLB, MP3, and a sandbox of HTML. The static image still has to exist as a fallback for previews.
Metaplex (Solana) — on-chain vs off-chain metadata
Solana's Metaplex standard splits metadata across two layers. Some fields live on-chain in the Metadata account; the rest live off-chain at the URI:
// Metaplex 온체인 필드 (account 자체에 저장)
// - name, symbol, uri (off-chain JSON 위치)
// - sellerFeeBasisPoints (royalty, 100 = 1%)
// - creators (verified, share)
// - collection (collection mint 주소)
// off-chain JSON (uri가 가리키는 곳) — OpenSea 스키마와 거의 동일
{
"name": "DeGod #2999",
"symbol": "DEGOD",
"description": "...",
"image": "https://metadata.degods.com/g/2999.png",
"external_url": "https://degods.com",
"attributes": [
{ "trait_type": "version", "value": "v3" },
{ "trait_type": "background", "value": "blue" }
],
"properties": {
"files": [
{ "uri": "...", "type": "image/png" }
],
"category": "image",
"creators": [{ "address": "...", "share": 100 }]
}
}Two Solana-specific fields to remember: properties.category (must be one of image, video, audio, vr, html) and properties.files (an array of every file with explicit MIME types). Magic Eden uses both to decide which renderer to fire.
Bugs that ship to mainnet
1. The metadata server cannot handle the launch traffic
A 10k drop hits mint, ten thousand wallets request /{id}.json from your server at once, the server returns 503, OpenSea caches the failure and shows gray boxes for the next hour. The fix is either CDN-fronted hosting (Cloudflare, S3 + CloudFront) or IPFS with multiple pinning providers. Mainnet is not the time to discover your hosting tier.
2. Reveal logic that points everything at the same placeholder
Pre-reveal collections often return a single placeholder URI for every token. Switching to the real metadata after reveal requires updating the base URI in the contract. If the contract forgot to expose setBaseURI or the owner key is lost, the collection is stuck at the placeholder forever. Test the reveal flow on testnet end-to-end before deploying.
3. Forgetting that marketplaces cache aggressively
OpenSea caches metadata. Updating the JSON file does not refresh the listing — the marketplace must be told to re-fetch. There is a refresh button per token, an API endpoint for bulk refresh, and a recommended pattern of using tokenURI with a version query string when the metadata changes. Ship a migration plan with every metadata edit.
4. Inconsistent attribute keys across the collection
Token 1 has trait_type: "Background", token 2 has trait_type: "background". The marketplace treats them as different traits. Rarity tools count them as different traits. The collection looks like it has twice as many attribute categories as it should. A schema validation step before pinning fixes this.
Royalties — EIP-2981 vs whatever the marketplace decides
Royalties are not really a metadata field — they live on-chain via EIP-2981 — but every team treats them as part of the collection setup, and the gap between "set in contract" and "respected by marketplace" is where most royalty drama happens.
// EIP-2981 — 컨트랙트가 royaltyInfo()를 노출
interface IERC2981 {
function royaltyInfo(uint256 tokenId, uint256 salePrice)
external view returns (address receiver, uint256 royaltyAmount);
}
// 마켓플레이스는 이 함수를 호출해서 로열티를 결정해야 함
// 단, 강제는 아님 — Blur, X2Y2 등은 한때 로열티를 0으로 우회
// OpenSea Creator Fee Enforcement는 별도 컨트랙트 레지스트리 필요Three enforcement patterns that decide whether royalties actually arrive:
- EIP-2981 만 구현 — 표준만 따름. OpenSea, Magic Eden, LooksRare는 보통 respect. Blur, X2Y2은 사용자 설정에 따라 우회 가능.
- Operator Filter Registry (OpenSea) — 특정 마켓플레이스 컨트랙트로의 transfer를 차단. 강제력 강하지만 NFT의 자유로운 이동을 제한한다는 비판도 큼. Yuga Labs도 2023년에 폐기 결정.
- Metaplex Token-2022 (Solana) — 토큰 표준 자체에 transfer fee를 박아 넣음. 어떤 마켓플레이스를 거치든 자동으로 떼감. 가장 강력하지만 Solana 전용이고 마이그레이션 어려움.
The off-chain metadata can also declare royalties as a hint (seller_fee_basis_points in OpenSea's contract-level metadata, sellerFeeBasisPoints on Metaplex), but these are advisory — the marketplace can override. If royalty is part of the project economics, plan around the chance that some venue will not honor it, and do not rely on the metadata hint as the source of truth.
Wrapping up
NFT metadata is one of those areas where the official spec is permissive and the de facto spec is whatever the top three marketplaces parse. Build to OpenSea and Magic Eden, lint every file for consistent attribute keys, choose persistent storage from the start, and the worst-case failure mode shifts from "invisible on launch day" to "the gateway is slow this week."
For checking a single metadata JSON before pinning or rolling a collection, the NFT metadata builder validates against the ERC-721/1155/Metaplex schemas in one place and shows what a marketplace will read.
Related Tools
NFT Metadata Builder
Build NFT metadata JSON for ERC-721, ERC-1155, and Metaplex with attributes, external URLs, and previews.
Ethereum Address Checksum
Convert Ethereum addresses to EIP-55 checksum format and validate address integrity.
Anchor IDL Decoder
Paste a Solana Anchor IDL JSON and browse instructions, accounts, types, and PDA seeds interactively.
Merkle Tree Generator
Build a Merkle tree from an address / amount list and generate the root plus inclusion proofs for each leaf.
Related Articles
How to Generate Secure Passwords in 2026: A Complete Guide
Credential attacks now lean on GPU clusters and ML pattern guessing. What entropy, length, and randomness actually buy you, plus the password manager picks that hold up in 2026.
2025-12-15 · 8 min readData FormatsJSON vs YAML: When to Use What — A Developer's Guide
JSON wins on APIs; YAML wins on configs. Side-by-side syntax, parser behaviour, and where each fits across Kubernetes manifests, REST payloads, and GitHub Actions.
2025-12-28 · 10 min read