Examples

End-to-end usage examples for common XChain Platform SDK workflows.

Table of Contents


Setup

const { XChainSDK } = require('xchain-sdk');

// Minimal — action string generation only (no network calls)
const sdk = new XChainSDK();

// With explorer (for querying blockchain data)
const sdk = new XChainSDK({
    network: 'bitcoin-mainnet',
    explorerUrl: 'explorer.xchain.io',
    explorerPort: 8080
});

// With encoder (for generating PSBTs)
const sdk = new XChainSDK({
    network: 'bitcoin-mainnet',
    explorerUrl: 'explorer.xchain.io',
    encoderUrl: 'encoder.xchain.io',
    encoderPort: 3000
});

Query Balances

const balances = await sdk.getBalances('bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh');
console.log(balances);
// { total: 5, data: [{ tick: 'MYTOKEN', amount: '1000', ... }, ...] }

// With pagination
const page2 = await sdk.getBalances('bc1q...', { page: 2, limit: 50, sortorder: 'DESC' });

Get Token Info

const token = await sdk.getToken('MYTOKEN');
console.log(token.info.tick);       // 'MYTOKEN'
console.log(token.supply.current);  // '500000'
console.log(token.supply.max);      // '21000000'
console.log(token.locks.mint);      // true/false

Send Tokens

// Generate the ACTION string only (no PSBT)
const result = await sdk.send({
    tick: 'MYTOKEN',
    amount: '100',
    destination: 'bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh',
    memo: 'Payment for services'
});

console.log(result.actionString); // 'SEND|0|MYTOKEN|100|bc1q...|Payment for services'
console.log(result.version);     // 0
console.log(result.psbt);        // null (no encoder options provided)

Send Tokens with PSBT

// Generate ACTION string AND encode into a PSBT
const result = await sdk.send(
    {
        tick: 'MYTOKEN',
        amount: '100',
        destination: 'bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh'
    },
    {
        pubkey: 'your-public-key-or-address',
        change: 'your-change-address',
        rbf: true
    }
);

console.log(result.actionString); // 'SEND|0|MYTOKEN|100|bc1q...'
console.log(result.psbt);        // '70736274ff...' (hex-encoded PSBT)
console.log(result.encoding);    // 'OP_RETURN' (auto-selected)

// Sign the PSBT with your wallet and broadcast to the network

Issue a New Token

const result = await sdk.issue({
    tick: 'NEWTOKEN',
    maxSupply: '21000000',
    maxMint: '1000',
    decimals: 8,
    description: 'A new token on XChain',
    mintSupply: '0',
    lockMaxSupply: 1,
    lockDescription: 0
});

console.log(result.version);     // 0 (full create)
console.log(result.actionString); // 'ISSUE|0|NEWTOKEN|21000000|1000|8|A new token on XChain|0|...'

Update Token Description

// Only provide tick + description → SDK auto-selects v1 (shortest format)
const result = await sdk.issue({
    tick: 'NEWTOKEN',
    description: 'Updated description for my token'
});

console.log(result.version); // 1 (description update — much shorter than v0)

Lock Token Properties

const result = await sdk.issue({
    tick: 'NEWTOKEN',
    lockMaxSupply: 1,
    lockMint: 1,
    lockMintSupply: 0,
    lockMaxMint: 0,
    lockDescription: 1,
    lockSleep: 0,
    lockCallback: 0
});

console.log(result.version); // 3 (lock update format)

Mint Tokens

const result = await sdk.mint({
    tick: 'NEWTOKEN',
    amount: '1000',
    destination: 'bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh',
    memo: 'Initial mint'
});

Destroy Tokens

const result = await sdk.destroy({
    tick: 'NEWTOKEN',
    amount: '500',
    memo: 'Burning supply'
});

Create a Dispenser

const result = await sdk.dispenser({
    giveTick: 'MYTOKEN',
    giveAmount: '100',
    giveEscrow: '1000',
    getTick: 'PAYTOKEN',
    getAmount: '50',
    fiatCode: 'USD',
    fiatAmount: '10.00',
    memo: 'Token vending machine'
});

Cancel a Dispenser

// Provide only the dispenser action index → SDK selects v1 (cancel)
const result = await sdk.dispenser({
    dispenserActionIndex: 12345,
    memo: 'Closing dispenser'
});

console.log(result.version); // 1

Place a DEX Order

const result = await sdk.order({
    giveTick: 'TOKEN_A',
    giveAmount: '100',
    getTick: 'TOKEN_B',
    getAmount: '200',
    expiration: 1735689600,
    memo: 'Sell A for B'
});

Cancel a DEX Order

const result = await sdk.order({
    orderActionIndex: 99999,
    memo: 'Cancelling order'
});

console.log(result.version); // 1 (cancel format)

Batch Multiple Actions

// Fluent builder API
const result = await sdk.batch()
    .send({ tick: 'TOKEN_A', amount: '50', destination: 'bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh' })
    .send({ tick: 'TOKEN_B', amount: '25', destination: 'bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh' })
    .mint({ tick: 'TOKEN_C', amount: '1000', destination: 'bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh' })
    .build();

console.log(result.action);       // 'BATCH'
console.log(result.actionString);  // 'BATCH|0|SEND|0|TOKEN_A|50|bc1q...;SEND|0|TOKEN_B|25|bc1q...;MINT|0|TOKEN_C|1000|bc1q...'

// With encoder to get a PSBT
const tx = await sdk.batch()
    .send({ tick: 'A', amount: '10', destination: 'bc1q...' })
    .destroy({ tick: 'B', amount: '5' })
    .build({ pubkey: 'your-pubkey' });

console.log(tx.psbt); // PSBT hex

Cross-Chain Swap

const result = await sdk.swap({
    giveTick: 'BTC_TOKEN',
    giveAmount: '100',
    getTick: 'LTC_TOKEN',
    getAmount: '500',
    expiration: 1735689600,
    memo: 'Cross-chain swap'
});

Sweep All Balances

const result = await sdk.sweep({
    destination: 'bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh',
    balances: 1,
    ownerships: 1,
    escrows: 0,
    memo: 'Moving everything to new address'
});

Broadcast a Message

const result = await sdk.broadcast({
    message: 'Hello XChain!',
    value: '42'
});

Send a Plaintext Message

// Low-level: generate the action string only
const result = await sdk.message({
    destination: 'bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh',
    coin: 'BTC',
    plaintextMessage: 'Hello, this is a direct message'
});

console.log(result.version); // 3 (plaintext format)

// High-level: create, sign, and broadcast in one call
const sent = await sdk.sendMessage({
    wif: senderWIF,
    destination: 'bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh',
    coin: 'BTC',
    message: 'This is a public message visible to everyone!',
    method: null,  // null = plaintext
    encoder: { pubkey: senderPubkeyHex }
});
console.log('Sent:', sent.txid);

Send an Encrypted Message (ECIES)

ECIES is the default encryption method. It encrypts directly to the recipient’s public key — no prior key exchange needed, and messages can be decrypted on any device that holds the recipient’s private key.

const result = await sdk.sendMessage({
    wif: senderWIF,
    destination: 'bc1qrecipient...',
    coin: 'BTC',
    message: 'Hello, this is a private ECIES message!',
    // method: 1 is the default, no need to specify
    encoder: { pubkey: senderPubkeyHex }
});

console.log('Sent encrypted message:', result.txid);
console.log('Action string:', result.actionString);

Read and Decrypt Messages

Fetch messages for an address. Pass your WIF to auto-decrypt ECIES messages addressed to you.

const messages = await sdk.getMessagesForAddress('bc1qmyaddress...', {
    wif: myWIF,           // Needed to decrypt ECIES messages
    type: 'received',     // 'sent', 'received', or 'all'
    limit: 10
});

for (const msg of messages) {
    console.log(`From: ${msg.from}`);
    console.log(`  Text: ${msg.text}`);
    console.log(`  Encrypted: ${msg.encrypted}`);
    console.log(`  Method: ${msg.method}`);
    console.log(`  Block: ${msg.block}`);
}

ECIES Encrypt/Decrypt Without Sending

Use the low-level ECIES methods directly for encryption without creating an on-chain transaction.

// Look up recipient's public key
const recipientPubkey = await sdk.getPublicKey('bc1qrecipient...');

// Encrypt
const encrypted = sdk.messaging.eciesEncrypt('Secret message content', recipientPubkey);
console.log('Ciphertext:', encrypted.ciphertext);

// Decrypt (recipient side)
const decrypted = sdk.messaging.eciesDecrypt(encrypted.ciphertext, recipientWIF);
console.log('Decrypted:', decrypted.plaintext);

ECDH Session Messaging

ECDH requires both parties to exchange public keys via format 0/1 messages before sending encrypted messages.

// Step 1: Sender generates their session key
const senderSession = sdk.messaging.generateSessionKey(senderWIF);

// Step 2: Send format 0 key exchange request (on-chain)
const keyExchange = await sdk.message({
    destination: 'bc1qrecipient...',
    coin: 'BTC',
    encryptionMethod: 2,
    encryptionKey: senderSession.publicKey
}, { pubkey: senderPubkeyHex });
// Sign and broadcast keyExchange.psbt...

