Radiant SDK: Build Radiant dapps in TypeScript

@radiant-core/sdk v0.2.0 Last Updated: July 2026

1. What Is the Radiant SDK?

@radiant-core/sdk is the clean TypeScript SDK most web and app developers reach for first. It lets you build Radiant (RXD) dapps — wallets, marketplaces, games, payment flows — without reading internal wallet or library source. It wraps radiantjs (the low-level tx/script library) and adds the ecosystem-specific pieces app devs actually need.

SDK vs. radiantjs. radiantjs is the low-level engine (keys, raw transactions, scripts, signing). @radiant-core/sdk is the application layer built on top of it (networking, token-safe funding, Glyph, WAVE, units). You install only the SDK — radiantjs comes along as a dependency and you never touch its API directly.

2. Installation

npm install @radiant-core/sdk
# Node < 22 has no global WebSocket — also install the optional peer:
npm install ws

Requires Node 20.19+ or 22+ (radiantjs pulls in an ESM-only dependency that needs require(ESM) support). Ships dual ESM + CJS with bundled types.

Units. Every amount in the SDK is in photons as a BigInt1 RXD = 100,000,000 photons. Convert only at your UI edge with rxdToPhotons() / photonsToRxd(). Never do balance math in floating point.

3. Connect & Read Balances

The ElectrumClient speaks JSON-RPC over a single reconnecting WebSocket. Scripthash methods accept a plain address (the scripthash is computed for you).

import { ElectrumClient, photonsToRxd } from '@radiant-core/sdk';

const client = new ElectrumClient({ network: 'mainnet' });
await client.connect();

const address = '1Yourradiantaddress...';
const { confirmed, unconfirmed } = await client.getBalance(address);
console.log(`Confirmed: ${photonsToRxd(confirmed)} RXD`);

const utxos = await client.listUnspent(address); // Utxo[] (photons as BigInt)

// React to incoming payments:
await client.subscribe(address, (status) => {
  console.log('address changed:', status); // re-fetch balance/UTXOs here
});

The default mainnet endpoint is wss://electrumx.radiantcore.org:443; pass endpoint to point at your own ElectrumX/RXinDexer.

4. Wallets & Keys

Create or restore a BIP39 HD wallet. Derivation defaults to the Radiant path m/44'/512'/account'/0/index.

import { HDWallet } from '@radiant-core/sdk';

// New wallet
const mnemonic = HDWallet.generateMnemonic();
const wallet = HDWallet.fromMnemonic(mnemonic, { network: 'mainnet' });

const me = wallet.deriveKey(0);   // m/44'/512'/0'/0/0
console.log(me.path, me.address, me.scriptHash);

// Batch derive a gap of receive addresses
const receive = wallet.deriveRange(0, 20);   // DerivedKey[]

5. Sending RXD (Ref-Safe)

The single most important safety property of the SDK: funding is never gathered by a value heuristic. A UTXO that carries a token ref (FT/NFT/dMint) looks like plain value to a naive selector, but spending it as funding burns the token. selectRxdFunding screens every candidate two ways — indexer-reported refs and a local script scan — and silently excludes token UTXOs.

import { ElectrumClient, HDWallet, buildRxdTransfer, rxdToPhotons } from '@radiant-core/sdk';

const client = new ElectrumClient({ network: 'mainnet' });
const wallet = HDWallet.fromMnemonic(mnemonic);
const me = wallet.deriveKey(0);

const utxos = await client.listUnspent(me.address);

const { hex, txid } = buildRxdTransfer({
  address: me.address,
  wif: me.wif,
  to: '1Recipient...',
  amount: rxdToPhotons('1.25'), // 1.25 RXD -> photons
  utxos,                        // token-bearing UTXOs are auto-excluded
  feeRate: 10_000n,             // photons/byte (mainnet min-relay)
});

await client.broadcastTx(hex);
console.log('sent', txid);
Why this matters. On Radiant, tokens live in ordinary-looking UTXOs guarded by ref opcodes. Spend one as fee/change and the token is gone — irreversibly. If you select inputs yourself, always filter with filterFundingCandidates(utxos) or guard with assertFundingSafe(utxos) first.

6. Glyph Tokens

Mint and move Glyph tokens with the commit/reveal pattern handled for you. Fund from token-free UTXOs.

