Everything an agent, library author, or app developer needs to look up a Bitcoin Cash Domain Name
directly โ no navigate.st, no public gateway, just the chain and (optionally) one
Electrum server. One page of protocol, one record table, a 60-line resolver, and a map to every
production implementation.
Follow it top to bottom and you'll be able to:
sirius.x or hello.bch, hit the Bitcoin Cash chain,
and get back the records its owner published โ without trusting
navigate.st, silentmode.st,
or any other operator-run service.s3, u, ip, h,
p, a, โฆ), fetch the content and present it to your user.The complete spec lives in
Decentralized.DNS/PROTOCOL.md on the forge. This page is the orientation that
tells an agent where to look and what to copy.
Names are anchored by CashTokens certificates. Records travel in OP_RETURN. Discovery uses a beacon address. That is the whole protocol.
| Mechanism | What it carries |
|---|---|
| Immutable CashTokens NFT ("certificate") | Ownership. The certificate's commitment is the UTF-8 name. Whoever holds the certificate owns the name โ consensus guarantees nobody can mint a new output of an existing category without spending it. |
| OP_RETURN payload | Data. The literal bytes BNS1 followed by a JSON object with the
operation, name, and records. โค 200 bytes total. |
| Beacon dust output (600 sat) | Discovery. Every protocol transaction pays 600 sat to a well-known address, so
blockchain.address.get_history(beacon) enumerates every BNS transaction
ever made with a single Electrum call. |
REG โ one tx that mints the certificate and carries
BNS1{"o":"REG","n":"<name>","r":{โฆ}} and pays the beacon.UPD โ one tx that spends-and-recreates the certificate (ownership proof)
and carries BNS1{"o":"UPD","n":"<name>","r":{โฆ}}.REL โ spends the certificate without recreating it (burns it) and
carries BNS1{"o":"REL","n":"<name>"}. The name returns to the pool after
a one-block gap.TLDs use the same shape with TREG / TUPD opcodes against a second
beacon.
// Everything you need to query the chain. These are constants โ
// they do not change until the network is bumped.
const PREFIX = "BNS1";
const BEACON = "bchtest:qp8njaa9a3ahg7259s5qajnce526n0rttsq8qwx4jl";
const BEACON_SCRIPTHASH = "e1cdbba719436f49acb4bf245b44df875c6d6b405eda46a9d3e736d138102f3b";
const TLD_BEACON = "bchtest:qzc4c76vtw9nd49g6x8nmvc0jz62uvk4dc7hmpmerh";
const TLD_SCRIPTHASH = "c85db10e106606273422b2733b2a6fb8b35a4d0e7dbfb20cf59bdb0833c008b8";
const BEACON_DUST_SAT = 600;
const NAME_RE = /^[a-z0-9-]{1,32}$/;
The authoritative table โ chipnet beacons, mainnet beacons (unpinned until mainnet launches),
the Electrum pool, the operator address โ is in Argus/src/lib/resolver-web.js.
Memory rule: that file is the only source of truth; divergence is a consensus split.
(height, txid); unconfirmed last.Records are a JSON object. Keep the full OP_RETURN payload under 200 bytes. All keys optional; mix freely.
| Key | Meaning | Resolver action |
|---|---|---|
u | HTTPS URL the name points to | Open the URL. The gateway form navigate.st/bns/<name>/
redirects here. |
h | Inline HTML โ a tiny on-chain page | Render the string directly. Most "proof of concept" sites use this. |
a | CashAddress for payments | Resolve alice.bch โ address for a Send flow. |
ip | IPv4 of a server that answers for the name directly | The Thread's gateway proxies here, pinning TLS with tls. |
p | Upstream URL to reverse-proxy | Local gateway proxies the response; address bar stays on the BNS name. |
s3 | bucket/prefix/ on the Sia network |
Fetch via a Sia S3 gateway (local or operator) and serve the files. |
tls | SHA-256 of the server's TLS cert (hex, DER) | Pin the cert when connecting; replaces the public CA. |
c | Reference to a signed records manifest | Pull _records.json from Sia, verify signature, then use its DNS block
(A/AAAA/MX/TXT/NS/SRV/CAA, per-host rules). See
DESIGN-signed-records-manifest.md. |
bl | Blocklist feed address | Only used on blocklist-authority names; see PROTOCOL-ADDENDUM-blocklist.md. |
The resolver picks the first record type it understands. The canonical precedence used by
Theseus and Ariadne lives in
Argus/src/lib/record-picker.js โ follow it so answers match across clients.
The whole flow: open one Electrum WebSocket, pull the beacon's history, parse the OP_RETURNs, apply the rules, return a record. This runs without any Silent Mode code at all.
// bns-mini.mjs โ a one-file BNS resolver for chipnet.
// node bns-mini.mjs sirius.x
import WebSocket from "ws";
import { createHash } from "crypto";
const BEACON_SCRIPTHASH =
"e1cdbba719436f49acb4bf245b44df875c6d6b405eda46a9d3e736d138102f3b";
const ELECTRUM = "wss://chipnet.imaginary.cash:50004";
const PREFIX = "BNS1";
const query = process.argv[2] || "sirius.x";
const [label, tld = "bch"] = query.toLowerCase().split(".");
const wantName = tld === "bch" ? label : `${label}.${tld}`;
// --- 1. open the Electrum socket ---
const ws = new WebSocket(ELECTRUM);
await new Promise(r => ws.on("open", r));
let id = 0;
const rpc = (method, params = []) => new Promise((resolve) => {
const mine = ++id;
ws.on("message", function h(m) {
const msg = JSON.parse(m);
if (msg.id === mine) { ws.off("message", h); resolve(msg.result); }
});
ws.send(JSON.stringify({ id: mine, method, params }));
});
// --- 2. pull the beacon's history ---
const hist = await rpc("blockchain.scripthash.get_history", [BEACON_SCRIPTHASH]);
hist.sort((a, b) => (a.height || 1e9) - (b.height || 1e9) ||
a.tx_hash.localeCompare(b.tx_hash));
// --- 3. for each tx, parse the OP_RETURN; rebuild the name index ---
const names = new Map(); // name -> records
for (const { tx_hash } of hist) {
const tx = await rpc("blockchain.transaction.get", [tx_hash, true]);
for (const vout of tx.vout || []) {
const script = vout.scriptPubKey?.hex || "";
if (!script.startsWith("6a")) continue; // not OP_RETURN
const payload = extractPayload(script);
if (!payload?.startsWith(PREFIX)) continue;
let p;
try { p = JSON.parse(payload.slice(PREFIX.length)); } catch { continue; }
if (p.o === "REG" && !names.has(p.n)) names.set(p.n, p.r || {});
else if (p.o === "UPD" && names.has(p.n)) names.set(p.n, p.r || {});
else if (p.o === "REL") names.delete(p.n);
}
}
console.log(wantName, "โ", names.get(wantName) ?? "<not registered>");
ws.close();
// --- OP_RETURN parser (one push, enough for BNS1) ---
function extractPayload(hex) {
let i = 2, len;
const op = parseInt(hex.slice(i, i + 2), 16); i += 2;
if (op <= 0x4b) len = op;
else if (op === 0x4c) { len = parseInt(hex.slice(i, i + 2), 16); i += 2; }
else return null;
return Buffer.from(hex.slice(i, i + len * 2), "hex").toString("utf8");
}
Run it:
$ npm i ws
$ node bns-mini.mjs sirius.x
sirius.x โ { s3: 'bns/sirius.x/' }
$ node bns-mini.mjs theseus.x
theseus.x โ { s3: 'bns/theseus/', u: 'https://silentmode.st/tools/#theseus' }
Every production BNS client is open-source. If your language or runtime matches one of these, port the parsers verbatim โ divergence is a consensus bug.
The reference indexer, gateway, and resolver library. Browsers and the daemon share
resolver-web.js; the Theseus desktop app uses the same module.
WebView browser plus a system-wide VpnService; the resolver is a Kotlin port that mirrors
the JS rules. BnsNetwork.java is generated from the same JS table.
Standalone resolver that hooks into NRPT. Node runtime, same Argus libraries, local HTTPS gateway with a per-machine CA.
The HTTP translation layer that turns navigate.st/bns/<name>/ and
<name>.x VHOST into resolver calls. Flat JS, deployable in minutes.
PROTOCOL.md โ BNS1 coreDESIGN-tld-registry.md โ per-TLD NFTs on the TLD beaconDESIGN-signed-records-manifest.md โ wallet-signed DNS blockPROTOCOL-ADDENDUM-release.md โ REL opcodePROTOCOL-ADDENDUM-blocklist.md โ community blocklistsEverything above gets you a resolver that trusts only the chain. The following endpoints are convenience caches on top; use them for prototyping, UX speed-ups, or servers that don't want to track the chain themselves โ but understand the trust shift.
| Endpoint | What it returns | Trust |
|---|---|---|
https://silentmode.st/api/name/<name> |
Current records as JSON | Silentmode indexer โ verify against chain |
https://silentmode.st/api/tlds |
Every registered TLD + records | Silentmode indexer |
https://silentmode.st/api/dns/<name> |
Signed DNS records manifest (verified) | Signature-checked against on-chain owner |
https://navigate.st/bns/<name>/ |
Website behind the name (gateway) | Operator-run; use only when a native client isn't available |
https://dl.silentmode.st/bns-name-snapshot.json |
Raw beacon history + txs (clients rebuild their own index) | Snapshot of chain state; verify by replaying rules |
None of these are required. Theseus and Ariadne use them as warm-start caches; the same client also runs a live Electrum query in parallel so the authoritative chain answer always wins.
If you ship software with a browser surface โ an Electron app, a Chromium wrapper, a game launcher, a wallet, a CLI, a Node service โ you can give your users BNS resolution for free. Three paths, in order of effort:
Easiest. The Ariadne's Thread installer is a
standalone resolver that hooks into NRPT and the HTTPS loopback. Once it is installed, every
browser and every app that follows the OS DNS stack can resolve .bch,
.x, and the other BNS TLDs โ Firefox, curl, your mail client, your own app.
In your installer, add a chain-install step that silently runs the Thread's
AriadneResolver-Setup.exe /S after your own install finishes. The service is
opt-out: present a checkbox and respect it. Theseus does this today.
; NSIS fragment (works in Inno Setup similarly)
Section "BNS resolution โ Ariadne's Thread (recommended)" secAriadne
SetOutPath "$TEMP"
File "AriadneResolver-Setup.exe"
ExecWait '"$TEMP\AriadneResolver-Setup.exe" /S'
Delete "$TEMP\AriadneResolver-Setup.exe"
SectionEnd
If your app is a JavaScript runtime (Node, Electron, browser-with-crypto), import the Argus resolver and skip the service:
import { resolveName } from
"https://code.silentmode.st/silentmode/ariadne/raw/branch/master/Argus/src/lib/resolver-web.js";
const result = await resolveName("sirius.x");
// โ { s3: "bns/sirius.x/" }
The function talks to the public Electrum pool out of the box; pass your own
electrum array in the options to pin servers. Full API in the module header.
License is MPL-2.0 โ your project can be any license as long as changes to
resolver-web.js itself stay MPL.
If the user has the Thread installed, it exposes a tiny API on 127.0.0.1:4626
(and https://ariadne.local with the per-machine CA):
GET /api/name/<name> โ { records, owner, category, chain_tip }
GET /api/dns/<name> โ { A, AAAA, MX, TXT, NS, SRV, CAA, ... } (verified)
GET /api/tlds โ { tlds: [...] }
GET /health โ { ok, tip, electrum, snapshot_age_s }
Use this when you want BNS resolution without redistributing the Thread yourself. If
the /health probe fails, degrade gracefully โ the user may not have it installed
yet, which is a good moment to link them at
silentmode.st/releases.
| Platform | Status | Where |
|---|---|---|
| Windows 10/11 (x64) | Shipping | Standalone installer + Theseus bundle |
| Android 5.0+ | Shipping | Ariadne Navigator APK (browser + system VpnService) |
| Node.js / Electron | Shipping | Argus library (import directly) |
| Linux (systemd) | Planned | Resolver code works; packaging in progress |
| macOS | Planned | Needs the system-wide resolver service rewrapped |
| iOS | Research | Blocked on sideload policy for BCN resolvers |