mcpField
H
hedera-token-service

hedera-token-service

github.com/hedera-devDevelopment28567 installsUpdated 8w ago
Repository

About hedera-token-service

hedera-token-service is a JavaScript SDK skill for Hedera Token Service, using `@hiero-ledger/sdk`. It covers fungible tokens and NFTs, along with token transactions, queries, keys, and compliance controls.

Key features

  1. 01Create fungible tokens and NFTs
  2. 02Mint, burn, and transfer tokens
  3. 03Associate accounts with tokens
  4. 04Apply KYC, freeze, wipe, and pause controls
  5. 05Query token and NFT details
  6. 06Configure custom fee schedules

Use cases

  1. 01Creating a fungible token or NFT collection
  2. 02Managing token issuance and transfers
  3. 03Applying account-level compliance controls
  4. 04Inspecting token supply and NFT information

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/hedera-dev/hedera-skills --skill hedera-token-service

After installing, type/hedera-token-servicein Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

evals/
evals.json
spec.json
references/
api-reference.md
custom-fees.md

Details

From the project README

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);   // locks the transaction body

await tx.sign(privateKey); // add signatures (call multiple times for multi-sig)

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";

// Fungible — specify amount
await new TokenMintTransaction()
    .setTokenId(tokenId)
    .setAmount(500)
    .execute(client);

// NFT — specify metadata per serial
const { serials } = await (
    await new TokenMintTransaction()
        .setTokenId(nftId)
        .addMetadata(Buffer.from("ipfs://QmFirst..."))
        .addMetadata(Buffer.from("ipfs://QmSecond..."))
        .execute(client)
).getReceipt(client);
// serials = [Long(1), Long(2)]

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)      // account owner must sign
).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";

// Fungible transfer
await new TransferTransaction()
    .addTokenTransfer(tokenId, senderId, -100)
    .addTokenTransfer(tokenId, receiverId, 100)
    .execute(client);

// NFT transfer (tokenId, serial, sender, receiver)
await new TransferTransaction()
    .addNftTransfer(nftId, 1, senderId, receiverId)
    .execute(client);

// Mix Hbar + token transfers in one transaction
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.

KeyPurpose
adminKeyUpdate/delete the token; rotate other keys
supplyKeyMint and burn
freezeKeyFreeze/unfreeze accounts from transferring this token
kycKeyGrant/revoke KYC status on accounts
wipeKeyWipe token balance from an account
pauseKeyPause/unpause all token operations globally
feeScheduleKeyUpdate the custom fee schedule
metadataKeyUpdate token or NFT metadata

Compliance Operations

// Grant KYC (required when token has kycKey)
await new TokenGrantKycTransaction()
    .setTokenId(tokenId).setAccountId(accountId).execute(client);

// Freeze an account
await new TokenFreezeTransaction()
    .setTokenId(tokenId).setAccountId(accountId).execute(client);

// Unfreeze
await new TokenUnfreezeTransaction()
    .setTokenId(tokenId).setAccountId(accountId).execute(client);

// Wipe tokens from an account
await new TokenWipeTransaction()
    .setTokenId(tokenId).setAccountId(accountId).setAmount(10).execute(client);

// Pause all transfers
await new TokenPauseTransaction().setTokenId(tokenId).execute(client);

// Unpause
await new TokenUnpauseTransaction().setTokenId(tokenId).execute(client);

Burning & Deleting

import { TokenBurnTransaction, TokenDeleteTransaction } from "@hiero-ledger/sdk";

// Burn fungible
await new TokenBurnTransaction()
    .setTokenId(tokenId).setAmount(100).execute(client);

// Burn NFT serials
await new TokenBurnTransaction()
    .setTokenId(nftId).setSerials([1, 2]).execute(client);

// Delete entire token (requires adminKey)
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()); // current owner

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";

// Send airdrop
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);

// Check pending airdrops
const { newPendingAirdrops } = record;

// Recipient claims a pending airdrop
await (
    await new TokenClaimAirdropTransaction()
        .addPendingAirdropId(newPendingAirdrops[0].airdropId)
        .freezeWith(client)
        .sign(recipientKey)
).execute(client);

// Sender cancels a pending airdrop
await new TokenCancelAirdropTransaction()
    .addPendingAirdropId(newPendingAirdrops[1].airdropId)
    .execute(client);

// Recipient rejects tokens they already received
await (
    await new TokenRejectTransaction()
        .setOwnerId(recipientId)
        .addTokenId(tokenId)           // reject fungible
        .addNftId(new NftId(nftId, 1)) // reject NFT
        .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)  // 1%
    .setMin(1).setMax(1000);

const royaltyFee = new CustomRoyaltyFee()
    .setFeeCollectorAccountId(collectorId)
    .setNumerator(5).setDenominator(100)  // 5%
    .setFallbackFee(
        new CustomFixedFee().setHbarAmount(new Hbar(2))
    );

await new TokenCreateTransaction()
    .setCustomFees([fixedFee, fractionalFee])
    // ... other fields
    .execute(client);

Common Gotchas

  1. Association before transfer: Recipients must associate with the token or have auto-association slots. Without it, transfers fail with TOKEN_NOT_ASSOCIATED_TO_ACCOUNT.

  2. Signed amounts: addTokenTransfer uses signed amounts. Sender gets negative, receiver gets positive. They must net to zero.

  3. NFTs have no decimals or initial supply: Set TokenType.NonFungibleUnique and mint serials individually.

  4. Keys are permanent if no adminKey: Without an admin key, you cannot update or delete the token, and cannot change any other keys.

  5. KYC gate: If a token has a kycKey, accounts must receive KYC approval before they can transact with that token.

  6. Supply key for mint/burn: You need a supply key set at creation to later mint or burn.

  7. Treasury auto-association: The treasury account is automatically associated with the token.

  8. 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

Comments

Markdown supported