import { mintFT, mintNFT, transferToken, transferFungible, filterFundingCandidates } from '@radiant-core/sdk';

const funding = filterFundingCandidates(await client.listUnspent(me.address));

// Fungible token (FT amount == output photons)
const ft = await mintFT({
  client, address: me.address, wif: me.wif,
  ticker: 'DEMO',
  supply: 1_000_000n,
  metadata: { name: 'Demo Token', desc: 'Minted with @radiant-core/sdk' },
  fundingUtxos: funding,
});
console.log('FT ref:', ft.refDisplay, ft.commitTxid, ft.revealTxid);

// Non-fungible token (singleton)
const nft = await mintNFT({
  client, address: me.address, wif: me.wif,
  metadata: { name: 'My NFT', attrs: { rarity: 'rare' } },
  fundingUtxos: funding,
});

// Transfer a token (FT or NFT) to a new owner — moves one UTXO whole
await transferToken({
  client, address: me.address, wif: me.wif,
  tokenUtxo,                 // the FT/NFT UTXO (must include its on-chain script)
  toAddress: '1Recipient...',
  fundingUtxos: funding,     // covers the fee; token value is conserved
});

// Send PART of a fungible balance (new in 0.2.0). Accumulates across your
// FT UTXOs and returns the remainder to you as an FT change output.
await transferFungible({
  client, address: me.address, wif: me.wif,
  tokenUtxos,                // your FT UTXOs for this token (with scripts)
  amount: 50n,               // send 50 of e.g. a 500 balance
  toAddress: '1Recipient...',
  fundingUtxos: funding,
});
WAVE names & mutable NFTs (fixed in 0.2.0). transferToken now rebuilds outputs canonically instead of editing the on-chain script in place. Previously a WAVE-name or mutable-NFT transfer carried the mutable covenant's auth preamble (OP_REQUIREINPUTREF) into the output and the node rejected it (invalid-transaction-reference-operations). The fix is proven on-chain against a regtest node.

Validate before you trust (new in 0.2.0). When a counterparty hands you a prevout — a swap maker "advertising" a token, a listing, anything you didn't build yourself — parse it with the shape-exact parsers instead of eyeballing refs:

import { parseNftScript, parseFtScript, tokenScriptKind } from '@radiant-core/sdk';

const kind = tokenScriptKind(prevout.script);  // 'nft' | 'ft' | null

const { ref, addressHash } = parseNftScript(prevout.script);
if (ref) console.log(ref, addressHash);        // set only if the script is
                                               // EXACTLY an NFT shape

(parseP2pkhScript does the same for plain pay-to-pubkey-hash outputs.)

Why shape-exact matters. On auth-form (mutable/WAVE) NFTs the script carries two refs. A loose "first ref in the script" scan returns the mutable ref; parseNftScript returns the singleton ref that actually identifies the token. If you match a maker's advertised token by a loose scan, you can be paid in the wrong asset.
Finding your tokens. The indexer keys a token UTXO under scriptHash(zeroRefs(tokenScript)), not the owner's plain address. To list a held token, query client.listUnspent(scriptHash(zeroRefs(ftScript(owner, ref)))) — the entries come back with refs:[{ ref, type }] (type is "normal" for FT, "single" for an NFT singleton).
Test on regtest first. mintFT / mintNFT / transferToken broadcast real, irreversible transactions. They reproduce the proven Photonic-Wallet on-chain templates and are regtest-validated, but you should smoke-test your own flow on regtest/testnet before mainnet.

7. Token Discovery

