13 KiB
How to do things — Silent Mode runbook
Operational companion to SESSION-CONTEXT.md. That file is
what the product is; this one is how to do a thing. Every command below was
run on 2026-07-30 and works.
All commands run from D:\Dev\SilentMode\Argus unless stated. Prefix with rtk
per project convention. Everything is chipnet.
Before you touch a
.jsinArgus/src/lib/, grep for it first. Two sessions have written here concurrently andregistry-lifecycle.js,portfolio.jsandElectrumPoolwere each independently rebuilt once. If a test file exists for a module you are about to create, implement to the test — the test is the contract.
1. Read the registry
node src/resolve.js # list every name, with records and category
node src/resolve.js demo.p2p # resolve one
Real output today (abridged):
silentmode.bch -> {"s3":"bns/silentmode/"}
theseus.bch -> {"h":"<h1>Theseus</h1>…"}
coinspectrum.bch-> {"ip":"195.184.247.106","p":"https://…","tls":"84e4cd…"}
demo.p2p -> {"h":"<h1>demo.p2p</h1>…"}
syskypo.de -> {"s3":"bns/syskypo.de/"}
Zero-dependency alternative (the resolver clients actually ship):
node --input-type=module -e "
import WebSocket from 'ws';
import { resolveName } from './src/lib/resolver-web.js';
console.log(await resolveName('silentmode.bch', { WebSocket }));
process.exit(0);"
2. Register a name
node src/register.js <name> <url-or-json>
node src/register.js mysite https://example.org # shorthand → {"u": …}
node src/register.js mysite '{"h":"<h1>Hello</h1>"}' # explicit records
node src/register.js mysite.nav '{"s3":"bns/mysite/"}' # any TLD, same command
Registers from the project wallet (wallets.json). It mints the certificate,
publishes the records and pays the beacon in one transaction.
Verify:
node src/resolve.js mysite.nav
Records can be omitted at registration and set later — see §4.
Buyer-facing registration (built-in wallet, pay-by-QR) goes through
site/register.html, not the CLI. The CLI is for operator-held names and testing.
3. Add a new TLD
Superseded 2026-08-29. The old "TLDs are just a convention, no registry
change needed" model is retired. The per-TLD registry
(Decentralized.DNS/DESIGN-tld-registry.md)
is now the source of truth: a name REG under a TLD that has no on-chain TLD
certificate will be rejected by conforming clients once enforcement is on (the
code is in buildIndex({enforceTld:true}) in both bns.js and
resolver-web.js; default is still off during the mainnet migration, but the
registrar treats the on-chain list as authoritative today).
So adding a TLD is now three steps: TREG on the beacon, UI, deploy.
1. Register the TLD on the TLD beacon (chipnet).
The idempotent way — the operator wallet holds the seed for the TLD-beacon address and the seed script skips anything already present:
cd Argus
# Edit src/seed-tld-beacon.mjs SEED_TLDS[] — append your new TLD label
node src/seed-tld-beacon.mjs
Or a one-off from a Node REPL if you don't want to touch the script:
node --input-type=module -e "
import { loadWallet } from './src/lib/wallet.js';
import { registerTld } from './src/lib/bns.js';
const w = await loadWallet('main');
console.log(await registerTld(w, 'newtld', {}));
process.exit(0);"
Either way you get {tld, txid, category}. The TLD certificate is minted to
the operator wallet — that address becomes the TLD's owner and can UPD the
records with updateTld(wallet, tld, records, category) later.
Cost: ~1,300 chipnet sats per TREG. Empty records {} mean "defaults apply"
per the design doc §3.
2. Add the TLD to the registrar UI.
The dropdown is gone. site/sirius/register.html now uses a TLDS array
(around line 275). Append your new TLD with onchain: true:
const TLDS = [
...,
{ tld: "newtld", onchain: true },
];
If you registered the cert but want to hold the TLD without offering it in
the registrar (the .com pattern — cert on-chain so nobody else claims it, but
no second-level names offered), just leave it out of the TLDS array. The
cert stays valid on the beacon.
3. Deploy site/sirius/register.html to the VPS.
silentmode.st is nginx-served from /opt/silent-mode/site/, NOT Sia
(see DEPLOY-split-st-from-bch.md):
scp D:/Dev/SilentMode/site/sirius/register.html silentmode:/opt/silent-mode/site/sirius/register.html
(silentmode.bch — the Sia mirror — takes a full sia-upload.js of site/.
Only touch that if you also want the on-chain-hosted copy refreshed.)
4. Republish the TLD snapshot (optional, updates mirrors).
If a downstream is reading from the Sia mirror or the Nostr replaceable event rather than doing its own chain scan, refresh the snapshot:
cd Argus
node src/publish-tld-mirror.mjs
Uploads bns/tld-list.json (floating) and bns/tld-list-<root>.json
(immutable content-addressed) to Sia, and publishes NIP-33 kind 30078 (d-tag
bns-tld-list) to the configured relays. Dry-run: BNS_MIRROR_DRY_RUN=1.
The thing to think about before adding one
Under the per-TLD registry the operator's key is the countersignature gate —
so you're the registrar for any TLD you mint. On mainnet the covenant will
enforce a price floor and an operator-countersig for public mints
(Decentralized.DNS/DESIGN-tld-registry.md §5).
On chipnet nothing enforces this; a rogue script could TREG anything under a
different key. Only the operator's key produces TLD certs that clients trust
once the enforcement rules ship.
.bch is special in one way only: its on-chain commitment is the bare label
(silentmode, not silentmode.bch) for backward compatibility. Every other TLD
stores the full label.tld. normalizeName handles this; don't hand-roll it.
Current chipnet TLD set (2026-08-29)
Bench-checkable via node src/seed-tld-beacon.mjs (idempotent verify) or by
fetching the mirror at https://s3.silentmode.st:8600/bns/tld-list.json (403
to anon today; the Nostr replaceable event kind:30078 d:bns-tld-list from
pubkey f2c925194c531c7c398017e35b2396df64a61a613aa5fadebdf1f4990f2267f4 on
wss://nos.lol / wss://relay.damus.io is publicly readable).
Live: bch p2p bit nav test x asm neo gt sc sia com dex cex nt (15 total).
.com is held by the operator — cert on-chain, excluded from the public
registrar UI so nobody else can claim it.
4. Set or change records
node src/update.js <name> <url-or-json>
node src/update.js mysite '{"ip":"203.0.113.9"}'
node src/update.js mysite.nav '{"s3":"bns/mysite/","tls":"a1b2c3…"}'
Proves ownership by moving the certificate — only the holder can do it.
Record types
| Key | Meaning | Example |
|---|---|---|
h |
inline HTML, stored on-chain | {"h":"<h1>Hi</h1>"} |
u |
redirect to a URL | {"u":"https://example.org"} |
ip |
IPv4 of your server | {"ip":"203.0.113.9"} |
p |
reverse-proxy upstream (address bar stays on the name) | {"p":"https://backend.example"} |
s3 |
Sia object or prefix | {"s3":"bns/mysite/"} |
tls |
SHA-256 fingerprint of the name's TLS cert (hex, over DER) | {"tls":"84e4cd…"} |
The 200-byte ceiling
The whole payload — "BNS1" + the JSON — must be ≤ 200 bytes. For
sirius.nav that leaves about 161 bytes for records. An oversized payload throws
at build time rather than producing a broken registration.
Check before you commit to a long h:
node --input-type=module -e "
import { payloadBudget } from './src/lib/register-tx.js';
console.log('bytes free for records:', payloadBudget('sirius.nav'));
process.exit(0);"
h is for a page, a profile, a link hub. Anything real goes on Sia or a server.
5. Put a site on Sia storage
Two steps: upload the bytes, then point the name at them.
# from D:\Dev\SilentMode
node Argus/src/lib/sia-upload.js <file-or-dir> <bucket[/prefix]>
node Argus/src/lib/sia-upload.js D:/Dev/SilentMode/site/mysite bns/mysite
node Argus/src/lib/sia-upload.js ./page.html bns/mysite
Then the record — a trailing slash means "site root", no slash means a single object:
cd Argus
node src/update.js mysite '{"s3":"bns/mysite/"}' # directory → site
node src/update.js mysite '{"s3":"bns/page.html"}' # one file
Verify through the public gateway:
curl -s -o /dev/null -w "%{http_code}\n" https://navigate.st/bns/mysite.bch/
Upload the whole directory if the page has assets — sia-upload.js walks it
and preserves relative paths, which is what makes /js/… and /vendor/… resolve.
sia-s3.json is a secret (SECURITY.md rule 0). Never print it, commit it, or
copy its values into a bundle. The build refuses to emit a bundle containing them.
6. Build and deploy the site
cd Argus && node build-web.mjs # bundles
cd .. && node Argus/src/lib/sia-upload.js D:/Dev/SilentMode/site bns/silentmode
Two rules that will bite otherwise:
- Run the upload from the repo root, not from
site/. The path is relative to cwd andsite/Argus/...does not exist. - Upload the whole
site/directory.register.htmldepends onsite/js/andsite/vendor/; a page-only upload leaves a broken live page.
build-web.mjs refuses to write a bundle that contains a live secret value
(it reads wallets.json/sia-s3.json and greps for their contents) or a
build-machine path. Do not weaken those checks.
libauth is one shared module behind an import map (site/js/libauth.js).
Do not bundle it per-file: its WASM init uses top-level await, and a second copy
hands out undefined for secp256k1 on cold load. The build verifies every
import the other bundles emit is satisfied.
Verify what is actually live:
curl -s https://navigate.st/bns/silentmode.bch/register.html | grep -c 'bns-register.js'
7. Tests
cd Argus
node --test test/*.test.mjs # offline; no network, no coins
Live scripts spend real chipnet coins and are run deliberately:
node test/chipnet-acceptance.mjs # built-in wallet, full registration
node test/chipnet-paybyqr.mjs # registrar mints to a buyer
node test/chipnet-escrow-refund.mjs # unsigned order → refund, name never minted
They exit 2 when the chain is unreachable, so an environment failure is
distinguishable from a product failure. Add --keep to the acceptance test to
print the throwaway wallet's phrase.
8. Gotchas that have actually cost time
Node cannot resolve some hostnames on the dev machine. getaddrinfo returns
EAI_FAIL for coinspectrum.duckdns.org and the chipnet electrum hosts while
nslookup and c-ares resolve them fine. Symptom: uploads and live tests fail
instantly with a DNS error. Workaround is a --require shim that falls back to
dns.resolve4; the real fix is on the machine. If a network command fails
instantly, suspect this before suspecting the code.
Electrum uses ports 50004/62002, which corporate networks and some VPNs block
outright — and every configured server shares those ports, so one firewall rule
takes the whole resolver down. REGISTRAR.selfHosted puts your own Fulcrum on
443 first; see Argus/OPERATIONS-electrum.md.
Two resolvers must stay in lockstep. resolver-web.js (zero-dep, shipped in
clients) and bns.js (mainnet-js). Change one, change both. A disagreement is a
consensus split between clients.
Terminology is enforced. It is a certificate / registration record /
title — never an "NFT" in code, comments or UI. NFTCapability and .nft are
mainnet-js API names and stay.
The certificate lives in a 1,000-sat UTXO. A wallet that does not understand
CashTokens sees dust; a "sweep all funds" would destroy the name. This is why
destinations are normalised to the token-aware z… form.
h records are HTML you are publishing permanently. There is no edit — only
an update transaction, and the old payload stays on-chain forever.
Never register a name you cannot spell twice. Registration is irreversible and there is no expiry on chipnet today, so a typo is a permanently burned name.
9. What not to do
- Do not touch
D:\Dev\NameCoin— separate live system. - Do not flip anything to mainnet. It is a deliberate switch (new electrum
servers,
bitcoincash:prefix, a new beacon, real money), not a config change. - Do not commit or print
wallets.json,sia-s3.json,ca/root-ca.key, or any seed phrase. - Do not put the operator's name or email in any file (SECURITY.md rule 3).
- Do not add a TLD to the dropdown without registering a test name on it first.
- Do not weaken the build's secret scan or the LGPL licence checks.