A WAVE name is a human-readable name on the Radiant blockchain — for example satoshi.rxd — that points at a payment address. Instead of asking someone to send RXD to 1Rxd…long…address, they can send to satoshi.rxd and your wallet resolves the name to the right address.
Under the hood, every WAVE name is a mutable Glyph NFT. The NFT holds the name's metadata — including its current target address — and whoever holds the NFT controls the name. Because the NFT is mutable, the owner can re-point the name at a new address at any time (e.g. when rotating wallets) without losing the name itself.
WAVE names are claimed on a first-registration-wins basis. If two people both mint an NFT claiming the name satoshi, the indexer treats the earliest registration (lowest block height, then transaction order) as the canonical one. Every later registration of the same name is recorded as a duplicate and is never returned by resolution.
This means resolution is deterministic and unambiguous: resolveWaveName('satoshi') always returns the canonical owner's current address, regardless of how many copycats exist. The canonical NFT can still be transferred or sold — rights follow the token — but a newcomer cannot hijack a name simply by minting a clone after the fact.
The easiest way to register a WAVE name today is Photonic Wallet, the reference Radiant wallet that mints the WAVE NFT for you and lets you set and update its target address:
Once the registration transaction confirms and the indexer ingests it, the name resolves through the public API immediately — no extra publishing step.
You don't need to parse any on-chain data yourself. Drop the dependency-free wave-resolver.js module into your dapp. It calls the public RXinDexer REST API and returns a plain { address, ref, owner } object (or null if the name is unregistered). It works in any environment with a global fetch — modern browsers, Deno, and Node.js 18+.
⬇ Download wave-resolver.js (ESM, no dependencies)
const DEFAULT_API_BASE = 'https://radiantcore.org/api';
// Normalize: trim, lowercase, strip an optional ".rxd" suffix.
function normalizeWaveName(name) {
if (typeof name !== 'string') throw new TypeError('WAVE name must be a string');
let n = name.trim().toLowerCase();
if (n.endsWith('.rxd')) n = n.slice(0, -4);
return n;
}
async function getJson(url) {
const res = await fetch(url, { headers: { accept: 'application/json' } });
if (!res.ok) throw new Error(`WAVE API returned HTTP ${res.status} for ${url}`);
return res.json();
}
// Resolve a name -> { address, ref, owner } | null (null = unregistered).
export async function resolveWaveName(name, apiBase = DEFAULT_API_BASE) {
const bare = normalizeWaveName(name);
if (!bare) return null;
const base = apiBase.replace(/\/+$/, '');
const data = await getJson(`${base}/wave/resolve/${encodeURIComponent(bare)}`);
// Unregistered names come back as { available: true, resolved: false }.
if (!data || data.available || data.resolved === false) return null;
const address = data.target || (data.zone && data.zone.address) || null;
if (!address) return null;
return { address, ref: data.ref ?? null, owner: data.owner ?? null };
}
// Is a name free to register?
export async function isWaveAvailable(name, apiBase = DEFAULT_API_BASE) {
const bare = normalizeWaveName(name);
if (!bare) return false;
const base = apiBase.replace(/\/+$/, '');
const data = await getJson(`${base}/wave/available/${encodeURIComponent(bare)}`);
return Boolean(data && data.available);
}
import { resolveWaveName } from './wave-resolver.js';
const hit = await resolveWaveName('satoshi.rxd');
if (!hit) {
console.log('Name is not registered — nothing to pay.');
} else {
console.log('Pay this address:', hit.address); // current target address
console.log('Canonical ref:', hit.ref); // "txid_vout"
console.log('Owner scripthash:', hit.owner);
// Build the payment with radiantjs (https://github.com/Radiant-Core/radiantjs).
// Amounts are in photons (1 RXD = 100,000,000 photons).
const tx = new radiant.Transaction()
.from(utxos)
.to(hit.address, 100_000_000) // send 1 RXD
.change(myChangeAddress)
.sign(myPrivateKey);
await electrum.broadcast(tx.toString());
}
resolveWaveName returns null only when a name is genuinely unregistered. If the indexer is unreachable or returns an error (e.g. HTTP 503 when the WAVE index is offline), it throws — so you can tell "no such name" apart from "couldn't reach the indexer" and avoid sending funds on a failed lookup.import { isWaveAvailable } from './wave-resolver.js';
if (await isWaveAvailable('myhandle')) {
// Free — register it in Photonic Wallet: https://photonic-wallet.com/
} else {
console.log('Taken — try another name.');
}
All endpoints are served by the public RXinDexer instance under the base URL https://radiantcore.org/api. They are GET-only, return JSON, and require no authentication. If you run your own indexer, point apiBase at it instead.
Resolve a name to its canonical registration and current target address.
| Parameter | In | Description |
|---|---|---|
name | path | The name to resolve, 1–63 chars (without the .rxd suffix). |
include_duplicates | query | Optional. true to also list non-canonical duplicate registrations. |
Registered → 200
{
"name": "satoshi",
"ref": "txid_vout",
"target": "1RxdPaymentAddress...", // the address to pay (mutable)
"zone": { // DNS-like records (any field may be null)
"address": "1RxdPaymentAddress...",
"display": "Satoshi",
"avatar": null,
"url": "https://example.com",
"TXT": ["hello"]
},
"owner": "32-byte-hex-scripthash",
"available": false,
"canonical": true,
"has_duplicates": false
}
Unregistered → 200
{ "name": "nobody", "available": true, "resolved": false }
Index offline → 503 { "detail": "WAVE index not available" }
Check whether a name is free to register.
| Parameter | In | Description |
|---|---|---|
name | path | The name to check, 1–63 chars. |
Available → { "available": true, "name": "myhandle" }
Taken → { "available": false, "ref": "txid_vout", "name": "satoshi" }
List all canonical (first-registration) WAVE names. Results are paginated with an opaque cursor.
| Parameter | In | Description |
|---|---|---|
limit | query | Max results per page. Default 500, maximum 2000. |
cursor | query | Opaque pagination token. Pass the next_cursor from the previous response to fetch the next page. Omit for the first page. |
include_duplicates | query | Optional. true adds a has_duplicates flag to each entry. |
Response → 200
{
"names": [
{
"name": "satoshi",
"domain": "rxd",
"full_name": "satoshi.rxd",
"target": "1RxdPaymentAddress...",
"ref": "txid_vout",
"height": 410100,
"spent": false,
"canonical": true
}
],
"total": 1,
"next_cursor": "eyJjaGFyX2lkeCI6IDUsInJlZiI6IC..." // null when no more pages
}
To page through every name, keep calling the endpoint and feeding the previous next_cursor back in until next_cursor is null:
async function* allWaveNames(apiBase = 'https://radiantcore.org/api') {
let cursor = null;
do {
const url = new URL(`${apiBase}/wave/names`);
url.searchParams.set('limit', '1000');
if (cursor) url.searchParams.set('cursor', cursor);
const page = await fetch(url).then(r => r.json());
for (const entry of page.names) yield entry;
cursor = page.next_cursor;
} while (cursor);
}
for await (const name of allWaveNames()) {
console.log(name.full_name, '->', name.target);
}
| Endpoint | Purpose |
|---|---|
GET /wave/registrations/{name} | Canonical registration plus every duplicate registration of a name. |
GET /wave/reverse/{scripthash} | Reverse lookup: every name owned by a given scripthash. |
GET /wave/stats | Index health and totals (names, zones, owners). |
Does the target address change? Yes — that's the point of a mutable name. Always resolve a name fresh at payment time rather than caching the address indefinitely; the owner may have re-pointed it.
What about the .rxd suffix? The on-chain name is the bare label (e.g. satoshi); .rxd is just the display domain. The resolver strips it for you, so satoshi and satoshi.rxd resolve identically.
Can I run my own resolver backend? Yes. Every endpoint above is part of RXinDexer, which is open source. Run your own instance and pass its base URL as the apiBase argument to keep resolution fully self-hosted.
Is resolution trustless? The canonical-registration rule is computed from on-chain data, so independent indexers converge on the same answer. For high-value payments, resolve against more than one indexer (or your own node) and confirm the ref matches before sending.