AI agents: the machine-readable documentation index for this site is at https://docs.ckbccc.com/llms.txt. Append ".md" to any documentation page URL to fetch its canonical Markdown source, which is preferred over rendered HTML for retrieval, indexing, question answering, and code generation.

Product-specific agent operating guidance (read before generating CKB/CCC code): https://docs.ckbccc.com/skill.md

Guides

Spore Protocol

Create and manage digital objects (DOBs) using the Spore Protocol SDK.

Edit on GitHub

Create permanent, ownable Digital Objects (DOBs) directly on the CKB blockchain. A Spore encodes arbitrary content (images, text, JSON, etc.) into a cell — ownership is enforced by a lock script, and transfers are atomic on-chain operations without any marketplace or indexer dependency.

What you'll get

After this guide you'll be able to:

  • Create a Spore — store content (text, image, JSON, …) permanently on-chain
  • Transfer and melt Spores — change ownership or destroy and reclaim CKB
  • Organize with Clusters — group related Spores into named collections
  • Query Spores — iterate owned Spores and filter by cluster

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

Key concepts

  • Spore — a cell containing arbitrary content + a type script that gives it a unique ID. Content is permanent; ownership is transferable.
  • Cluster — an optional collection cell with a name and description. Any Spore can reference a Cluster ID to declare membership.
  • Melt — destroy a Spore cell and reclaim its locked CKB capacity. Irreversible.

Installation

npm install @ckb-ccc/spore
import { createSpore, transferSpore, meltSpore } from "@ckb-ccc/spore";
import { createSporeCluster, transferSporeCluster } from "@ckb-ccc/spore";

Create a Spore

createSpore encodes your content into a new Spore cell. You provide a MIME content type and raw content bytes; CCC handles the on-chain data packing:

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

const { tx, id } = await createSpore({
  signer,
  data: {
    contentType: "text/plain",
    content: new TextEncoder().encode("Hello, Spore!"),
  },
  // Optional: send the Spore to a different address
  // to: recipientLockScript,
});

// Balance capacity and pay fee
await tx.completeInputsByCapacity(signer);
await tx.completeFeeBy(signer);

const txHash = await signer.sendTransaction(tx);
console.log("Spore ID:", id);              // "0x..." — unique type script args, store this!
console.log("Transaction hash:", txHash);  // "0x..." — 32-byte tx hash

The returned id is the sporeId — the args field of the Spore's type script. Store it; you will need it for transfers and melts.

Spore inside a Cluster

If you want the Spore to belong to a Cluster, include clusterId in the data and set clusterMode:

const { tx, id } = await createSpore({
  signer,
  data: {
    contentType: "image/png",
    content: pngBytes,
    clusterId: "0xabc123...", // id returned by createSporeCluster
  },
  clusterMode: "lockProxy", // or "clusterCell"
});
clusterModeBehaviour
"lockProxy"Puts a cell with the Cluster's lock in both inputs and outputs — lower cost
"clusterCell"Puts the Cluster cell itself in inputs and outputs — proves ownership directly
"skip"Skips cluster logic entirely — use only if you handle it yourself

Transfer a Spore

Use this when: you want to change ownership of an existing Spore (e.g. sell, gift, or move to a different address).

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

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

const { tx } = await transferSpore({
  signer,
  id: sporeId,   // "0x..."
  to: newOwner,
});

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

Melt (destroy) a Spore

Use this when: you want to permanently destroy a Spore and reclaim its locked CKB capacity:

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

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

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

Melting is irreversible. The Spore's content is removed from the chain and its CKB is released. There is no undo.

Clusters

Clusters are optional named collections. Create a Cluster first, then reference its ID when creating Spores.

Create a Cluster

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

const { tx, id: clusterId } = await createSporeCluster({
  signer,
  data: {
    name: "My Collection",
    description: "A collection of on-chain art.",
  },
});

await tx.completeInputsByCapacity(signer);
await tx.completeFeeBy(signer);
const txHash = await signer.sendTransaction(tx);
console.log("Cluster ID:", clusterId); // "0x..." — pass this as clusterId when creating Spores

Transfer a Cluster

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

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

const { tx } = await transferSporeCluster({
  signer,
  id: clusterId,
  to: newOwner,
});

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

Query Spores and Clusters

All query functions return async generators — they stream results lazily so you can process large collections without loading everything into memory.

import { findSporesBySigner, findSporeClusters } from "@ckb-ccc/spore";

// Iterate all Spores owned by the signer
for await (const { spore, sporeData } of findSporesBySigner({ signer })) {
  console.log(spore.outPoint, sporeData.contentType);
}

// Filter: only Spores in a specific Cluster
for await (const { sporeData } of findSporesBySigner({
  signer,
  clusterId: "0xabc123...",
})) {
  console.log(sporeData);
}

