// 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. // BCH/USD rate. Live: the gateway's /api/price (median of several exchanges) // or, if that is unreachable, the same public tickers straight from the // browser; the last good rate is remembered per browser; the constant is // only the very first paint before anything answers. Chipnet coins have no // market, so the mainnet rate is what makes "$7 per name" mean something. const FALLBACK_SATS_PER_USD = 400_000; let SATS_PER_USD = FALLBACK_SATS_PER_USD; let priceMeta = { usd: null, at: 0, sources: 0, origin: "fallback" }; try { const c = JSON.parse(localStorage.getItem("siriusPrice") || "null"); if (c && c.usd > 0) { SATS_PER_USD = Math.round(1e8 / c.usd); priceMeta = { ...c, origin: "cached" }; } } catch {} const usdToSats = (usd) => BigInt(Math.max(0, Math.round(Number(usd) * SATS_PER_USD))); const PRICE_API = "https://silentmode.st/api/price"; const BROWSER_SOURCES = [ ["coinbase", "https://api.coinbase.com/v2/prices/BCH-USD/spot", (j) => Number(j?.data?.amount)], ["kraken", "https://api.kraken.com/0/public/Ticker?pair=BCHUSD", (j) => Number(Object.values(j?.result || {})[0]?.c?.[0])], ["binance", "https://api.binance.com/api/v3/ticker/price?symbol=BCHUSDT", (j) => Number(j?.price)], ["coingecko", "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin-cash&vs_currencies=usd", (j) => Number(j?.["bitcoin-cash"]?.usd)], ]; function applyPrice(usd, sources, origin) { if (!(usd > 0)) return false; SATS_PER_USD = Math.round(1e8 / usd); priceMeta = { usd, at: Date.now(), sources, origin }; try { localStorage.setItem("siriusPrice", JSON.stringify(priceMeta)); } catch {} try { window.dispatchEvent(new CustomEvent("sirius:price", { detail: priceMeta })); } catch {} return true; } let priceLoad = null; function loadPrice({ force = false } = {}) { if (priceLoad && !force) return priceLoad; priceLoad = (async () => { try { const r = await fetch(PRICE_API, { cache: "no-store", signal: AbortSignal.timeout(6000) }); const j = await r.json(); if (j && j.usd > 0 && !j.stale) return applyPrice(j.usd, j.median_of || 0, "gateway"); } catch {} const got = (await Promise.allSettled(BROWSER_SOURCES.map(async ([name, url, pick]) => { const r = await fetch(url, { signal: AbortSignal.timeout(6000) }); if (!r.ok) throw new Error(String(r.status)); const v = pick(await r.json()); if (!(v > 0)) throw new Error("no price"); return v; }))).filter((x) => x.status === "fulfilled").map((x) => x.value).sort((a, b) => a - b); if (got.length) { const m = Math.floor(got.length / 2); return applyPrice(got.length % 2 ? got[m] : (got[m - 1] + got[m]) / 2, got.length, "exchanges"); } return false; })(); return priceLoad; } const priceInfo = () => priceMeta.usd ? `${priceMeta.origin === "gateway" || priceMeta.origin === "exchanges" ? `median of ${priceMeta.sources} exchange${priceMeta.sources === 1 ? "" : "s"}` : priceMeta.origin === "cached" ? "last known rate" : "fallback rate"}${priceMeta.at ? ", " + Math.max(0, Math.round((Date.now() - priceMeta.at) / 60000)) + " min ago" : ""}` : "fallback rate"; loadPrice(); setInterval(() => loadPrice({ force: true }), 5 * 60 * 1000); 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", // cosign: anyone may register, but the TLD owner must co-sign the // registration (indexers reject a REG under a cosign/hidden TLD that // does not carry the TLD certificate). hidden implies the same rule // on chain; in the UI hidden is owner-only, cosign is request-based. cosign: policy === "cosign", policy, category: row && row.category ? row.category : null, 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 }; // Sellable, but the registration needs the owner's co-signature: the // flow builds the request and parks it in the gateway's approval queue. if (p.cosign && !owner) return { ok: true, reason: "cosign", 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)}`; } // Money is shown in BCH (prices) and bits (fees, balances); 1 bit = 100 // satoshi, 1 BCH = 1,000,000 bits. Satoshi never appears in the UI. const group = (s) => String(s).replace(/\B(?=(\d{3})+(?!\d))/g, ","); function formatBch(sats) { const n = BigInt(sats || 0n); const neg = n < 0n; const a = neg ? -n : n; const whole = a / 100000000n, frac = (a % 100000000n).toString().padStart(8, "0").replace(/0+$/, ""); return `${neg ? "-" : ""}${group(whole)}${frac ? "." + frac : ""} BCH`; } function formatBits(sats) { const n = BigInt(sats || 0n); const neg = n < 0n; const a = neg ? -n : n; const whole = a / 100n, frac = (a % 100n).toString().padStart(2, "0").replace(/0+$/, ""); return `${neg ? "-" : ""}${group(whole)}${frac ? "." + frac : ""} bits`; } const formatSats = formatBits; // legacy name, same unit policy const bchToSats = (bch) => BigInt(Math.max(0, Math.round(Number(String(bch).replace(/[^\d.]/g, "")) * 1e8))); const satsToBch = (sats) => Number(BigInt(sats || 0n)) / 1e8; function round2(n) { return Math.round(Number(n) * 100) / 100; } window.siriusPricing = { priceForName, priceForTld, formatUsd, formatSats, formatBch, formatBits, bchToSats, satsToBch, usdToSats, // live rate (getters: callers that read the old constant see the current value) get CHIPNET_SATS_PER_USD() { return SATS_PER_USD; }, get SATS_PER_USD() { return SATS_PER_USD; }, get bchUsd() { return priceMeta.usd || 1e8 / SATS_PER_USD; }, priceInfo, loadPrice, priceReady: () => priceLoad || loadPrice(), // 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(); })();