Skip to main content

Minting NFTs


There are many ways to realize NFTs with Cardano. However, in this guide, we will concentrate on the most dominant way, to attach storage references of other services like IPFS to our tokens.

What's the difference?#

What is the difference between native assets and NFTs?
From a technical point of view, NFTs are the same as native assets. But some additional characteristics make a native asset truly an NFT:

  1. As the name states - it must be 'non-fungible. This means you need to have unique identifiers or attributes attached to a token to make it distinguishable from others.
  2. Most of the time, NFT's should live on the blockchain forever. Therefore we need some mechanism to ensure an NFT stays unique and can not be duplicated.

The policyID#

Native assets in Cardano feature the following characteristics:

  1. An amount/value (how much are there?)
  2. A name
  3. A unique policyID

Since asset names are not unique and can be easily duplicated, Cardano NFTs need to be identified by the policyID.
This ID is unique and attached permanently to the asset. The policy ID stems from a policy script that defines characteristics such as who can mint tokens and when those actions can be made.

Many NFT projects make the policyID under which the NFTs were minted publicly available, so anyone can differentiate fraudulent/duplicate NFTs from the original tokens.

Some services even offer to register your policyID to detect tokens that feature the same attributes as your token but were minted under a different policy.

Metadata attributes#

In addition to the unique policyID we can also attach metadata with various attributes to a transaction.

Here is an example from

{  "721": {    "{policy_id}": {      "{policy_name}": {        "name": "<required>",        "description": "<optional>",        "sha256": "<required>",        "type": "<required>",        "image": "<required>",        "location": {          "ipfs": "<required>",          "https": "<optional>",          "arweave": "<optional>"        }      }    }  }}

Metadata helps us to display things like image URIs and stuff that truly makes it an NFT. With this workaround of attaching metadata, third-party platforms like can easily trace back to the last minting transaction, read the metadata, and query images and attributes accordingly. The query would look something like this:

  1. Get asset name and policyID.
  2. Look up the latest minting transaction of this asset.
  3. Check the metadata for label 721.
  4. Match the asset name and (in this case) the {policy_name}-entry.
  5. Query the IPFS hash and all other attributes to the corresponding entry.

There is currently no agreed standard as to how an NFT or the metadata is defined. However, there is a Cardano Improvement Proposal if you want to follow the discussion.

Time locking#

Since NFTs are likely to be traded or sold, they should follow a more strict policy. Most of the time, a value is defined by the (artificial) scarcity of an asset.

You can regulate such factors with multi-signature scripts.

For this guide, we will choose the following constraints:

  1. There should be only one defined signature allowed to mint (or burn) the NFT.
  2. The signature will expire in 10000 blocks from now to leave the room if we screw something up.


Apart from the same requisites as on the minting native assets guide, we will additionally need:

  1. Obviously, what / how many NFTs you want to make.
    --> We are going to make only one NFT
  2. An already populated metadata.json
  3. Know how your minting policy should look like. --> Only one signature allowed (which we will create in this guide)
    --> No further minting or burning of the asset allowed after 10000 blocks have passed since the transaction was made
  4. Hash if uploaded image to IPFS
    --> We will use this image

We recommend upload images to IPFS as it is the most common decentralized storage service. There are alternatives, but IFPS has the biggest adoption in terms of how many NFTs got minted.


Since the creation of native assets is documented extensively in the minting chapter, we won't go into much detail here. Here's a little recap and needed setup

Working directory#

First of all, we are going to set up a new working directory and change into it.

mkdir nftcd nft/

Set variables#

We will set important values in a more readable variable for better readability and debugging of failed transactions.

tokenname="NFT1"tokenamount="1"fee="0"output="0"ipfs_hash="please insert your ipfs hash here"

The IFPS hash is a key requirement and can be found once you upload your image to IPFS. Here's an example of how the IPFS looks like when an image is uploaded in pinata img

Generate keys and address#

We will be generating new keys and a new payment address:

cardano-cli address key-gen --verification-key-file payment.vkey --signing-key-file payment.skey

Those two keys can now be used to generate an address.

cardano-cli address build --payment-verification-key-file payment.vkey --out-file payment.addr --mainnet

We will save our address hash in a variable called address.

address=$(cat payment.addr)

Fund the address#

Submitting transactions always require you to pay a fee. Sending native assets requires sending at least 1 ada.
So make sure the address you are going to use as the input for the minting transaction has sufficient funds. For our example, the newly generated address was funded with 10 ada.

cardano-cli query utxo --address $address --mainnet

