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

# On-chain XP

> The soulbound XP token, what mint and burn do, which balance a caller is reading, and why the chain is the one it is.

The XP token is a soulbound ERC20 that holds every customer's XP on chain. A transfer reverts unless one side of it is the zero address, so a balance moves by mint and by burn and by nothing else. {/* trace: A-18 */}

## Mint and burn are the two halves you already know

A mint is the on-chain half of an earn, and a burn is the on-chain half of a redemption. Each XP event is one chain transaction rather than a batched one. {/* trace: A-26 */}

Both are gated. Mint takes `MINTER_ROLE` and burn takes `BURNER_ROLE`, and each has an equivalent that accepts a signature from a signer the contract has registered instead of a role. The contract carries four roles, `ADMIN_ROLE`, `MINTER_ROLE`, `BURNER_ROLE` and `SET_SIGNER_ROLE`, with `ADMIN_ROLE` administering all four. {/* trace: A-19, A-20 */}

Both carry a hash the contract consumes once, so the same operation cannot be applied twice even if it reaches the chain twice. {/* trace: A-21 */}

Both emit an event carrying the customer id alongside the address and the amount, a mint as `Issue` and a burn as `Redeem`, so a chain record maps back to a customer without a lookup on your side. {/* trace: A-22 */}

The transfer that is refused fails with the contract's own `KintraXPTransferDisabled` error, raised whenever neither side of a movement is the zero address. That is the guarantee stated as code: no customer can send XP to another, and no integration can move it for them. {/* trace: T-19, A-18 */}

## There is one balance, and the chain holds it

This is the part to get right before you build against it. Kintra keeps no separate XP total of its own. A balance read sums the on-chain balances of the wallets Kintra manages for that customer, an earn increments no stored number, and a redemption decrements none. {/* trace: T-21 */}

So a caller reading `GET /gateway/customers/{userId}/xp/balance` is reading the chain's number, arrived at through an indexer that follows the token's transfer events and calls the token back for the balances they name. {/* trace: A-08, A-09 */}

What you are not reading is a ledger the API maintains alongside it. The internal transaction record is the intent and the audit trail: what was granted or spent, for which customer, and which chain transaction carried it. Its `amount` is an unsigned magnitude and its direction is in `type`, so it is a history rather than a balance, and summing it is not how you find out what a customer holds. {/* trace: A-07, A-15, A-32 */}

The practical consequence is timing, and it is the same one on both pages that mention it. A balance read taken straight after an accepted earn can still answer the old number, because the credit lands when the mint is indexed rather than when the write returns. A redemption behaves the same way in reverse. {/* trace: A-17 */}

A chain transaction that reverts leaves the internal transaction at `failed` and the balance where it was. {/* trace: A-14 */}

## What the token is and is not

XP is a whole number. The token's `decimals` is fixed in code at zero rather than stored as a setting, so there is no fractional unit of a point and there never will be for this implementation. {/* trace: T-18, A-06 */}

One token holds every tenant's XP. The XP asset is one record per chain, and the separation between tenants is enforced in the API rather than by a contract per tenant. {/* trace: A-24 */}

The token runs behind a proxy and is upgradeable, with the upgrade gated on `ADMIN_ROLE`. The address you read a balance from is the proxy's, and it stays the same when the implementation behind it changes. {/* trace: A-23, T-20 */}

Because it is an ERC20 on an EVM chain, a balance can be read from the contract directly as well as through the API, which is how the platform reads it too. Responses name `chain_id` and the asset address as facts about where the token lives. {/* trace: A-09, A-18, A-27 */}

## The chain

Kintra runs on LightLink, and it is the only chain the platform supports. {/* trace: A-25 */}

Nothing you write names a chain. No request body carries a chain field, and the two list reads that accept `chain_id` take it as an optional filter, which narrows what comes back rather than choosing where anything is written. {/* trace: A-27 */}

What the design asks of a chain is small writes, often, one per member event, because every earn is a mint and every redemption is a burn. {/* trace: A-26 */}

## Where to go next

* [XP and the ledger](/concepts/xp-and-the-ledger) for what an earn and a redemption look like from the API side.
* [Rewards and redemptions](/concepts/rewards-and-redemptions) for the record a burn is written against.
* [Architecture](/architecture) for tenant isolation and the settlement path in one place.
* [Tiers](/concepts/tiers) for what the balance is compared against.
