Skip to main content
Spore Protocol is CKB’s native NFT standard. Unlike most NFT systems, Spore stores all content — images, text, binary data — directly inside CKB cells. There is no IPFS link or external server that can go offline. Burning (melting) a Spore releases the stored CKB capacity back to the owner.

Installation

npm install @ckb-ccc/spore

Import

import { ccc } from "@ckb-ccc/spore";
The package re-exports the full ccc namespace and adds Spore-specific functions.

Create a Spore

Use createSpore() to mint a new NFT. Pass a SporeDataView that describes the content type and raw content bytes.
import { ccc, createSpore } from "@ckb-ccc/spore";

const { tx, id } = await createSpore({
  signer,
  data: {
    contentType: "text/plain",
    content: new TextEncoder().encode("Hello, Spore!"),
  },
});

// Complete capacity inputs and pay the network fee
await tx.completeInputsByCapacity(signer);
await tx.completeFeeBy(signer);

const txHash = await signer.sendTransaction(tx);
console.log("Spore ID:", id);
console.log("Transaction hash:", txHash);
createSpore() returns:
  • tx — a transaction skeleton with the Spore output and required cell deps. You must call completeInputsByCapacity(signer) and completeFeeBy(signer) before broadcasting.
  • id — the unique spore ID (a hex string) that identifies this NFT permanently on-chain.

SporeDataView fields

FieldTypeRequiredDescription
contentTypestringYesMIME type of the content, e.g. "image/png", "text/plain".
contentUint8ArrayYesRaw bytes of the NFT content.
clusterIdccc.HexLikeNoID of a Spore cluster this NFT belongs to.

createSpore parameters

ParameterTypeDescription
signerccc.SignerAccount that pays for and owns the new Spore.
dataSporeDataViewContent and metadata for the Spore.
toccc.ScriptLikeOptional recipient lock script. Defaults to the signer’s own lock.
clusterMode"lockProxy" | "clusterCell" | "skip"How to handle the cluster cell when clusterId is set.
txccc.TransactionLikeOptional existing transaction to extend.

Cluster modes

When a Spore includes a clusterId, CCC must prove that the signer has permission to add a Spore to the cluster. The clusterMode parameter controls how this is done.
Puts the cluster cell itself into the transaction inputs and outputs. Use this for private clusters where you own the cluster cell directly.
If clusterId is set in the Spore data and clusterMode is not provided, createSpore() throws an error. Always specify a clusterMode when using clusters.

Transfer a Spore

Call transferSpore() to change the owner of a Spore. The transaction moves the Spore cell from the current owner’s lock to the recipient’s lock.
import { ccc, transferSpore } from "@ckb-ccc/spore";

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

const { tx } = await transferSpore({
  signer,
  id: sporeId,  // the hex ID returned by createSpore
  to: newOwnerLock,
});

await tx.completeInputsByCapacity(signer);
await tx.completeFeeBy(signer);
const txHash = await signer.sendTransaction(tx);
console.log("Transfer hash:", txHash);

transferSpore parameters

ParameterTypeDescription
signerccc.SignerCurrent owner who signs the transfer.
idccc.HexLikeThe spore ID to transfer.
toccc.ScriptLikeNew owner’s lock script.
txccc.TransactionLikeOptional existing transaction to extend.

Melt a Spore

Melting destroys a Spore permanently and releases the CKB capacity locked inside back to the signer’s address.
import { meltSpore } from "@ckb-ccc/spore";

const { tx } = await meltSpore({
  signer,
  id: sporeId,
});

await tx.completeInputsByCapacity(signer);
await tx.completeFeeBy(signer);
const txHash = await signer.sendTransaction(tx);
console.log("Melt hash:", txHash);
Melting is irreversible. The Spore and all its on-chain content are permanently destroyed. The reclaimed CKB capacity is returned to the signer.

Query Spores

Find Spores owned by the signer

findSporesBySigner() is an async generator that yields all Spores controlled by the connected wallet. Optionally filter by cluster ID.
import { findSporesBySigner } from "@ckb-ccc/spore";

for await (const { spore, sporeData, scriptInfo } of findSporesBySigner({ signer })) {
  console.log("Spore ID:", spore.cellOutput.type?.args);
  console.log("Content type:", sporeData.contentType);
  console.log("Cluster:", sporeData.clusterId ?? "none");
}
Filter to a specific cluster:
for await (const { sporeData } of findSporesBySigner({
  signer,
  clusterId: "0xabc123...",
})) {
  console.log(sporeData.contentType);
}

Find Spores by lock or cluster

findSpores() searches by lock script and optional cluster ID. Use it when you want to query spores for an arbitrary address rather than the connected signer.
import { findSpores } from "@ckb-ccc/spore";

const { script: ownerLock } = await ccc.Address.fromString(ownerAddress, client);

for await (const { sporeData } of findSpores({
  client,
  lock: ownerLock,
  clusterId: "0xabc123...",
})) {
  console.log("Content type:", sporeData.contentType);
}

findSpores parameters

ParameterTypeDescription
clientccc.ClientThe CKB client to query.
lockccc.ScriptLikeOptional lock script to filter by owner.
clusterIdccc.HexLikeOptional cluster ID to filter by cluster. Pass "" to find public spores (no cluster).
order"asc" | "desc"Creation order. Defaults to ascending.
limitnumberMax cells per query chunk.

Complete example

create-and-send-spore.ts
import { ccc, createSpore, transferSpore, findSporesBySigner } from "@ckb-ccc/spore";

async function demo(signer: ccc.Signer) {
  // 1. Create a Spore
  const { tx: createTx, id } = await createSpore({
    signer,
    data: {
      contentType: "text/plain",
      content: new TextEncoder().encode("My first on-chain NFT"),
    },
  });
  await createTx.completeInputsByCapacity(signer);
  await createTx.completeFeeBy(signer);
  const createHash = await signer.sendTransaction(createTx);
  console.log("Created spore:", id, "tx:", createHash);

  // 2. List all spores
  for await (const { sporeData } of findSporesBySigner({ signer })) {
    console.log("Spore content type:", sporeData.contentType);
  }

  // 3. Transfer the Spore to another address
  const { script: newOwner } = await ccc.Address.fromString(
    "ckt1qzda0cr08m85hc8jlnfp3sdrp5mec2azpfhsaz6ghptrs4m9k0mj...",
    signer.client,
  );
  const { tx: transferTx } = await transferSpore({ signer, id, to: newOwner });
  await transferTx.completeInputsByCapacity(signer);
  await transferTx.completeFeeBy(signer);
  const transferHash = await signer.sendTransaction(transferTx);
  console.log("Transferred spore tx:", transferHash);
}

Next steps

UDT Tokens

Work with fungible tokens on CKB using the UDT package.

Send CKB

Review the core transaction building primitives CCC uses under the hood.