You should see something like this.

                           TxHash                                 TxIx        Amount--------------------------------------------------------------------------------------974e98c4529f8fc75fa8baf5618f7b5ade81aa9ed29ce33cd1c2f2e70838180e     0        10000000 lovelace

Export protocol parameters#

For our transaction calculations, we need some of the current protocol parameters. The parameters can be saved in a file called protocol.json with this command:

cardano-cli query protocol-parameters --mainnet --out-file protocol.json

Creating the policyID#

Just as in generating native assets, we will need to generate some policy-related files like key pairs and a policy script.

mkdir policy

We don’t change into this directory, and everything is done from our working directory.

Generate a new set of key pairs:

cardano-cli address key-gen \    --verification-key-file policy/policy.vkey \    --signing-key-file policy/policy.skey

Instead of only defining a single signature (as we did in the native asset minting guide), our script file needs to implement the following characteristics (which we defined above):

  1. Only one signature allowed
  2. No further minting or burning of the asset allowed after 10000 blocks have passed since the transaction was made

For this specific purpose policy.script file which will look like this:

{  "type": "all",  "scripts":  [    {      "type": "before",      "slot": <insert slot here>    },    {      "type": "sig",      "keyHash": "insert keyHash here"    }  ]}

As you can see, we need to adjust two values here, the slot number as well as the keyHash.

To set everything at once and copy and paste it, use this command(s): You need to have the jq installed to parse the tip correctly!

echo "{" >> policy/policy.scriptecho "  \"type\": \"all\"," >> policy/policy.script echo "  \"scripts\":" >> policy/policy.script echo "  [" >> policy/policy.script echo "   {" >> policy/policy.script echo "     \"type\": \"before\"," >> policy/policy.script echo "     \"slot\": $(expr $(cardano-cli query tip --mainnet | jq .slot?) + 10000)" >> policy/policy.scriptecho "   }," >> policy/policy.script echo "   {" >> policy/policy.scriptecho "     \"type\": \"sig\"," >> policy/policy.script echo "     \"keyHash\": \"$(cardano-cli address key-hash --payment-verification-key-file policy/policy.vkey)\"" >> policy/policy.script echo "   }" >> policy/policy.scriptecho "  ]" >> policy/policy.script echo "}" >> policy/policy.script

If this command is not working, please set the key hash and correct slot manually.

To generate the keyHash, use the following command:

cardano-cli address key-hash --payment-verification-key-file policy/policy.vkey

To calculate the correct slot, query the current block and add 10000 to it:

cardano-cli query tip --mainnet

Make a new file called policy.script in the policy folder

touch policy/policy.script

Paste the JSON from above, populated with your keyHash and your slot number into it

nano policy/policy.script

Be aware the slot number is defined as an integer and therefore needs no double quotation marks, whereas the keyHash is defined as a string and needs to be wrapped in double quotation marks.

Please take note of your slot number and save it in a variable.

slotnumber="Replace this with your slot number"

And save the location of the script file into a variable as well.


The last step is to generate the policyID:

cardano-cli transaction policyid --script-file ./policy/policy.script >> policy/policyID


Since we now have our policy as well as our policyID defined, we need to adjust our metadata information.

Here’s an example of the metadata.json which we’ll use for this guide:

{        "721": {            "please_insert_policyID_here": {              "NFT1": {                "description": "This is my first NFT thanks to the Cardano foundation",                "name": "Cardano foundation NFT guide token",                "id": 1,                "image": ""              }            }        }}

The third element in the hierarchy needs to have the same name as our NFT native asset.

Save this file as metadata.json.

If you want to generate it "on the fly," use the following commands:

echo "{" >> metadata.jsonecho "  \"721\": {" >> metadata.json echo "    \"$(cat policy/policyID)\": {" >> metadata.json echo "      \"$(echo $tokenname)\": {" >> metadata.jsonecho "        \"description\": \"This is my first NFT thanks to the Cardano foundation\"," >> metadata.jsonecho "        \"name\": \"Cardano foundation NFT guide token\"," >> metadata.jsonecho "        \"id\": \"1\"," >> metadata.jsonecho "        \"image\": \"ipfs://$(echo $ipfs_hash)\"" >> metadata.jsonecho "      }" >> metadata.jsonecho "    }" >> metadata.json echo "  }" >> metadata.json echo "}" >> metadata.json

Please make sure the image value / IFPS hash is set with the correct protocol pre-fix ipfs://
(for example "ipfs://QmRhTTbUrPYEw3mJGGhQqQST9k86v1DPBiTTWJGKDJsVFw")

