NEP-177: NFT metadata
NEP-177 NFT metadata standard on NEAR: contract-level nft_metadata with spec "nft-1.0.0", per-token title, media, media_hash and reference fields.
Intermediate6 min read3-question check
NEP-177 defines the display data for NFTs at two levels. Contract metadata describes the collection and is returned by the nft_metadata() view. Token metadata describes each token and is returned inside the metadata field of nft_token, nft_tokens and friends.
Unlike ERC-721, where tokenURI points to a JSON file somewhere off-chain, NEP-177 keeps the core fields on-chain in the token record and uses links only for the heavy media and optional extra JSON.
Contract metadata#
| Field | Meaning |
|---|---|
spec | Must be "nft-1.0.0". |
name, symbol | Collection name and ticker. |
icon | Optional small image, ideally a data URL. |
base_uri | Optional gateway (e.g. an IPFS gateway) that relative media / reference values are resolved against. |
reference, reference_hash | Optional link to extra collection JSON plus its base64 sha256 (the hash is required when reference is set). |
Token metadata#
| Field | Meaning |
|---|---|
title, description | Human-readable name and description of this token. |
media, media_hash | Link to the image/video (preferably decentralized storage) and the base64 sha256 of that file, so clients can verify it. |
copies | Number of copies of this set of metadata in existence when the token was minted (editions). |
issued_at, expires_at, starts_at, updated_at | Timestamps as Unix epoch in milliseconds. |
extra | Free-form string for anything else — often stringified JSON for traits or game data. |
reference, reference_hash | Link to an off-chain JSON with more attributes, plus its base64 sha256. |
{
"token_id": "42",
"owner_id": "alice.near",
"metadata": {
"title": "Sunrise #42",
"description": "One of 100 sunrises.",
"media": "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
"media_hash": "Vdr1nZ8vWqU2hD2nX6m7rT5fKpNs0Y1cJbQwEaLxZ3o=",
"copies": 100,
"issued_at": 1735689600000,
"expires_at": null,
"starts_at": null,
"updated_at": null,
"extra": "{\"rarity\":\"rare\"}",
"reference": null,
"reference_hash": null
},
"approved_account_ids": {}
}// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
contract Sunrise is ERC721 {
constructor() ERC721("Sunrise", "SUN") {}
// metadata is an off-chain JSON pointed at by a URI
function _baseURI() internal pure override returns (string memory) {
return "ipfs://bafy.../";
}
}use near_contract_standards::non_fungible_token::metadata::TokenMetadata;
use near_contract_standards::non_fungible_token::{Token, TokenId};
use near_sdk::{env, near, require, AccountId};
#[near]
impl Contract {
// metadata is stored on-chain with the token (tokens: NonFungibleToken
// was created with a TokenMetadata storage prefix)
#[payable]
pub fn nft_mint(
&mut self,
token_id: TokenId,
receiver_id: AccountId,
metadata: TokenMetadata,
) -> Token {
require!(env::predecessor_account_id() == self.tokens.owner_id, "Unauthorized");
// internal_mint charges the attached deposit for the new storage
self.tokens.internal_mint(token_id, receiver_id, Some(metadata))
}
}tokenURI(id) ↔ the metadata object on nft_token(id). If media is relative, clients prefix it with the contract’s base_uri.
internal_mint stores the metadata, refunds unused deposit and emits the NEP-297 nft_mint event for you.
Check yourself
3 questions · progress saved in this browser