// Step 3: Recipient responds with format 1 containing their pubkey
const recipientSession = sdk.messaging.generateSessionKey(recipientWIF);
const keyResponse = await sdk.message({
    destination: senderAddress,
    coin: 'BTC',
    encryptionMethod: 2,
    encryptionKey: recipientSession.publicKey
}, { pubkey: recipientPubkeyHex });
// Sign and broadcast keyResponse.psbt...

// Step 4: Both sides derive the same shared secret
const senderSecret = sdk.messaging.deriveSharedSecret(senderWIF, recipientSession.publicKey);
const recipientSecret = sdk.messaging.deriveSharedSecret(recipientWIF, senderSession.publicKey);
console.log('Secrets match:', senderSecret.sharedSecret === recipientSecret.sharedSecret);

// Step 5: Encrypt/decrypt with the shared secret
const encrypted = sdk.messaging.sessionEncrypt('ECDH session message', senderSecret.sharedSecret);
const decrypted = sdk.messaging.sessionDecrypt(encrypted.ciphertext, recipientSecret.sharedSecret);
console.log('Decrypted:', decrypted.plaintext);

// Or use the high-level send with a pre-derived shared secret
const result = await sdk.sendMessage({
    wif: senderWIF,
    destination: 'bc1qrecipient...',
    coin: 'BTC',
    message: 'Encrypted with ECDH session key',
    method: 2,
    sharedSecret: senderSecret.sharedSecret,
    encoder: { pubkey: senderPubkeyHex }
});

AES Pre-Shared Key Messaging

AES uses a pre-shared key that both parties know, exchanged out-of-band.

const sharedKey = 'my-secret-passphrase-shared-between-parties';

// Send an AES-encrypted message
const result = await sdk.sendMessage({
    wif: senderWIF,
    destination: 'bc1qrecipient...',
    coin: 'BTC',
    message: 'This message is encrypted with a shared passphrase',
    method: 3,
    sharedKey: sharedKey,
    encoder: { pubkey: senderPubkeyHex }
});

console.log('Sent AES message:', result.txid);

// Decrypt manually (getMessagesForAddress can't auto-decrypt AES)
const encrypted = sdk.messaging.aesEncrypt('Test AES message', sharedKey);
const decrypted = sdk.messaging.aesDecrypt(encrypted.ciphertext, sharedKey);
console.log('AES roundtrip:', decrypted.plaintext);

Public Key Lookup

Look up the public key for any address that has sent at least one XChain transaction.

const pubkey = await sdk.getPublicKey('bc1qsomeaddress...');
if (pubkey) {
    console.log('Public key:', pubkey);  // '02a1b2c3...'
} else {
    console.log('No public key found');
    console.log('(Address may not have sent any XChain transactions yet)');
}

Sleep / Pause an Address

// Pause all actions from this address until block 800000
const result = await sdk.sleep({ resumeBlock: 800000 });

// Pause a specific token until block 800000
const result2 = await sdk.sleep({
    resumeBlock: 800000,
    tick: 'MYTOKEN'
});

console.log(result.version);  // 0 (address sleep)
console.log(result2.version); // 1 (tick sleep)

Create and Edit a List

// Create a TICK list
const create = await sdk.list({ type: 1, item: 'TOKEN_A,TOKEN_B,TOKEN_C' });

// Edit an existing list — add an item
const edit = await sdk.list({
    edit: 1,               // 1 = ADD
    listActionIndex: 12345,
    item: 'TOKEN_D'
});

console.log(create.version); // 0 (create)
console.log(edit.version);   // 1 (edit)

Pay Dividends

const result = await sdk.dividend({
    tick: 'MYTOKEN',
    dividendTick: 'PAYTOKEN',
    amount: '1',
    memo: 'Q1 dividend payout'
});

Query Market Data

// Get all markets
const markets = await sdk.getMarkets();

// Get markets for a specific token
const tokenMarkets = await sdk.getMarkets('MYTOKEN');

// Get a specific trading pair
const pair = await sdk.getMarket('TOKEN_A', 'TOKEN_B');

// Get the order book
const orderbook = await sdk.getOrderbook('TOKEN_A', 'TOKEN_B');

// Get trade history
const history = await sdk.getMarketHistory('TOKEN_A', 'TOKEN_B', null, {
    page: 1,
    limit: 50,
    sortorder: 'DESC'
});

Query Transaction History

// Get history for an address
const history = await sdk.getHistory('bc1q...', 'address', { limit: 20 });

// Get a specific transaction
const tx = await sdk.getTransaction('abc123...', 'tx_hash');

// Get a specific action by index
const action = await sdk.getAction(12345);

// Get sends for an address
const sends = await sdk.getSends('bc1q...', 'address', { sortorder: 'DESC' });

// Get all mints for a token
const mints = await sdk.getMints('MYTOKEN', 'token', { limit: 100 });

Validate Before Creating

