Skip to content

LEAF integration guide

For exchanges and integrators: check LEAF balances, accept deposits and send withdrawals. The same API reads AXBT, token statistics and the AXBT/LEAF pool.

Base URLhttps://crc20.org/api/v1read-only JSONno keyCORS

Overview

token
LEAF — a CRC-20 token on Bitcoin mainnet
decimals
8 · amounts are integers in atoms · 1 LEAF = 100000000 atoms
supply
1000000000 LEAF, fully minted
model
accounts · a balance belongs to a Bitcoin address; tokens are not attached to UTXOs
transfer
a Bitcoin transaction: vin[0] spends a UTXO of the sender, one OP_RETURN carries the amount, the next output is the receiver
finality
confirmed transactions only · reorganisations undo and replay blocks
also
AXBT, a BTC-backed token of the same ledger: read it with the same API (see AXBT)

API

Base URL https://crc20.org/api/v1 · read-only JSON · no key · CORS enabled · full spec openapi.json. Amounts are decimal strings (LEAF in atoms, BTC in satoshis): parse them with BigInt, never as JSON numbers. Only market.unit_price_sat has decimals ("1.5"). Lists return next_cursor; pass it back as ?cursor= until it is null.

Balance

GET/api/v1/addresses/{address}

LEAF balance (confirmed, at height), AXBT balance and confirmed BTC balance in satoshis (from mempool.space, cached 30 s; btc is null when it is unavailable, which never means zero) of an address. An address with no activity answers a balance of "0", not a 404.

addresspath
a mainnet address: bc1q… or bc1p… (either case, answered in lower case), 1…, 3…
$ curl https://crc20.org/api/v1/addresses/bc1pnjm803d0erzgvvmx6ue99gunsht8vpl3kxa3k8d00pke32argr6syrvu6u

History

GET/api/v1/addresses/{address}/history

LEAF operations of an address, newest first, with direction = in · out · self. Use it to detect deposits.

addresspath
a mainnet address: bc1q… or bc1p… (either case, answered in lower case), 1…, 3…
limitquery
1 to 100, default 50
cursorquery
the next_cursor of the previous page, unchanged
tickquery
LEAF (default), AXBT or all, case-insensitive (see AXBT)
$ curl "https://crc20.org/api/v1/addresses/bc1pnjm803d0erzgvvmx6ue99gunsht8vpl3kxa3k8d00pke32argr6syrvu6u/history?limit=1"
type
transfer, mint, swap (AXBT/LEAF pool: in when the address bought LEAF, out when it sold) or liquidity (a deposit into the pool, out), and the AXBT vault types with ?tick=AXBT · credit only direction = in
template
transfer (ordinary), market (marketplace sale, see market sales), mint, swap, liquidity, vault, or null until the transaction is decoded (retry later)
market
for a sale: price_sat, platform_fee_sat, network_fee_sat, buyer_paid_sat, unit_price_sat · otherwise null
sender_balance_before
the sender's LEAF balance just before the transfer · null for every other type
swap
see pool
liquidity
see pool
vault
see AXBT
block_hash
compare with your own node to detect reorganisations

Transaction

GET/api/v1/transactions/{txid}

The accepted LEAF operations of a transaction, in operations (same fields as a history item, without direction). 404 means it moved no LEAF: unknown, unconfirmed, or confirmed but invalid. Use it to confirm a withdrawal. While the index may be behind, the lookup answers 503 instead of a 404: retry.

txidpath
64 hexadecimal characters, either case
tickquery
LEAF (default), AXBT or all, case-insensitive (see AXBT)
$ curl https://crc20.org/api/v1/transactions/1b356577165847e2e3b1cfe285a8d05f1803f0b4125584cc0914e97801089641

Status

GET/api/v1/status

Indexer position: wait for synced = true and height ≥ your transaction's block before trusting a 404.

$ curl https://crc20.org/api/v1/status

All operations

GET/api/v1/history

Every operation, newest first, as { items, next_cursor } with the history items above (without direction). Pages never shift: to follow new activity, read the first page again until you reach an item you already have (see limits).

