feat(sirius-x): tier-based label pricing; badges on search cards + TLD list
Single source of truth (js/pricing.js) mirrored on landing, tld.html
and the mint flow — same label always shows the same price everywhere.
Name tiers (label part):
1 char $5.00 premium-1
2 chars $3.00 short-2
3 chars $2.00 short-3
4–5 chars $1.00 standard
6–7 chars $0.50 long
8–16 chars $0.25 extended
17+ chars $0.10 very-long
all-digits ×0.5 (less brandable)
contains hyphen ×0.7
TLD tiers (label part):
1 char $10.00 premium-1
2 chars $8.00 short-2
3 chars $6.00 short-3
4–8 chars $5.00 standard
9+ chars $4.00 long
Rendering:
- Landing #search-results: acid-tinted price badge on each available
row; grid grew a fourth column so name + badge + price + button
all sit on one line
- tld.html search result-card: same price badge next to the Register
button on available TLDs
- tld.html registered-TLDs table: new 'Mint price' column showing the
tier price for every registered TLD, so buyers see the pricing
ladder next to concrete examples
Mint pipe:
- startFlow(name)/startTldFlow(label) resolve the label price via
window.siriusPricing (default) but honour opts.serviceFeeSats when
the caller passes one — leaves the door open for coupons /
operator discounts / oracle overrides in a future revision without
reshuffling call sites
- The overridden service fee is threaded through quoteRegistration
AND registerWithBuiltInWallet/registerWithExternalWallet so the
confirm screen and the on-chain output agree
Chipnet placeholder rate (250,000 sat/USD) stays in pricing.js;
mainnet will swap in a live BCH/USD oracle. Operator-side discounts
and coupon codes are the next iteration — deliberately not disclosed
in this shipping copy.
This commit is contained in:
parent
4f776844a0
commit
ce9248a327
4 changed files with 133 additions and 13 deletions
17
index.html
17
index.html
|
|
@ -126,11 +126,15 @@
|
|||
|
||||
<style>
|
||||
/* Inline search card styling: mirrors register.html's .card look. */
|
||||
#search-results .row-card{display:grid;grid-template-columns:1fr auto auto;gap:12px;align-items:center;
|
||||
#search-results .row-card{display:grid;grid-template-columns:1fr auto auto auto;gap:12px;align-items:center;
|
||||
background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:11px 16px;min-height:48px}
|
||||
#search-results .row-card.checking{opacity:.55}
|
||||
#search-results .row-card .n{font-family:ui-monospace,monospace;font-size:15px;overflow-wrap:anywhere}
|
||||
#search-results .row-card .btn{padding:6px 12px;font-size:12.5px}
|
||||
#search-results .row-card .price{
|
||||
color:var(--acid);font-family:ui-monospace,monospace;font-size:14px;font-weight:600;
|
||||
padding:2px 8px;border-radius:6px;background:rgba(214,255,61,.10);white-space:nowrap
|
||||
}
|
||||
#search-results .badge{font-size:11px;padding:2px 10px;border-radius:999px;font-weight:600;white-space:nowrap}
|
||||
#search-results .b-ok{background:rgba(79,209,165,.15);color:var(--ok)}
|
||||
#search-results .b-taken{background:rgba(246,118,138,.15);color:var(--taken)}
|
||||
|
|
@ -162,12 +166,20 @@
|
|||
const badge = state === "ok" ? '<span class="badge b-ok">available</span>'
|
||||
: state === "taken" ? '<span class="badge b-taken">taken</span>'
|
||||
: '<span class="badge b-check">checking…</span>';
|
||||
// Label price (rendered only for available names). Tier logic lives in
|
||||
// pricing.js so landing / tld.html / the mint modal all agree.
|
||||
let price = "";
|
||||
if (state === "ok" && window.siriusPricing) {
|
||||
const label = full.split(".")[0];
|
||||
const p = window.siriusPricing.priceForName(label);
|
||||
price = `<span class="price" title="${esc(p.tier)}">${esc(window.siriusPricing.formatUsd(p.usd))}</span>`;
|
||||
}
|
||||
const btn = state === "ok"
|
||||
? `<button class="btn acid" data-register="${esc(full)}">Register →</button>`
|
||||
: state === "taken"
|
||||
? `<a class="btn ghost" href="https://navigate.st/bns/${encodeURIComponent(full)}" target="_blank" rel="noopener">view →</a>`
|
||||
: "<span></span>";
|
||||
return `<div class="row-card ${state === "checking" ? "checking" : ""}"><span class="n">${esc(full)}</span>${badge}${btn}</div>`;
|
||||
return `<div class="row-card ${state === "checking" ? "checking" : ""}"><span class="n">${esc(full)}</span>${badge}${price}${btn}</div>`;
|
||||
}
|
||||
|
||||
async function search(label) {
|
||||
|
|
@ -436,6 +448,7 @@ process.exit(0);"</pre>
|
|||
</script>
|
||||
<script type="module" src="./js/register-flow.js?v=20260908tabs"></script>
|
||||
|
||||
<script src="./js/pricing.js?v=20260909tiers"></script>
|
||||
<script defer src="./js/site-footer.js?v=20260907rel"></script>
|
||||
<script defer src="./js/profile-menu.js?v=20260908tabs"></script>
|
||||
</body>
|
||||
|
|
|
|||
71
js/pricing.js
Normal file
71
js/pricing.js
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
// 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).
|
||||
//
|
||||
// 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, "");
|
||||
|
||||
// Tier picker — LENGTH-first, 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) {
|
||||
const s = cleanLabel(label);
|
||||
if (!s) return { usd: 0, tier: "invalid" };
|
||||
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) };
|
||||
}
|
||||
|
||||
function priceForTld(label) {
|
||||
const s = cleanLabel(label);
|
||||
if (!s) return { usd: 0, tier: "invalid" };
|
||||
let usd, tier;
|
||||
if (s.length === 1) { usd = 10.00; tier = "premium-1"; }
|
||||
else if (s.length === 2) { usd = 8.00; tier = "short-2"; }
|
||||
else if (s.length === 3) { usd = 6.00; tier = "short-3"; }
|
||||
else if (s.length <= 8) { usd = 5.00; tier = "standard"; }
|
||||
else { usd = 4.00; tier = "long"; }
|
||||
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,
|
||||
};
|
||||
})();
|
||||
|
|
@ -337,10 +337,18 @@ function adoptSignedInWallet() {
|
|||
return false;
|
||||
}
|
||||
|
||||
function startFlow(name) {
|
||||
function startFlow(name, opts = {}) {
|
||||
state.name = name;
|
||||
state.tld = null;
|
||||
state.signInOnly = false;
|
||||
// Tier-based service fee (siriusPricing owns the tiers so landing search
|
||||
// badges, tld cards and this mint agree). Caller can override with
|
||||
// opts.serviceFeeSats when a coupon or manual price is in play.
|
||||
const label = String(name).split(".")[0];
|
||||
const p = window.siriusPricing?.priceForName(label);
|
||||
state.nameFeeSats = opts.serviceFeeSats != null
|
||||
? BigInt(opts.serviceFeeSats)
|
||||
: (p ? p.sats : null);
|
||||
open();
|
||||
if (adoptSignedInWallet()) stepFund();
|
||||
else stepWallet();
|
||||
|
|
@ -528,7 +536,10 @@ async function stepExternalConfirm(session, label) {
|
|||
// steps which know to call registerTldWithExternalWallet.
|
||||
if (state.tld) { state.session = session; return stepConfirmTld(); }
|
||||
|
||||
const quote = await BNS.quoteRegistration(c, { name: state.name, ownerAddress: owner, records: {} });
|
||||
const feeOverride = state.nameFeeSats != null
|
||||
? { serviceFee: { ...(BNS.REGISTRAR?.serviceFee ?? {}), sats: state.nameFeeSats } }
|
||||
: {};
|
||||
const quote = await BNS.quoteRegistration(c, { name: state.name, ownerAddress: owner, records: {}, ...feeOverride });
|
||||
const rows = BNS.priceSummary(quote.costs).map((l) => `
|
||||
<tr class="${l.total ? "total" : ""}"><td>${esc(l.label)}${l.note ? `<small>${esc(l.note)}</small>` : ""}</td>
|
||||
<td>${sats(l.sats)} sat</td></tr>`).join("");
|
||||
|
|
@ -552,7 +563,7 @@ async function stepExternalConfirm(session, label) {
|
|||
const say = (s) => { const d = document.createElement("div"); d.textContent = s; log.appendChild(d); };
|
||||
try {
|
||||
const res = await BNS.registerWithExternalWallet(await client(), {
|
||||
session, name: state.name, ownerAddress: owner, records: {}, onProgress: say,
|
||||
session, name: state.name, ownerAddress: owner, records: {}, onProgress: say, ...feeOverride,
|
||||
});
|
||||
state.result = res;
|
||||
state.session = session;
|
||||
|
|
@ -732,10 +743,14 @@ async function stepConfirm() {
|
|||
if (state.tld) return stepConfirmTld();
|
||||
const avail = await BNS.checkAvailability(c, state.name);
|
||||
if (!avail.available) throw new Error(`"${avail.name}" was just registered by someone else`);
|
||||
const feeOverride = state.nameFeeSats != null
|
||||
? { serviceFee: { ...(BNS.REGISTRAR?.serviceFee ?? {}), sats: state.nameFeeSats } }
|
||||
: {};
|
||||
const quote = await BNS.quoteRegistration(c, {
|
||||
name: state.name, ownerAddress: state.wallet.address, records: {},
|
||||
name: state.name, ownerAddress: state.wallet.address, records: {}, ...feeOverride,
|
||||
});
|
||||
state.quote = quote;
|
||||
state._feeOverride = feeOverride;
|
||||
|
||||
const rows = BNS.priceSummary(quote.costs).map((l) => `
|
||||
<tr class="${l.total ? "total" : ""}"><td>${esc(l.label)}${l.note ? `<small>${esc(l.note)}</small>` : ""}</td>
|
||||
|
|
@ -848,6 +863,7 @@ async function stepRegister() {
|
|||
const res = await BNS.registerWithBuiltInWallet(await client(), {
|
||||
wallet: state.wallet, name: state.name, records: {},
|
||||
onProgress: (s) => say(words[s] ?? s),
|
||||
...(state._feeOverride ?? {}),
|
||||
});
|
||||
state.result = res;
|
||||
stepDone();
|
||||
|
|
|
|||
34
tld.html
34
tld.html
|
|
@ -74,6 +74,11 @@
|
|||
table.tlds th{color:var(--mut);font-weight:500;font-size:11.5px;text-transform:uppercase;letter-spacing:.4px}
|
||||
table.tlds .tld{font-family:ui-monospace,monospace;color:var(--acid);font-size:13.5px;font-weight:600}
|
||||
table.tlds .cat{font-family:ui-monospace,monospace;color:var(--mut);font-size:11.5px;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.price, table.tlds .price{
|
||||
color:var(--acid);font-family:ui-monospace,monospace;font-size:13px;font-weight:600;
|
||||
padding:2px 8px;border-radius:6px;background:rgba(214,255,61,.10);white-space:nowrap;display:inline-block
|
||||
}
|
||||
.result-card{grid-template-columns:1fr auto auto auto !important}
|
||||
footer{border-top:1px solid var(--line);padding:2rem 0 3rem;color:var(--dim);font-size:13px;text-align:center;margin-top:2.5rem}
|
||||
</style>
|
||||
<script type="importmap">
|
||||
|
|
@ -132,7 +137,7 @@
|
|||
<p class="muted">Fetched from the on-chain TLD registry via the public gateway. Refresh the page for the latest.</p>
|
||||
<div id="tlds-status" class="status">Loading…</div>
|
||||
<table class="tlds" id="tlds-table" style="display:none">
|
||||
<thead><tr><th>TLD</th><th>Category</th></tr></thead>
|
||||
<thead><tr><th>TLD</th><th>Mint price</th><th>Category</th></tr></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
|
@ -162,9 +167,15 @@
|
|||
const j = await r.json();
|
||||
const tlds = (j.tlds || []).map((t) => (typeof t === "string" ? { tld: t } : t));
|
||||
tlds.sort((a, b) => a.tld.localeCompare(b.tld));
|
||||
tbody.innerHTML = tlds.map((t) =>
|
||||
`<tr><td class="tld">.${esc(t.tld)}</td><td class="cat">${esc(t.certificate || t.category || "")}</td></tr>`
|
||||
).join("");
|
||||
tbody.innerHTML = tlds.map((t) => {
|
||||
const p = window.siriusPricing?.priceForTld(t.tld);
|
||||
const priceCell = p ? `<span class="price" title="${esc(p.tier)}">${esc(window.siriusPricing.formatUsd(p.usd))}</span>` : "—";
|
||||
return `<tr>
|
||||
<td class="tld">.${esc(t.tld)}</td>
|
||||
<td class="price-cell">${priceCell}</td>
|
||||
<td class="cat">${esc(t.certificate || t.category || "")}</td>
|
||||
</tr>`;
|
||||
}).join("");
|
||||
$("tlds-table").style.display = "";
|
||||
status.className = "status ok";
|
||||
status.textContent = `${tlds.length} TLDs registered on chipnet.`;
|
||||
|
|
@ -195,9 +206,14 @@
|
|||
different label — TLDs are first-come, first-served.</p>`;
|
||||
return;
|
||||
}
|
||||
const p = window.siriusPricing?.priceForTld(label);
|
||||
const priceBadge = p
|
||||
? `<span class="price" title="${esc(p.tier)}">${esc(window.siriusPricing.formatUsd(p.usd))}</span>`
|
||||
: "";
|
||||
res.innerHTML = `<div class="result-card">
|
||||
<div class="lbl"><span class="dot">.</span>${esc(label)}</div>
|
||||
<span class="badge b-ok">available</span>
|
||||
${priceBadge}
|
||||
<button class="btn acid" id="go-register">Register .${esc(label)} →</button>
|
||||
</div>`;
|
||||
$("go-register").onclick = () => beginRegister(label);
|
||||
|
|
@ -231,9 +247,12 @@
|
|||
// — window.siriusRegisterTld). That modal owns the wallet-choice, fund,
|
||||
// confirm, and mint steps — same UX as name registration.
|
||||
function beginRegister(label) {
|
||||
const call = () => window.siriusRegisterTld(label, {
|
||||
serviceFeeSats: BigInt(DEFAULT_USD * CHIPNET_SATS_PER_USD),
|
||||
});
|
||||
// Label-based tier pricing lives in siriusPricing (js/pricing.js) so
|
||||
// landing / tld / mint agree. Fall back to the flat DEFAULT_USD if the
|
||||
// pricing module hasn't loaded for some reason.
|
||||
const p = window.siriusPricing?.priceForTld(label);
|
||||
const fee = p ? p.sats : BigInt(DEFAULT_USD * CHIPNET_SATS_PER_USD);
|
||||
const call = () => window.siriusRegisterTld(label, { serviceFeeSats: fee });
|
||||
if (typeof window.siriusRegisterTld === "function") { call(); return; }
|
||||
// Module still loading — retry until it lands.
|
||||
const btn = $("go-register");
|
||||
|
|
@ -263,6 +282,7 @@
|
|||
</script>
|
||||
|
||||
<script type="module" src="./js/register-flow.js?v=20260908tabs"></script>
|
||||
<script src="./js/pricing.js?v=20260909tiers"></script>
|
||||
<script defer src="./js/site-footer.js?v=20260907rel"></script>
|
||||
<script defer src="./js/profile-menu.js?v=20260908tabs"></script>
|
||||
</body>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue