| name | hedera-token-service |
| description | How to create, manage, and transfer tokens on Hedera using the Hiero JavaScript SDK (@hiero-ledger/sdk). Use this skill whenever the user wants to work with fungible tokens, NFTs, token creation, minting, burning, transfers, token association, custom fees (fixed, fractional, royalty), airdrops, KYC/freeze/wipe/pause operations, or any HTS (Hedera Token Service) operation in JavaScript or TypeScript. Also trigger when users mention @hashgraph/sdk token operations, ERC-20/ERC-721 equivalents on Hedera, or tokenization on the Hedera network. |
Hedera Token Service (HTS) — JavaScript SDK
HTS is Hedera's native token engine. It lets you create and manage fungible tokens and NFTs without writing smart contracts. Tokens created through HTS are first-class network entities with built-in compliance controls (KYC, freeze, wipe, pause) and custom fee schedules.
Setup
All imports come from @hiero-ledger/sdk. Two setup patterns exist:
Client + setOperator (direct)
import { Client, AccountId, PrivateKey } from "@hiero-ledger/sdk";
const client = Client.forName(process.env.HEDERA_NETWORK)
.setOperator(
AccountId.fromString(process.env.OPERATOR_ID),
PrivateKey.fromStringECDSA(process.env.OPERATOR_KEY),
);
Wallet + LocalProvider (signer-based)
import { Wallet, LocalProvider } from "@hiero-ledger/sdk";
const provider = new LocalProvider();
const wallet = new Wallet(process.env.OPERATOR_ID, process.env.OPERATOR_KEY, provider);
With the signer pattern, use freezeWithSigner(wallet), signWithSigner(wallet), executeWithSigner(wallet), and getReceiptWithSigner(wallet) instead of freezeWith(client) / execute(client).
Transaction Lifecycle
Every transaction follows this flow: configure -> freeze -> sign -> execute -> receipt.
const tx = await new SomeTransaction()
.setSomeField(value)
.freezeWith(client);
await tx.sign(privateKey);
const response = await tx.execute(client);
const receipt = await response.getReceipt(client);
When only the operator key is needed, you can skip explicit freeze/sign — execute(client) handles it:
const response = await new SomeTransaction()
.setSomeField(value)
.execute(client);
const receipt = await response.getReceipt(client);
Creating Tokens
Fungible Token
import {
TokenCreateTransaction, TokenType, TokenSupplyType,
PrivateKey, Hbar,
} from "@hiero-ledger/sdk";
const supplyKey = PrivateKey.generateECDSA();
const { tokenId } = await (
await new TokenCreateTransaction()
.setTokenName("My Token")
.setTokenSymbol("MTK")
.setDecimals(2)
.setInitialSupply(10000)
.setTreasuryAccountId(operatorId)
.setAdminKey(operatorKey)
.setSupplyKey(supplyKey)
.execute(client)
).getReceipt(client);
NFT (Non-Fungible Token)
NFTs require TokenType.NonFungibleUnique. They have no decimals and no initial supply — you mint individual serials after creation.
const { tokenId: nftId } = await (
await new TokenCreateTransaction()
.setTokenName("My NFT Collection")
.setTokenSymbol("MNFT")
.setTokenType(TokenType.NonFungibleUnique)
.setSupplyType(TokenSupplyType.Finite)
.setMaxSupply(1000)
.setTreasuryAccountId(treasuryId)
.setAdminKey(adminKey)
.setSupplyKey(supplyKey)
.execute(client)
).getReceipt(client);
Minting
import { TokenMintTransaction } from "@hiero-ledger/sdk";
await new TokenMintTransaction()
.setTokenId(tokenId)
.setAmount(500)
.execute(client);
const { serials } = await (
await new TokenMintTransaction()
.setTokenId(nftId)
.addMetadata(Buffer.from("ipfs://QmFirst..."))
.addMetadata(Buffer.from("ipfs://QmSecond..."))
.execute(client)
).getReceipt(client);
Token Association
Hedera accounts must associate with a token before they can receive it (unless the account has automatic token associations enabled via setMaxAutomaticTokenAssociations).
import { TokenAssociateTransaction } from "@hiero-ledger/sdk";
await (
await (
await new TokenAssociateTransaction()
.setAccountId(recipientId)
.setTokenIds([tokenId])
.freezeWith(client)
).sign(recipientKey)
).execute(client);
Alternatively, set setMaxAutomaticTokenAssociations(-1) on account creation to allow unlimited auto-association.
Transfers
Use TransferTransaction for both fungible tokens and NFTs. Amounts are signed: negative debits, positive credits. They must net to zero per token.
import { TransferTransaction } from "@hiero-ledger/sdk";
await new TransferTransaction()
.addTokenTransfer(tokenId, senderId, -100)
.addTokenTransfer(tokenId, receiverId, 100)
.execute(client);
await new TransferTransaction()
.addNftTransfer(nftId, 1, senderId, receiverId)
.execute(client);
await new TransferTransaction()
.addHbarTransfer(senderId, new Hbar(-5))
.addHbarTransfer(receiverId, new Hbar(5))
.addTokenTransfer(tokenId, senderId, -50)
.addTokenTransfer(tokenId, receiverId, 50)
.execute(client);
Key Roles
Each key is optional. If omitted at creation, that capability is permanently disabled.
| Key | Purpose |
|---|
adminKey | Update/delete the token; rotate other keys |
supplyKey | Mint and burn |
freezeKey | Freeze/unfreeze accounts from transferring this token |
kycKey | Grant/revoke KYC status on accounts |
wipeKey | Wipe token balance from an account |
pauseKey | Pause/unpause all token operations globally |
feeScheduleKey | Update the custom fee schedule |
metadataKey | Update token or NFT metadata |
Compliance Operations
await new TokenGrantKycTransaction()
.setTokenId(tokenId).setAccountId(accountId).execute(client);
await new TokenFreezeTransaction()
.setTokenId(tokenId).setAccountId(accountId).execute(client);
await new TokenUnfreezeTransaction()
.setTokenId(tokenId).setAccountId(accountId).execute(client);
await new TokenWipeTransaction()
.setTokenId(tokenId).setAccountId(accountId).setAmount(10).execute(client);
await new TokenPauseTransaction().setTokenId(tokenId).execute(client);
await new TokenUnpauseTransaction().setTokenId(tokenId).execute(client);
Burning & Deleting
import { TokenBurnTransaction, TokenDeleteTransaction } from "@hiero-ledger/sdk";
await new TokenBurnTransaction()
.setTokenId(tokenId).setAmount(100).execute(client);
await new TokenBurnTransaction()
.setTokenId(nftId).setSerials([1, 2]).execute(client);
await new TokenDeleteTransaction()
.setTokenId(tokenId).execute(client);
Querying Token Info
import { TokenInfoQuery, TokenNftInfoQuery, NftId } from "@hiero-ledger/sdk";
const info = await new TokenInfoQuery().setTokenId(tokenId).execute(client);
console.log(info.name, info.symbol, info.totalSupply.toString());
const nftInfo = await new TokenNftInfoQuery()
.setNftId(new NftId(nftId, 1))
.execute(client);
console.log(nftInfo.accountId.toString());
Airdrops
Airdrops distribute tokens to multiple recipients. If a recipient can't auto-associate, the airdrop becomes pending and must be claimed.
import {
TokenAirdropTransaction, TokenClaimAirdropTransaction,
TokenCancelAirdropTransaction, TokenRejectTransaction, NftId,
} from "@hiero-ledger/sdk";
const record = await (
await (
await new TokenAirdropTransaction()
.addTokenTransfer(tokenId, treasuryId, -300)
.addTokenTransfer(tokenId, recipient1, 100)
.addTokenTransfer(tokenId, recipient2, 100)
.addTokenTransfer(tokenId, recipient3, 100)
.addNftTransfer(nftId, 1, treasuryId, recipient1)
.freezeWith(client)
.sign(treasuryKey)
).execute(client)
).getRecord(client);
const { newPendingAirdrops } = record;
await (
await new TokenClaimAirdropTransaction()
.addPendingAirdropId(newPendingAirdrops[0].airdropId)
.freezeWith(client)
.sign(recipientKey)
).execute(client);
await new TokenCancelAirdropTransaction()
.addPendingAirdropId(newPendingAirdrops[1].airdropId)
.execute(client);
await (
await new TokenRejectTransaction()
.setOwnerId(recipientId)
.addTokenId(tokenId)
.addNftId(new NftId(nftId, 1))
.freezeWith(client)
.sign(recipientKey)
).execute(client);
Custom Fees
HTS supports three fee types. See references/custom-fees.md for full details.
import {
CustomFixedFee, CustomFractionalFee, CustomRoyaltyFee, Hbar,
} from "@hiero-ledger/sdk";
const fixedFee = new CustomFixedFee()
.setFeeCollectorAccountId(collectorId)
.setHbarAmount(new Hbar(1));
const fractionalFee = new CustomFractionalFee()
.setFeeCollectorAccountId(collectorId)
.setNumerator(1).setDenominator(100)
.setMin(1).setMax(1000);
const royaltyFee = new CustomRoyaltyFee()
.setFeeCollectorAccountId(collectorId)
.setNumerator(5).setDenominator(100)
.setFallbackFee(
new CustomFixedFee().setHbarAmount(new Hbar(2))
);
await new TokenCreateTransaction()
.setCustomFees([fixedFee, fractionalFee])
.execute(client);
Common Gotchas
-
Association before transfer: Recipients must associate with the token or have auto-association slots. Without it, transfers fail with TOKEN_NOT_ASSOCIATED_TO_ACCOUNT.
-
Signed amounts: addTokenTransfer uses signed amounts. Sender gets negative, receiver gets positive. They must net to zero.
-
NFTs have no decimals or initial supply: Set TokenType.NonFungibleUnique and mint serials individually.
-
Keys are permanent if no adminKey: Without an admin key, you cannot update or delete the token, and cannot change any other keys.
-
KYC gate: If a token has a kycKey, accounts must receive KYC approval before they can transact with that token.
-
Supply key for mint/burn: You need a supply key set at creation to later mint or burn.
-
Treasury auto-association: The treasury account is automatically associated with the token.
-
Freeze before multi-sig: When multiple parties need to sign, call freezeWith(client) first, then chain .sign(key) calls.
Reference Files
references/api-reference.md — Complete list of all HTS transaction and query classes with their methods
references/custom-fees.md — Detailed guide to fixed, fractional, and royalty fees