limitquery
1 to 100, default 50
cursorquery
the next_cursor of the previous page, unchanged
tickquery
LEAF (default), AXBT or all, case-insensitive (see AXBT)
$ curl "https://crc20.org/api/v1/history?tick=all&limit=100"

Decoded transaction

GET/api/v1/transactions/{txid}/details

Any transaction, decoded: status (accepted or not_leaf), template, the operations, and every input and output with its role (sender, payload, receiver, change, …), sighash and value; market holds the economics of a sale and notes explains anomalies (a transfer that confirmed but was not applied, a block newer than the index). Unknown txid → 404.

txidpath
64 hexadecimal characters, either case
tickquery
LEAF (default), AXBT or all, case-insensitive (see AXBT)
$ curl "https://crc20.org/api/v1/transactions/737e9ef7bde669d926976381c15c9592c5bdeabd3754027bdfa2ed7727616971/details?tick=all"

Errors

Every error is { "error": { "code", "message" } }; a bad parameter is a 400 even while the indexer is down.

400
invalid_address, invalid_txid, invalid_cursor, invalid_limit, invalid_query (unknown, duplicate or malformed parameter)
404
not_found: unknown route or token, or no accepted operation in the transaction
405
method_not_allowed: only GET, HEAD and OPTIONS
503
unavailable: indexer unreachable or catching up, or load shedding (Retry-After: 1): retry later

AXBT

AXBT is the BTC-backed token of the PRECOP ledger: it is minted against BTC collateral locked in a vault and burned when the vault is repaid and closed. 8 decimals, 1 atom = 1 sat; no maximum supply. The LEAF routes stay unchanged: AXBT is read with one more parameter.

balance
/addresses/{address} returns axbt.balance (atoms = sats), last field
operations
add ?tick=AXBT to /history, /addresses/{address}/history, /transactions/{txid} and /details: AXBT transfers, vault operations, and the pool swaps and liquidity (which move AXBT too). ?tick=all lists both tokens and the liquidity paybacks
tick
every operation ends with tick, the token of its amount: LEAF or AXBT
identity
with tick=all, a payback reuses its deposit's txid and one transaction can touch two vaults: identify an item by block_height, txid, type, liquidity.action and vault.funding_txid, not by txid alone
transfer
the LEAF layout (see withdrawals) with "tick":"AXBT" in the payload, amt in sats

Vault operations

