Skip to main content

Mint an NFT

An NFT is just a native token with a quantity of 1, made permanently unique by a minting policy that can only ever run once. The name, image, and description are attached to the minting transaction as CIP-25 metadata (label 721). This guide mints one and sends it to a wallet, pick your tool below.

New to policies and what makes a token "non-fungible"? Read Minting policies and What are native tokens first. This page is the hands-on version.

What you'll build

  • A minting policy only you can mint from (time-locked, so the supply is provably fixed)
  • One NFT (quantity 1) carrying CIP-25 metadata
  • A transaction that mints it, attaches the metadata, and pays it to a recipient

Prerequisites

  • Test ADA on Preview or Pre-Production (faucet)
  • A provider key (Blockfrost) for the SDK tabs, or a running node for cardano-cli
  • An image pinned to IPFS (the ipfs://... URI goes in the metadata)
CIP-25 or CIP-68?

CIP-25 stores metadata in the minting transaction (label 721). Simplest, and what this guide uses. CIP-68 stores metadata in an on-chain datum that a smart contract can read and update later. Choose CIP-68 only if your NFT's metadata needs to change or be read on-chain. See Token metadata & registry.

Mint it

import {
Address, Assets, NativeScripts, Bytes, TransactionMetadatum,
preprod, Client
} from "@evolution-sdk/evolution"

const client = Client.make(preprod)
.withBlockfrost({
baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
projectId: process.env.BLOCKFROST_API_KEY!,
})
.withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })

const myKeyHash = Bytes.fromHex("abc123def456abc123def456abc123def456abc123def456abc123de")
const mintingPolicy = NativeScripts.makeScriptPubKey(myKeyHash)
const nativeScript = new NativeScripts.NativeScript({ script: mintingPolicy })

const policyId = "abc123def456abc123def456abc123def456abc123def456abc123de"
const assetName = "4d794e4654303031" // "MyNFT001" in hex

let mintAssets = Assets.fromLovelace(0n)
mintAssets = Assets.addByHex(mintAssets, policyId, assetName, 1n)

let sendAssets = Assets.fromLovelace(2_000_000n) // min ADA travels with the NFT
sendAssets = Assets.addByHex(sendAssets, policyId, assetName, 1n)

const nftMetadata = new Map([
[policyId, new Map([
[assetName, new Map([
["name", "My First NFT"],
["image", "ipfs://QmYourImageHashHere"],
["mediaType", "image/png"],
["description", "Minted with Evolution SDK"],
])]
])]
])

const tx = await client
.newTx()
.mintAssets({ assets: mintAssets })
.attachScript({ script: nativeScript })
.attachMetadata({ label: 721n, metadata: nftMetadata }) // 721n, bigint
.payToAddress({ address: Address.fromBech32("addr_test1..."), assets: sendAssets })
.build()

const signed = await tx.sign()
const txHash = await signed.submit()

The builder handles fees, coin selection, and change. mintAssets with quantity 1n is what makes it non-fungible; attachMetadata under 721n is the CIP-25 standard.

Make it a true one-of-one

An NFT derives value from guaranteed scarcity. A time-locked policy (the before slot above, or a time-lock on the native script in the SDK tabs) means no more tokens can ever be minted under that policy once the deadline passes, enforced at the protocol level. Buyers can verify it by inspecting the policy. See Validity intervals.

Updatable metadata: CIP-68

CIP-25 writes the metadata into the minting transaction, where it is permanent and readable only off-chain. CIP-68 instead stores it in an inline datum on a reference token, so it can be updated later and read on-chain by smart contracts through reference inputs. Each asset becomes a pair: a reference token (asset-name label 100) held at a script address carrying the metadata datum, and a user token (label 222) that lives in the holder's wallet. For when to choose it over CIP-25, see Token metadata & registry.

Minting both tokens in one transaction needs a Plutus minting policy and an always-succeed reference-token holder (see Smart contracts). Both SDKs ship CIP-68 helpers:

