// Sirius.X pricing model — deterministic per-label cost so users can predict // what a name or TLD will cost before they click Register. Public tiers only; // operator-level discounts and coupon overrides land in a future revision // (the plan is oracle-fed rates + coupon-code redemption at mint time). // // Two layers decide what a NAME costs: // 1. The TLD owner's on-chain policy (`price`, `hidden`, `policy`) — read // from the gateway's /api/tlds snapshot of the TLD beacon. A TLD owner // who sets `price: 7` sells every name under that TLD for $7, flat. // 2. Otherwise the length tiers below. // // Exposed as window.siriusPricing so both the register-flow modal and the // inline search widgets on landing/tld.html use one source of truth. (() => { // Chipnet placeholder rate: mainnet swaps this for a real BCH/USD oracle. // The numbers keep the demo transactions in the "few chipnet coins" range // so a single faucet visit covers a name or a TLD. const CHIPNET_SATS_PER_USD = 250_000; const usdToSats = (usd) => BigInt(Math.max(0, Math.round(Number(usd) * CHIPNET_SATS_PER_USD))); const cleanLabel = (s) => String(s || "").toLowerCase().replace(/[^a-z0-9-]/g, ""); // ---------- TLD owner policies ---------- // tld -> { price, hidden, frozen, policy, owner, records } // Filled from /api/tlds (see loadTldPolicies). Unknown TLDs fall through // to tier pricing and are treated as sellable, so a gateway blip never // blocks a purchase; the chain is the authority at mint time anyway. // include_hidden=1 so the policy map knows about switched-off (private) // TLDs too — their owners register names under them from the portal, and // the register flow must be able to price and gate them. The public // listing (visibleTlds) still drops anything hidden. const TLD_API = "https://navigate.st/api/tlds?include_hidden=1"; const TLD_POLICIES = new Map(); let tldList = null; // raw /api/tlds rows, once loaded (hidden ones excluded) let tldLoad = null; // memoised in-flight fetch function normalisePolicy(row) { const rec = row && row.records && typeof row.records === "object" ? row.records : {}; const priceRaw = row && row.price_usd != null ? row.price_usd : rec.price; const price = typeof priceRaw === "number" && Number.isFinite(priceRaw) && priceRaw >= 0 ? priceRaw : null; const hidden = !!(row && row.hidden) || rec.hidden === 1 || rec.hidden === true; const policy = typeof rec.policy === "string" ? rec.policy : "open"; return { tld: row.tld, price, hidden, hiddenBy: row && row.hidden_by ? row.hidden_by : (hidden ? "owner" : null), frozen: policy === "frozen", policy, owner: row && row.owner ? row.owner : null, records: rec, }; } function setTldPolicies(rows) { TLD_POLICIES.clear(); for (const r of rows || []) { const row = typeof r === "string" ? { tld: r } : r; const tld = cleanLabel(row && row.tld); if (!tld) continue; TLD_POLICIES.set(tld, normalisePolicy({ ...row, tld })); } } // Fetch the TLD list once per page. Resolves to the array of visible TLD // rows (or null on failure) — never rejects, callers just fall back. function loadTldPolicies({ url = TLD_API, force = false } = {}) { if (tldLoad && !force) return tldLoad; tldLoad = (async () => { try { const r = await fetch(url, { cache: "no-store" }); if (!r.ok) throw new Error("api " + r.status); const j = await r.json(); const rows = Array.isArray(j.tlds) ? j.tlds : []; setTldPolicies(rows); // Only a beacon-sourced list is a sellable list. A gateway still // serving the legacy `tlds.bch`-derived union includes every suffix // that ever appeared on a name (".de", ".silentmode", …), which is // not what the registry sells — keep the page's own fallback then. tldList = j.source === "TLD_BEACON" ? rows.filter((t) => !t.hidden) : null; return tldList; } catch { return null; } })(); return tldLoad; } // Resolves once the list is loaded, or after `ms` — search UIs await this // so the first paint already shows owner prices without ever hanging. function tldReady(ms = 2500) { return Promise.race([ loadTldPolicies(), new Promise((res) => setTimeout(() => res(null), ms)), ]); } function tldPolicy(tld) { return TLD_POLICIES.get(cleanLabel(tld)) || null; } // Does one of `addresses` (the signed-in wallet's) hold this TLD's NFT? // `owner` comes from the gateway's beacon snapshot (mint output, updated // on every TUPD carrier), so a wallet that owns the TLD matches here. function isTldOwner(tld, addresses) { const p = tldPolicy(tld); if (!p || !p.owner || !addresses) return false; const list = Array.isArray(addresses) ? addresses : [addresses]; return list.some((a) => typeof a === "string" && a === p.owner); } // Can names be sold under this TLD right now? // hidden → owner switched the TLD off: private TLD. The public cannot // buy, but the OWNER still can — pass their wallet addresses // and a hidden TLD they hold answers ok with owner: true. // frozen → owner set policy "frozen" (no new registrations, owner included) function tldSellable(tld, addresses) { const p = tldPolicy(tld); if (!p) return { ok: true, reason: null, owner: false }; const owner = isTldOwner(tld, addresses); if (p.frozen) return { ok: false, reason: "frozen", policy: p, owner }; if (p.hidden && !owner) return { ok: false, reason: "hidden", policy: p, owner }; return { ok: true, reason: null, policy: p, owner }; } // Visible TLD labels in listing order, or null if no beacon-sourced list // is available (callers keep their own fallback list then). function visibleTlds() { return tldList ? tldList.map((t) => t.tld) : null; } // ---------- name pricing ---------- // Owner-set flat price first; else LENGTH-first tiers, then downshift for // "less brandable" shapes (all-digits, hyphenated). Kept as multiplicative // modifiers so tiers remain the primary story and mods just soften an // edge case. function priceForName(label, tld) { const s = cleanLabel(label); if (!s) return { usd: 0, tier: "invalid" }; const p = tld ? tldPolicy(tld) : null; if (p && p.price != null) { return { usd: p.price, tier: "tld-set", sats: usdToSats(p.price), tld: p.tld, ownerSet: true }; } let usd, tier; if (s.length === 1) { usd = 5.00; tier = "premium-1"; } else if (s.length === 2) { usd = 3.00; tier = "short-2"; } else if (s.length === 3) { usd = 2.00; tier = "short-3"; } else if (s.length <= 5) { usd = 1.00; tier = "standard"; } else if (s.length <= 7) { usd = 0.50; tier = "long"; } else if (s.length <= 16) { usd = 0.25; tier = "extended"; } else { usd = 0.10; tier = "very-long"; } // Modifiers. if (/^\d+$/.test(s)) { usd = round2(usd * 0.5); tier += "+digits"; } if (s.includes("-")) { usd = round2(usd * 0.7); tier += "+hyphen"; } return { usd, tier, sats: usdToSats(usd) }; } // Per-label overrides — TLDs the operator wants priced outside the tiers. // Cheap namespaces (`.test`, `.dev`, `.local`) exist for experimentation, // school assignments and demo throwaways: length-tier pricing prices them // like premium TLDs, which isn't the point. Add labels here to opt them // out of the tier and (optionally) mark them renewal-free. // // Shape: label -> { usd, tier, noRenewal? } const SPECIAL_TLDS = { "test": { usd: 0.01, tier: "sandbox", noRenewal: true }, }; function priceForTld(label) { const s = cleanLabel(label); if (!s) return { usd: 0, tier: "invalid" }; if (Object.prototype.hasOwnProperty.call(SPECIAL_TLDS, s)) { const o = SPECIAL_TLDS[s]; return { usd: o.usd, tier: o.tier, sats: usdToSats(o.usd), noRenewal: !!o.noRenewal }; } let usd, tier; if (s.length === 1) { usd = 500.00; tier = "premium-1"; } else if (s.length === 2) { usd = 250.00; tier = "short-2"; } else if (s.length === 3) { usd = 150.00; tier = "short-3"; } else if (s.length <= 6) { usd = 100.00; tier = "standard"; } else if (s.length <= 10) { usd = 50.00; tier = "long"; } else { usd = 25.00; tier = "extended"; } return { usd, tier, sats: usdToSats(usd) }; } // Rounded USD → display string. Under $1 shows cents ($0.25); $1 and up // shows whole dollars ($5) unless there's a fractional part. function formatUsd(usd) { const n = Number(usd || 0); if (n >= 1 && n === Math.floor(n)) return `$${n}`; return `$${n.toFixed(2)}`; } function formatSats(sats) { return `${BigInt(sats || 0n).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")} sat`; } function round2(n) { return Math.round(Number(n) * 100) / 100; } window.siriusPricing = { priceForName, priceForTld, formatUsd, formatSats, usdToSats, CHIPNET_SATS_PER_USD, // TLD owner policy layer loadTldPolicies, setTldPolicies, tldReady, tldPolicy, tldSellable, isTldOwner, visibleTlds, TLD_API, }; // Warm the policy cache as soon as the script lands; every consumer // awaits tldReady() before it paints a price. loadTldPolicies(); })();