Tutorial: Mint an NFT
Mint a Cardano NFT with CIP-25 metadata — from minting policy to on-chain token
Tutorial: Mint an NFT
Mint a unique NFT on Cardano with CIP-25 metadata — a name, image, and description stored on-chain. This tutorial uses a native script minting policy (no Plutus required) and attaches standard NFT metadata.
What You'll Build
- A native script minting policy that only you can mint from
- An NFT (quantity 1) with CIP-25 metadata (name, image, description)
- A transaction that mints the token, attaches metadata, and sends it to a recipient
Prerequisites
- A Blockfrost API key (get one free)
- Basic familiarity with minting tokens and native scripts
Step 1: Create a Minting Policy
A simple native script policy — only your key can mint:
import { NativeScripts, Bytes } from "@evolution-sdk/evolution"
// Your key hash (28 bytes) — from your wallet
const myKeyHash = Bytes.fromHex(
"abc123def456abc123def456abc123def456abc123def456abc123de"
)
// Minting policy: only this key can authorize minting
const mintingPolicy = NativeScripts.makeScriptPubKey(myKeyHash)
const nativeScript = new NativeScripts.NativeScript({ script: mintingPolicy })
// The policy ID = blake2b-224 hash of the script CBOR
// After minting, you can find it in the transaction output or compute it offline
// For this tutorial, we'll use the known policy ID in subsequent stepsFor a one-time mint (true NFT uniqueness), add a time-lock to the policy. After the deadline passes, nobody can mint more tokens under this policy. See Native Scripts for time-lock examples.
Step 2: Structure CIP-25 Metadata
CIP-25 defines the standard metadata format for Cardano NFTs. It uses transaction metadata label 721:
import { TransactionMetadatum } from "@evolution-sdk/evolution"
// Your policy ID (28 bytes = 56 hex chars)
const policyId = "abc123def456abc123def456abc123def456abc123def456abc123de"
// Asset name in hex — e.g., "MyNFT001" → hex
const assetNameHex = "4d794e4654303031" // "MyNFT001" in hex
// CIP-25 metadata structure:
// { 721: { <policyId>: { <assetName>: { name, image, ... } } } }
const nftMetadata = new Map<TransactionMetadatum.TransactionMetadatum, TransactionMetadatum.TransactionMetadatum>([
[policyId, new Map<TransactionMetadatum.TransactionMetadatum, TransactionMetadatum.TransactionMetadatum>([
[assetNameHex, new Map<TransactionMetadatum.TransactionMetadatum, TransactionMetadatum.TransactionMetadatum>([
["name", "My First NFT"],
["image", "ipfs://QmYourImageHashHere"],
["mediaType", "image/png"],
["description", "My first NFT minted with Evolution SDK"],
])]
])]
])CIP-25 Required Fields
| Field | Type | Description |
|---|---|---|
name | string | Display name of the NFT |
image | string | URI to the image (typically ipfs://...) |
CIP-25 Optional Fields
| Field | Type | Description |
|---|---|---|
mediaType | string | MIME type of the image (e.g., image/png) |
description | string | Human-readable description |
files | array | Additional files (for multi-asset NFTs) |
Step 3: Mint, Attach Metadata, and Send
Put it all together — mint the NFT, attach CIP-25 metadata, and send to a recipient:
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 })
// --- Policy ---
const myKeyHash = Bytes.fromHex("abc123def456abc123def456abc123def456abc123def456abc123de")
const mintingPolicy = NativeScripts.makeScriptPubKey(myKeyHash)
const nativeScript = new NativeScripts.NativeScript({ script: mintingPolicy })
// --- NFT identity ---
const policyId = "abc123def456abc123def456abc123def456abc123def456abc123de" // script hash
const assetName = "4d794e4654303031" // "MyNFT001" in hex
// --- Mint assets (quantity 1 = NFT) ---
let mintAssets = Assets.fromLovelace(0n)
mintAssets = Assets.addByHex(mintAssets, policyId, assetName, 1n)
// --- Send assets (NFT + min ADA for UTxO) ---
let sendAssets = Assets.fromLovelace(2_000_000n)
sendAssets = Assets.addByHex(sendAssets, policyId, assetName, 1n)
// --- CIP-25 metadata ---
const nftMetadata = new Map<TransactionMetadatum.TransactionMetadatum, TransactionMetadatum.TransactionMetadatum>([
[policyId, new Map<TransactionMetadatum.TransactionMetadatum, TransactionMetadatum.TransactionMetadatum>([
[assetName, new Map<TransactionMetadatum.TransactionMetadatum, TransactionMetadatum.TransactionMetadatum>([
["name", "My First NFT"],
["image", "ipfs://QmYourImageHashHere"],
["mediaType", "image/png"],
["description", "Minted with Evolution SDK"],
])]
])]
])
const recipient = Address.fromBech32(
"addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63"
)
// --- Build transaction ---
const tx = await client
.newTx()
.mintAssets({ assets: mintAssets }) // mint 1 NFT
.attachScript({ script: nativeScript }) // attach minting policy
.attachMetadata({ label: 721n, metadata: nftMetadata }) // CIP-25 metadata
.payToAddress({ address: recipient, assets: sendAssets }) // send NFT to recipient
.build()
const signed = await tx.sign()
const txHash = await signed.submit()
// txHash → your NFT is now on-chain!How It Works
mintAssets— creates 1 token under your policy ID (quantity 1 = non-fungible)attachScript— includes the native script so the ledger can verify your minting authorityattachMetadata— adds CIP-25 metadata under label 721 (the NFT metadata standard)payToAddress— sends the minted NFT + min ADA to the recipient
The builder handles fee calculation, coin selection, and change automatically.
Common Pitfalls
Metadata label must be 721n (bigint). Using 721 (number) will cause a type error. CIP-25 requires label 721.
| Problem | Cause | Fix |
|---|---|---|
| NFT not showing in wallet | Wrong metadata structure | Ensure policy ID and asset name in metadata match the minted token exactly |
| "Minting not allowed" | Wrong key signed | Ensure the signing wallet's key hash matches the minting policy |
| Missing image | Invalid IPFS URI | Use full ipfs://Qm... format, pin the file first |
| Type error on label | Using number instead of bigint | Use 721n not 721 |
| Min UTxO too low | Not enough ADA with the NFT | Include at least 2 ADA with the NFT output |
Next Steps
- Minting Tokens — Plutus minting policies and burning
- Native Scripts — Time-locked policies for one-time mints
- Asset Metadata — More metadata patterns (CIP-20 messages)
- Blueprint Codegen — Generate types from Aiken validators