compressed-nfts-basics
SKILL.md
Compressed NFTs Basics
Role framing: You are a Solana NFT engineer specializing in state compression. Your goal is to help developers create, transfer, and manage compressed NFTs cost-effectively at scale.
Initial Assessment
- What's the collection size: hundreds, thousands, or millions?
- Minting pattern: all at once, on-demand, or continuous?
- Who pays: creator upfront or buyers on mint?
- Metadata: on-chain, off-chain, or hybrid?
- Do you need to query/filter NFTs by attributes?
- Transfer frequency: high (trading) or low (soulbound-ish)?
- Budget: what's acceptable cost per NFT?
Core Principles
- Compression trades account rent for tree rent: Instead of paying ~0.002 SOL per NFT account, pay ~0.5-2 SOL for a tree that holds thousands-millions.
- Trees are immutable config: Max depth and buffer size are set at creation. Choose wisely.
- Proofs are required for operations: Every transfer/burn needs a Merkle proof from an indexer.
- Indexers are essential: Without Helius, Triton, or your own, you can't query or operate on cNFTs.
- Not all marketplaces support cNFTs: Verify listing venue support before choosing compression.
- Decompression is possible but costly: Can convert cNFT to regular NFT, but defeats the purpose.
Workflow
1. Understanding the Economics
Cost comparison (approximate):
| Collection Size | Regular NFTs | Compressed NFTs | Savings |
|---|---|---|---|
| 1,000 | ~2 SOL | ~0.5 SOL | 75% |
| 10,000 | ~20 SOL | ~1 SOL | 95% |
| 100,000 | ~200 SOL | ~2 SOL | 99% |
| 1,000,000 | ~2000 SOL | ~5 SOL | 99.75% |
The tree cost is upfront; minting is nearly free after.
2. Merkle Tree Configuration
// Tree parameters
interface TreeConfig {
maxDepth: number; // Max NFTs = 2^maxDepth
maxBufferSize: number; // Concurrent operations buffer
canopyDepth: number; // Proof size optimization
}
// Common configurations:
const TREE_CONFIGS = {
small: { // Up to 16,384 NFTs
maxDepth: 14,
maxBufferSize: 64,
canopyDepth: 11,
approxCost: '0.5 SOL',
},
medium: { // Up to 1,048,576 NFTs
maxDepth: 20,
maxBufferSize: 256,
canopyDepth: 14,
approxCost: '1.5 SOL',
},
large: { // Up to 1 billion NFTs
maxDepth: 30,
maxBufferSize: 2048,
canopyDepth: 17,
approxCost: '5+ SOL',
},
};
// Calculate tree capacity
function getTreeCapacity(maxDepth: number): number {
return Math.pow(2, maxDepth);
}
// Calculate approximate rent
async function estimateTreeRent(
connection: Connection,
maxDepth: number,
maxBufferSize: number,
canopyDepth: number
): Promise<number> {
const space = getConcurrentMerkleTreeAccountSize(
maxDepth,
maxBufferSize,
canopyDepth
);
return connection.getMinimumBalanceForRentExemption(space);
}
3. Creating a Merkle Tree
import {
createTree,
mplBubblegum,
} from '@metaplex-foundation/mpl-bubblegum';
import { generateSigner, createSignerFromKeypair } from '@metaplex-foundation/umi';
import { createUmi } from '@metaplex-foundation/umi-bundle-defaults';
async function createMerkleTree(
connection: Connection,
payer: Keypair,
config: TreeConfig
): Promise<PublicKey> {
// Setup UMI
const umi = createUmi(connection.rpcEndpoint)
.use(mplBubblegum());
const payerSigner = createSignerFromKeypair(umi, {
publicKey: payer.publicKey,
secretKey: payer.secretKey,
});
umi.use(payerSigner);
// Generate tree keypair
const merkleTree = generateSigner(umi);
// Create tree
await createTree(umi, {
merkleTree,
maxDepth: config.maxDepth,
maxBufferSize: config.maxBufferSize,
canopyDepth: config.canopyDepth,
public: false, // Only tree authority can mint
}).sendAndConfirm(umi);
console.log('Tree created:', merkleTree.publicKey);
return new PublicKey(merkleTree.publicKey);
}
4. Minting Compressed NFTs
import { mintV1 } from '@metaplex-foundation/mpl-bubblegum';
interface CNFTMetadata {
name: string;
symbol: string;
uri: string;
sellerFeeBasisPoints: number;
creators: Creator[];
collection?: {
key: PublicKey;
verified: boolean;
};
}
async function mintCompressedNFT(
umi: Umi,
treeAddress: PublicKey,
recipient: PublicKey,
metadata: CNFTMetadata
): Promise<string> {
const { signature } = await mintV1(umi, {
leafOwner: recipient,
merkleTree: treeAddress,
metadata: {
name: metadata.name,
symbol: metadata.symbol,
uri: metadata.uri,
sellerFeeBasisPoints: metadata.sellerFeeBasisPoints,
creators: metadata.creators.map(c => ({
address: c.address,
share: c.share,
verified: c.verified,
})),
collection: metadata.collection ? {
key: metadata.collection.key,
verified: metadata.collection.verified,
} : null,
uses: null,
primarySaleHappened: false,
isMutable: true,
},
}).sendAndConfirm(umi);
return signature;
}
// Batch minting
async function batchMintCNFTs(
umi: Umi,
treeAddress: PublicKey,
mints: { recipient: PublicKey; metadata: CNFTMetadata }[],
batchSize: number = 5 // Transactions per batch
): Promise<string[]> {
const signatures: string[] = [];
for (let i = 0; i < mints.length; i += batchSize) {
const batch = mints.slice(i, i + batchSize);
const txPromises = batch.map(({ recipient, metadata }) =>
mintV1(umi, {
leafOwner: recipient,
merkleTree: treeAddress,
metadata: {
name: metadata.name,
symbol: metadata.symbol,
uri: metadata.uri,
sellerFeeBasisPoints: metadata.sellerFeeBasisPoints,
creators: metadata.creators,
collection: metadata.collection,
uses: null,
primarySaleHappened: false,
isMutable: true,
},
}).sendAndConfirm(umi)
);
const results = await Promise.all(txPromises);
signatures.push(...results.map(r => r.signature));
console.log(`Minted ${Math.min(i + batchSize, mints.length)}/${mints.length}`);
}
return signatures;
}
5. Transferring Compressed NFTs
Transfers require Merkle proofs from an indexer:
import { transfer } from '@metaplex-foundation/mpl-bubblegum';
import { getAssetWithProof } from '@metaplex-foundation/mpl-bubblegum';
async function transferCNFT(
umi: Umi,
assetId: PublicKey,
currentOwner: PublicKey,
newOwner: PublicKey
): Promise<string> {
// Get asset with proof from indexer (DAS API)
const assetWithProof = await getAssetWithProof(umi, assetId, {
truncateCanopy: true,
});
// Execute transfer
const { signature } = await transfer(umi, {
...assetWithProof,
leafOwner: currentOwner,
newLeafOwner: newOwner,
}).sendAndConfirm(umi);
return signature;
}
// Using Helius DAS API directly
async function getAssetProofHelius(
assetId: string,
heliusApiKey: string
): Promise<AssetProof> {
const response = await fetch(
`https://mainnet.helius-rpc.com/?api-key=${heliusApiKey}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
id: 'my-id',
method: 'getAssetProof',
params: { id: assetId },
}),
}
);
const { result } = await response.json();
return result;
}
6. Querying Compressed NFTs
Using DAS (Digital Asset Standard) API:
// Get all cNFTs by owner
async function getCNFTsByOwner(
ownerAddress: string,
heliusApiKey: string
): Promise<Asset[]> {
const response = await fetch(
`https://mainnet.helius-rpc.com/?api-key=${heliusApiKey}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
id: 'my-id',
method: 'getAssetsByOwner',
params: {
ownerAddress,
page: 1,
limit: 1000,
},
}),
}
);
const { result } = await response.json();
return result.items;
}
// Get cNFTs by collection
async function getCNFTsByCollection(
collectionAddress: string,
heliusApiKey: string
): Promise<Asset[]> {
const response = await fetch(
`https://mainnet.helius-rpc.com/?api-key=${heliusApiKey}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
id: 'my-id',
method: 'getAssetsByGroup',
params: {
groupKey: 'collection',
groupValue: collectionAddress,
page: 1,
limit: 1000,
},
}),
}
);
const { result } = await response.json();
return result.items;
}
// Search by attributes
async function searchCNFTs(
heliusApiKey: string,
params: {
ownerAddress?: string;
creatorAddress?: string;
collectionAddress?: string;
compressed?: boolean;
}
): Promise<Asset[]> {
const response = await fetch(
`https://mainnet.helius-rpc.com/?api-key=${heliusApiKey}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
id: 'my-id',
method: 'searchAssets',
params: {
...params,
compressed: true,
page: 1,
limit: 1000,
},
}),
}
);
const { result } = await response.json();
return result.items;
}
7. Collection Management
import { createCollection } from '@metaplex-foundation/mpl-bubblegum';
// Create collection NFT first (regular NFT)
async function createCollectionNFT(
umi: Umi,
metadata: CollectionMetadata
): Promise<PublicKey> {
const collectionMint = generateSigner(umi);
await createNft(umi, {
mint: collectionMint,
name: metadata.name,
symbol: metadata.symbol,
uri: metadata.uri,
sellerFeeBasisPoints: metadata.sellerFeeBasisPoints,
isCollection: true,
}).sendAndConfirm(umi);
return new PublicKey(collectionMint.publicKey);
}
// Mint cNFT with collection
async function mintWithCollection(
umi: Umi,
treeAddress: PublicKey,
collectionMint: PublicKey,
collectionAuthority: Keypair,
recipient: PublicKey,
metadata: CNFTMetadata
): Promise<string> {
const { signature } = await mintToCollectionV1(umi, {
leafOwner: recipient,
merkleTree: treeAddress,
collectionMint,
metadata: {
name: metadata.name,
symbol: metadata.symbol,
uri: metadata.uri,
sellerFeeBasisPoints: metadata.sellerFeeBasisPoints,
creators: metadata.creators,
},
}).sendAndConfirm(umi);
return signature;
}
Templates / Playbooks
Tree Size Calculator
function recommendTreeConfig(expectedNFTs: number): TreeConfig {
// Find minimum depth to fit NFTs
let maxDepth = Math.ceil(Math.log2(expectedNFTs));
// Add buffer for future mints (2x capacity)
maxDepth = Math.max(maxDepth + 1, 14);
// Cap at practical limits
maxDepth = Math.min(maxDepth, 30);
// Buffer size based on expected concurrency
const maxBufferSize = expectedNFTs < 10000 ? 64 :
expectedNFTs < 100000 ? 256 :
expectedNFTs < 1000000 ? 1024 : 2048;
// Canopy depth (higher = smaller proofs but more rent)
const canopyDepth = Math.min(maxDepth - 3, 17);
return {
maxDepth,
maxBufferSize,
canopyDepth,
capacity: Math.pow(2, maxDepth),
};
}
cNFT Launch Checklist
## cNFT Collection Launch Checklist
### Pre-Launch
- [ ] Collection size determined
- [ ] Tree configuration calculated
- [ ] Tree rent funded
- [ ] Collection NFT created
- [ ] Metadata JSON uploaded to Arweave/IPFS
- [ ] Metadata URIs generated for all NFTs
- [ ] Helius/indexer API key obtained
### Tree Creation
- [ ] Tree created with correct config
- [ ] Tree authority set correctly
- [ ] Tree address recorded
### Minting
- [ ] Test mint on devnet
- [ ] Batch minting script tested
- [ ] Error handling in place
- [ ] Progress tracking implemented
### Post-Launch
- [ ] All mints verified via indexer
- [ ] Collection showing on marketplaces
- [ ] Transfer functionality tested
- [ ] Holders can see NFTs in wallets
Metadata Template
{
"name": "Collection Name #1",
"symbol": "COL",
"description": "Description of this NFT",
"image": "https://arweave.net/...",
"animation_url": "https://arweave.net/...",
"external_url": "https://your-site.com",
"attributes": [
{
"trait_type": "Background",
"value": "Blue"
},
{
"trait_type": "Rarity",
"value": "Legendary"
}
],
"properties": {
"files": [
{
"uri": "https://arweave.net/...",
"type": "image/png"
}
],
"category": "image"
}
}
Common Failure Modes + Debugging
"Tree creation fails"
- Cause: Insufficient SOL for rent
- Detection: Transaction error mentions rent
- Fix: Calculate rent with
estimateTreeRent()and ensure payer has enough
"Minting fails after some NFTs"
- Cause: Tree full or buffer exceeded
- Detection: "Tree is full" or similar error
- Fix: Create new tree; ensure depth was sufficient
"Can't find minted cNFTs"
- Cause: Indexer not synced yet
- Detection: DAS API returns empty
- Fix: Wait 10-30 seconds; verify with getSignatureStatuses
"Transfer fails with proof error"
- Cause: Stale proof (tree updated since fetch)
- Detection: "Invalid proof" error
- Fix: Fetch fresh proof immediately before transfer
"cNFTs not showing in wallet"
- Cause: Wallet doesn't support cNFTs or indexer issue
- Detection: NFTs visible on Helius but not wallet
- Fix: Use wallet that supports cNFTs (Phantom, Backpack); check indexer
Quality Bar / Validation
Implementation is complete when:
- Tree created with appropriate configuration
- Test mint successful
- Minted cNFTs queryable via DAS API
- Transfer works with fresh proofs
- Collection displays correctly on marketplaces
- Error handling covers common failures
Output Format
Provide:
- Tree configuration: Depth, buffer, canopy with reasoning
- Cost estimate: Tree rent + any per-mint costs
- Code: Tree creation, minting, and transfer functions
- Indexer setup: DAS API queries needed
- Testing plan: What to verify before production
Examples
Simple Example: Small PFP Collection
Input: "Create a 5,000 NFT PFP collection with compression"
Output:
// Configuration for 5,000 NFTs
const config = {
maxDepth: 14, // Capacity: 16,384 (room to grow)
maxBufferSize: 64, // Sufficient for moderate minting
canopyDepth: 11, // Reasonable proof sizes
};
// Estimated cost: ~0.5 SOL for tree + ~0.00001 SOL per mint
// Total: ~0.55 SOL for 5,000 NFTs (vs ~10 SOL regular)
// Step 1: Create collection NFT
const collectionMint = await createCollectionNFT(umi, {
name: 'Cool Cats Collection',
symbol: 'CATS',
uri: 'https://arweave.net/collection-metadata.json',
sellerFeeBasisPoints: 500, // 5% royalty
});
// Step 2: Create Merkle tree
const treeAddress = await createMerkleTree(connection, payer, config);
// Step 3: Mint all NFTs
const metadataList = generateMetadataForAll(5000); // Your metadata generation
const signatures = await batchMintCNFTs(umi, treeAddress, metadataList);
console.log(`Minted ${signatures.length} cNFTs to tree ${treeAddress}`);
Complex Example: On-Demand Minting Platform
Input: "Build a platform where users mint cNFTs on-demand"
Output: See complete implementation in examples including:
- Large tree with high buffer (handles concurrent mints)
- User-pays-fee model
- Real-time minting API
- Webhook for mint events
- Admin dashboard for monitoring tree usage
Weekly Installs
2
Repository
sanctifiedops/solana-skillsFirst Seen
3 days ago
Installed on
opencode2
codex2
claude-code2
antigravity2
gemini-cli2
windsurf1