Crafting the transaction#

Let's begin building our transaction. Before we start, we will again need some setup to make the transaction building easier. Query your payment address and take note of the different values present.

cardano-cli query utxo --address $address --mainnet

Your output should look something like this (fictional example):

                           TxHash                                 TxIx        Amount--------------------------------------------------------------------------------------b35a4ba9ef3ce21adcd6879d08553642224304704d206c74d3ffb3e6eed3ca28     0        1000000000 lovelace

Since we need each of those values in our transaction, we will store them individually in a corresponding variable.

txhash="insert your txhash here"txix="insert your TxIx here"funds="insert Amount in lovelace here"policyid=$(cat policy/policyID)

If you're unsure, check if all of the other needed variables for the transaction are set:

echo $feeecho $addressecho $outputecho $tokenamountecho $policyidecho $tokennameecho $slotnumberecho $script

If everything is set, run this command to generate a raw transaction file.

cardano-cli transaction build-raw \--fee $fee  \--tx-in $txhash#$txix  \--tx-out $address+$output+"$tokenamount $policyid.$tokenname" \--mint="$tokenamount $policyid.$tokenname" \--minting-script-file $script \--metadata-json-file metadata.json  \--invalid-hereafter $slotnumber \--out-file matx.raw

As with every other transaction, we need to calculate the fee and the output and save them in the corresponding variables (currently set to zero).

Use this command to set the $fee variable.

fee=$(cardano-cli transaction calculate-min-fee --tx-body-file matx.raw --tx-in-count 1 --tx-out-count 1 --witness-count 1 --mainnet --protocol-params-file protocol.json | cut -d " " -f1)

And this command calculates the correct value for $output.

output=$(expr $funds - $fee)

With the newly set values, re-issue the building of the raw transaction.

cardano-cli transaction build-raw \--fee $fee  \--tx-in $txhash#$txix  \--tx-out $address+$output+"$tokenamount $policyid.$tokenname" \--mint="$tokenamount $policyid.$tokenname" \--minting-script-file $script \--metadata-json-file metadata.json  \--invalid-hereafter $slotnumber \--out-file matx.raw

Sign the transaction

cardano-cli transaction sign  \--signing-key-file payment.skey  \--signing-key-file policy/policy.skey  \--mainnet --tx-body-file matx.raw  \--out-file matx.signed

The signed transaction will be saved in a new file called matx.signed instead of matx.raw.

Now we are going to submit the transaction, therefore minting our native assets:

cardano-cli transaction submit --tx-file matx.signed --mainnet

Congratulations, we have now successfully minted our own token. After a couple of seconds, we can check the output address

cardano-cli query utxo --address $address --mainnet

and should see something like this:

Displaying your NFT#

One of the most adopted NFT browsers is Enter your address in the search bar, hit enter, and your NFT will be displayed with all its attributes and the corresponding image.


You can check it out yourself and see the NFT created for this tutorial here.

Burn your token#

If you messed something up and want to re-start, you can always burn your token if the slot defined in your policy script isn't over yet. Assuming you have still every variable set, you need to re-set:

burnfee="0"burnoutput="0"txhash="Insert your utxo holding the NFT"txix="Insert your txix"

The transaction looks like this:

cardano-cli transaction build-raw --fee $burnfee --tx-in $txhash#$txix --tx-out $address+$burnoutput --mint="-1 $policyid.$tokenname" --minting-script-file $script --invalid-hereafter $slot --out-file burning.raw

The minting parameter is now called with a negative value, therefore destroying one token.

Calculate the fee to burn the token.

burnfee=$(cardano-cli transaction calculate-min-fee --tx-body-file burning.raw --tx-in-count 1 --tx-out-count 1 --witness-count 1 --mainnet --protocol-params-file protocol.json | cut -d " " -f1)

Calculate the leftovers/output.

burnoutput=$(expr $funds - $burnfee)

Re-run the transaction build.

cardano-cli transaction build-raw --fee $burnfee --tx-in $txhash#$txix --tx-out $address+$burnoutput --mint="-1 $policyid.$tokenname" --minting-script-file $script --invalid-hereafter 33005389 --out-file burning.raw

Sign the transaction.

cardano-cli transaction sign  --signing-key-file payment.skey  --signing-key-file policy/policy.skey --mainnet  --tx-body-file burning.raw --out-file burning.signed

Full send.

cardano-cli transaction submit --tx-file burning.signed --mainnet