Metadata, attributes, properties: deep dive

What is metadata?

Metadata is structured information that tells wallets, marketplaces, and block explorers how to display your NFT - the image, name, description, and traits like "rarity" or "power".

Without proper metadata, your NFT won't show images in wallets or display traits in marketplaces.

Why metadata standards matter

Different blockchains store NFT data differently. To make your NFTs work across platforms (Unique Scan, OpenSea-compatible marketplaces, wallets), you need a metadata standard - an agreement that everyone understands.

Unique Metadata 2.0 is:

  • OpenSea-compatible - follows widely accepted standards
  • Stored on-chain - no broken IPFS links
  • Automatically handled by SDK - you don't manage low-level properties manually

Understanding the architecture

Let's revise what we've learned about collections and NFTs in Unique Network, focusing on how metadata fits into the bigger picture:

1. Collection defines structure

When you create a collection, you define tokenPropertyPermissions - which properties tokens can have:

await sdk.collection.create({
  name: "Game Characters",
  tokenPropertyPermissions: [
    {
      key: "A",
      permission: { mutable: true, collectionAdmin: true, tokenOwner: true },
    },
    {
      key: "B",
      permission: { mutable: false, collectionAdmin: true, tokenOwner: false },
    },
    {
      key: "level",
      permission: { mutable: true, collectionAdmin: false, tokenOwner: true },
    },
  ],
});

2. Tokens are key-value pairs

At the blockchain core level, tokens are just a bunch of properties - key-value pairs:

// This is what's actually stored on the blockchain
{
  "A": "value A",
  "B": "value B",
  "level": "5",
}

3. Unique Metadata is a special property

Unique Metadata is stored in a property called tokenData. This is an agreement - a standardized place where different UIs (wallets, marketplaces, block explorers) know to find and parse metadata.

The tokenData property stores structured JSON as a string:

// tokenData property contains structured JSON
tokenData: {
  name: "Warrior #1",
  image: "https://example.com/warrior.png",
  attributes: [
    {trait_type: "Class", value: "Warrior"},
    {trait_type: "Power", value: 95}
  ]
}

4. SDK creates tokenData automatically

When using the SDK, you don't need to create the tokenData property manually. The SDK handles all the JSON serialization and property management automatically:

// You write this (clean, structured)
await sdk.token.mintNFTs({
  collectionId,
  tokens: [
    {
      data: {
        name: "Warrior #1",
        image: "https://example.com/warrior.png",
        attributes: [{ trait_type: "Power", value: 95 }],
      },
    },
  ],
});

// SDK automatically:
// 1. Converts to Unique Metadata 2.0 JSON
// 2. Stores in tokenData property
// 3. Sets schemaName and schemaVersion properties

Properties vs Metadata

This is the critical distinction:

AspectPropertiesMetadata
WhatRaw blockchain key-value storageStructured data in tokenData property
LevelBlockchain coreApplication-level agreement
FormatAny string valueStandardized JSON (Unique 2.0)
Limit64 properties, 40kB totalPart of the 64-property limit
SDK handlingManual (setProperties)Automatic (data parameter)
Third-party UIsWon't interpret custom keysAutomatically parsed and displayed
Use caseApplication-specific logicCross-platform NFT display

Custom properties vs metadata attributes

Rule of thumb:

  • Store everything important for third-party UIs (wallets, marketplaces) in metadata attributes
  • Store everything important for rarity and traits in metadata attributes
  • Store everything that's application-specific only in custom properties

Here's a practical example showing when to use each approach:

await sdk.token.mintNFTs({
  collectionId,
  tokens: [
    {
      // METADATA: For wallets, marketplaces, rarity tools
      data: {
        name: "Warrior #1",
        image: "https://example.com/warrior.png",
        attributes: [
          { trait_type: "Class", value: "Warrior" }, // Rarity trait
          { trait_type: "Power", value: 95 }, // Rarity trait
          { trait_type: "Level", value: 5 }, // Display trait
          { trait_type: "Experience", value: 1250 }, // Display trait
        ],
      },

      // PROPERTIES: For your application logic only
      properties: [
        { key: "last_battle_timestamp", value: "1704672000" },
        { key: "cooldown_expires", value: "1704675600" },
        { key: "equipped_weapon_id", value: "sword_001" },
        { key: "internal_game_state", value: "active" },
      ],
    },
  ],
});

