Create and transfer custom tokens on the LEZ
Use the wallet and SPEL CLIs to create custom tokens, transfer them, mint or burn supply, and rotate the mint authority.
This document is accurate for Testnet v0.3.0.
The Logos Execution Zone (LEZ) is a programmable blockchain that cleanly separates public and private state while keeping them fully interoperable. LEZ's token program is a single, shared program that creates and manages custom tokens—there's no separate contract deployment per token. You drive it with the SPEL CLI (spel), which sends the program's instructions directly, and the wallet CLI, which creates and lists the accounts those instructions use.
Token program accounts fall into three types:
- Token definition account
- Each token has exactly one token definition account.
- It defines the token globally: its name, total supply, optional metadata reference, and optional mint authority.
- Its address is the token identifier (similar to a mint address).
- Token holding account
- Any account that holds a balance of a token is called a token holding account for that token.
- Each token can have multiple token holding accounts.
- Token metadata account
- Created alongside a non-fungible token's definition to hold its standard, URI, and creators.
Transfers, mints, and burns are irreversible, and renouncing the mint authority can never be undone. Double-check all details before submitting a transaction.
- An LEZ CLI wallet set up and funded.
- The token program's IDL. It's committed at
artifacts/token-idl.jsonin thelez-programsrepository, so cloning the repository is enough—you don't need to build anything to get it.
What to expect
- You can create a fungible token with a fixed supply, a self-managed mint authority, or a mint authority delegated to another account.
- You can transfer, mint, and burn tokens, and rotate or permanently renounce a token's mint authority.
- You can inspect any token definition, holding, or metadata account to confirm the result of an instruction.
Step 1: Install spel
spel is a developer CLI tool used to help build and drive programs that run on the LEZ.
- Install
spel.
Step 2: Create a fungible token
Creating a token creates its token definition account and its first token holding account, credited with the full total supply, in one instruction. Both target accounts must be fresh (never written to) and must be accounts your wallet holds the keys for.
-
Create two accounts: one for the token definition, one for the first holding.
wallet account new public --label "Token A Definition"wallet account new public --label "Token A Holding" -
Confirm the accounts were created, and note their ids.
wallet account list- The listing shows each account's label and base58 id, for example
Public/CER21z16YgmWr3aN8FEHsrmfm2iRfQiwZTac3FQa21US [Token A Definition]. Use those ids wherever this procedure refers to<DEF>,<HOLDING>, or<AUTHORITY>.
- The listing shows each account's label and base58 id, for example
-
Create the token with
new-fungible-definition, replacing<TOKEN_PROGRAM_ID>with thetokenrow's ImageID (hex) value—the 64-character hex string—from DEPLOYMENTS.md.spel --programtakes only a 64-char hex ID, a name fromspel.toml, or a path to a program binary, so the base58 ImageID in the next column is read as a file path and fails.spel --idl artifacts/token-idl.json \--program <TOKEN_PROGRAM_ID> \-- new-fungible-definition \--name "TOKEN A" \--total-supply 1000000000000000000000 \--definition-target-account <DEF> \--holding-target-account <HOLDING> \--mint-authority <AUTHORITY>-
--total-supplyis au128in base units—the token program stores no decimals field, so1000000000000000000000is 1000 whole tokens only if your UI assumes 18 decimals. -
--mint-authorityis always required (spelhas no optional flags) and decides the token's supply model:Value Meaning <DEF>(the definition's own id)Self authority—mint later with mint(Step 5), signing as the definition account.Another account id External authority—mint later with mint-with-authority(Step 5), signing as that account.noneFixed supply—minting is permanently rejected. -
An all-zero authority id is rejected. Both target accounts must be fresh—re-running the command against the same accounts fails.
-
-
Confirm the definition and holding were created correctly.
spel --idl artifacts/token-idl.json inspect <DEF> --type TokenDefinitionspel --idl artifacts/token-idl.json inspect <HOLDING> --type TokenHolding
Step 3: Give someone a holding
Before an account can receive a transfer or a mint, it needs an initialised token holding account for that token's definition. initialize-account creates an empty holding—Fungible { balance: 0 } for a fungible definition.
-
Create the account that will hold the token.
wallet account new public --label "Token A Holding (Bob)" -
Initialise it against the token's definition.
spel --idl artifacts/token-idl.json \--program <TOKEN_PROGRAM_ID> \-- initialize-account \--definition-account <DEF> \--account-to-initialize <NEW_HOLDING><DEF>is read-only here and isn't signed—anyone can initialise a holding for any definition, as long as they sign the new holding account.- The definition must be owned by the token program build you're using, or the call is rejected.
Step 4: Transfer
transfer moves a balance between two holdings of the same definition. spel signs the sender; the recipient is only written to, so it must already be initialised (Step 3)—a fresh account's claim can't be authorised here because transfer never collects the recipient's signature.
-
Transfer tokens from a holding you own to an already-initialised recipient holding of the same definition.
spel --idl artifacts/token-idl.json \--program <TOKEN_PROGRAM_ID> \-- transfer \--sender <SENDER_HOLDING> \--recipient <RECIPIENT_HOLDING> \--amount-to-transfer <AMOUNT><SENDER_HOLDING>must be a holding you own, with a balance ≥<AMOUNT>.<RECIPIENT_HOLDING>must already be initialised and belong to the same token definition as the sender, or the transfer is rejected.
-
Confirm the recipient's new balance.
spel --idl artifacts/token-idl.json inspect <RECIPIENT_HOLDING> --type TokenHolding
Step 5: Mint more supply
Minting adds to both the holding's balance and the definition's total supply. It's fungible-only, and which instruction you use depends on where the definition's mint authority lives (Step 2). The target holding must already be initialised—it isn't a signer, so a fresh account can't be claimed by a mint.
Option A—Self authority
Use this when the definition's stored mint authority is the definition account itself.
-
Mint, signing with the definition account.
spel --idl artifacts/token-idl.json \--program <TOKEN_PROGRAM_ID> \-- mint \--definition-account <DEF> \--user-holding-account <HOLDING> \--amount-to-mint <AMOUNT>
Option B—External authority
Use this when a separate account holds the mint authority—the normal case when you passed a distinct --mint-authority at creation, or rotated it later (Step 6).
-
Mint, signing with the authority account.
spel --idl artifacts/token-idl.json \--program <TOKEN_PROGRAM_ID> \-- mint-with-authority \--definition-account <DEF> \--user-holding-account <HOLDING> \--authority-account <AUTHORITY> \--amount-to-mint <AMOUNT>
Using the variant that doesn't match where the authority actually lives fails with signer is not the current authority, even if you hold the right key. A revoked (none) authority rejects both variants with authority revoked, supply is fixed.
Step 6: Rotate or renounce the mint authority
Rotating or renouncing follows the same self-vs-external split as minting, and is fungible-only.
Option A—Current authority is the definition account
-
Set a new authority, signing with the definition account.
spel --idl artifacts/token-idl.json \--program <TOKEN_PROGRAM_ID> \-- set-authority \--definition-account <DEF> \--new-authority <NEW_AUTHORITY>
Option B—Current authority is a separate account
-
Set a new authority, signing with the current authority account.
spel --idl artifacts/token-idl.json \--program <TOKEN_PROGRAM_ID> \-- set-authority-with-authority \--definition-account <DEF> \--authority-account <CURRENT_AUTHORITY> \--new-authority <NEW_AUTHORITY>
Passing --new-authority none permanently renounces minting—the supply becomes fixed and no later rotation is possible. There is no undo.
-
Confirm the new authority.
spel --idl artifacts/token-idl.json inspect <DEF> --type TokenDefinition
Step 7: Burn
burn destroys supply from a holding you own. The holding signs; the definition is written but not signed, so any holder can burn their own tokens without the issuer's involvement, and burning is not gated on the mint authority—even a renounced, fixed-supply token can still shrink.
-
Burn from a holding you own.
spel --idl artifacts/token-idl.json \--program <TOKEN_PROGRAM_ID> \-- burn \--definition-account <DEF> \--user-holding-account <HOLDING> \--amount-to-burn <AMOUNT>- Burning more than the holding's balance is rejected.
Step 8: NFTs
A non-fungible token is a definition with a printable_supply plus a metadata account. Its first holding is an NftMaster; each print carves an NftPrintedCopy out of it. print_balance reserves one unit for the master itself, so a printable_supply of N yields N − 1 printable copies.
spel cannot currently create NFT (or metadata-bearing fungible) definitions—new-definition-with-metadata takes two structured arguments that spel has no way to encode (it can only encode enum-kinded IDL types, and neither argument's type appears in the types section of the IDL). This still holds as of CLI v0.7.0. Until it's supported, create these definitions programmatically; see token_program::new_definition::new_definition_with_metadata and the integration tests in programs/integration_tests/tests/token.rs in the lez-programs repository. Everything below works over the CLI against a definition created that way.
-
Print a copy. Both the master holding and the fresh printed-copy account must sign; the printed target must be fresh—
print-nftclaims it itself, so don't pre-initialise it.wallet account new public --label "NFT copy #1"spel --idl artifacts/token-idl.json \--program <TOKEN_PROGRAM_ID> \-- print-nft \--master-account <NFT_MASTER_HOLDING> \--printed-account <NEW_COPY_HOLDING>- Each print decrements the master's
print_balanceby 1. Printing requiresprint_balance > 1, so the last unit can never be printed.
- Each print decrements the master's
-
Move a printed copy. Initialise a slot for the recipient against the NFT definition (Step 3)—it produces an un-owned
NftPrintedCopy—then transfer1(Step 4).spel --idl artifacts/token-idl.json --program <TOKEN_PROGRAM_ID> \-- initialize-account --definition-account <NFT_DEF> --account-to-initialize <RECIPIENT_SLOT>spel --idl artifacts/token-idl.json --program <TOKEN_PROGRAM_ID> \-- transfer --sender <MY_COPY> --recipient <RECIPIENT_SLOT> --amount-to-transfer 1 -
Transfer a master. This is only meaningful into an existing
NftMasterholding sitting atprint_balance: 0, with the transferred amount equal to the sender's wholeprint_balance.initialize-accountalways produces a printed-copy slot, never a master, so in practice a master can only move to an account that previously gave one away—plan master ownership at creation time.
Step 9: Inspect a token account
Any token account can be read back at any time, with no signing and no transaction submitted.
spel --idl artifacts/token-idl.json inspect <DEF> --type TokenDefinition
spel --idl artifacts/token-idl.json inspect <HOLDING> --type TokenHolding
spel --idl artifacts/token-idl.json inspect <METADATA> --type TokenMetadata
--type must match the account's actual shape—decoding a holding as a definition fails. A holding's definition_id field is how you find the token it belongs to.
Troubleshooting
Transaction NOT confirmed on a transaction that actually landed
spel stops waiting after a fixed number of blocks and prints ❌ Transaction NOT confirmed: Transaction not found in preconfigured amount of blocks. It prints that same line whether the transaction was rejected by the program or simply confirmed too slowly. Check the affected accounts with inspect (Step 9) before re-running—re-running a transfer that did land moves the balance a second time.
Token definition must be owned by token program
The token definition was created by a different build of the token program than the one you're calling. Recompiling the token program changes its ProgramId, and tokens created by an older build stay owned by it—recreate the token against the build you're currently using.
A transfer or mint rejects a fresh recipient account
Neither transfer nor mint marks its recipient/holder as a signer, so the claim of a fresh account can't be authorised. Run initialize-account on the recipient first (Step 3). print-nft is the one exception—its printed target is a signer, so it must be fresh and must not be pre-initialised.
Option or account arguments are rejected as missing
spel has no optional arguments. --mint-authority and --new-authority must always be passed—use the literal none for a fixed-supply token or a renounced authority. Similarly, there's no optional-account mechanism, which is why authority operations are split into mint/mint-with-authority and set-authority/set-authority-with-authority; using the variant that doesn't match where the authority lives fails with signer is not the current authority, even when you hold the right key.
--dry-run submits the transaction anyway
--dry-run is a global flag and only works before the -- separator: spel --idl … --program … --dry-run -- transfer … resolves and prints the transaction without submitting it. Placed after the instruction name, it's silently ignored and the transaction is submitted.
spel rejects an account id argument
Account ids must be passed as bare base58 or 0x-prefixed hex. Strip the wallet's account_id(...) display wrapper before passing an id to spel.