import { Assets, Bytes, Text, Data, InlineDatum, Address, preprod, Client } from "@evolution-sdk/evolution"
import { CIP68Metadata } from "@evolution-sdk/evolution/plutus"

const client = Client.make(preprod)
.withBlockfrost({ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! })
.withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })

// Metadata lives on the reference token as a typed CIP-68 datum
const metadata = Data.map([
[Text.toBytes("name"), Text.toBytes("CIP-68 Token")],
[Text.toBytes("image"), Text.toBytes("ipfs://QmYourImageHashHere")],
])
const referenceDatum: CIP68Metadata.CIP68Datum = { metadata, version: 1n, extra: [] }

// Asset names carry the CIP-67 label prefix: (100) reference, (222) user
const name = Text.toBytes("MyCIP68Token")
const refNameHex = Bytes.toHex(new Uint8Array([0x00, 0x0f, 0x42, 0x00, ...name]))
const userNameHex = Bytes.toHex(new Uint8Array([0x00, 0x0f, 0x42, 0x02, ...name]))

// Your compiled minting policy and the always-succeed script address holding the reference token
declare const mintingScript: any
declare const policyId: string
const scriptAddress = Address.fromBech32("addr_test1...")

let mintAssets = Assets.fromLovelace(0n)
mintAssets = Assets.addByHex(mintAssets, policyId, refNameHex, 1n)
mintAssets = Assets.addByHex(mintAssets, policyId, userNameHex, 1n)

let refOutput = Assets.fromLovelace(2_000_000n)
refOutput = Assets.addByHex(refOutput, policyId, refNameHex, 1n)

const tx = await client
.newTx()
.mintAssets({ assets: mintAssets, redeemer: Data.constr(0n, []) })
.attachScript({ script: mintingScript })
// reference token (100) -> script address, metadata as its inline datum (the user token goes to change)
.payToAddress({
address: scriptAddress,
assets: refOutput,
datum: new InlineDatum.InlineDatum({ data: CIP68Metadata.Codec.toData(referenceDatum) }),
})
.build()

const signed = await tx.sign()
const txHash = await signed.submit()

Mesh's metadataToCip68 / CIP68_100 / CIP68_222 helpers and Evolution's typed CIP68Metadata schema reach the same result by different routes (helper functions versus a typed codec): encode the metadata as the reference token's datum and apply the CIP-67 label prefixes. To update the metadata later, spend the reference UTXO and recreate it with a new datum.

Royalties: CIP-27

A royalty is recorded as a single token (empty asset name) under metadata label 777, carrying a rate and a recipient address, minted once under the same policy as the NFTs it covers. Marketplaces that honor CIP-27 read label 777 to route a cut of secondary sales to the creator.

Evolution has no royalty-specific helper, so you attach the CIP-27 structure as plain metadata under label 777n:

import { Assets } from "@evolution-sdk/evolution"

// reuse the client and your single-signature native policy from above
const royaltyMetadata = new Map([
["rate", "0.05"], // 5%
["addr", "addr_test1qz..."], // royalty recipient
])

let royaltyToken = Assets.fromLovelace(0n)
royaltyToken = Assets.addByHex(royaltyToken, policyId, "", 1n) // empty asset name

const tx = await client
.newTx()
.mintAssets({ assets: royaltyToken })
.attachScript({ script: nativeScript })
.attachMetadata({ label: 777n, metadata: royaltyMetadata })
.build()

const signed = await tx.sign()
const txHash = await signed.submit()

Common pitfalls

ProblemCauseFix
NFT not showing in walletmetadata structure mismatchpolicy ID and asset name in metadata must exactly match the minted token
"Minting not allowed"wrong key signedthe signing key's hash must match the policy
Type error on label (Evolution)721 instead of 721nuse the bigint 721n
Min UTxO too lownot enough ADA with the NFTinclude about 2 ADA in the NFT output

Next steps