vault_openthe vault funding: BTC collateral lockedamount = the debt, credited to to (the owner)
vault_repaythe repayment (route owner_repay or operator_liquidation)amount = the debt, from from (the payer) to to (the vault's settlement account)
vault_closethe close: collateral releasedamount = the settled debt, burned from from; "0" and from null when nothing was settled
vault_liquidationthe operator's recovery of an expired, unpaid vaultnothing moves: amount = the debt left unbacked

These items carry vault: funding_txid, owner, collateral_sats, debt, status, liquidation_height, settlement_account and route (status and debt as the ledger reports the vault now; null on every other operation). In an address history, direction is null when the item is listed only because the address owns the vault.

$ curl "https://crc20.org/api/v1/addresses/bc1ppnrxdhrfu9dvpxgxeem5w5qcern689yhw5l2hfthghun0wz8sgcq5zwz9k/history?tick=AXBT&limit=1"

Replaying AXBT

For the items whose tick is AXBT, credit amount to to and debit it from from (a null side is the mint or the burn; vault_liquidation moves nothing). Pool operations carry tick LEAF but move AXBT too: see pool. This reproduces axbt.balance, which stays authoritative; treat a type you do not know as non-crediting.

Statistics

Token statistics

GET/api/v1/stats/{tick}

Holders, operations, volumes, a daily series and the top holders of a token at the index tip, computed once per block. Additive: fields may be added, existing ones keep their meaning. The example shortens top, daily and cdp.list to one item.

tickpath
LEAF or AXBT, case-insensitive
$ curl https://crc20.org/api/v1/stats/AXBT
token
type, supply, max_supply (null for AXBT), holders, height
operations
total, by_type (only the kinds that occurred: transfer, sale, mint, swap, liquidity_deposit, liquidity_payback, vault_*), last_24h and last_7d (windows ending at window_end), first and last
addresses
distinct addresses in the token's operations
volume
atoms transferred, swapped (the token's leg in the pool), minted, burned
holders
count, top (the 10 largest balances), exact (true when a replay of every operation matches them)
daily
per UTC day from the first operation: operations, volume, holders at the end of the day
pool
swaps and the last one (side, leaf and axbt atoms)
market
LEAF sales: sales, volume_sat, last_unit_price_sat, last · null for AXBT
cdp
AXBT vaults: open_debt, bad_debt, vaults, open_vaults, collateral_locked_sats, every vault in list · null for LEAF

AXBT/LEAF pool

The ledger runs one pool, SWAP_POOL:4:AXBT:4:LEAF. Its swaps and liquidity deposits are ordinary operations of the history and transaction routes, with their own field:

swap
type swap; amount is the LEAF leg, from the owner when it sells LEAF, to when it buys. swap: side (buy or sell), input_tick / input_atoms paid in, output_tick / output_atoms received, fee_atoms (in the input token, kept by the pool's liquidity)
deposit
type liquidity, liquidity.action deposit: from the owner, debited the LEAF amount; deposit (AXBT, LEAF), lock_blocks, maturity_height, creates_pool
payback
at maturity the pool pays the owner back with no transaction of its own: action payback, at the settlement block, with the deposit's txid; to the owner, credited (its share, fees included) and fees. Listed only with ?tick=AXBT or ?tick=all
{
"txid": "ac03dd0a634c2cc4eb21356703df183208f3c935351a1c4e57fffb2eb723740c",
"type": "swap",
"from": "bc1qm0eg260xdxxra96f4sdrghme3a58vxdtsnqn93",
"to": null,
"amount": "6000000000000",
"decimals": 8,
"block_height": 971870,
"block_hash": "000000000000000000007d7516ecda27d3056e5064dd7359dda78480f24adacb",
"block_time": "2026-10-17T14:03:42Z",
"confirmations": 4,
"template": "swap",
"market": null,
"sender_balance_before": null,
"swap": {
"pool": "SWAP_POOL:4:AXBT:4:LEAF",
"side": "sell",
"input_tick": "LEAF",
"input_atoms": "6000000000000",
"output_tick": "AXBT",
"output_atoms": "68026",
"fee_atoms": "60000000000"
},
"liquidity": null,
"vault": null,
"tick": "LEAF"
}

Replaying balances with the pool

In the default scope a deposit is listed but not its payback, so replaying LEAF misses what liquidity providers get back. With ?tick=all, replaying the items whose tick is LEAF is exact for every address. The AXBT legs: a swap debits input_atoms when input_tick is AXBT and credits output_atoms when output_tick is AXBT; a deposit debits liquidity.deposit.AXBT; a payback credits liquidity.credited.AXBT.

Swap counts and the last swap are in statistics (pool). The explorer's pool page also reads internal routes (reserves, price, positions); they are not part of this API and may change without notice.

Limits

usage
free and public, provided as is: no availability guarantee
anonymous
300 requests per minute per IP, bursts of 50 · every answer carries RateLimit-Policy: 300;w=60
over the quota
429 rate_limited with Retry-After: 1: wait, then retry
api key
a larger quota, counted per key instead of per IP: ask for one (contact)
unknown key
401 invalid_api_key

Send the key in the X-API-Key header (or as ?api_key=). Keep it on your servers, never in a web page.

api key
$ curl -H "X-API-Key: crc20_…" "https://crc20.org/api/v1/history?limit=100"

Watching many deposit addresses: poll /api/v1/history (every LEAF operation, newest first) every 30–60 s and stop at the last txid you have seen, rather than polling each address.

Deposits

  1. Give each user a single-key P2WPKH (bc1q…) or key-path P2TR (bc1p…) address. Other types (1…, 3…, P2WSH, multisig) can receive LEAF but can never send it again.
  2. Poll /addresses/{address}/history; credit items with direction = in, deduplicated by txid.
  3. Wait for confirmations (6 recommended) and check block_hash against your own node. Never credit from the mempool.
  4. Keep a small BTC UTXO on every deposit address: sending LEAF out requires spending one of its own UTXOs as vin[0].

Withdrawals

A LEAF transfer is a normal Bitcoin transaction with this layout:

vin[0]UTXO of the senderdefines who sends · P2WPKH or P2TR · signed SIGHASH_ALL (or DEFAULT)
vin[1…]fee UTXOsany of your UTXOs
vout[0]OP_RETURN payload · 0 satexactly one per transaction
vout[1]receiver ≥ dustmust directly follow the payload · 330 sat (P2TR) · 294 sat (P2WPKH)
vout[2…]BTC changeanywhere after the receiver

Payload

Exactly {"p":"crc-20","op":"transfer","tick":"LEAF","amt":"<atoms>"} — this key order, no spaces, amt as a decimal string of atoms (1500 LEAF = "150000000000"), pushed in one direct push after OP_RETURN. The script for 1500 LEAF:

scriptPubKey1500 LEAF
6a417b2270223a226372632d3230222c226f70223a227472616e73666572222c227469636b223a224c454146222c22616d74223a22313530303030303030303030227d

Confirm

After confirmation, /transactions/{txid} must return the expected from, to and amount. A 404 once the indexer is past that block means no LEAF moved: re-send only then.

Rulesa broken rule confirms on Bitcoin but moves no LEAF

  • The receiver is always the output right after the OP_RETURN: disable output sorting (BIP69) and change randomisation.
  • Never sign vin[0] with ANYONECANPAY, NONE or SINGLE.
  • One CRC-20 OP_RETURN per transaction: no batching.
  • Withdrawals from one address apply in block order against its balance: track in-flight amounts and confirm every txid.
  • Never send LEAF to 1…, 3…, P2WSH or multisig addresses.

Market sales

A marketplace sale is a LEAF transfer with a different template: the seller pre-signs two small "carrier" inputs with SINGLE|ANYONECANPAY, the buyer adds the payment, and the platform co-signs with its anchor. The API reports it as template = market. Integrators never need to build one.

vin[0], vin[1]seller carriers670 sat each, signed SINGLE|ANYONECANPAY · vin[0] makes the seller the sender
vin[2]platform anchor1000 sat, signed SIGHASH_ALL by the platform
vin[3…]buyer fundingpays the price and the fees
vout[0]seller paymentprice + the 1340 sat of carriers
vout[1]payloadthe LEAF amount
vout[2]buyer receiverreceives the LEAF
vout[3]platform fee
vout[4]anchor return1000 sat back to the platform
vout[5]buyer change

Price = vout[0] − 1340 · buyer paid = price + platform fee + network fee. These figures are in market.

Code

payload.py
def leaf_transfer_script(atoms: int) -> bytes:
"""OP_RETURN script of a LEAF transfer of `atoms` (1 LEAF = 100_000_000 atoms)."""
if type(atoms) is not int or not 0 < atoms < 2**64: # no floats: 0.29 * 1e8 != 29000000
raise ValueError("amount out of range")
payload = ('{"p":"crc-20","op":"transfer","tick":"LEAF","amt":"%d"}' % atoms).encode()
return bytes([0x6a, len(payload)]) + payload # OP_RETURN + one direct push
assert leaf_transfer_script(150_000_000_000).hex().startswith("6a417b2270223a226372632d3230")
withdraw.jsP2WPKH sender · bitcoinjs-lib v6
import * as bitcoin from "bitcoinjs-lib";
import * as ecc from "tiny-secp256k1";
bitcoin.initEccLib(ecc); // required for bc1p (P2TR) receiver or change addresses
const network = bitcoin.networks.bitcoin;
function leafTransferScript(atoms /* BigInt */) {
if (typeof atoms !== "bigint" || atoms <= 0n || atoms >= 2n ** 64n) throw new Error("amount out of range");
const json = `{"p":"crc-20","op":"transfer","tick":"LEAF","amt":"${atoms.toString()}"}`;
return bitcoin.script.compile([bitcoin.opcodes.OP_RETURN, Buffer.from(json, "utf8")]);
}
// sender: a UTXO of the LEAF-holding address (P2WPKH), keyPair: its key; fees: other UTXOs for the miner fee
function buildLeafWithdrawal({ sender, keyPair, fees, receiver, atoms, change, changeValue }) {
const psbt = new bitcoin.Psbt({ network });
psbt.setVersion(2);
psbt.setLocktime(0);
psbt.addInput({ hash: sender.txid, index: sender.vout, sequence: 0xfffffffd,
witnessUtxo: { script: sender.script, value: sender.value } }); // vin[0] = sender
for (const u of fees) {
psbt.addInput({ hash: u.txid, index: u.vout, sequence: 0xfffffffd,
witnessUtxo: { script: u.script, value: u.value } }); // vin[1..] = fees
}
psbt.addOutput({ script: leafTransferScript(atoms), value: 0 }); // vout[0] = payload
psbt.addOutput({ address: receiver, value: 330 }); // vout[1] = receiver
psbt.addOutput({ address: change, value: changeValue }); // vout[2] = BTC change
psbt.signInput(0, keyPair); // SIGHASH_ALL (default for P2WPKH) — do not override
return psbt; // sign the fee inputs, finalizeAllInputs(), then broadcast
}

P2TR key-path sender: add tapInternalKey (32-byte x-only key) to vin[0], sign with keyPair.tweak(bitcoin.crypto.taggedHash("TapTweak", internalKey)) and keep SIGHASH_DEFAULT.

Soft covenants

A soft covenant is a Taproot (P2TR) address whose only spending path is one script leaf that commits to a program (its CMR), a relative time lock and an owner key. Like any address it holds a LEAF balance, receives transfers and sends them. Its internal key is the BIP341 "nothing up my sleeve" point 50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0, which nobody can sign for: there is no key path.

tapleafleaf version 0xc0
<CMR: 32 bytes> OP_DROP <delay> OP_CHECKSEQUENCEVERIFY OP_DROP <owner x-only key: 32 bytes> OP_CHECKSIG
20 <32-byte CMR> 75 <delay push> b2 75 20 <32-byte key> ac
CMR
Commitment Merkle Root: the 32-byte hash that identifies a compiled Simplicity program; change one bit of the program and the CMR changes. Bitcoin mainnet does not execute Simplicity, so the leaf only pushes the CMR (20 = push 32 bytes) and drops it (75 = OP_DROP). The program is committed in the address but runs nowhere on chain — hence "soft". The CRC-20 indexer uses the CMR as the contract's identity at deploy and mint; it does not read it when the address sends LEAF.
delay
a relative time lock of 1 to 65535 blocks, enforced by Bitcoin (OP_CHECKSEQUENCEVERIFY): the UTXO spent must be at least that many blocks old. It locks UTXOs, not tokens: LEAF that has just arrived can leave with any UTXO of the address that is already old enough.
owner key
the 32-byte x-only key whose Schnorr signature is required (20 = push 32 bytes, ac = OP_CHECKSIG). Without it the leaf would be spendable by anyone once the delay has passed.
address
the NUMS key tweaked with this single leaf: the same owner and delay with a different CMR give a different address.

Bitcoin enforces the delay and the owner signature; these are the only protections of a transfer out of a soft covenant. To send LEAF from one, spend one of its UTXOs as vin[0] through the script path:

  • transaction version 2;
  • nSequence of vin[0] = the delay, as a block-based BIP68 value (bits 31 and 22 clear): 0xfffffffd, 0xfffffffe and 0xffffffff fail;
  • witness exactly [owner signature, leaf, control block], signed SIGHASH_DEFAULT or SIGHASH_ALL. Any other sighash confirms on Bitcoin but moves no LEAF.

Do not use a soft covenant as a deposit address, and do not send withdrawals to one unless the user asks for it.

Contact

Integrating LEAF or CRC-20? Reach the team: