> ## Documentation Index
> Fetch the complete documentation index at: https://ckbfans.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Spore NFTs

> Create, transfer, query, and melt Spore Protocol NFTs on CKB. Spore stores all NFT content on-chain inside CKB cells — no external storage required.

[Spore Protocol](https://spore.pro/) 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

<Tabs>
  <Tab title="npm">
    ```bash theme={null}
    npm install @ckb-ccc/spore
    ```
  </Tab>

  <Tab title="yarn">
    ```bash theme={null}
    yarn add @ckb-ccc/spore
    ```
  </Tab>

  <Tab title="pnpm">
    ```bash theme={null}
    pnpm add @ckb-ccc/spore
    ```
  </Tab>
</Tabs>

## Import

```typescript theme={null}
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.

```typescript theme={null}
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

| Field         | Type          | Required | Description                                                   |
| ------------- | ------------- | -------- | ------------------------------------------------------------- |
| `contentType` | `string`      | Yes      | MIME type of the content, e.g. `"image/png"`, `"text/plain"`. |
| `content`     | `Uint8Array`  | Yes      | Raw bytes of the NFT content.                                 |
| `clusterId`   | `ccc.HexLike` | No       | ID of a Spore cluster this NFT belongs to.                    |

### createSpore parameters

| Parameter     | Type                                     | Description                                                        |
| ------------- | ---------------------------------------- | ------------------------------------------------------------------ |
| `signer`      | `ccc.Signer`                             | Account that pays for and owns the new Spore.                      |
| `data`        | `SporeDataView`                          | Content and metadata for the Spore.                                |
| `to`          | `ccc.ScriptLike`                         | Optional recipient lock script. Defaults to the signer's own lock. |
| `clusterMode` | `"lockProxy" \| "clusterCell" \| "skip"` | How to handle the cluster cell when `clusterId` is set.            |
| `tx`          | `ccc.TransactionLike`                    | Optional 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.

<AccordionGroup>
  <Accordion title="lockProxy (recommended)">
    Adds a cell that shares the same lock as the cluster cell to both inputs and outputs. Suitable for public clusters where membership is proved by controlling the right lock script.
  </Accordion>

  <Accordion title="clusterCell">
    Puts the cluster cell itself into the transaction inputs and outputs. Use this for private clusters where you own the cluster cell directly.
  </Accordion>

  <Accordion title="skip">
    Does not add any cluster-related inputs or outputs. Use only when you have already handled the cluster logic manually, or for testing.
  </Accordion>
</AccordionGroup>

<Warning>
  If `clusterId` is set in the Spore data and `clusterMode` is not provided, `createSpore()` throws an error. Always specify a `clusterMode` when using clusters.
</Warning>

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

```typescript theme={null}
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

| Parameter | Type                  | Description                              |
| --------- | --------------------- | ---------------------------------------- |
| `signer`  | `ccc.Signer`          | Current owner who signs the transfer.    |
| `id`      | `ccc.HexLike`         | The spore ID to transfer.                |
| `to`      | `ccc.ScriptLike`      | New owner's lock script.                 |
| `tx`      | `ccc.TransactionLike` | Optional 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.

```typescript theme={null}
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);
```

<Warning>
  Melting is irreversible. The Spore and all its on-chain content are permanently destroyed. The reclaimed CKB capacity is returned to the signer.
</Warning>

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

```typescript theme={null}
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:

```typescript theme={null}
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.

```typescript theme={null}
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

| Parameter   | Type              | Description                                                                             |
| ----------- | ----------------- | --------------------------------------------------------------------------------------- |
| `client`    | `ccc.Client`      | The CKB client to query.                                                                |
| `lock`      | `ccc.ScriptLike`  | Optional lock script to filter by owner.                                                |
| `clusterId` | `ccc.HexLike`     | Optional cluster ID to filter by cluster. Pass `""` to find public spores (no cluster). |
| `order`     | `"asc" \| "desc"` | Creation order. Defaults to ascending.                                                  |
| `limit`     | `number`          | Max cells per query chunk.                                                              |

## Complete example

```typescript create-and-send-spore.ts theme={null}
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

<CardGroup cols={2}>
  <Card title="UDT Tokens" icon="coins" href="/guides/udt-tokens">
    Work with fungible tokens on CKB using the UDT package.
  </Card>

  <Card title="Send CKB" icon="paper-plane" href="/guides/send-ckb">
    Review the core transaction building primitives CCC uses under the hood.
  </Card>
</CardGroup>
