๐Ÿงญ

Resolve a BNS name from your own code

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.

What this page gets you

Follow it top to bottom and you'll be able to:

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.

BNS1 โ€” the protocol in one page

Names are anchored by CashTokens certificates. Records travel in OP_RETURN. Discovery uses a beacon address. That is the whole protocol.

Three mechanisms

MechanismWhat 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.

The three operations

TLDs use the same shape with TREG / TUPD opcodes against a second beacon.

Network constants (chipnet)

// 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.

Indexer rules you must apply (consensus-critical)

  1. Fetch beacon history, sort by (height, txid); unconfirmed last.
  2. Reorder causally โ€” never process a tx before one it spends. Otherwise an UPD can land before its own REG and be dropped silently and permanently.
  3. REG is valid only if the tx mints a certificate whose commitment matches the name, the name is free, and the category is created in this tx (output 0 spends the txid-outpoint) with exactly one NFT output and no fungible amount. First valid REG wins. If the name was released, REG must be at least one block after the release.
  4. UPD is valid if some output of the tx carries the name's category.
  5. REL is valid if the name is registered, no output carries its category, and an input spends an output of an earlier beacon tx carrying the certificate.
  6. One message per transaction. A tx with more than one BNS payload carries none. (Added 2026-10-04.)
Rules 2โ€“6 are load-bearing. Skipping any one of them gives you an index that mostly agrees with everyone else, which is worse than one that doesn't resolve at all. Compare your output against the Argus reference implementation on the same chipnet tip before shipping.

Record types

Records are a JSON object. Keep the full OP_RETURN payload under 200 bytes. All keys optional; mix freely.

KeyMeaningResolver action
uHTTPS URL the name points to Open the URL. The gateway form navigate.st/bns/<name>/ redirects here.
hInline HTML โ€” a tiny on-chain page Render the string directly. Most "proof of concept" sites use this.
aCashAddress for payments Resolve alice.bch โ†’ address for a Send flow.
ipIPv4 of a server that answers for the name directly The Thread's gateway proxies here, pinning TLS with tls.
pUpstream URL to reverse-proxy Local gateway proxies the response; address bar stays on the BNS name.
s3bucket/prefix/ on the Sia network Fetch via a Sia S3 gateway (local or operator) and serve the files.
tlsSHA-256 of the server's TLS cert (hex, DER) Pin the cert when connecting; replaces the public CA.
cReference 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.
blBlocklist 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.

Minimum viable resolver โ€” under 100 lines of Node.js

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' }
This is intentionally the shortest possible resolver. It skips things production code must do: the causal-ordering pass (rule 2), REG category validation (rule 3), the one-message rule (rule 6), subdomain collapsing, TLD cosign enforcement, signed-records manifests, and the parallel-source race (bundled snapshot + delta pull + live chain). It also trusts the single Electrum server it talks to. The next sections point you at code that does all of this.

Reference implementations

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.

Argus โ€” JavaScript / Node

The reference indexer, gateway, and resolver library. Browsers and the daemon share resolver-web.js; the Theseus desktop app uses the same module.

silentmode/ariadne ยท Argus/src/lib/

Ariadne Mobile โ€” Kotlin / Android

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.

AriadneResolver/mobile ยท bundled snapshot + Electrum

Ariadne's Thread โ€” Windows service

Standalone resolver that hooks into NRPT. Node runtime, same Argus libraries, local HTTPS gateway with a per-machine CA.

AriadneResolver/windows ยท signed installer

BNS gateway (navigate.st)

The HTTP translation layer that turns navigate.st/bns/<name>/ and <name>.x VHOST into resolver calls. Flat JS, deployable in minutes.

Argus/src/gateway/ ยท systemd unit shipped

Spec documents to copy into your test suite

Public endpoints (convenience, not consensus)

Everything 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.

EndpointWhat it returnsTrust
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.

Embed Ariadne's Thread in your own app

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:

1 โ€” Chain-install the Windows service

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

2 โ€” Use the resolver library directly

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.

3 โ€” Call the local HTTP API

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.

Platforms we cover today

PlatformStatusWhere
Windows 10/11 (x64)ShippingStandalone installer + Theseus bundle
Android 5.0+ShippingAriadne Navigator APK (browser + system VpnService)
Node.js / ElectronShippingArgus library (import directly)
Linux (systemd)PlannedResolver code works; packaging in progress
macOSPlannedNeeds the system-wide resolver service rewrapped
iOSResearchBlocked on sideload policy for BCN resolvers
Want it on another platform? Open an issue on silentmode/ariadne. The resolver is small โ€” porting it to Rust or Go is a weekend's work and we'd rather coordinate it than see parallel reinventions drift.

License, naming, attribution