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.');
}
resolveWaveName(name, apiBase?), isWaveAvailable(name, apiBase?), listWaveNames(apiBase?, pageSize?) (async iterator), the normalizeWaveName(name) helper, and the DEFAULT_API_BASE constant. Pass your own indexer's base URL as apiBase to self-host resolution.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
}
You rarely need to drive the cursor by hand: wave-resolver.js ships a listWaveNames() async iterator that follows next_cursor for you and yields one entry at a time, fetching each page lazily.
import { listWaveNames } from './wave-resolver.js';
// Pages are fetched lazily as you iterate — no cursor bookkeeping.
for await (const name of listWaveNames()) {
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). |
The public API is rate-limited to 600 requests per minute per IP (10/s, burst 600); going over returns HTTP 429. That's generous for normal use — you typically resolve one name per payment — so as long as you resolve on demand rather than hammering the endpoint, you'll never come close. If you run a high-traffic backend, resolve server-side and cache for the duration of a payment flow, or point apiBase at your own RXinDexer instance.
The /wave/* endpoints are meant to be called from the browser from any origin. If a browser fetch fails with a CORS error while the exact same URL works in curl or Node, that's a server-side CORS-header issue rather than a bug in your code — please open an issue.
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.