// Dry-run validation — no action string generated, no errors thrown
const result = sdk.validateAction('send', {
    tick: 'MYTOKEN',
    amount: '100',
    destination: 'bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh'
});

if (result.valid) {
    console.log('Input is valid, safe to create');
} else {
    console.log('Validation errors:', result.errors);
    // [{ code: 'MISSING_REQUIRED_FIELD', message: '...', details: {...} }]
}

Force Encoding Type

// Force P2SH encoding (even if data fits in OP_RETURN)
const result = await sdk.send(
    { tick: 'TOKEN', amount: '100', destination: 'bc1q...' },
    { pubkey: 'your-pubkey', encoding: 'P2SH' }
);
console.log(result.encoding); // 'P2SH'

// OP_RETURN — will throw if action string > 76 bytes (user-data limit; 80 bytes total per output)
try {
    await sdk.issue(
        { tick: 'TOKEN', maxSupply: '21000000', maxMint: '1000', decimals: 8, description: 'A long description...' },
        { pubkey: 'your-pubkey', encoding: 'OP_RETURN' }
    );
} catch (e) {
    console.log(e.code);             // 'ENCODING_DATA_TOO_LARGE'
    console.log(e.details.suggestion); // 'P2SH'
}

Error Handling

const { XChainSDK, SDKValidationError, SDKEncoderError, SDKExplorerError } = require('xchain-sdk');

try {
    await sdk.send({ tick: 'BAD|TOKEN', amount: '100', destination: 'bc1q...' });
} catch (e) {
    if (e instanceof SDKValidationError) {
        console.log('Validation error:', e.code);    // 'FORBIDDEN_CHARACTER'
        console.log('Details:', e.details);           // { action: 'SEND', errors: [...] }
    } else if (e instanceof SDKEncoderError) {
        console.log('Encoder error:', e.code);        // 'ENCODER_RPC_ERROR'
    } else if (e instanceof SDKExplorerError) {
        console.log('Explorer error:', e.code);       // 'EXPLORER_HTTP_404'
    }
}

Hub Discovery

const sdk = new XChainSDK({
    network: 'bitcoin-regtest',
    hubUrl: 'hub.xchain.io',
    hubPort: 8001
});

// Fetch config from hub — resolves explorer + encoder endpoints automatically
await sdk.init();

// Now explorer and encoder are configured
const balances = await sdk.getBalances('bc1q...');
const tx = await sdk.send(
    { tick: 'TOKEN', amount: '100', destination: 'bc1q...' },
    { pubkey: 'your-pubkey' }
);

Request Logging

const sdk = new XChainSDK({
    network: 'bitcoin-mainnet',
    explorerUrl: 'explorer.xchain.io',
    encoderUrl: 'encoder.xchain.io',
    hooks: {
        onRequest: (info) => {
            console.log(`[${info.service}] → ${info.method} ${info.url || ''}`);
        },
        onResponse: (info) => {
            console.log(`[${info.service}] ← ${info.status || 'OK'}`);
        },
        onError: (info) => {
            console.error(`[${info.service}] ERROR: ${info.error}`);
        },
        onRetry: (info) => {
            console.warn(`[${info.service}] Retry #${info.attempt} in ${info.delay}ms: ${info.error}`);
        }
    }
});

Real-Time: Listen for New Blocks

await sdk.connectWs();

const unsub = sdk.onBlock((event) => {
    console.log('Block', event.data.block_index, '—', event.data.action_count, 'actions');
});

// Later: stop listening
unsub();

Real-Time: Watch an Address

await sdk.connectWs();

sdk.onAddress('1abc...', (event) => {
    if (event.type === 'ADDRESS_UPDATE') {
        console.log('Balances:', event.data.balances);
    } else {
        console.log(event.type, ':', event.data.action_index);
    }
}, { snapshot: true });

Real-Time: COINPay Auto-Responder

await sdk.connectWs();

sdk.onCoinpayRequired('1BotAddr...', async (event) => {
    const { payee_address, coin_amount, expiration } = event.data;
    console.log('COINPay needed:', coin_amount, 'BTC to', payee_address);
    console.log('Deadline:', new Date(expiration * 1000));

    // Construct and broadcast a COINPAY transaction
    const result = await sdk.coinpay({
        order_match_action_index: event.data.order_match_action_index
    }, { pubkey: 'your-pubkey' });
    console.log('COINPay broadcast:', result.psbt);
});

Real-Time: Multi-Address Portfolio Tracker

await sdk.connectWs();

const addresses = ['1wallet-a...', '1wallet-b...', '1wallet-c...'];

// Subscribe to all addresses in one batch
sdk.ws.subscribe(['address'], {
    addresses: addresses,
    types: ['SEND', 'AIRDROP', 'DIVIDEND'],
    snapshot: true
});

