// The Glitch Card's numbers, in one place. The setup script writes them on-chain, the server // enforces the parts the chain can't (the $GLITCH burn amounts), and the app shows them. // Plan and reasoning: reports/glitch-card-nft-plan.md. The whitepaper (whitepaper/glitch-card.md) // already states the supply, the caps, the price and the waves, so change them there too. // // Platform-neutral: imported by the browser, the Pages Functions and the Node scripts. export const TIER_IDS = ['debit', 'gold', 'platinum', 'black', 'infinite'] as const export type TierId = (typeof TIER_IDS)[number] export interface Tier { readonly id: TierId readonly name: string /** Cards of this tier that can ever exist: the tier machine's itemsAvailable. */ readonly supply: number /** Most cards of this tier one wallet can activate (a mintLimit guard), or null for no limit. */ readonly perWallet: number | null } export const TIERS: readonly Tier[] = [ { id: 'debit', name: 'Debit', supply: 4650, perWallet: null }, { id: 'gold', name: 'Gold', supply: 3000, perWallet: null }, { id: 'platinum', name: 'Platinum', supply: 888, perWallet: null }, // No per-wallet limit on Black or Infinite (plan O14 (c), decided 2026-10-28): anyone can fund a // wallet's mintLimit record first and block that wallet for good, and a second wallet always got around it. { id: 'black', name: 'Black', supply: 300, perWallet: null }, { id: 'infinite', name: 'Infinite', supply: 50, perWallet: null }, ] export const BLANK = { supply: 8888, /** 0.1 SOL, paid to the payout wallet by the solPayment guard. */ priceLamports: 100_000_000, perWave: 2222, perWalletPerWave: 5, } as const export const WAVE_LABELS = ['w1', 'w2', 'w3', 'w4'] as const export type WaveLabel = (typeof WAVE_LABELS)[number] /** Guard group that activates a blank into a card. Upgrades ("upg") come after Nov 19. */ export const ACTIVATE_GROUP = 'act' /** pump.fun's Token-2022 coins have 6 decimals. */ export const GLITCH_DECIMALS = 6 // mintLimit ids are per guard. On the blank guard each wave has its own (1-4), so the five-blank // limit resets every wave. A tier with a perWallet limit (none has one now) always uses id 1. export const waveMintLimitId = (label: WaveLabel): number => WAVE_LABELS.indexOf(label) + 1 export const TIER_MINT_LIMIT_ID = 1 /** localnet is the local rehearsal stack (scripts/local-chain.ts): throwaway seeded keys, nothing real. */ export type Cluster = 'devnet' | 'mainnet' | 'localnet' export interface ClusterSettings { /** The $GLITCH mint. On devnet a stand-in Token-2022 mint with 6 decimals. Null until launch. */ readonly glitchMint: string | null /** Receives the 0.1 SOL from each blank. A hot wallet used for nothing else (plan D5, D13). */ readonly payout: string | null /** Ends up holding the guard authority: wave start dates, key rotation, the upgrade group. */ readonly authority: string | null /** When each wave opens (ISO 8601 in UTC, ending in Z; see utcMillis). Null keeps it shut: the guard gets a far-future date. */ readonly waves: Readonly> /** When activation opens (ISO 8601 in UTC, ending in Z); null until the burn amounts are published. */ readonly activationOpensAt: string | null /** Whole $GLITCH burned to activate each tier, as a decimal string. Null until published. */ readonly burn: Readonly> } export const SETTINGS: Readonly> = { devnet: { // Stand-in coin made by `cli.ts token` (2026-10-28); keys/devnet-*.json hold the wallets. glitchMint: '5sVoECYst56PToGukucrpVXcL358kyQr4LSdkKzA9BbB', payout: '9wspKsXuozzyvGMsF6UHyiLGpDVutBf1zGJs3e8KVQsi', authority: 'H5HTBowWKgEceyMWLY3Y18kBsfysEGNYbLUhL9EZvsmD', // Null through setup and the lock, as on mainnet; then all four opened together with sync-waves, // as mainnet will (one sale of 8,888, plan D6). waves: { w1: '2026-10-28T23:30:00Z', w2: '2026-10-28T23:30:00Z', w3: '2026-10-28T23:30:00Z', w4: '2026-10-28T23:30:00Z' }, activationOpensAt: '2026-10-28T23:40:00Z', burn: { debit: '1500', gold: '4500', platinum: '15000', black: '45000', infinite: '300000' }, }, localnet: { glitchMint: 'BTaAD2z1P8NBQxsuHdNvguCxDHoN5stuFzGveK8cvMV8', payout: '4RMnyNc7opYeemwEdCu54QSB9MqRDTuhn7VCwVffrApi', authority: '73Sku9gVdZCHmZgowJsNn6wP4Uime85Bj769niafx4rG', waves: { w1: '2026-01-01T00:00:00Z', w2: null, w3: null, w4: null }, activationOpensAt: '2026-01-01T00:00:00Z', burn: { debit: '1000', gold: '3000', platinum: '10000', black: '30000', infinite: '200000' }, }, mainnet: { // The official $GLITCH (pump.fun, Token-2022), launched 2026-10-16. glitchMint: '8Xg89xT8Ve34sKiYqYuMs46mycREsCA2F362UscRpump', // A fresh Phantom account used for nothing else (plan D5), given by the user 2026-10-29. payout: 'CkPBDmAE5tWDBQvi1vJwHrd7kTW9QVTnfD9H2szJVYns', // keys/mainnet-authority.json, made 2026-11-01; kept offline after the lock (plan O9). authority: '7dbXEajAWjKD1H59647DRHqsyYH2fMAb6HyLCrzxyyDY', // All four groups open together (one sale of 8,888, plan D6), set with sync-waves after the lock. waves: { w1: '2026-11-01T20:00:00Z', w2: '2026-11-01T20:00:00Z', w3: '2026-11-01T20:00:00Z', w4: '2026-11-01T20:00:00Z' }, // Activation opens with the sale (decided 2026-10-28; the opening moved to 20:00 UTC on launch day). activationOpensAt: '2026-11-01T20:00:00Z', // About $5 for Debit at $0.00318 (2026-10-29), then 3x, 10x, 30x and 200x (plan §8). Fixed once open. burn: { debit: '1500', gold: '4500', platinum: '15000', black: '45000', infinite: '300000' }, }, } /** pump.fun coins have a fixed supply of 1,000,000,000 tokens: no burn amount can exceed it. */ export const GLITCH_SUPPLY_BASE = 1_000_000_000n * 10n ** BigInt(GLITCH_DECIMALS) /** * Base units (6 decimals) for a whole-token decimal string such as "250000" or "12.5". Refuses 0 * (a free activation) and anything over the coin's whole supply, so a typo in the config can't go out. */ export function toBaseUnits(ui: string): bigint { const m = /^(\d{1,15})(?:\.(\d{1,6}))?$/.exec(ui) if (!m) throw new Error(`Not a token amount: ${ui}`) const frac = (m[2] ?? '').padEnd(GLITCH_DECIMALS, '0') const v = BigInt(m[1]!) * 10n ** BigInt(GLITCH_DECIMALS) + BigInt(frac) if (v <= 0n || v > GLITCH_SUPPLY_BASE) throw new Error(`Burn amount out of range: ${ui}`) return v } export const tierById = (id: string): Tier | undefined => TIERS.find((t) => t.id === id) /** * A wave or activation time in milliseconds, or null if it isn't ISO 8601 in UTC ending in Z * ("2026-11-01T16:00:00Z"). A time without the Z is read in the reader's own zone, and the CLI, the * server and every browser are in different ones, so anything else is refused: the server keeps * that wave shut and the CLI won't write it. */ export function utcMillis(iso: string): number | null { const m = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?Z$/.exec(iso) if (!m) return null const [y, mo, d, h, mi, s = 0, ms = 0] = m.slice(1).map((x, i) => (x === undefined ? undefined : i === 6 ? Number(x.padEnd(3, '0')) : Number(x))) as number[] // A year outside 2020–2099 is a typo ("0226"), not a schedule: it would open a wave at once. if (y! < 2020 || y! > 2099) return null const t = Date.UTC(y!, mo! - 1, d!, h!, mi!, s, ms) // A date that doesn't exist (Nov 31, 24:00) would roll over to another day, and browsers disagree // on it: refused, so a typo can't open a wave at the wrong time. const back = new Date(t) const exact = back.getUTCFullYear() === y && back.getUTCMonth() === mo! - 1 && back.getUTCDate() === d && back.getUTCHours() === h && back.getUTCMinutes() === mi && back.getUTCSeconds() === s return exact ? t : null } /** toBaseUnits, or null for an amount it refuses: that tier then stays closed instead of erroring. */ export function burnBaseUnits(ui: string | null): bigint | null { if (ui === null) return null try { return toBaseUnits(ui) } catch { return null } }