A Kaspa token balance is not a number in a global ledger. It is the sum of unspent covenant cells — UTXOs whose script encodes both ownership and amount — scattered across the BlockDAG. Finding those cells, confirming they still exist on the canonical chain, and keeping the count accurate as Kaspa's selected chain shifts requires a dedicated piece of infrastructure: a reorg-aware BlockDAG indexer.
This article explains why that indexer is necessary, how it reconstructs Kaspa live cells step by step, and where it fits in the Kaspa Forge architecture. Kaspa Tokens are planned and not live; the indexer described here is part of that plan, not a deployed service.
Why a Normal Wallet Scan Fails
In Bitcoin and most UTXO chains, a wallet discovers its balance by scanning transaction outputs that pay to its own address. The address is a direct encoding of the public key hash, so any output paying to that address belongs to whoever holds the corresponding private key. A simple address-history lookup is sufficient.
Kaspa's token covenant cells break this assumption. The owner's public key is embedded inside the covenant's on-chain state — a 77-byte structure — not in a standard address field. A normal P2PK wallet scanning its own address history will not enumerate these outputs because they do not appear as payments to a conventional Kaspa address. The wallet must understand the specific covenant template, decode each output's state, and extract the owner field to determine whether it matches.
This is not a minor inconvenience. It means no general-purpose Kaspa wallet can display token balances without template-specific decoding logic. The Kaspa wiki documents how Kaspa's virtual block and selected chain work, but finding token cells within that structure requires an additional layer of interpretation on top.
GHOSTDAG Reorgs: The River That Shifts
Kaspa does not have a single linear chain. Its GHOSTDAG protocol produces a directed acyclic graph of blocks, where multiple blocks can be mined in parallel. The virtual block — a node-local construct pointing at all current tips — selects a "selected parent" chain through this DAG. As new blocks arrive, the selected tip can change, and with it the selected chain. This is called a reorg.
The Kaspa wiki states it plainly: "the selected chain can change (since virtual point-of-view changes), it's called a reorg. In a wide DAG small reorgs are frequent." These are not catastrophic events; they are normal operation. A block that was on the selected chain a moment ago may find itself on a side branch, and a previously non-selected block may become part of the canonical history.
Think of it like a river with multiple channels. The "main channel" — the selected chain — can shift as new water arrives. Small shifts happen constantly. For token tracking, this matters enormously. A token cell that appeared spent in a transaction accepted by one selected chain may become unspent again if that chain is deselected. Conversely, a cell that was unspent may suddenly be spent by a transaction on a newly selected branch. Any indexer that ignores reorgs will report incorrect Kaspa token balances.
Covenant Cells: Token State Lives in UTXOs
Covenant cell: A UTXO whose script contains a covenant — a spending condition that constrains not just who can spend the output, but how it can be spent. In Kaspa Forge Tokens, each covenant cell carries a 77-byte state encoding the token amount, owner key, and protocol metadata.
A Kaspa token is not a single object. It is a family of UTXO cells, all sharing the same Covenant ID — a 32-byte identifier derived from the genesis transaction's funding outpoint and the set of bare token outputs. Each cell holds some amount of the token and is owned by a specific Schnorr public key.
When a user sends tokens, the transaction consumes one or more existing cells (inputs) and creates new cells (successors) with the same Covenant ID. The covenant script enforces conservation: the total token amount across all successor outputs must equal the total across all consumed inputs. In the KF20-FIXED-v1 profile, there is no minting path after genesis; supply is fixed at creation.
This means that at any moment, the set of live token cells — unspent UTXOs on the selected chain — defines the complete state of the token. There is no global balance mapping. There is no account ledger. The only way to determine how many tokens a key holds is to find all live cells whose owner field matches and sum their amounts.
The Indexer Pipeline, Step by Step
A reorg-aware BlockDAG indexer for Kaspa tokens must continuously perform the following cycle:
1. Follow the accepted virtual chain. The indexer connects to a Kaspa node and watches the accepted chain — the sequence of blocks on the selected parent path. It maintains a chain_cursor recording the current accepted DAA score and hash.
2. Decode token events. For each accepted block, the indexer examines every transaction. It identifies outputs that match known covenant templates (by template hash and program bytes), classifies them using a versioned classifier, and records new token cells. It also identifies inputs that spend existing token cells and marks those cells as spent.
3. Record transitions. Each transaction that moves tokens produces a token_transition record: the transaction ID, the DAA score at which it was accepted, the kind of operation (genesis or transfer), input/output totals, and validation status.
4. Handle reorgs atomically. When the selected tip changes, the indexer walks backward from the old tip and forward from the new tip, identifying blocks that left or entered the selected chain. For each removed block, it consults an undo_journal — a log of deltas sufficient to reverse every state change — and rolls back affected cells and transitions. For each newly accepted block, it applies the decode-and-classify pipeline forward. This is atomic: the index is never in a partially rolled-back state.
5. Derive balances. With the canonical set of live cells established, the indexer computes balances, holder counts, circulating supply, and activity as derived views. These can be materialized for query speed, but they are always recomputable from the canonical cell set — they are not a second source of truth.
The entity model underlying this pipeline:
token_families — one row per token (Covenant ID, genesis, declared supply)
token_cells — one row per UTXO (outpoint, owner, amount, spent/live)
token_transitions — one row per accepted tx (kind, totals, validation)
token_metadata — canonical name, ticker, decimals, image digest
chain_cursor — current accepted DAA score and hash
undo_journal — rollback deltas for atomic reorg handling
Balances, holders, and circulating supply are always derived from canonical live cells. They can be cached for speed, but the live cell set is the single source of truth.
Why the Encrypted Profile Cannot Be the Source of Truth
Kaspa Forge's Desk stores an encrypted profile on the user's device. This profile contains the HD seed, vault configurations, escrow references, and UI state. It is the source of truth for keys and user preferences.
But it cannot be the source of truth for token balances. Three reasons:
The profile does not watch the chain. It is a static encrypted blob, not a live process. It has no connection to the Kaspa node, no awareness of new blocks, and no ability to detect reorgs. A balance recorded at time T may be wrong at T+1 if a reorg reorders the selected chain.
Token cells are not address-indexed. The owner is inside the covenant state, not in a standard address field. The profile's wallet keys are necessary to authorize spending, but not sufficient to discover which cells exist. Discovery requires template-aware chain scanning — a normal P2PK wallet address history may not enumerate all token cells.
Cells can live on parallel branches. In the BlockDAG, multiple blocks may reference the same transaction differently. A cell that appears spent on one branch may be unspent on another. Only the indexer, tracking the continuously shifting selected chain, can determine which cells are canonical at any moment.
The Desk profile therefore stores only what it can know independently: which tokens the user created (createdTokens[]), which tokens they follow (followedTokens[]), and any pending transaction intents. The actual balance is always fetched from the indexer's read API.
How Kaspa Forge Plans to Use It
The indexer is the read backbone for every token-related surface in Kaspa Forge. The planned API routes:
GET /api/safe/tokens — list known tokens
GET /api/safe/tokens/{covenant_id} — single token detail
GET /api/safe/tokens/{covenant_id}/holders — holder distribution
GET /api/safe/tokens/{covenant_id}/activity — transfer history
GET /api/safe/tokens/{covenant_id}/cells?owner=... — live cells for owner
POST /api/safe/tokens/submit — submit signed KF20 tx
Read endpoints require no authentication — they serve public chain facts. The submit endpoint accepts only a fully signed transaction built by the client-side token-wallet-wasm signer; the server never sees keys or constructs transactions. Public responses preserve raw amount, owner, and state provenance so that any third-party client can independently re-verify the representation.
In Desk, the planned Tokens tab will use these endpoints to display balances, build transfer previews, and show activity. The signer boundary ensures that even if the server were compromised, it cannot alter a transaction's recipient, amount, or template without the signer rejecting the mismatch.
Kaspa Tokens and its reorg-aware indexer are planned and not live. The architecture, covenant contract, typed admission, and golden fixtures are under active development but have not been deployed to mainnet. To follow current progress or explore Kaspa Forge's live non-custodial tools — Kaspa Safe, Escrow, Deposit, Boards, and Arena Blackjack — visit the architecture docs.
Trade-offs and Honest Limitations
Indexer dependency for discovery. Unlike Kaspa Safe, where a single node is sufficient for full vault discovery, token balance lookup depends on the indexer. The owner field inside the covenant state means a standard P2PK wallet scan will not find token cells. If the indexer goes offline, tokens remain safe on-chain — the indexer has no custody — but balance queries and transaction building become unavailable until it restarts or an alternative read endpoint is configured.
Reorg depth vs. storage. The undo journal must cover enough depth to handle the longest realistic reorg. In Kaspa's GHOSTDAG, reorgs are typically shallow, but the indexer must be conservative. A deeper journal means more storage; a shallower one risks incomplete rollback.
Classifier versioning. As new token profiles are added — for example, a future KF20-KCC20-v1 — the classifier must be versioned. Old cells must remain classifiable by the version that originally recognized them. This adds complexity to the decode pipeline but prevents silent misclassification across profile generations.
Recovery independence. Kaspa Forge plans to publish the indexer as open source, along with a descriptor format for each token (network, Covenant ID, genesis outpoint, template hash, metadata digest) and a CLI recovery tool. The goal: a user with a seed and a token descriptor can rebuild their balance against their own node and a compatible indexer, without depending on Kaspa Forge's hosted infrastructure. This is a design commitment, not yet a delivered artifact.
No hidden mint authority. The KF20-FIXED-v1 profile has no minting path after genesis. The indexer tracks existing cells; it does not authorize new supply. This simplifies the threat model but means any future mintable profile will require its own separate audit, classifier version, and consensus proof package — it is not a hidden capability of the fixed-supply design.
Continue exploring
Kaspa Forge product documentation
Next useful step: open the non-custodial Desk
FAQ
Why can't a normal Kaspa wallet show my token balances?
Token ownership is encoded inside covenant script state, not in a standard P2PK address. A wallet scanning its own address history will miss token cells unless it understands the specific covenant template and decodes each output's 77-byte state to extract the owner field.
What is a reorg in Kaspa's BlockDAG?
The virtual block's selected tip can change as new blocks arrive, shifting which chain is canonical. This is normal GHOSTDAG operation — small reorgs are frequent. An indexer must atomically undo and reapply state changes when the selected chain shifts.
Can the Desk profile store token balances as a source of truth?
No. The encrypted Desk profile stores UI preferences — followed tokens, pending intents — but not live balances. Balances must be continuously reconstructed from canonical live cells on the selected chain by a reorg-aware indexer.
What happens if the Kaspa token indexer goes offline?
Tokens remain safe on-chain — the indexer has no custody or signing authority. Balance discovery and transaction building become unavailable until the indexer restarts or an alternative read endpoint is configured.
Is the Kaspa token indexer live?
No. Kaspa Tokens and its reorg-aware indexer are planned and not live. The covenant contract, typed admission, and golden fixtures are under active development but have not been deployed to mainnet.
Put your KAS where theft can be cancelled
A covenant vault on Kaspa mainnet: your keys, your rules, our tooling. Free on-chain, forever.
Create a vault