sdk.ws.on('ADDRESS_UPDATE', (msg) => {
    console.log(msg.data.address, '→', msg.data.balances);
});

sdk.ws.on('SNAPSHOT', (msg) => {
    console.log('Initial state for', msg.data.address, ':', msg.data.balances);
});

Deploy a Smart Contract

// Read contract source code
const contractSource = `
module.exports = {
    greet: function(xchain) {
        let name = xchain.getInputParam(0) || 'world';
        return 'Hello, ' + name + '!';
    }
};
`;

// Validate the contract source before deploying
let check = sdk.contracts.validate(contractSource);
if (!check.valid) {
    console.error('Contract validation failed:', check.error);
} else {
    // Deploy (auto hex-encodes the source code)
    let result = await sdk.deploy({
        code: contractSource,
        gasLimit: 200000
    }, { pubkey: 'yourPubkey', encoding: 'P2WSH' });

    console.log(result.actionString);  // DEPLOY|0|<hex>|200000
    console.log(result.psbt);          // PSBT for signing
}

Execute a Contract Method

// Call a method on a deployed contract
let result = await sdk.execute({
    contractActionIndex: 12345,
    method: 'greet',
    params: ['Alice']
}, { pubkey: 'yourPubkey' });

console.log(result.actionString);  // EXECUTE|0|12345|greet|Alice

// After the transaction confirms, check execution results via explorer
let exec = await sdk.getExecution(actionIndex);
if (exec.success) {
    console.log('Return value:', exec.returnValue);
} else {
    console.log('Execution failed:', exec.error);
}

Deposit Tokens into a Contract

// Fund a contract with tokens
let result = await sdk.deposit({
    contractActionIndex: 12345,
    tick: 'MYTOKEN',
    quantity: '1000'
}, { pubkey: 'yourPubkey' });

console.log(result.actionString);  // DEPOSIT|0|12345|MYTOKEN|1000

Withdraw Tokens from a Contract

// Withdraw tokens (must be contract owner)
let result = await sdk.withdraw({
    contractActionIndex: 12345,
    tick: 'MYTOKEN',
    quantity: '500'
}, { pubkey: 'yourPubkey' });

console.log(result.actionString);  // WITHDRAW|0|12345|MYTOKEN|500

Using the Contract Client

// Create a bound client for repeated interactions with a contract
const amm = sdk.contract(12345);

// Execute a method
await amm.call('swap', ['TOKENA', '100'], { pubkey: 'yourPubkey' });

// Deposit tokens
await amm.deposit('TOKENA', '500', { pubkey: 'yourPubkey' });

// Withdraw tokens
await amm.withdraw('TOKENA', '250', { pubkey: 'yourPubkey' });

// Query contract data from the explorer
let info = await amm.getInfo();           // contract metadata
let state = await amm.getState();          // all state key-value pairs
let state_key = await amm.getState('reserveA');  // specific state key
let balance = await amm.getBalance('TOKENA');     // contract token balance
let history = await amm.getExecutions();   // execution history

Contract Authoring Utilities

// Hex encode/decode
let hex = sdk.contracts.encode('module.exports = {}');
let source = sdk.contracts.decode(hex);

// Check code size
let sizeCheck = sdk.contracts.checkCodeSize(contractSource);
console.log(sizeCheck.bytes + ' / ' + sizeCheck.limit + ' bytes');
console.log('Within limit:', sizeCheck.withinLimit);

// Syntax validation (requires acorn)
let validation = sdk.contracts.validate(contractSource);
if (!validation.valid) {
    console.log('Error:', validation.error);
}
if (validation.warnings) {
    for (let w of validation.warnings) console.log('Warning:', w);
}

// Float usage detection
let floatWarnings = sdk.contracts.checkFloatUsage(contractSource);
for (let w of floatWarnings) console.log(w);

// Gas limit suggestion (heuristic)
let gas = sdk.contracts.suggestGasLimit(contractSource);
console.log('Suggested gas:', gas.suggested, '(' + gas.rationale + ')');

Generate a Key Pair

const sdk = new XChainSDK({ network: 'bitcoin-mainnet' });

const kp = sdk.generateKeyPair();
console.log('WIF:', kp.wif);
console.log('Public key:', kp.publicKeyHex);
console.log('Compressed:', kp.compressed); // true

// Uncompressed key pair
const kpUncompressed = sdk.generateKeyPair({ compressed: false });
console.log('Uncompressed public key length:', kpUncompressed.publicKey.length); // 65

Import a WIF Key

const sdk = new XChainSDK({ network: 'bitcoin-mainnet' });

const kp = sdk.importWIF('L1aW4aubDFB7yfras...'); // your WIF key
console.log('Public key:', kp.publicKeyHex);
console.log('Address:', sdk.deriveAddress(kp.publicKey));

