Guides

UDT Tokens

Issue and transfer User Defined Tokens (UDT) and xUDT on CKB.

Edit on GitHub

Issue, transfer, and query fungible tokens on CKB. UDT (User Defined Token) amounts are stored in the data field of cells; CCC's ccc.udt.Udt class handles encoding, SSRI execution, and legacy xUDT fallback automatically.

What you'll get

After this guide you'll be able to:

  • Transfer UDT tokens with automatic input selection and change handling
  • Mint new tokens (requires owner-mode authority)
  • Read on-chain metadata (name, symbol, decimals, icon) for SSRI-compliant tokens
  • Understand how xUDT and sUDT relate

All examples assume you already have a connected signer. See Connect Wallets (browser) or Node.js Backend (server) first.

xUDT vs sUDT

sUDTxUDT
StandardCKB RFC 25CKB RFC 52
Extension supportNoYes (owner-mode, RCE rules)
KnownScript value— (not in CCC)ccc.KnownScript.XUdt
Production useLegacyRecommended

The Udt class in CCC supports both. By default it uses the SSRI execution model (on-chain script execution), and falls back gracefully to off-chain construction for legacy xUDT tokens.

Installation

npm install @ckb-ccc/ccc

UDT support is bundled in the main package — no extra install is required.

Import

import { ccc } from "@ckb-ccc/ccc";

The Udt class is available as ccc.udt.Udt.

Construct a Udt instance

Before you can transfer or mint, you need a Udt object. It takes two pieces of information:

  • code — the OutPoint of the cell that holds the UDT script code (cell dep)
  • script — the type script that uniquely identifies this token (its args is typically the issuer's lock hash)
const type = await ccc.Script.fromKnownScript(
  signer.client,
  ccc.KnownScript.XUdt,
  // The args uniquely identify this token (issuer lock hash)
  "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);
// Optional 3rd arg: { executor } for SSRI execution. Omit for legacy xUDT.

Transfer tokens

Use this when: you want to send UDT tokens from the signer to another address. The four-step pattern mirrors CKB transfers, with an extra UDT-specific input-filling step:

udt.transfer creates a transaction with the desired output cells. It does not yet balance inputs — that's the next two steps.

const receiver = await signer.getRecommendedAddress();
const { script: lock } = await ccc.Address.fromString(receiver, signer.client);

let { res: tx } = await udt.transfer(signer, [
  { to: lock, amount: ccc.fixedPointFrom(1) }, // 1 token unit
]);

udt.completeBy collects the signer's UDT cells until there is enough token balance to cover the outputs. Any surplus UDT is returned as a change cell automatically.

tx = await udt.completeBy(tx, signer);

UDT cells also need CKB capacity to exist on-chain. This step adds CKB inputs to cover all output cells:

await tx.completeInputsByCapacity(signer);
await tx.completeFeeBy(signer);
const txHash = await signer.sendTransaction(tx);
console.log("UDT transfer hash:", txHash); // "0x..." — 32-byte tx hash

Mint tokens

Use this when: you are the token issuer and want to create new token supply. The signer must have mint authority (typically owner-mode in xUDT — the signer's lock hash matches the type script args).

Minting creates new token outputs without requiring existing UDT inputs:

const { script: to } = await ccc.Address.fromString(receiver, signer.client);

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

await tx.completeInputsByCapacity(signer);
await tx.completeFeeBy(signer);
const txHash = await signer.sendTransaction(tx);

Read token metadata (SSRI tokens)

Use this when: you want to display token name, symbol, decimals, or icon in your UI. Only works for tokens that implement the SSRI UDT interface on-chain:

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`);

These methods return undefined for legacy sUDT/xUDT tokens that do not implement the SSRI UDT interface. Check the return value before using it.

API reference

MethodDescription
new ccc.udt.Udt(code, script)Construct a UDT instance
udt.transfer(signer, transfers, tx?)Build transfer outputs
udt.mint(signer, mints, tx?)Build mint outputs
udt.completeBy(tx, signer)Fill UDT inputs and add change
udt.completeChangeToLock(tx, signer, lock)Fill UDT inputs with a specific change lock
udt.name()Token name (SSRI only)
udt.symbol()Token symbol (SSRI only)
udt.decimals()Token decimals (SSRI only)
udt.icon()Token icon URI (SSRI only)

Troubleshooting

udt.completeBy throws "not enough UDT balance" The signer doesn't own enough UDT cells to cover the transfer amount. Check the signer's UDT balance or fund the address with more tokens.

udt.transfer returns { res: tx } — what is res? All UDT methods return an ssri.ExecutorResponse<T> wrapper. The actual Transaction is in the .res property. This enables CCC to attach SSRI execution metadata alongside the result.

udt.name() / udt.symbol() returns undefined The token does not implement the SSRI UDT metadata interface. This is expected for legacy sUDT/xUDT tokens. You'll need to source metadata from an off-chain registry.

I want to transfer UDT to an address that has never held this token That works out of the box. udt.transfer creates a new output cell with the token type script at the recipient's lock. The recipient just needs enough CKB capacity (occupied by the new cell) — which the signer pays for via completeInputsByCapacity.

Next steps

On this page