New in 0.2.0: query global, newest-first token lists — every Glyph asset on the chain, or one type at a time — without running your own indexer. Backed by the public RXinDexer v4 discovery indexes (live on https://radiantcore.org/api since 2026-07-18; the raw REST endpoints are documented in the Developer Guide).

import { getRecentTokens, getTokensByType, GLYPH_TOKEN_TYPE } from '@radiant-core/sdk';

// Newest tokens across ALL types (one page)
const { tokens, nextCursor } = await getRecentTokens({ limit: 100 });
for (const t of tokens) {
  console.log(t.type_name, t.name ?? t.ticker, t.ref, t.deploy_height);
}

// Newest NFTs only — either form works:
await getRecentTokens({ typeId: GLYPH_TOKEN_TYPE.NFT });
await getTokensByType(GLYPH_TOKEN_TYPE.NFT, { order: 'recent' });

Pages are cursor-paginated: pass the previous page's nextCursor back as cursor to fetch the next page (null = no more pages). Cursors are opaque, URL-safe strings and are order-specific — never reuse a cursor across a change of order or type. getTokensByType defaults to order: 'ref' (the stable pre-v4 hash order); pass order: 'recent' for newest-first.

The watermark-sync pattern. Because the lists are newest-first, an indexer, gallery, or bot never has to re-walk the chain: do one full walk, save the newest deploy_height you saw, then on later runs page newest-first and stop as soon as a token's height drops below your watermark. You only ever pay for tokens minted since the last run.

import { getRecentTokens } from '@radiant-core/sdk';

async function syncNewTokens(watermark /* newest deploy_height already seen */) {
  let cursor, newest = watermark;

  paging: while (true) {
    const { tokens, nextCursor } = await getRecentTokens({ limit: 200, cursor });
    for (const t of tokens) {
      if (t.deploy_height < watermark) break paging; // everything older is known
      newest = Math.max(newest, t.deploy_height);
      await upsertToken(t);   // idempotent — heights at the boundary can repeat
    }
    if (!nextCursor) break;
    cursor = nextCursor;
  }
  return newest; // save as the next run's watermark
}
Type ids. GLYPH_TOKEN_TYPE = FT: 1, NFT: 2, DAT: 3, DMINT: 4, WAVE: 5, CONTAINER: 6, AUTHORITY: 7 (UNKNOWN: 0). Each row is a GlyphTokenSummaryref, ref_hex, type/type_name, protocols, name, ticker, deploy_height, deploy_txid, is_spent, plus whatever else the indexer knows (supply, icon, attrs, …).

8. WAVE Names

Resolve WAVE names to addresses through the public indexer.

import { waveResolve, waveResolveAddress } from '@radiant-core/sdk';

const rec = await waveResolve('alice');        // "alice.rxd" works too
if (rec.registered) console.log(rec.address, rec.owner, rec.expires);

const addr = await waveResolveAddress('alice'); // string | null

9. API Overview

AreaExports
ClientElectrumClient
WalletHDWallet, Keys
FundingselectRxdFunding, filterFundingCandidates, isFundingSafe, assertFundingSafe, estimateFee, sumValue
TransactionsbuildRxdTransfer, buildTx
TokensmintFT, mintNFT, transferToken, transferFungible, encodeGlyph, ftScript, nftScript, parseTokenRef
DiscoverygetRecentTokens, getTokensByType, GLYPH_TOKEN_TYPE
WAVEwaveResolve, waveResolveAddress, waveLabel
ScriptscriptHash, addressToScriptHash, p2pkhScript, parseNftScript, parseFtScript, parseP2pkhScript, tokenScriptKind, isTokenBearing, zeroRefs, packRef, unpackRef
UnitsrxdToPhotons, photonsToRxd
ErrorsRadiantSdkError, InsufficientFundsError, TokenBurnGuardError, ElectrumError, ValidationError

Full reference (every export, options, and return types) is in docs/API.md.

10. Notes & Links

Validated end-to-end. The SDK ships with unit tests (CI on Node 20/22/24), on-chain consensus validation against a radiantd regtest node (FT/NFT mint + transfer accepted), and indexer validation against RXinDexer/ElectrumX (tokens surfaced via listUnspent with their refs).

Fee policy. Mainnet min-relay is 10,000 photons/byte; testnet/regtest is 1,000. The SDK sets the fee from the measured signed transaction size, so token transactions reliably clear the floor.

Fee sanity guard (new in 0.2.0). buildTx now refuses to build a transaction whose fee lands above ~2× the network floor (plus size headroom), throwing a ValidationError instead of broadcasting. This catches the three silent overpay paths — photons/byte ↔ sats/kB unit slips, over-funding with addChange: false, and radiantjs rolling sub-dust change into the fee — all of which are irrecoverable once mined. The guard is automatic; there is nothing to configure.

ResourceLink
npm package@radiant-core/sdk
Source & issuesgithub.com/Radiant-Core/radiant-sdk
API referencedocs/API.md
Low-level libraryradiantjs
Related guidesDeveloper Guide · Token Creation · WAVE Names