Derive Addresses

const sdk = new XChainSDK({ network: 'bitcoin-mainnet' });
const kp = sdk.generateKeyPair();

// Legacy (P2PKH)
const legacy = sdk.deriveAddress(kp.publicKey);
console.log('P2PKH:', legacy); // 1...

// Native SegWit (bech32)
const segwit = sdk.deriveAddress(kp.publicKey, { type: 'p2wpkh' });
console.log('P2WPKH:', segwit); // bc1q...

// Wrapped SegWit (P2SH-P2WPKH)
const wrapped = sdk.deriveAddress(kp.publicKey, { type: 'p2sh-p2wpkh' });
console.log('P2SH-P2WPKH:', wrapped); // 3...

// Works with hex string too
const sameAddress = sdk.deriveAddress(kp.publicKeyHex);
console.log(legacy === sameAddress); // true

Validate an Address

const sdk = new XChainSDK({ network: 'bitcoin-mainnet' });

// Check against configured network
let result = sdk.validateAddress('bc1qexample...');
console.log(result.valid);   // true
console.log(result.type);    // 'p2wpkh'
console.log(result.network); // 'bitcoin-mainnet'

// Check against a specific network
result = sdk.validateAddress('ltc1q...', 'litecoin-mainnet');
console.log(result.valid);   // true

// Invalid address — returns { valid: false }, never throws
result = sdk.validateAddress('not-an-address');
console.log(result.valid);   // false
console.log(result.error);   // 'Address does not match any supported network.'

// No network configured — checks all 9 networks
const noNetSdk = new XChainSDK();
result = noNetSdk.validateAddress('bc1qexample...');
console.log(result.network); // 'bitcoin-mainnet' (auto-detected)

Challenge-Response Wallet Verification

Full server + client flow for proving a user owns a wallet address.

const XChainSDK = require('xchain-sdk');

// --- Server side ---

const serverSdk = new XChainSDK({ network: 'bitcoin-mainnet' });

// Step 1: Generate a challenge for the user to sign
const challenge = serverSdk.generateChallenge('bc1quseraddress...', {
    appId: 'MyMusicApp'
});
// Store challenge.nonce in your session/database
// Send challenge.challenge to the client

console.log(challenge.challenge);
// "XChain wallet verification\nApp: MyMusicApp\nAddress: bc1quseraddress...\nNonce: a1b2c3...\nTimestamp: 2026-04-07T..."

console.log(challenge.expiresAt);
// "2026-04-07T17:50:00.000Z" (5 minutes from now)


// --- Client side (wallet app) ---

const clientSdk = new XChainSDK({ network: 'bitcoin-mainnet' });

// Step 2: User signs the challenge with their wallet
const signed = clientSdk.signMessage(challenge.challenge, userWIF);
// Send signed.signature back to the server


// --- Server side ---

// Step 3: Verify the signature
const result = serverSdk.verifyOwnership(
    'bc1quseraddress...',
    challenge.challenge,
    signed.signature
);

if (result.valid) {
    console.log('Wallet ownership verified for', result.address);
    // Grant access, issue session token, etc.
} else {
    console.log('Verification failed:', result.error);
}

Custom Message Signing

Sites can generate their own messages instead of using the SDK’s default format. This lets the server verify using any tool — the SDK, bitcoinjs-message, or the coin node’s verifymessage RPC.

const sdk = new XChainSDK({ network: 'bitcoin-mainnet' });

// Site generates its own message (no SDK needed for this part)
const myMessage = `Welcome to MyCoolSite!\nPlease sign to verify wallet ownership.\nNonce: ${crypto.randomUUID()}`;

// Pass it through generateChallenge for consistent metadata tracking
const challenge = sdk.generateChallenge('bc1quseraddr...', { message: myMessage });
// challenge.challenge === myMessage (unchanged)
// challenge.nonce, challenge.timestamp still returned for bookkeeping

// Client signs it
const signed = sdk.signMessage(myMessage, wif);

// Server verifies — using SDK
const result = sdk.verifyOwnership('bc1quseraddr...', myMessage, signed.signature);

// OR verify without the SDK at all:
const bitcoinMessage = require('bitcoinjs-message');
const valid = bitcoinMessage.verify(myMessage, 'bc1quseraddr...', signed.signature);

Sign and Broadcast a Transaction

Complete workflow: create an action, encode it as a PSBT, sign it, and broadcast.

const sdk = new XChainSDK({
    network: 'bitcoin-mainnet',
    encoderUrl: 'encoder.example.com',
    encoderPort: 3000
});

const kp = sdk.importWIF(process.env.WALLET_WIF);
const address = sdk.deriveAddress(kp.publicKey, { type: 'p2wpkh' });