// Filter: only public Spores (not in any cluster)
for await (const { spore } of findSporesBySigner({
  signer,
  clusterId: "", // empty string = public spores only
})) {
  console.log(spore.outPoint);
}

API reference

FunctionDescription
createSpore(params)Create a new Spore cell
transferSpore(params)Transfer a Spore to a new owner
meltSpore(params)Destroy a Spore and reclaim CKB
findSpore(client, id)Look up a single Spore by ID
findSpores(params)Query Spores by lock and/or cluster
findSporesBySigner(params)Query Spores owned by a signer
createSporeCluster(params)Create a new Cluster cell
transferSporeCluster(params)Transfer a Cluster to a new owner
findCluster(client, id)Look up a single Cluster by ID
findSporeClusters(params)Query Clusters by lock
findSporeClustersBySigner(params)Query Clusters owned by a signer

Troubleshooting

createSpore fails with "not enough capacity" Spore cells store content on-chain, so they require more CKB capacity than a basic transfer. A text Spore needs ~200+ CKB; an image Spore can need thousands. Make sure the signer has sufficient balance.

Spore ID is lost after creation The id returned by createSpore is the type script args of the Spore cell. It is deterministic (derived from the first input + output index), but you should store it immediately. You can also recover it via findSporesBySigner.

clusterMode — which one should I use?

  • "lockProxy" (recommended) — lower cost, proves cluster ownership via a proxy cell with the same lock.
  • "clusterCell" — higher cost, puts the actual cluster cell in the transaction. Use this when the cluster's lock is different from the signer's.
  • "skip" — only if you are manually handling cluster inputs/outputs yourself.

Spore v3 (upcoming)

The proposed contracts in spore-contract #75 and #76 introduce native Mutants and optional core CoBuild Actions. Existing presets and the default V2 behavior stay unchanged. V3 presets remain empty until reviewed deployments are published; supply custom SporeScriptInfoLike descriptors to use a local candidate.

Optional intent

All five Spore/Cluster builders accept withIntent: false to omit their new Actions. Use it only when every affected script accepts absent Actions. Existing transaction Actions are preserved and reattached after Lock preparation, including when a builder adds inputs without a new Action.

Before displaying metadata, call checkSporeIntent from @ckb-ccc/spore/advanced. Display only its returned verified Spore Actions as verified. Foreign Actions remain untouched in the transaction but are excluded from that result. Always show the actual transaction changes too: an empty result does not certify that no assets move.

The checker recognizes existing Spore/Cluster presets by code hash and hash type. Pass reviewed custom deployments by category through scripts: { Spore: [v3Spore], Cluster: [v3Cluster], ClusterProxy: [proxyInfo], ClusterAgent: [agentInfo] }. These are trust anchors supplied by the application, not claims of deployment provenance. If a builder uses a custom scriptInfoHash, explicitly allow it through scriptInfoHashes; the default is DEFAULT_COBUILD_INFO_HASH.

All eleven Action variants bind their IDs, owners and operation shape to resolved cells. Transfers preserve data; Spore/Cluster retain contract-valid ID suffixes, and Proxy IDs use the first 32 bytes of valid 32/40-byte args. Agent creation resolves the referenced Proxy from code deps or depGroups, checks its approved deployment, ID and Cluster binding. Missing inputs/dependencies, unknown known-family metadata, duplicate cells/Actions/Messages and malformed CoBuild layouts fail closed. The checker scans every witness so a trailing ordinary witness cannot hide a Message. Signature verification, fees and application authorization remain separate wallet responsibilities.

Native rules

A native Mutant stores a compiled RISC-V ELF in immutable Cell data. Use checkNativeMutantCode for bounded ELF header preflight and createMutant with a reviewed data2 deployment to construct publication. The VM still decides executable validity and the rule result.

withMutants and getMutantIds manage comma-separated 32-byte IDs without a 0x prefix. Parsing includes every mutant[] parameter and rejects global duplicate IDs. findMutant resolves either 32-byte args or the 40-byte ID/payment form, validates deployment identity and rejects ambiguous matches. Use injectMutantReferences before mint, transfer and melt, including every declared rule dependency; a payment suffix does not change the referenced ID. checkMutantPayments checks net-capacity thresholds for one Spore type-script group per call. Callers must partition the declared Mutant outpoints and invoke it separately for each group, even when payee Locks match; the helper does not infer group membership, so passing independent groups together overcounts their thresholds and can reject a valid transaction.

These are SDK construction and offline validation helpers. Local package checks do not replace fresh CCC-built signed devnet transactions and exact-byte VM replay for release acceptance. The obsolete SDK PR #142's acceptance evidence does not certify this rewrite.

Next steps

Last updated on

On this page