Skip to content

Non-fungible token collection with unique token IDs, metadata URIs, and badge-based ownership

NFT Collection

Instruction text for agents that use the NFT Collection skill, loaded by bb dev skills nft-collection and the MCP get_skill_instructions tool.

Try it

Paste this into Claude Code, Codex, or Cursor with the BitBadges MCP server wired. The agent loads the skill, builds, verifies, and hands back a link to review and sign.

text
Load the nft-collection skill and build me one: non-fungible token collection with unique token IDs, metadata URIs, and badge-based ownership. Use the session tools, run validate, review, and simulate in parallel, fix anything critical, then call get_review_url and give me the link.

From a shell, bb dev skills nft-collection prints the same instructions.

Summary

Required standards: ["NFTs"]

  • For tradable NFTs: ["NFTs", "NFTMarketplace", "NFTPricingDenom:ubadge"]

  • validTokenIds: set to the range of unique token IDs (e.g. [{ "start": "1", "end": "100" }])

  • Each token ID represents a unique NFT; amount in transfers is typically "1"

  • Use {id} placeholder in tokenMetadata URI for per-token metadata (e.g. "ipfs://QmHash/{id}")

  • Mint approvals must have overridesFromOutgoingApprovals: true

  • Ownership times are usually forever for NFTs

Instructions

NFT Collection Configuration

When creating an NFT collection, follow this pattern:

Required Configuration

  1. Standards: Include "NFTs" in the standards array

    • Example: "standards": ["NFTs"]
    • For tradable NFTs: "standards": ["NFTs", "NFTMarketplace", "NFTPricingDenom:ubadge"]
  2. validTokenIds: Set to the range of unique token IDs

    • Example for 100 NFTs: [{ "start": "1", "end": "100" }]
    • Each token ID represents a unique NFT
  3. Token Metadata: Each tokenMetadata entry must include tokenIds matching the range

    • Use {id} placeholder for per-token metadata URIs

Pattern Example

json
{
  "updateValidTokenIds": true,
  "validTokenIds": [{ "start": "1", "end": "100" }],
  "updateCollectionMetadata": true,
  "collectionMetadata": {
    "uri": "ipfs://QmCollectionMetadata"
  },
  "updateTokenMetadata": true,
  "tokenMetadata": [{
    "uri": "ipfs://QmTokenMetadata/{id}",
    "tokenIds": [{ "start": "1", "end": "100" }]
  }],
  "updateCollectionApprovals": true,
  "collectionApprovals": [{
    "fromListId": "Mint",
    "toListId": "All",
    "initiatedByListId": "bb1creator...",
    "approvalId": "manager-mint",
    "tokenIds": [{ "start": "1", "end": "100" }],
    "transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
    "ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
    "approvalCriteria": {
      "overridesFromOutgoingApprovals": true
    }
  }],
  "updateStandards": true,
  "standards": ["NFTs"]
}
{
  "updateValidTokenIds": true,
  "validTokenIds": [{ "start": "1", "end": "100" }],
  "updateCollectionMetadata": true,
  "collectionMetadata": {
    "uri": "ipfs://QmCollectionMetadata",
    "customData": ""
  },
  "updateTokenMetadata": true,
  "tokenMetadata": [{
    "uri": "ipfs://QmTokenMetadata/{id}",
    "customData": "",
    "tokenIds": [{ "start": "1", "end": "100" }]
  }],
  "updateCollectionApprovals": true,
  "collectionApprovals": [{
    "fromListId": "Mint",
    "toListId": "All",
    "initiatedByListId": "bb1creator...",
    "approvalId": "manager-mint",
    "tokenIds": [{ "start": "1", "end": "100" }],
    "transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
    "ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
    "approvalCriteria": {
      "overridesFromOutgoingApprovals": true
    }
  }],
  "updateStandards": true,
  "standards": ["NFTs"]
}

Sequential Minting with predeterminedBalances

For NFTs, use predeterminedBalances with incrementTokenIdsBy: "1" to mint tokens sequentially (token 1, then 2, then 3, etc.):

json
{
  "approvalCriteria": {
    "overridesFromOutgoingApprovals": true,
    "predeterminedBalances": {
      "incrementedBalances": {
        "startBalances": [{ "amount": "1", "tokenIds": [{ "start": "1", "end": "1" }], "ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }] }],
        "incrementTokenIdsBy": "1",
        "recurringOwnershipTimes": { "startTime": "0", "intervalLength": "0", "chargePeriodLength": "0" }
      },
      "orderCalculationMethod": {
        "useOverallNumTransfers": true
      }
    },
    "maxNumTransfers": {
      "overallMaxNumTransfers": "100",
      "perInitiatedByAddressMaxNumTransfers": "1",
      "amountTrackerId": "nft-mint-tracker",
      "resetTimeIntervals": { "startTime": "0", "intervalLength": "0" }
    }
  }
}
{
  "approvalCriteria": {
    "overridesFromOutgoingApprovals": true,
    "predeterminedBalances": {
      "manualBalances": [],
      "incrementedBalances": {
        "startBalances": [{ "amount": "1", "tokenIds": [{ "start": "1", "end": "1" }], "ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }] }],
        "incrementTokenIdsBy": "1",
        "incrementOwnershipTimesBy": "0",
        "durationFromTimestamp": "0",
        "allowOverrideTimestamp": false,
        "recurringOwnershipTimes": { "startTime": "0", "intervalLength": "0", "chargePeriodLength": "0" },
        "allowOverrideWithAnyValidToken": false
      },
      "orderCalculationMethod": {
        "useOverallNumTransfers": true,
        "usePerToAddressNumTransfers": false,
        "usePerFromAddressNumTransfers": false,
        "usePerInitiatedByAddressNumTransfers": false,
        "useMerkleChallengeLeafIndex": false,
        "challengeTrackerId": ""
      }
    },
    "maxNumTransfers": {
      "overallMaxNumTransfers": "100",
      "perInitiatedByAddressMaxNumTransfers": "1",
      "perToAddressMaxNumTransfers": "0",
      "perFromAddressMaxNumTransfers": "0",
      "amountTrackerId": "nft-mint-tracker",
      "resetTimeIntervals": { "startTime": "0", "intervalLength": "0" }
    }
  }
}

Key: incrementTokenIdsBy: "1" means each mint gets the next sequential token ID. Use maxNumTransfers to cap total mints and per-user mints.

Important: predeterminedBalances and approvalAmounts are incompatible, use one or the other. NFTs use predeterminedBalances (not approvalAmounts).

NFT-Specific Gotchas

  • Each token ID is unique and represents a distinct NFT
  • Amount in transfers is typically "1" (one NFT per transfer)
  • Ownership times are usually forever for NFTs
  • Mint approvals must have overridesFromOutgoingApprovals: true
  • Use {id} in metadata URIs for per-token metadata

Common Mistakes

  • Don't reuse token IDs across editions without understanding ownership times, each token ID is unique and represents a distinct NFT.
  • Don't forget tokenIds in canUpdateTokenMetadata permission structure, the permission must specify which token ID ranges it covers.
  • Don't use {id} in metadata name, description, or image fields, the {id} placeholder only works in the URI string itself (e.g. "ipfs://abc/{id}").
  • Don't forget overridesFromOutgoingApprovals: true on Mint approvals, required for all minting operations.
  • Don't use custom list IDs, only reserved IDs are valid: "All", "Mint", "!Mint", "AllWithoutMint", or direct bb1... addresses.

Edit this page on GitHub

For agents

How agents read these docs