// 1. Create and encode the action
const action = await sdk.send(
    { tick: 'MYTOKEN', amount: '50', destination: 'bc1qrecipient...' },
    { pubkey: kp.publicKeyHex }
);

console.log('Action:', action.actionString);
console.log('Unsigned PSBT:', action.psbt);

// 2. Sign the PSBT
const signed = sdk.signPsbt(action.psbt, kp.wif);
console.log('Transaction ID:', signed.txid);

// 3. Broadcast
const broadcast = await sdk.broadcastTx(signed.txHex);
console.log('Broadcast confirmed:', broadcast.txid);

Token-Gated Content Access

Check if a user holds a token before granting access. Combines wallet verification with balance checks.

const express = require('express');
const { XChainSDK } = require('xchain-sdk');
const { bignumber, largerEq } = require('mathjs');

const app = express();
app.use(express.json());

const sdk = new XChainSDK({
    network: 'bitcoin-mainnet',
    explorerUrl: 'explorer.example.com'
});

// Step 1: Issue a challenge
app.post('/auth/challenge', (req, res) => {
    const { address } = req.body;
    const challenge = sdk.generateChallenge(address, { appId: 'StreamApp' });
    // Store challenge.nonce → address mapping (Redis, DB, etc.)
    res.json({ message: challenge.challenge, nonce: challenge.nonce });
});

// Step 2: Verify signature + check token balance
app.post('/auth/verify', async (req, res) => {
    const { address, signature, nonce } = req.body;

    // Look up the stored challenge by nonce (your storage layer)
    const storedChallenge = await getStoredChallenge(nonce);
    if (!storedChallenge) return res.status(400).json({ error: 'Invalid or expired challenge' });

    // Verify wallet ownership
    const result = sdk.verifyOwnership(address, storedChallenge, signature);
    if (!result.valid) return res.status(401).json({ error: result.error });

    // Check token balance
    const balances = await sdk.getBalances(address);
    const entry = balances.find(b => b.tick === 'ALBUMTOKEN');
    if (!entry || !largerEq(bignumber(entry.amount), bignumber('1'))) {
        return res.status(403).json({ error: 'Must hold at least 1 ALBUMTOKEN' });
    }

    // Issue session token (your session layer)
    const sessionToken = createSession(address);
    res.json({ sessionToken });
});

// Step 3: Protected route
app.get('/stream/:trackId', requireSession, (req, res) => {
    res.json({ streamUrl: getSignedUrl(req.params.trackId) });
});

Fetch UTXOs

const sdk = new XChainSDK({
    network: 'bitcoin-mainnet',
    encoderUrl: 'encoder.example.com'
});

const utxos = await sdk.getUTXOs('bc1qmyaddress...');
console.log(`Found ${utxos.length} UTXOs`);

for (const utxo of utxos) {
    console.log(`  ${utxo.txid}:${utxo.vout}${utxo.value} sats`);
}

// Use UTXOs when creating a transaction
const action = await sdk.send(
    { tick: 'MYTOKEN', amount: '10', destination: 'bc1qrecipient...' },
    { pubkey: publicKeyHex, utxos: utxos }
);

Submit Action (Full Lifecycle)

Submit an action through the complete pipeline — create, encode, sign, broadcast, and wait for the indexer — in one call:

const sdk = new XChainSDK({
    network: 'bitcoin-mainnet',
    explorerUrl: 'explorer.example.com',
    encoderUrl: 'encoder.example.com'
});

const result = await sdk.submitAction(
    { action: 'SEND', params: { tick: 'MYTOKEN', amount: '100', destination: 'bc1qrecipient...' } },
    { pubkey: '02abc123...' },
    {
        wif: 'your-wif-key',
        onProgress: (step, data) => console.log(`Step: ${step}`)
    }
);

console.log(result.txid);         // transaction hash
console.log(result.encoding);     // 'OP_RETURN', 'P2SH', etc.
console.log(result.indexed);      // action data from the indexer
console.log(result.spentInputs);  // UTXOs consumed by this transaction

Wallet Session

Create a session bound to a single key and send multiple transactions:

const session = sdk.session('your-wif-key');

console.log(`Address: ${session.address}`);
console.log(`Public key: ${session.pubkey}`);

// Send multiple transactions back-to-back (UTXO chaining prevents double-spend)
await session.send({ tick: 'TOKEN', amount: '10', destination: 'bc1qaddr1...' });
await session.send({ tick: 'TOKEN', amount: '20', destination: 'bc1qaddr2...' });
await session.send({ tick: 'TOKEN', amount: '30', destination: 'bc1qaddr3...' });

// Query balances scoped to the session address
const balances = await session.getBalances();
console.log(balances);

Fee Estimation

Estimate fees before committing to a transaction:

const estimate = await sdk.estimateFees(
    { action: 'SEND', params: { tick: 'TOKEN', amount: '100', destination: 'bc1q...' } },
    { pubkey: '02abc123...' }
);

console.log(`Fee: ${estimate.fee} satoshis`);
console.log(`Encoding: ${estimate.encoding}`);
console.log(`Input total: ${estimate.inputTotal}`);
console.log(`Output total: ${estimate.outputTotal}`);

// The returned PSBT can be signed directly to skip re-encoding
if (estimate.fee < 5000) {
    const signed = sdk.signPsbt(estimate.psbt, wif);
    await sdk.broadcastTx(signed.txHex);
}

Issue and Distribute Workflow

Issue a token and send it to multiple recipients in one call:

const result = await sdk.issueAndDistribute(
    'your-wif-key',
    { tick: 'NEWTOKEN', maxSupply: '1000000', decimals: 8 },
    [
        { destination: 'bc1qaddr1...', amount: '500000' },
        { destination: 'bc1qaddr2...', amount: '300000' },
        { destination: 'bc1qaddr3...', amount: '200000' }
    ]
);

console.log(`Token issued: ${result.issue.txid}`);
for (const send of result.sends) {
    console.log(`Sent: ${send.txid}`);
}

Stake and Delegate

Stake XCHAIN tokens and delegate a signing key (capability staking; BTC chain only):

const result = await sdk.stakeAndDelegate(
    'your-wif-key',
    {
        version: 1,                   // 1 = new stake, 2 = top up an existing pubkey
        amount: '1000',               // XCHAIN staked — capabilities are auto-qualified from the aggregate amount
        signingPubkey: 'aabbccdd...'  // 64 hex characters (Ed25519 public key)
    },
    {
        newSigningPubkey: 'eeff0011...'  // optional — omit to skip delegation
    }
);

console.log(`Staked: ${result.stake.txid}`);
console.log(`Delegated: ${result.delegate.txid}`);

// Later: collect rewards
const session = sdk.session('your-wif-key');
await session.collect({});

// Later: unstake
await session.unstake({ signingPubkey: 'aabbccdd...' });  // releases the pubkey's full aggregate stake

Cross-Chain Parallel Actions

Execute actions on multiple chains simultaneously:

const { XChainSDK, CrossChainHelper } = require('xchain-sdk');

const btcSdk = new XChainSDK({ network: 'bitcoin-mainnet', explorerUrl: '...', encoderUrl: '...' });
const ltcSdk = new XChainSDK({ network: 'litecoin-mainnet', explorerUrl: '...', encoderUrl: '...' });

const bridge = new CrossChainHelper({ BTC: btcSdk, LTC: ltcSdk });

// Send tokens on both chains at the same time
const results = await bridge.parallel([
    {
        chain: 'BTC',
        wif: btcWIF,
        actionData: { action: 'SEND', params: { tick: 'BTCTOKEN', amount: '50', destination: btcAddr } }
    },
    {
        chain: 'LTC',
        wif: ltcWIF,
        actionData: { action: 'SEND', params: { tick: 'LTCTOKEN', amount: '100', destination: ltcAddr } }
    }
]);

console.log(`BTC tx: ${results[0].txid}`);
console.log(`LTC tx: ${results[1].txid}`);

// Get balances across all chains
const allBalances = await bridge.getAllBalances('bc1qmyaddress...');

Interactive REPL

Start an interactive session for exploration and prototyping:

npm run repl
  XChain SDK REPL
  Network: bitcoin-regtest

  Available in scope:
    sdk            - XChainSDK instance
    session(wif)   - Create a WalletSession
    keygen()       - Generate a new keypair

  Commands:
    .actions       - List all action types
    .status        - Show SDK configuration
    .fields ACTION - Show fields for an action

xchain> const keys = keygen()
xchain> keys.wif
'cNjFk...'
xchain> const s = session(keys.wif)
xchain> s.address
'mwCwTc...'
xchain> .fields SEND
  v0: VERSION|TICK|AMOUNT|DESTINATION|MEMO
  v1: VERSION|TICK|AMOUNT|DESTINATION|AMOUNT|DESTINATION|MEMO
  v2: VERSION|TICK|AMOUNT|DESTINATION|TICK|AMOUNT|DESTINATION|MEMO
  v3: VERSION|TICK|AMOUNT|DESTINATION|MEMO|TICK|AMOUNT|DESTINATION|MEMO

Copyright © 2025–2026 Dankest, LLC

Based on XChain Platform by Dankest, LLC – https://dankest.llc

Licensed under the GNU Affero General Public License v3.0 (AGPL-3.0-or-later) with a commercial license available for proprietary use.

You may use, modify, and distribute this material under the terms of the License. See LICENSE and NOTICE for full terms. See the licensing overview.

Edit this page on GitHub ↗