A comprehensive guide to learning ChiaLisp for Chia blockchain development
CATs are Chia’s fungible token standard. If you have ever used ERC-20 tokens on Ethereum, CATs are the Chia equivalent – but designed very differently. Instead of a single smart contract tracking balances, each CAT is an individual coin on the blockchain with its own puzzle.
This chapter builds on everything you learned about puzzles, currying, inner puzzles, and drivers. CATs are a real-world application of puzzle composition.
A Chia Asset Token (CAT) is a fungible token that lives on the Chia blockchain. Each CAT type is identified by its asset ID (also called the TAIL hash), which is derived from a special program called the TAIL.
Key properties:
Every CAT coin has these layers:
+------------------------------------------+
| CAT Outer Puzzle |
| (enforces token rules, conservation) |
| |
| +------------------------------------+ |
| | Inner Puzzle | |
| | (defines spending conditions) | |
| | (usually: standard transaction) | |
| +------------------------------------+ |
| |
| Asset ID: hash of the TAIL program |
+------------------------------------------+
You might ask: “Why not just track tokens as regular XCH coins with special puzzles?”
The answer is conservation guarantees. The CAT outer puzzle mathematically ensures that:
Without the CAT puzzle, someone could write a puzzle that claims to be a “token” but actually creates tokens from nothing. The CAT standard prevents this.
The current standard is CAT2. The original CAT1 standard had a vulnerability that was discovered and responsibly disclosed. All CAT1 tokens were migrated to CAT2 in a coordinated effort. When we say “CAT” today, we always mean CAT2.
| Token | Description | Type |
|---|---|---|
| USDS | Stably USD stablecoin | Stablecoin |
| SBX | Spacebucks community token | Community |
| DBX | dexie bucks | DEX utility token |
| HOA | Chia Holiday 2021 token | Commemorative |
The asset ID is the tree hash of the TAIL program:
Asset ID = sha256tree(TAIL_program)
Two tokens with the same TAIL have the same asset ID (same token type). Changing anything about the TAIL – even one curried parameter – produces a different asset ID, and therefore a completely different token.
This is the most important concept. A CAT coin is not a new type of coin – it is a regular Chia coin whose puzzle has been wrapped in a special outer layer.
+--------------------------------------------------+
| CAT Outer Puzzle |
| - Enforces token rules (conservation, lineage) |
| - Transforms CREATE_COIN to wrap in CAT puzzle |
| |
| +--------------------------------------------+ |
| | Inner Puzzle (any valid puzzle) | |
| | - Controls spending (e.g., standard tx) | |
| | - Returns conditions as normal | |
| +--------------------------------------------+ |
| |
+--------------------------------------------------+
The inner puzzle can be anything:
The CAT outer puzzle does not care what the inner puzzle is. It only cares about enforcing token-level rules.
The CAT outer puzzle has three curried parameters:
(MOD_HASH TAIL_HASH INNER_PUZZLE)
| Parameter | Description |
|---|---|
MOD_HASH |
The tree hash of the CAT outer puzzle itself (for self-reference) |
TAIL_HASH |
The hash of the TAIL program (this is the asset_id) |
INNER_PUZZLE |
The actual inner puzzle (e.g., standard transaction puzzle) |
MOD_HASH is needed so the CAT puzzle can recreate itself when wrapping new coins.
When the inner puzzle says “create a coin with puzzle hash X”, the CAT outer puzzle
wraps X inside a new CAT puzzle, and it needs its own hash to compute that wrapping.
When a CAT coin is spent:
Step 1: Run the Inner Puzzle
(a INNER_PUZZLE inner_puzzle_solution)
This returns conditions like any normal coin spend:
((CREATE_COIN 0xabc123... 1000)
(AGG_SIG_ME 0xpubkey... 0xmessage...))
Step 2: Transform CREATE_COIN Conditions
The CAT puzzle intercepts every CREATE_COIN condition and wraps the target puzzle
hash inside a new CAT puzzle:
Original: (CREATE_COIN inner_puzzle_hash amount)
Becomes: (CREATE_COIN (cat_puzzle_hash MOD_HASH TAIL_HASH inner_puzzle_hash) amount)
This is automatic and transparent. The inner puzzle does not need to know it is inside a CAT.
Step 3: Verify Conservation
The CAT puzzle checks that total tokens in = total tokens out:
sum(all input amounts) - sum(all output amounts) + sum(all extra_deltas) = 0
If extra_delta is non-zero for any coin, the TAIL must authorize it.
Step 4: Verify Lineage
Each CAT must prove it descended from a valid CAT of the same type through a lineage proof containing:
(parent_parent_coin_id parent_inner_puzzle_hash parent_amount)
Without lineage proofs, anyone could create fake CATs by wrapping a normal coin in the CAT puzzle. The lineage proof ensures an unbroken chain back to the original minting event.
CAT Coin Spend
|
+-- 1. Run inner puzzle --> get conditions
|
+-- 2. For each CREATE_COIN:
| wrap inner_puzzle_hash in CAT puzzle
|
+-- 3. Check conservation:
| total_in = total_out (or TAIL approves delta)
|
+-- 4. Verify lineage:
parent was a valid CAT of same type
TAIL stands for Token and Asset Issuance Limiter. It is a ChiaLisp program that answers one question:
“Is this minting or melting event authorized?”
The TAIL is only consulted when extra_delta != 0 – when tokens are being created
or destroyed. For normal transfers, the TAIL never runs. This is an elegant
optimization since most CAT spends are simple transfers.
When a CAT spend has a non-zero extra_delta, the CAT outer puzzle calls the TAIL:
; The TAIL receives:
(mod (
Truths ; Information about the CAT coin (amount, puzzle hash, etc.)
parent_is_cat ; Whether the parent was a CAT (for lineage)
lineage_proof ; Proof of parent
extra_delta ; The amount being minted (+) or melted (-)
inner_conditions ; Conditions from the inner puzzle
... )
; Return conditions to authorize, or (x) to reject
)
If the TAIL returns successfully (does not raise), the minting/melting is authorized.
The simplest and most common TAIL. Allows minting exactly once, tied to a specific coin.
How it works:
GENESIS_COIN_ID – the coin ID of a coin you controlUse case: Fixed-supply tokens. You decide the total supply at creation time.
See: examples/simple_tail.clsp
Allows ongoing minting, controlled by a public key.
How it works:
AUTHORIZED_PUBKEYUse case: Tokens where you want to mint more over time (stablecoins, reward tokens).
See: examples/authorized_minter_tail.clsp
The most flexible option. Accepts a delegated puzzle in the solution.
How it works:
PUBKEYUse case: Different minting rules at different times without redeploying.
from chia.types.blockchain_format.program import Program
tail_program = Program.fromhex("...")
# If currying parameters:
curried_tail = tail_program.curry(Program.to(genesis_coin_id))
asset_id = curried_tail.get_tree_hash()
print(f"Asset ID: {asset_id.hex()}")
This is the most complex part. When you spend one or more CAT coins in a transaction, they form a ring (a circular linked list):
+--------+ +--------+ +--------+
| CAT #1 | ----> | CAT #2 | ----> | CAT #3 |
+--------+ +--------+ +--------+
^ |
| |
+----------------------------------+
(ring closes)
Even a single CAT spend forms a ring of size 1 (it points to itself).
Each CAT in the ring knows about:
In Chia’s coin set model, all coins in a spend bundle are spent simultaneously. There is no “first” or “last.” A ring has no beginning or end, which matches the parallel nature of coin spends. Each coin can independently verify its part of the conservation check without needing a position.
The ring enables the conservation check through a running subtotal:
For each CAT in the ring:
subtotal = prev_subtotal + this_coin.amount - sum(output_amounts)
After going around the full ring:
final_subtotal must equal 0 (or equal sum of extra_deltas)
When spending a CAT, the solution contains:
(
inner_puzzle_solution ; Solution for the inner puzzle
lineage_proof ; Proof that parent was a valid CAT
prev_coin_id ; Previous CAT in the ring
this_coin_info ; (parent_id, inner_puzzle_hash, amount)
next_coin_proof ; Info to verify next CAT in ring
prev_subtotal ; Running subtotal from previous CAT
extra_delta ; Minting(+) or melting(-), usually 0
)
Alice has a CAT coin worth 100 tokens. She sends 30 to Bob, keeps 70.
INPUT: OUTPUTS:
Alice's CAT (100 tokens) ---> Bob's CAT (30 tokens)
---> Alice's CAT (70 tokens) [change]
Conservation: 100 = 30 + 70 [balanced, extra_delta = 0]
Ring: size 1 (Alice’s coin points to itself).
Alice has two CAT coins (60 and 40) and wants to send 90 to Bob.
INPUTS: OUTPUTS:
Alice's CAT #1 (60 tokens) ---> Bob's CAT (90 tokens)
Alice's CAT #2 (40 tokens) ---> Alice's CAT (10 tokens) [change]
Conservation: 60 + 40 = 90 + 10 [balanced]
Ring: CAT #1 ---> CAT #2 ---> (back to CAT #1)
When extra_delta is not zero:
The TAIL must authorize any non-zero extra_delta.
| Need | TAIL Type |
|---|---|
| Fixed supply, mint once | Genesis by Coin ID |
| Ongoing minting by authority | Everything with Signature |
| Complex/changing rules | Delegated TAIL |
| Custom rules | Write your own |
from clvm_tools_rs import compile_clvm_text
from chia.types.blockchain_format.program import Program
tail_source = """
(mod (Truths parent_is_cat lineage_proof extra_delta inner_conditions)
(if parent_is_cat
(x) ; Reject: no minting after genesis
() ; Allow: this is the genesis mint
)
)
"""
compiled = compile_clvm_text(tail_source, [])
tail_program = Program.fromhex(compiled)
# For an authorized minter TAIL, curry in the minter's public key
curried_tail = tail_program.curry(Program.to(minter_public_key))
asset_id = curried_tail.get_tree_hash()
print(f"Asset ID: {asset_id.hex()}")
Save this – it is how your token is identified everywhere.
from chia.wallet.cat_wallet.cat_utils import construct_cat_puzzle, CAT_MOD
cat_puzzle = construct_cat_puzzle(
CAT_MOD,
asset_id, # Your TAIL hash
inner_puzzle # The inner puzzle for the first recipient
)
Minting is the most complex part. You need to:
extra_delta to the desired token supplyIn practice, most people use the CAT admin tool:
# Install chia-dev-tools
pip install chia-dev-tools
# Issue a new CAT
cats --tail ./my_tail.clsp \
--send-to <your_address> \
--amount 1000000 \
--fee 100000000
# Add to your wallet
chia wallet add_token -id <asset_id> -n "My Token"
# Check balance
chia wallet show
# Send tokens
chia wallet send -i <wallet_id> -a <amount> -t <target_address>
1. Write TAIL 2. Compile & Curry 3. Get Asset ID
(.clsp file) --> (CLVM bytecode) --> (tree hash)
|
6. Distribute 5. Mint Tokens 4. Build CAT Puzzle
(send to <-- (spend genesis <-- (curry CAT mod with
users) coin + create TAIL hash + inner)
CAT coins)
from chia.wallet.puzzles.cat_loader import CAT_MOD, CAT_MOD_HASH
# CAT_MOD is the compiled CAT outer puzzle (Program)
# CAT_MOD_HASH is its tree hash (bytes32)
full_cat_puzzle = CAT_MOD.curry(
CAT_MOD_HASH, # The CAT module hash (always the same)
asset_id, # Your TAIL hash (bytes32)
inner_puzzle # The inner puzzle (Program)
)
from chia.wallet.cat_wallet.cat_utils import match_cat_puzzle
# Given a puzzle_reveal from a coin spend:
matched = match_cat_puzzle(puzzle_reveal)
if matched is not None:
cat_mod_hash, tail_hash_program, inner_puzzle = matched
asset_id = tail_hash_program.as_atom()
print(f"This is a CAT with asset_id: {asset_id.hex()}")
print(f"Inner puzzle hash: {inner_puzzle.get_tree_hash().hex()}")
else:
print("This is not a CAT coin")
from chia.wallet.cat_wallet.cat_utils import (
construct_cat_puzzle,
unsigned_spend_bundle_for_spendable_cats,
SpendableCAT,
CAT_MOD,
)
from chia.wallet.lineage_proof import LineageProof
# Create a SpendableCAT for each input coin
spendable = SpendableCAT(
coin=cat_coin,
limitations_program_hash=asset_id, # TAIL hash
inner_puzzle=inner_puzzle,
inner_solution=inner_solution,
lineage_proof=LineageProof(
parent_name=parent_parent_id,
inner_puzzle_hash=parent_inner_ph,
amount=parent_amount,
),
extra_delta=0, # 0 for normal transfer
limitations_program_reveal=None, # Only for minting/melting
limitations_solution=None, # Only for minting/melting
)
# Build the spend bundle (ring construction is automatic)
spend_bundle = unsigned_spend_bundle_for_spendable_cats(
CAT_MOD,
[spendable],
)
from chia.types.blockchain_format.program import Program
from chia.types.blockchain_format.coin import Coin
from chia.wallet.cat_wallet.cat_utils import (
construct_cat_puzzle,
unsigned_spend_bundle_for_spendable_cats,
SpendableCAT,
CAT_MOD,
CAT_MOD_HASH,
)
from chia.wallet.lineage_proof import LineageProof
from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import (
puzzle_for_pk,
solution_for_delegated_puzzle,
)
# 1. Define your token
asset_id = bytes.fromhex("abcd1234...")
# 2. Build inner puzzle (standard transaction)
inner_puzzle = puzzle_for_pk(my_public_key)
# 3. Build CAT puzzle
cat_puzzle = construct_cat_puzzle(CAT_MOD, asset_id, inner_puzzle)
# 4. Identify the coin to spend
cat_coin = Coin(parent_id, cat_puzzle.get_tree_hash(), 1000)
# 5. Build inner solution (send 600 to Bob, keep 400)
delegated_puzzle = Program.to((1, [
[51, bob_inner_puzzle_hash, 600],
[51, my_inner_puzzle_hash, 400],
]))
inner_solution = solution_for_delegated_puzzle(delegated_puzzle, Program.to(0))
# 6. Get lineage proof
lineage = LineageProof(parent_parent_id, parent_inner_ph, parent_amount)
# 7. Create SpendableCAT
spendable = SpendableCAT(
coin=cat_coin,
limitations_program_hash=asset_id,
inner_puzzle=inner_puzzle,
inner_solution=inner_solution,
lineage_proof=lineage,
)
# 8. Build spend bundle
spend_bundle = unsigned_spend_bundle_for_spendable_cats(CAT_MOD, [spendable])
# 9. Sign and submit (depends on your key management)
For standard operations, the wallet RPC is simplest:
from chia.rpc.wallet_rpc_client import WalletRpcClient
async def send_cat(wallet_id, recipient, amount, fee=0):
client = await get_wallet_client()
try:
result = await client.cat_spend(
wallet_id=wallet_id,
amount=amount,
inner_address=recipient,
fee=fee,
)
print(f"Transaction ID: {result['transaction_id']}")
finally:
client.close()
await client.await_closed()
One of the most powerful features – trustless trading:
# Offer 100 of my CAT (wallet 2) for 0.1 XCH (wallet 1)
offer_dict = {
2: -100, # I give 100 CAT tokens
1: 100000000000 # I want 0.1 XCH
}
result = await wallet_client.create_offer_for_ids(offer_dict)
offer_str = result['offer'] # Shareable offer string
For reference, the implementation lives in:
chia/wallet/cat_wallet/
cat_utils.py - Core utilities for CAT puzzle construction
cat_wallet.py - CAT wallet implementation
cat_info.py - Data structures for CAT info
lineage_store.py - Storage for lineage proofs
chia/wallet/puzzles/
cat_v2.clvm - The CAT2 outer puzzle
genesis_by_coin_id.clvm - Genesis TAIL
everything_with_signature.clvm - Signature-based TAIL
delegated_tail.clvm - Delegated TAIL
See the examples/ directory:
simple_tail.clsp - A single-issuance TAIL programauthorized_minter_tail.clsp - A TAIL for authorized mintingcat_with_custom_inner.clsp - A CAT with a custom inner puzzlecat_driver.py - Python driver for CAT operationsPrevious chapter: Chapter 4 - Python Drivers
Next chapter: Chapter 6 - Advanced Examples