Minting phases
Phases are time-gated by epoch ranges and resolved byget_current_phase(). Up to three sequential phases per collection.
Durations are measured in epochs, the chain’s native time unit.
MIN_PHASE_EPOCHS = 240, about 40 minutes. That floor is set from the fastest epoch rate the chain has actually held rather than a nominal ten seconds, so a phase usually runs 42 to 49 minutes. GTD and FCFS are bounded above by 259200 epochs (roughly a month). Set windows in epochs against the live chain rate rather than assuming a fixed seconds-per-epoch, since epoch wall-clock can drift. GTD and public both require a valid window; FCFS is the optional one. Phase ordering is enforced on-chain: fcfs_start >= gtd_end and pub_start >= fcfs_end.
Required here means the phase needs a valid time window. You can still make a phase mint nothing by setting its cap to
0 (closed). So a “no public sale” collection schedules a public window but leaves its cap at 0.A phase cap of
0 means the phase is closed, not unlimited. To leave a phase open to the full supply, set its cap to the max supply. The global total_minted <= max_supply guard bounds everything regardless.set_phase_override(); return to time-based resolution via clear_phase_override().
Caps share one budget
The three phase caps are not independent ceilings. The contract sums them with the Xholder reserve and anything already airdropped, and rejects a configuration whose total exceeds supply:The Xholder reserve
Every Xcollection reserves an allocation for Xpectra holders, computed at deploy time as:xholder_mint(xpectra_id) during GTD. The contract verifies the caller currently owns that Xpectra token (owner_of(xpectra_id) == caller) and marks the claim before the external call. Xpectra holders pay the GTD price, with no discount.
The reserve is its own pool, additive on top of the creator’s GTD allowlist allocation. It is decoupled from the GTD allocation: an xholder mint does not increment gtd_minted, does not consume gtd_cap, and is not bound by gtd_wallet_cap, so the reserve can never erode a creator’s own allowlist. It is still bound by the collection’s global max_per_wallet.
One claim per wallet, not one per Xpectra held.
xholder_claimed[caller] is checked and set per wallet, so a wallet holding ten Xpectra gets one reserved mint on a given collection, the same as a wallet holding one. Read it with has_claimed_xholder(wallet).Separately, used_xpectra_ids records which Xpectra token IDs have been spent on a collection, readable with is_xpectra_id_used(id), so the same token cannot be passed between wallets to claim twice.get_xholder_status(), which returns xholder_minted_count | injection_cap | xpectra_contract | injection_swept.
After GTD ends, unclaimed reserve slots return to the public pool through trigger_sweep(). The sweep is permissionless once GTD has ended, so no party can withhold it.
Allowlist
Xcollection calls its bound Xlist contract to verify GTD and FCFS access at mint time. See Xlist.Payment splitting
Every mint splits payment at the contract level:
Both collect via
claim_proceeds(); balances are read with get_pending_proceeds(addr). Proceeds are never pushed alongside cross-contract calls. The pull model keeps funds isolated from AML atomicity risk.
Reveal
Collections deploy unrevealed.token_uri() returns the unrevealed_uri until reveal. The creator commits a provenance hash before mint opens, then calls reveal(base_uri) to publish the real metadata. Tokens then resolve as base_uri + token_id + .json.
On Xpectrum the base URI is the collection’s Circle (oct://<circle_id>/), so revealed metadata and art are on-chain. Reveal is irreversible. See Provenance for how the held-back metadata is verified against the pre-committed hash.
Airdrop
The owner can airdrop up to 20 tokens per transaction viaairdrop(). Airdrops bypass phase, price, and allowlist checks and count toward total_minted and max_supply.
Burning
burn(token_id) is callable only by the token’s current holder. Not the collection owner, not an admin, not an approved spender, not the trusted factory.
max_supplyis written once atinitand has no setter, so burning never lowers the advertised maximum.total_mintedis monotonic and is the id source, so a burn never reopens a mint slot and ids are never reused.- Live supply is derived as
total_minted - burnedand is never stored. - The tombstone is
tokens[id].owner == 0. Every token view rejects a burned id.
burned() and is_burned(token_id).
total_supply() returns live supply and total_minted() returns the id count. If you previously read total_supply expecting the number ever minted, you now get a different number. This is the breaking change in v4.0.0.Holder enumeration
Xcollection maintains a holder index, so the platform can page through current holders on-chain viaget_holders_page(offset, limit) for distribution analytics, without scanning every token.