Why separate level and experience into attributes?

  • Marketplaces can filter "Level > 10"
  • Rarity tools can calculate "Only 5% of warriors have Level 5+"
  • Users see traits in wallets
  • Your game still reads them from metadata

Why keep cooldown and timestamps in properties?

  • Not relevant for rarity or marketplace display
  • Change frequently (metadata updates are more expensive)
  • Only your game logic needs them
  • Faster to read/write as simple properties

Querying properties

// Get token with properties
const token = await sdk.token.get({ collectionId, tokenId });

// Access custom properties
const cooldownProp = token.properties.find((p) => p.key === "cooldown_expires");
console.log("Cooldown expires:", cooldownProp?.value);

Mutating properties

// Update custom property (if mutable)
await sdk.token.setProperties({
  collectionId,
  tokenId,
  properties: [
    { key: "last_battle_timestamp", value: String(Date.now()) },
    { key: "cooldown_expires", value: String(Date.now() + 3600000) },
  ],
});

Images and media

Unique Metadata 2.0 properly supports multiple media types:

await sdk.token.mintNFTs({
  collectionId,
  tokens: [
    {
      data: {
        name: "Epic Character",
        image: "https://example.com/character.png", // Main image

        media: {
          video: {
            type: "video",
            url: "https://example.com/animation.mp4",
            poster: { url: "https://example.com/poster.png" },
          },
          soundtrack: {
            type: "audio",
            url: "https://example.com/theme.mp3",
            thumbnail: { url: "https://example.com/album-art.png" },
          },
        },
      },
    },
  ],
});

Metadata mutability

Metadata mutability is defined on the collection level. Remember, metadata is stored in the tokenData property, and its mutability is controlled by tokenPropertyPermissions.

By default, when you create a collection with SDK, tokenData is mutable by collection admin only.

// Create collection with mutable metadata
await sdk.collection.create({
  name: "Evolving Characters",
  tokenPropertyPermissions: [
    {
      key: "tokenData",
      permission: { mutable: true, collectionAdmin: true, tokenOwner: false },
    },
  ],
});

// Later, update metadata
await sdk.token.updateNft({
  collectionId,
  tokenId,
  data: {
    attributes: [
      { trait_type: "Power", value: 120 }, // Evolved!
      { trait_type: "Level", value: 10 },
    ],
  },
});

For immutable collections (like art), set mutable: false for tokenData.

Working with metadata

Minting with metadata

await sdk.token.mintNFTs({
  collectionId,
  tokens: [
    {
      data: {
        name: "Warrior #1",
        description: "A legendary warrior from the Fire Kingdom",
        image: "https://example.com/warrior.png",

        attributes: [
          { trait_type: "Class", value: "Warrior" },
          { trait_type: "Power", value: 85 },
          { trait_type: "Element", value: "Fire" },
        ],

        media: {
          animation: {
            type: "video",
            url: "https://example.com/warrior-animation.mp4",
            poster: { url: "https://example.com/poster.png" },
          },
          theme: {
            type: "audio",
            url: "https://example.com/theme.mp3",
          },
        },

        royalties: [{ address: account.address, percent: 5.0 }],
      },
    },
  ],
});

Querying metadata

const token = await sdk.token.get({ collectionId, tokenId });

// SDK automatically parses tokenData
console.log(token.name); // "Warrior #1"
console.log(token.image); // "https://example.com/warrior.png"
console.log(token.attributes); // [{trait_type: "Class", value: "Warrior"}, ...]

// Raw properties also available
console.log(token.properties); // [{key: "tokenData", value: "{...}"}, ...]

Updating metadata

If metadata is mutable (controlled by tokenData property permissions), you can update it:

// Only works if tokenData property is mutable
await sdk.token.updateNft({
  collectionId,
  tokenId,
  data: {
    name: "Warrior #1 [Evolved]",
    attributes: [
      { trait_type: "Class", value: "Warrior" },
      { trait_type: "Power", value: 100 }, // Increased!
      { trait_type: "Evolution", value: "Stage 2" }, // New trait
    ],
  },
});

Next steps

For complete technical reference of all metadata fields and types, see: