diff --git a/docs/DESIGN-signed-records-manifest.md b/docs/DESIGN-signed-records-manifest.md
new file mode 100644
index 0000000..a6c6fc6
--- /dev/null
+++ b/docs/DESIGN-signed-records-manifest.md
@@ -0,0 +1,154 @@
+# Signed records manifest (BNS records v1)
+
+**Status:** shipped v1 — gateway `GET/POST /api/records/` and
+verified `GET /api/dns/` live on `silentmode.st/navigate.st`;
+owner-tracking in the indexer live; portal DNS editor + wallet-side
+`signMessage` + `signRecordsManifest` shipped. Native DNS server
+(UDP :53 → BCDN name → verified records) is a v2 follow-up.
+**Related:** `DESIGN-tld-registry.md`, `PROTOCOL.md`, public explainer at
+`silentmode.st/sirius-x/docs/#signed-records`. Mirrored to
+`site-sirius-x/docs/DESIGN-signed-records-manifest.md` so it rides the
+next push-split to the sirius forge.
+
+## Problem
+
+Every records edit today is a chain UPD:
+- costs ~1,300 sat + service fee
+- caps records at ~150 bytes of payload
+- rules out DNS-style records (A/AAAA/MX/TXT/CNAME/NS) at any useful scale
+- makes TXT-verification churn (ACME, DKIM, DMARC) actively expensive
+
+We want records to be:
+- **free to edit** — no chain tx per change
+- **unlimited size** — accommodate whole DNS record sets
+- **trustless** — no operator can substitute their own records for the owner's
+- **backwards compatible** — existing on-chain `h` / `s3` / `p` / `u` records keep working
+
+## Design
+
+The chain says *who owns the name*. A signed manifest says *what the owner's
+records are*. Resolvers verify both.
+
+### The manifest — `_records.json`
+
+Lives at the Sia bucket the name's on-chain `records.s3` already points at,
+under the fixed filename `_records.json`. Shape:
+
+```json
+{
+ "v": 1,
+ "name": "bitcoin.cash",
+ "seq": 42,
+ "updated_at": "2026-09-12T00:00:00Z",
+ "dns": {
+ "A": ["1.2.3.4"],
+ "AAAA": ["2001:db8::1"],
+ "MX": [{"pref": 10, "host": "mail.example.com"}],
+ "TXT": ["v=spf1 include:_spf.silentmode.st -all"],
+ "CNAME": null,
+ "NS": []
+ },
+ "meta": { "note": "optional free-form" },
+ "sig": "H3n… (BCH message signature, base64)"
+}
+```
+
+**Rules:**
+- `v`: schema version, currently `1`.
+- `name`: the fully-qualified name the manifest belongs to. Resolver rejects
+ a manifest whose `name` field doesn't match the URL it was fetched from.
+- `seq`: monotonic integer. Resolvers cache the highest seen; blobs with an
+ older or equal `seq` are rejected as replays.
+- `updated_at`: ISO-8601 UTC. Human-readable only — verification is on `seq`.
+- `dns.*`: DNS record slots. `null` or `[]` means "not set". `A`/`AAAA`/`TXT`
+ are string arrays. `MX` is objects `{pref, host}`. `NS` is a string array
+ of host names. Additional slots (`CAA`, `SRV`) reserved for v2.
+- `meta`: unstructured; ignored by DNS resolvers, useful for portal UI.
+- `sig`: BCH-style message signature (same format `libauth.signMessage`
+ produces) over the canonical bytes of the envelope (see below).
+
+### Canonicalisation and signing
+
+The signable bytes are the JSON serialisation of the manifest **without the
+`sig` field**, with:
+- keys sorted lexicographically at every level
+- no insignificant whitespace (`JSON.stringify` with no `space` arg)
+- `\u`-escape every non-ASCII character
+- `null` fields OMITTED (`"CNAME": null` becomes absent)
+
+The signature is `sign(sha256(canonical_bytes))` with the wallet key whose
+address currently holds the name's NFT certificate.
+
+Verification, at the resolver:
+1. Read the name's chain record. Follow `records.s3` to the Sia bucket.
+2. Fetch `_records.json` from that bucket.
+3. Extract `sig`, recompute `sha256(canonical_bytes)` over the rest.
+4. Recover the signing pubkey → derive the CashAddress → compare to the
+ current NFT owner address from the chain.
+5. Compare `seq` to the cached last-seen `seq` for this name. Reject if not
+ strictly greater.
+6. If everything matches: cache the new `seq`, apply the DNS records.
+
+Any step failing means "no records" — the resolver falls back to whatever
+was already on the chain (`h`, `s3` for content, etc.).
+
+### Endpoints (gateway)
+
+**GET `/api/records/`** — read-through of the signed manifest.
+Returns 200 with the JSON if present, 404 if missing, 5xx on Sia errors.
+Cacheable for a minute. Anyone can call this; it's public.
+
+**POST `/api/records/`** — write. Body IS the manifest JSON.
+The gateway:
+1. Verifies the signature matches the current on-chain NFT owner.
+2. Verifies `seq` is strictly greater than the last-seen `seq` (from
+ `GET`ing the current manifest if any).
+3. Uploads the JSON to the name's Sia bucket at `_records.json`.
+4. Returns 200 with `{seq, bytes, sia_key}`.
+
+Rate-limit: 1 write / 5 s per name. Refuses if the on-chain record has no
+`s3` pointer yet — the name has no storage location, no place to write.
+
+### Precedence and interaction with existing records
+
+The signed manifest **complements** the on-chain records — it doesn't replace
+them:
+- **Content** (`h`, `s3`, `p`, `u`, `ip`) still comes from chain.
+- **DNS records** come from the manifest — chain has never carried them.
+
+A future extension (v2) could allow the manifest to override `h`/`s3`/etc.
+for owners who want everything off-chain. Not shipped in v1 because the
+existing content model already works.
+
+### Threat model
+
+- **Operator tampers with the manifest.** Signature verification catches it —
+ the resolver rejects and falls back to on-chain records.
+- **Replay of an old manifest.** `seq` monotonicity catches it — resolvers
+ keep the highest-seen `seq`.
+- **Owner's key compromised.** Same as chain-side compromise: attacker
+ controls the NFT and can sign whatever. Move the name to a new key.
+- **Sia object deleted.** The name still resolves via chain records; DNS
+ records disappear until the manifest is republished.
+- **Gateway hostility.** The gateway signs nothing itself — it only relays
+ and enforces `seq` monotonicity. A hostile gateway can refuse to accept
+ writes but cannot forge them; users can bypass it entirely by writing
+ directly to Sia with their own credentials (v2 flow).
+
+### What ships in v1
+
+- The spec (this doc).
+- Gateway `/api/records/` (GET + POST) with signature verification and
+ Sia read-through.
+- Portal DNS editor that composes a manifest, asks the wallet to sign, and
+ POSTs to the gateway.
+- Resolver support in Theseus + Ariadne staged behind a flag until the
+ portal-side has been in production for a week.
+
+### What ships in v2
+
+- Direct-to-Sia write flow for users who hold their own S3 credentials.
+- Owner-controlled manifest that overrides `h`/`s3`/etc. entirely.
+- CAA/SRV/HTTPS records.
+- Federated resolver caches for `seq` (mitigates the case where two writes
+ race and each thinks its `seq` is winning).
diff --git a/docs/index.html b/docs/index.html
index 62550b3..222d93f 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -120,6 +120,7 @@
TLD registryRegister a nameRecords & hosting
+ Signed off-chain recordsHost your nameResolver / gatewayPricing (mainnet)
@@ -193,6 +194,89 @@
(Host-header semantics). Full rule is in Argus/src/lib/record-picker.js.
+
+
Free edits — signed off-chain records
+
The on-chain records above (h, s3, ip, u,
+ tls, np, el) cost a chain transaction to change and are capped
+ at 200 bytes of OP_RETURN. For DNS-style records (A / AAAA / MX / TXT / CNAME / NS) and any records
+ you edit often, Sirius.X uses a companion mechanism: a signed off-chain manifest that costs
+ nothing per change and has no size cap.
+
+
The chain says who owns the name. A signed blob says what
+ the records are. Resolvers verify both. No third party is trusted for records: the manifest is
+ signed by the same key that holds the on-chain NFT certificate, so an operator (or Sia farmer, or
+ anyone else in the middle) can't rewrite records without breaking the signature.
+
+
+
Concern
Where it lives
Cost per change
+
+
Who owns the name
Bitcoin Cash chain (NFT certificate)
1 tx to transfer
+
Where the site lives
Chain record: s3, ip, u, h
1 tx per change
+
DNS records, meta, TXT churn, etc.
Sia at <s3-bucket>/_records.json
free
+
+
+
+
How the manifest is verified
+
+
Resolver reads the name's chain record, follows records.s3 to the Sia bucket,
+ fetches _records.json.
+
Signature check — recovers the signing pubkey from the manifest's sig field,
+ derives the CashAddress, compares to the current NFT-holder address from the chain. Mismatch = reject.
+
Replay check — the manifest carries a monotonic seq number. Resolvers cache the
+ highest seen and reject anything less-or-equal. An old signed blob can't be re-served.
+
Fall-back — if the manifest is missing, invalid, or its signature doesn't match, the resolver
+ silently falls back to the on-chain records. Signed records never override a name; they extend it.
Canonicalisation: sorted keys, no whitespace, null
+ fields omitted. The signable bytes are the manifest with the sig field removed. Full
+ spec: DESIGN-signed-records-manifest.md
+ — signable byte order, seq-monotonicity rules, threat model, v2 roadmap.
+
+
Endpoints
+
+
Method
Path
Effect
+
+
GET
/api/records/<name>
Fetches the raw signed manifest from the name's Sia bucket. Public, cacheable.
+
POST
/api/records/<name>
Accepts a signed manifest, verifies against the on-chain owner + seq monotonicity, uploads to Sia.
+
GET
/api/dns/<name>
Resolver read. Fetches the manifest, verifies the signature against the on-chain owner and rejects rollback of the seq, then returns only the verified dns block. Callers can trust the response without doing their own signature or chain lookups. 30-second cache hint.
+
+
+
The distinction between /api/records/<name>
+ and /api/dns/<name> is deliberate: the former is transparent (raw manifest as
+ uploaded, including the signature so clients can re-verify), the latter is opinionated (only the
+ verified DNS records, with the gateway having done the sig + seq work). Bridges and shims should
+ use /api/dns/; wallets and tools that want to see the full signed blob should use
+ /api/records/.
+
A name needs records.s3 set on chain first —
+ without a storage pointer, the manifest has nowhere to live. Portal and CLI both refuse to
+ publish a manifest until s3 is present.
+
+
+ Threat model at a glance. Operator tampers → sig check fails, resolver falls back to chain
+ records. Replay of an old blob → seq check fails. Owner's key compromised → same as chain-side
+ compromise; move the name to a new key. Sia object deleted → name still resolves via chain
+ records, DNS records disappear until republished. Hostile gateway can refuse writes but cannot
+ forge them; users can PUT directly to Sia with their own credentials to bypass.
+
+
+
Host your name
The end-to-end recipe for putting a static site under a BCNR name. Chipnet
@@ -358,6 +442,7 @@ process.exit(0);"
owner collects a share (fee_bps, default 5%, cap 50%). Chain-enforced when
the TLD's policy is covenant; honour-system otherwise. Name registration is
yearly and priced separately.
+
The TLD owner also sets what names under it sell for and whether the TLD is on or off, straight from the portal: a price record (flat USD per name, replaces the length tiers for every buyer) and a hidden record (1 makes it a private TLD: off the public list, nobody but the owner can register names under it — the owner still can from the portal, paying only the platform share — until it is switched back on). Both ride the same signed TUPD, so every client reads them from the chain; the owner keeps 90% of each sale.
diff --git a/index.html b/index.html
index e9c1928..3701fee 100644
--- a/index.html
+++ b/index.html
@@ -195,11 +195,24 @@
// against the client-side TLDS list. Register buttons hand the label off
// to register.html?q= for the full mint flow.
(function () {
- const TLDS = [
+ // Fallback list, used until /api/tlds answers (js/pricing.js loads it).
+ // The live list drops TLDs their owners switched off and carries the
+ // owner-set prices, so it wins whenever it is available.
+ let TLDS = [
"bch","p2p","bit","nav","test","x","asm","neo","gt","sc",
"sia","dex","cex","nt","dea","hub","os","vpn",
- // .com is held off the public registry (2026-08-29) — skip.
];
+ // .com is held off the public registry (2026-08-29) regardless of what
+ // the API says — the operator holds it; nobody buys names under it yet.
+ const NEVER_SELL = new Set(["com"]);
+ async function liveTlds() {
+ const sp = window.siriusPricing;
+ if (!sp) return TLDS;
+ await sp.tldReady();
+ const live = sp.visibleTlds();
+ if (live && live.length) TLDS = live.filter((t) => !NEVER_SELL.has(t) && sp.tldSellable(t).ok);
+ return TLDS;
+ }
const $ = (id) => document.getElementById(id);
const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({"&":"&","<":"<",">":">",'"':""","'":"'"}[c]));
const clean = (v) => v.toLowerCase().replace(/[^a-z0-9-]/g, "").slice(0, 32);
@@ -213,9 +226,10 @@
// 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 = `${esc(window.siriusPricing.formatUsd(p.usd))}`;
+ const [label, tld] = full.split(".");
+ const p = window.siriusPricing.priceForName(label, tld);
+ const why = p.ownerSet ? `price set by the .${tld} owner` : p.tier;
+ price = `${esc(window.siriusPricing.formatUsd(p.usd))}`;
}
const btn = state === "ok"
? ``
@@ -233,7 +247,10 @@
out.innerHTML = TLDS.map((t) => row(`${label}.${t}`, "checking")).join("");
let taken = new Set();
try {
- const r = await fetch(`https://silentmode.st/api/labels/${encodeURIComponent(label)}`);
+ const [r] = await Promise.all([
+ fetch(`https://silentmode.st/api/labels/${encodeURIComponent(label)}`),
+ liveTlds(), // owner prices + on/off state, same round-trip
+ ]);
if (r.ok) {
const j = await r.json();
for (const m of (j.matches || [])) taken.add(m.name);
@@ -489,9 +506,9 @@ process.exit(0);"
-
+
-
+
diff --git a/js/pricing.js b/js/pricing.js
index 8a0baed..cac78c9 100644
--- a/js/pricing.js
+++ b/js/pricing.js
@@ -3,6 +3,12 @@
// 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.
@@ -16,12 +22,127 @@
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) {
+ // ---------- 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"; }
@@ -36,9 +157,24 @@
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"; }
@@ -68,5 +204,18 @@
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();
})();
diff --git a/js/register-flow.js b/js/register-flow.js
index 8ed76aa..834ba4f 100644
--- a/js/register-flow.js
+++ b/js/register-flow.js
@@ -353,14 +353,42 @@ async 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
+ // Service fee: the TLD owner's flat price when they set one, else the
+ // length tier (siriusPricing owns both 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);
+ const [label, tldRaw] = String(name).split(".");
+ const tld = tldRaw || "bch";
+ const sp = window.siriusPricing;
+ if (sp?.tldReady) await sp.tldReady();
+ // A TLD its owner switched off is private: the public cannot buy under
+ // it, but the owner still can (from the portal's TLDs tab). Frozen means
+ // nobody, owner included. Check against the signed-in wallet's addresses
+ // so the owner's own purchase goes through.
+ const mine = (window.siriusWallet?.watchedAddresses)
+ || (window.siriusWallet?.address ? [window.siriusWallet.address] : [])
+ || [];
+ const sell = sp?.tldSellable ? sp.tldSellable(tld, mine) : { ok: true, owner: false };
+ if (!sell.ok) {
+ open();
+ const why = sell.reason === "frozen"
+ ? `The owner of .${esc(tld)} has frozen it — no new names are being registered under it right now.`
+ : `.${esc(tld)} is a private TLD — its owner has switched it off for the public. Only the owner can register names under it right now. Check back later or pick another TLD.`;
+ render(`
${esc(name)} is not available
${why}
+ `);
+ $("back").onclick = close;
+ return;
+ }
+ const p = sp?.priceForName(label, tld);
+ state.ownerMint = !!sell.owner;
+ // Owner registering under their own TLD: they would receive 90% of the
+ // fee from themselves, so only the platform's share is charged. The
+ // registrar lib already routes the whole fee to the platform when the
+ // TLD owner is the buyer (no self-payment output).
+ const ownerRate = (sats) => (sats == null ? null : BigInt(sats) / 10n);
state.nameFeeSats = opts.serviceFeeSats != null
? BigInt(opts.serviceFeeSats)
- : (p ? p.sats : null);
+ : (p ? (sell.owner ? ownerRate(p.sats) : p.sats) : null);
open();
if (await adoptSessionWalletAsync()) stepFund();
else stepWallet();
@@ -844,6 +872,7 @@ async function stepConfirm() {
render(`
Confirm ${esc(quote.displayName)}
One transaction. It mints the certificate to your address and pays the fees.
+ ${state.ownerMint ? `
You own this TLD, so this is an owner registration: only the platform's 10% share of the name price is charged — the 90% owner share would just be paid to yourself.
` : ""}
${rows}
The certificate will be minted to
${esc(state.wallet.address)} — your key, your name.
diff --git a/portal.html b/portal.html
index 9ec87be..eb929d8 100644
--- a/portal.html
+++ b/portal.html
@@ -54,6 +54,33 @@
letter-spacing:inherit;text-transform:inherit;transition:color .12s}
.font-picker button:hover{color:var(--ink)}
.font-picker button.active{background:var(--acid);color:var(--bg);font-weight:600}
+ /* Profile-section tabs — horizontal menu inside the signed-in section.
+ Sticky under the topnav so context stays visible on scroll. Panes are
+ just
TLDs are per-registry NFTs on the chipnet TLD beacon.
- Public mints go through a service-fee output to fund the registry. Chipnet uses a placeholder
- rate; mainnet will price by label length via a real oracle.
Every BCNR name your wallet's addresses currently hold a CashTokens NFT for. Edit records inline to publish an on-chain UPD.
+
Loading names from the chain…
+
+
+
+
+
+
TLDs you own
+
Every per-TLD registry NFT held by this wallet. Set the price every name under your TLD sells for, switch the TLD on or off in the public registry (off = a private TLD: nobody else can buy names under it, you still can from here), and tune policy — fee, min length, reserved labels, renewal period. Every change is one signed TUPD on chain; you receive 90% of each sale.
+
Loading TLDs from the chain…
+
+
+
Register a new TLD
+
+
TLDs are per-registry NFTs on the chipnet TLD beacon.
+ Public mints go through a service-fee output to fund the registry. Chipnet uses a placeholder
+ rate; mainnet will price by label length via a real oracle.
+
+
+
+
+
= 0 sats · rate 0 sats/USD
+
+
+
+
+
+
+
+
+
+
+
+
Wallet
+
Your addresses and current balance. Keys stay in this browser.
Reader preferences live per-browser; wallet changes require a signature.
+
+
+
Fraunces (Silent Mode family) or Ubuntu (Bitcoin Cash brand font). The pill lives in the top nav.
+
+
PIN unlocks the wallet without re-typing your password. Wipes after 3 wrong attempts.
+
+
Clears the signed-in state in this browser. Your on-chain names are unaffected.
+
+
+
+
+
@@ -245,6 +345,101 @@
+
+
+
+
+
DNS records for —
+
DNS-style records live in a signed manifest on Sia — not on chain.
+ Free per edit, no size cap. The signature proves you own the name; the
+ gateway rejects anything not signed by the current NFT holder. See
+ docs.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Policy for .—
+
Set policy for names registered under this TLD. Only your key can sign this TUPD;
+ the transaction fee is a few cents in chain dust. Leave a field blank to omit that policy
+ (defaults apply — open policy, tier pricing).