Skip to main content
UDT (User Defined Token) is CKB’s standard for fungible tokens — similar to ERC-20 on Ethereum. The @ckb-ccc/udt package provides a high-level Udt class that handles token transfers, minting, change calculation, and metadata retrieval. The Udt class supports two token standards:
  • SSRI-compliant UDTs — the modern standard with richer on-chain metadata.
  • Legacy xUDT / sUDT — the original CKB token standard, also supported for compatibility.

Installation

npm install @ckb-ccc/udt

Import

import { ccc } from "@ckb-ccc/udt";
The @ckb-ccc/udt package re-exports the full ccc namespace and adds ccc.udt.Udt.

Create a Udt instance

Instantiate Udt with the code cell’s OutPoint and the token’s type Script.
const udt = new ccc.udt.Udt(
  // OutPoint of the UDT's code cell (the on-chain script code)
  {
    txHash: "0x4e2e832e0b1e7b5994681b621b00c1e65f577ee4b440ef95fa07db9bb3d50269",
    index: 0,
  },
  // Type script that identifies this specific token
  {
    codeHash: "0xcc9dc33ef234e14bc788c43a4848556a5fb16401a04662fc55db9bb201987037",
    hashType: "type",
    args: "0x71fd1985b2971a9903e4d8ed0d59e6710166985217ca0681437883837b86162f",
  },
);
txHash / index point to the cell that contains the UDT script code on-chain — find these values in the token’s documentation or on a CKB explorer. The type script args field uniquely identifies the specific token.

Using a known script (xUDT)

For the standard xUDT script, use ccc.Script.fromKnownScript() to resolve the type script automatically:
import { ccc } from "@ckb-ccc/udt";

const type = await ccc.Script.fromKnownScript(
  signer.client,
  ccc.KnownScript.XUdt,
  "0xf8f94a13dfe1b87c10312fb9678ab5276eefbe1e0b2c62b4841b1f393494eff2",
);

const code = (
  await signer.client.getCellDeps(
    (await signer.client.getKnownScript(ccc.KnownScript.XUdt)).cellDeps,
  )
)[0].outPoint;

const udt = new ccc.udt.Udt(code, type);

Transfer tokens

Call udt.transfer() to produce a transaction that sends tokens to one or more recipients.
const { script: toLock } = await ccc.Address.fromString(toAddress, signer.client);

// Build the transfer outputs
const { res: tx } = await udt.transfer(signer, [
  { to: toLock, amount: ccc.fixedPointFrom(1) },
]);
transfer() returns an ExecutorResponse wrapping the transaction. Destructure .res to get the Transaction object.

Complete the transaction

After calling transfer() the transaction only has the desired outputs. You must add:
  1. UDT change inputs — cells holding enough of the token to cover the transfer.
  2. CKB inputs — cells covering the byte cost of all outputs.
  3. Fee — the network transaction fee.
// 1. Fill in UDT inputs and add a change output for leftover tokens
tx = await udt.completeBy(tx, signer);

// 2. Fill in CKB inputs to cover output capacity
await tx.completeInputsByCapacity(signer);

// 3. Calculate and pay the transaction fee
await tx.completeFeeBy(signer);

// 4. Sign and broadcast
const txHash = await signer.sendTransaction(tx);
console.log("Transaction hash:", txHash);

Full transfer example

transfer-udt.ts
import { ccc } from "@ckb-ccc/udt";

async function transferUdt(
  signer: ccc.Signer,
  udt: ccc.udt.Udt,
  toAddress: string,
  amount: bigint,
) {
  const { script: toLock } = await ccc.Address.fromString(toAddress, signer.client);

  // Describe desired outputs
  let { res: tx } = await udt.transfer(signer, [
    { to: toLock, amount },
  ]);

  // Balance UDT inputs and outputs
  tx = await udt.completeBy(tx, signer);

  // Cover cell capacity and fee
  await tx.completeInputsByCapacity(signer);
  await tx.completeFeeBy(signer);

  return signer.sendTransaction(tx);
}

Mint tokens

udt.mint() works identically to transfer() but creates new tokens rather than moving existing ones. Use it only if the signer has minting authority over the token.
const { script: toLock } = await ccc.Address.fromString(toAddress, signer.client);

let { res: tx } = await udt.mint(signer, [
  { to: toLock, amount: ccc.fixedPointFrom(1000) },
]);

await tx.completeInputsByCapacity(signer);
await tx.completeFeeBy(signer);

const txHash = await signer.sendTransaction(tx);
Minting is only permitted by accounts authorized by the token’s type script logic. Calling mint() with an unauthorized signer will produce a transaction that is rejected by the CKB network.

Query token metadata

For SSRI-compliant UDTs, you can read metadata directly from the chain. Each method returns an ExecutorResponse — access the value via .res.
const { res: name } = await udt.name();
const { res: symbol } = await udt.symbol();
const { res: decimals } = await udt.decimals();
const { res: icon } = await udt.icon();

console.log(`${name} (${symbol}), decimals: ${decimals}`);
Metadata methods return undefined when the UDT was created without an SSRI executor or the token does not implement the optional metadata methods. Always check for undefined before using the value.

API reference

new ccc.udt.Udt(code, script, config?)

ParameterTypeDescription
codeccc.OutPointLikeOutPoint of the cell containing the UDT script code.
scriptccc.ScriptLikeType script that uniquely identifies this token.
config.executorssri.Executor | nullOptional SSRI executor for on-chain method calls.

udt.transfer(signer, transfers, tx?)

ParameterTypeDescription
signerccc.SignerThe account sending the tokens.
transfers{ to: ScriptLike, amount: NumLike }[]Array of recipients and amounts.
txccc.TransactionLike | nullOptional existing transaction to extend.
Returns Promise<ExecutorResponse<ccc.Transaction>>.

udt.mint(signer, mints, tx?)

Same signature as transfer(). Returns a transaction that creates new tokens at the specified outputs.

udt.completeBy(tx, signer)

Scans the signer’s cells for UDT inputs, balances them against the outputs, and adds a change output for any leftover tokens. Returns the updated ccc.Transaction.

udt.name() / symbol() / decimals() / icon()

Query SSRI on-chain metadata. Return Promise<ExecutorResponse<string | ccc.Num | undefined>>.

Next steps

Spore NFTs

Create and manage on-chain NFTs with the Spore Protocol.

Send CKB

Review the transaction building pattern used for plain CKB transfers.