Building NFTs on Hedera: A Complete Guide Using the Token Services
Hedera Hashgraph is one of the most underrated public ledgers for NFT projects. As a Blockchain Full Stack Developer I have worked with Ethereum, Solana, and Hedera – and for high-throughput, low-cost NFT use cases, Hedera consistently wins.
In this guide I will walk you through creating and minting an NFT collection on Hedera using the Hedera Token Service (HTS) – no EVM smart contract needed.
Why Hedera for NFTs?
| Feature | Ethereum | Hedera |
|---|---|---|
| Transaction fee | $1–$50+ | ~$0.001 |
| Finality time | ~12 seconds | ~3 seconds |
| Energy usage | Moderate | Carbon-negative |
| Throughput | ~15 TPS | 10,000+ TPS |
| NFT standard | ERC-721 | HTS native |
Hedera achieves this through the hashgraph consensus algorithm – a DAG-based gossip protocol that gives deterministic finality in seconds.
Setting Up the Hedera SDK
Install the official JavaScript SDK:
npm install @hashgraph/sdk
Create a client connected to testnet:
import { Client, AccountId, PrivateKey } from "@hashgraph/sdk";
const operatorId = AccountId.fromString(process.env.OPERATOR_ID);
const operatorKey = PrivateKey.fromStringECDSA(process.env.OPERATOR_KEY);
const client = Client.forTestnet();
client.setOperator(operatorId, operatorKey);
For mainnet just replace Client.forTestnet() with Client.forMainnet().
Step 1: Create the NFT Token Type
Before you can mint individual NFTs you must create the token type – think of this as deploying your collection contract on Ethereum, except there is no contract.
import {
TokenCreateTransaction,
TokenType,
TokenSupplyType,
} from "@hashgraph/sdk";
const createTx = await new TokenCreateTransaction()
.setTokenName("MrBns Genesis Collection")
.setTokenSymbol("MBNS")
.setTokenType(TokenType.NonFungibleUnique) // NFT
.setSupplyType(TokenSupplyType.Finite)
.setMaxSupply(100) // hard cap of 100 NFTs
.setTreasuryAccountId(operatorId)
.setSupplyKey(operatorKey) // key allowed to mint/burn
.setAdminKey(operatorKey) // key allowed to update the token
.execute(client);
const receipt = await createTx.getReceipt(client);
const tokenId = receipt.tokenId;
console.log(`Collection created: ${tokenId}`); // e.g. 0.0.1234567
Step 2: Mint Individual NFTs
Each NFT on Hedera carries metadata – a byte array typically pointing to an IPFS JSON file following the HIP-412 metadata standard.
import { TokenMintTransaction } from "@hashgraph/sdk";
// IPFS CID of your metadata JSON
const metadata = Buffer.from("ipfs://QmYourCID/1.json");
const mintTx = await new TokenMintTransaction()
.setTokenId(tokenId)
.addMetadata(metadata) // one addMetadata per NFT
.execute(client);
const mintReceipt = await mintTx.getReceipt(client);
const serial = mintReceipt.serials[0];
console.log(`Minted NFT serial #${serial}`);
Batch minting (up to 10 per transaction):
const batchMint = new TokenMintTransaction().setTokenId(tokenId);
for (let i = 1; i <= 10; i++) {
batchMint.addMetadata(Buffer.from(`ipfs://QmYourCID/${i}.json`));
}
await batchMint.execute(client);
Step 3: Associate and Transfer an NFT
Before an account can receive an HTS token it must associate with it (a security feature that prevents spam airdrops):
import {
TokenAssociateTransaction,
TransferTransaction,
NftId,
} from "@hashgraph/sdk";
const recipientId = AccountId.fromString("0.0.9999999");
const recipientKey = PrivateKey.fromStringECDSA(process.env.RECIPIENT_KEY);
// Associate recipient with the token
const assocTx = await new TokenAssociateTransaction()
.setAccountId(recipientId)
.setTokenIds([tokenId])
.freezeWith(client)
.sign(recipientKey);
await assocTx.execute(client);
// Transfer NFT serial #1 to recipient
const transferTx = await new TransferTransaction()
.addNftTransfer(new NftId(tokenId, serial), operatorId, recipientId)
.execute(client);
const transferReceipt = await transferTx.getReceipt(client);
console.log(`Transfer status: ${transferReceipt.status}`);
Step 4: Query NFT Info
You can read on-chain metadata and ownership without any indexer:
import { TokenNftInfoQuery } from "@hashgraph/sdk";
const nftInfo = await new TokenNftInfoQuery()
.setNftId(new NftId(tokenId, serial))
.execute(client);
console.log({
accountId: nftInfo[0].accountId.toString(),
metadata: Buffer.from(nftInfo[0].metadata).toString(),
});
Metadata Standard (HIP-412)
Follow HIP-412 for marketplace compatibility. Your IPFS JSON should look like:
{
"name": "MrBns Genesis #1",
"description": "A genesis NFT from MrBns – Mr Binary Sniper",
"image": "ipfs://QmImageCID/1.png",
"type": "image/png",
"properties": {
"rarity": "legendary",
"creator": "MrBns"
}
}
Summary
Hedera’s Token Service gives you a production-ready NFT system with:
- Sub-cent transaction fees
- 3-second finality
- No smart contract deployment risk
- Carbon-negative footprint
- Built-in royalty enforcement (via
setRoyaltyFee)
As MrBns I use HTS for any project where cost, speed, and reliability matter more than EVM compatibility. In the next article I will cover the Hedera Token Service for fungible tokens – stablecoins, reward tokens, and governance tokens.