Sphere SDK - Node.js Quick Start¶
On 0.15.0. The base SDK pin moved to
@unicitylabs/state-transition-sdk@3.0.1— a wire break that no client can straddle, so your wallet-api deployment has to bump with you (see the flag-day section). What that changes in this guide issphere.paymentsV2— the alias is removed, andsphere.paymentsthrowsNOT_INITIALIZEDwhere the alias returnednullwhile no vertical was running — plus the payment-journal key prefix below (pv2:→pv2g2:, swept for you). Everything else is untouched, and everything below already usessphere.payments.
Get up and running with Sphere SDK in Node.js in under 5 minutes.
Installation¶
| Package | Required | Description |
|---|---|---|
@unicitylabs/sphere-sdk |
Yes | The SDK |
ws |
Node < 22 | WebSocket for the Nostr transport (messaging); optional on Node ≥ 22 (global WebSocket) |
Node.js version: 18.0.0 or higher
Note: No API key is bundled with the SDK. The
testnetgateway (testnet2, see below) requires one — inject it viaoracle: { apiKey: '...' }. The testnet2 key is not a secret (see.env.example):sk_ddc3cfcc001e4a28ac3fad7407f99590. A mainnet key, by contrast, IS a secret — keep it in your deploy environment only.Networks: since the v1→v2 cutover,
network: 'testnet'points at the testnet2 gateway network (https://gateway.testnet2.unicity.network; the network id comes from the trust base).'testnet2'is an alias of the same configuration.mainnet/devstill point at v1-era aggregators and cannot serve the engine — wallet operations there fail withAGGREGATOR_ERROR. The "2" in testnet2 names the gateway network, not the base-SDK major: testnet2 is still testnet2 on state-transition-sdk 3.x.
CLI (Quick Testing)¶
The CLI has moved to a dedicated package — @unicity-sphere/cli. See QUICKSTART-CLI.md for the full command reference.
The examples below use sphere <command> — replace any old npm run cli -- <command> references accordingly.
# Initialize wallet
sphere init --network testnet
# Initialize wallet WITH nametag (publishes a Nostr identity binding)
sphere init --network testnet --nametag alice
# Check status
sphere status
# Check balance (fetches pending transfers first)
sphere balance
# Send tokens (the sender certifies the transfer on-chain, then delivers
# the finished token via wallet-api mailbox)
sphere send @alice 1 UCT
# Show receive address
sphere receive
# Top up with test tokens (self-mint via the token engine — no faucet)
sphere topup 10 UCT
# Register nametag (publishes a Nostr identity binding)
sphere nametag myname
# Verify tokens against the gateway (detect spent tokens)
sphere verify-balance
Note: Nametag registration publishes a Nostr identity binding (name ↔ chain pubkey, first-seen-wins); runtime name resolution uses only the Nostr binding.
Transfer Mode¶
Transfers are sender-driven: the sender certifies the transfer on-chain (collects the inclusion proof) and deposits the finished token into the recipient's wallet-api mailbox — the receiver verifies and stores it as confirmed with no finalization phase. There is a single transfer flow; the old instant/conservative modes no longer exist.
Address Management¶
sphere addresses # List all tracked addresses
sphere switch 1 # Switch to address at HD index 1
sphere hide 2 # Hide address from active list
sphere unhide 2 # Unhide address
Direct Messages¶
sphere dm @alice "Hello, how are you?" # Send a DM
sphere dm-inbox # List conversations + unread counts
sphere dm-history @alice # Show conversation history
sphere dm-history @alice --limit 20 # Limit messages shown
Group Chat (NIP-29)¶
sphere group-list # List available groups
sphere group-create "Trading Chat" --description "Discuss trades" # Create group
sphere group-create "Private" --private # Create private group
sphere group-join <groupId> # Join a group
sphere group-join <groupId> --invite <code> # Join with invite code
sphere group-send <groupId> "Hello everyone!" # Send message
sphere group-send <groupId> "Reply" --reply <eventId> # Reply to message
sphere group-messages <groupId> --limit 20 # Show messages
sphere group-members <groupId> # List members
sphere group-info <groupId> # Show group details
sphere group-leave <groupId> # Leave group
sphere group-my # List your groups
Market (Intent Bulletin Board)¶
sphere market-post "Buying 100 UCT" --type buy # Post buy intent
sphere market-post "Selling ETH" --type sell --price 50 --currency USD # Post sell intent
sphere market-post "Web dev services" --type service # Post service intent
sphere market-search "UCT tokens" --type sell --limit 5 # Search intents
sphere market-search "tokens" --min-score 0.7 # Search with score threshold
sphere market-my # List own intents
sphere market-close <id> # Close an intent
sphere market-feed # Watch live feed (WebSocket)
sphere market-feed --rest # Fetch recent (REST fallback)
Wallet Profiles¶
Manage multiple wallets for testing:
sphere wallet create alice # Create profile "alice"
sphere init --nametag alice # Initialize wallet in profile
sphere wallet create bob # Create another profile
sphere init --nametag bob # Initialize second wallet
sphere wallet list # List all profiles
sphere wallet use alice # Switch to alice
sphere send @bob 0.1 BTC # Send from alice to bob
sphere wallet use bob # Switch to bob
sphere balance # Check bob's balance
CLI stores data in ./.sphere-cli/ directory.
Storage¶
Node.js implementation uses file-based storage for local state; token custody is the wallet-api backend:
| Data | Location | Format |
|---|---|---|
Wallet (keys, nametag) + payment journals (pv2g2:*) |
dataDir/wallet.json (or custom file name) |
JSON (plaintext or password-encrypted mnemonic) |
| Token inventory + transfer intents + mailbox + history | Wallet API server | Server custody |
Minimal Example¶
import { Sphere } from '@unicitylabs/sphere-sdk';
import { createNodeProviders } from '@unicitylabs/sphere-sdk/impl/nodejs';
import { createWalletApiProviders } from '@unicitylabs/sphere-sdk/impl/shared/wallet-api';
async function main() {
// 1. Create base providers (handles storage, transport, oracle)
const base = createNodeProviders({
network: 'testnet',
dataDir: './wallet-data',
oracle: {
apiKey: 'sk_ddc3cfcc001e4a28ac3fad7407f99590', // Public testnet2 key
},
});
// 2. Attach the wallet-api transport config (REQUIRED — money rides the wallet-api vertical)
const providers = createWalletApiProviders(base, {
baseUrl: 'https://wallet-api.unicity.network',
network: 'testnet2',
deviceId: 'my-stable-device-id',
});
// 3. Initialize wallet (auto-creates if doesn't exist)
const { sphere, created, generatedMnemonic } = await Sphere.init({
...providers,
network: 'testnet2', // Required: it selects the token registry, and it must equal
// walletApi.network (a mismatch throws INVALID_CONFIG)
autoGenerate: true,
});
// 4. Save mnemonic on first run!
if (created && generatedMnemonic) {
console.log('SAVE THIS MNEMONIC:', generatedMnemonic);
}
// 5. Use the wallet
console.log('Direct Address:', sphere.identity?.directAddress);
// 6. Cleanup
await sphere.destroy();
}
main().catch(console.error);
What Gets Created¶
./wallet-data/
└── wallet.json # Wallet data (mnemonic stored plaintext or password-encrypted)
# + per-address payment journals under pv2g2:* keys
Tokens live in the wallet-api backend (server custody) — no local token files.
The pv2g2: prefix is 0.15.0's generation of the scoped KV (it was pv2: through 0.14.x). The
rename is the migration — the superseded keys are swept once when the wallet composes its
payments vertical, and there is nothing for you to run or delete.
Configuration Options¶
// Step 1: Create base providers
const base = createNodeProviders({
// Network: 'mainnet' | 'testnet' | 'testnet2' | 'dev'
// ('testnet' IS testnet2 — the v2 gateway network; mainnet/dev are still
// v1-era and cannot serve the engine)
network: 'testnet',
// Storage directory (required)
dataDir: './wallet-data',
// Custom wallet file name (default: 'wallet.json')
// Use .txt extension for plain mnemonic files (no JSON wrapper)
walletFileName: 'my-wallet.json',
// Transport options
transport: {
relays: ['wss://custom-relay.com'], // Replace default relays
additionalRelays: ['wss://extra-relay.com'], // Add to defaults
timeout: 5000,
autoReconnect: true,
debug: false,
},
// Oracle (v2 gateway) options
oracle: {
url: 'https://gateway.testnet2.unicity.network', // Replace default gateway URL
trustBasePath: './trustbase.json', // Local trust base file (optional)
apiKey: 'sk_ddc3cfcc001e4a28ac3fad7407f99590', // Gateway API key (public testnet2 key)
},
// Price provider (optional — enables fiat value display)
price: {
platform: 'coingecko', // Currently supported: 'coingecko'
apiKey: 'CG-xxx', // Optional (free tier works without key)
cacheTtlMs: 60000, // Cache TTL in ms (default: 60s)
},
});
// Step 2: Attach the wallet-api transport config (required for money)
const providers = createWalletApiProviders(base, {
baseUrl: 'https://wallet-api.unicity.network', // Wallet-api server
network: 'testnet2', // Must match v2 network
deviceId: 'my-device-id', // Stable per-device label (optional — random if omitted)
});
Common Operations¶
Check Balance & Assets¶
// Get assets with price data (price fields are null without PriceProvider)
const assets = await sphere.payments.assets();
for (const asset of assets) {
console.log(`${asset.symbol}: ${asset.totalAmount} (${asset.tokenCount} tokens)`);
if (asset.fiatValueUsd != null) {
console.log(` Value: $${asset.fiatValueUsd.toFixed(2)}`);
}
}
// Individual tokens (synchronous inventory view)
const tokens = sphere.payments.tokens();
// Total portfolio value in USD
const totalUsd = assets.reduce((sum, a) => sum + (a.fiatValueUsd ?? 0), 0);
console.log('Total USD:', totalUsd);
Top Up (Testnet Self-Mint)¶
There is no faucet — on testnet you top up by self-minting tokens via the token engine:
import { TokenRegistry } from '@unicitylabs/sphere-sdk';
// mint takes the hex coin id, not the symbol
const coinId = TokenRegistry.getInstance().getCoinIdBySymbol('UCT');
const res = await sphere.payments.mint(coinId!, 100_000_000n);
if (res.success) {
console.log('Minted token:', res.tokenId);
} else {
console.error('Mint failed:', res.error);
}
Look Up Asset Metadata¶
The TokenRegistry provides metadata (symbol, name, decimals, icons) for all registered assets on the network:
import { TokenRegistry } from '@unicitylabs/sphere-sdk';
const registry = TokenRegistry.getInstance();
// List all registered assets
const allAssets = registry.getAllDefinitions();
const coins = registry.getFungibleTokens();
const nfts = registry.getNonFungibleTokens();
// Look up a specific asset
const uct = registry.getDefinitionBySymbol('UCT');
console.log(uct?.name, uct?.decimals); // 'Unicity Token', 8
// Reverse lookup: symbol → coin ID
const coinId = registry.getCoinIdBySymbol('UCT');
Note: The registry is configured automatically by
createNodeProviders()andSphere.init(). Data is fetched from the network and cached to disk.
Send Tokens¶
// Send to nametag — the sender certifies the transfer on-chain (collects the
// inclusion proof) and delivers a finished token via wallet-api mailbox
const result = await sphere.payments.send({
recipient: '@alice',
amount: '1000000', // In base units
coinId: 'UCT', // Short symbols are resolved via the TokenRegistry
});
console.log('Transfer ID:', result.id);
console.log('Status:', result.status);
if (result.deliveryPending) {
console.log('Note: certified on-chain, delivery deferred (normal behavior)');
}
// Send to direct address
const result2 = await sphere.payments.send({
recipient: 'DIRECT://0000be36...',
amount: '500000',
coinId: 'UCT',
});
Fetch Pending Transfers (Explicit Receive)¶
For batch/CLI apps, use receive() to explicitly drain the wallet-api mailbox:
// Fetch and process all pending incoming transfers
const { transfers } = await sphere.payments.receive();
console.log(`Received ${transfers.length} transfers`);
for (const transfer of transfers) {
for (const token of transfer.tokens) {
console.log(` ${token.amount} ${token.symbol}`);
}
console.log(`From: ${transfer.senderNametag ?? transfer.senderPubkey}`);
}
receive()takes no options: transfers arrive as finished tokens, verified against the trust base and stored confirmed immediately — there is no finalization phase. While the wallet runs, the mailbox is also drained automatically.
Register Nametag¶
Note:
registerNametag()registers the name by publishing a Nostr identity binding (name ↔ chain pubkey, first-seen-wins). Runtime name resolution uses only the Nostr binding.
// Publishes the Nostr binding; throws if the name is already taken
await sphere.registerNametag('myusername');
console.log('Registered:', sphere.identity?.nametag);
Listen for Incoming Transfers¶
// Handlers receive the event payload directly (IncomingTransfer)
sphere.on('transfer:incoming', (transfer) => {
for (const token of transfer.tokens) {
console.log('Received:', token.amount, token.symbol);
}
console.log('From:', transfer.senderNametag ?? transfer.senderPubkey);
});
Send Direct Messages¶
await sphere.communications.sendDM('@alice', 'Hello!');
sphere.communications.onDirectMessage((msg) => {
console.log('Message from', msg.senderNametag ?? msg.senderPubkey, ':', msg.content);
});
Import Existing Wallet¶
// From mnemonic (plaintext storage — default)
const { sphere } = await Sphere.init({
...providers,
network: 'testnet2',
mnemonic: 'your twelve word mnemonic phrase here ...',
});
// From mnemonic with password encryption
const { sphere } = await Sphere.init({
...providers,
network: 'testnet2',
mnemonic: 'your twelve word mnemonic phrase here ...',
password: 'my-secret-password',
});
// From master key (legacy)
const sphere = await Sphere.import({
masterKey: '64-char-hex-master-key',
chainCode: '64-char-hex-chain-code',
basePath: "m/84'/1'/0'",
derivationMode: 'bip32',
network: 'testnet2',
...providers,
});
Password Encryption¶
By default, the mnemonic is stored as plaintext in wallet.json. You can optionally encrypt it with a password:
// Create wallet with password encryption
const { sphere } = await Sphere.init({
...providers,
network: 'testnet2',
autoGenerate: true,
password: 'my-secret-password',
});
// Load wallet with password
const { sphere } = await Sphere.init({
...providers,
network: 'testnet2',
password: 'my-secret-password',
});
// Load wallet without password (plaintext mnemonic — default)
const { sphere } = await Sphere.init({ ...providers, network: 'testnet2' });
Backwards compatibility: Wallets created with older SDK versions (encrypted with the internal default key) will load correctly without a password.
Custom Wallet File Names¶
// Use a custom file name
const providers = createNodeProviders({
network: 'testnet',
dataDir: './wallet-data',
walletFileName: 'my-wallet.json',
});
// Use .txt extension — stores only the mnemonic (no JSON wrapper)
const providers = createNodeProviders({
network: 'testnet',
dataDir: './wallet-data',
walletFileName: 'mnemonic.txt',
});
Loading External Wallet Files¶
If you have a plaintext mnemonic file from another source, simply point FileStorageProvider at it:
import { FileStorageProvider } from '@unicitylabs/sphere-sdk/impl/nodejs';
// Load from any .txt file containing a mnemonic
const storage = new FileStorageProvider({
dataDir: './wallet-data',
fileName: 'external-mnemonic.txt',
});
const base = createNodeProviders({
network: 'testnet',
dataDir: './wallet-data',
oracle: {
apiKey: 'sk_ddc3cfcc001e4a28ac3fad7407f99590',
},
});
const providers = createWalletApiProviders(base, {
baseUrl: 'https://wallet-api.unicity.network',
network: 'testnet2',
deviceId: 'my-device-id',
});
const { sphere } = await Sphere.init({
...providers,
storage, // Override with custom storage
network: 'testnet2',
});
Multi-Address Wallet¶
// Get current address index
const index = sphere.getCurrentAddressIndex(); // 0
// Switch to different address
await sphere.switchToAddress(1);
console.log('New address:', sphere.identity?.directAddress);
// Register nametag for this address
await sphere.registerNametag('myname-work');
// Derive address without switching
const addr = sphere.deriveAddress(2);
console.log(addr.path, addr.publicKey);
Event Handling¶
// The payments vertical emits exactly 8 events; identity/comms events ride the
// same bus (see SphereEventMap in types/index.ts for the full list)
sphere.on('transfer:incoming', handler); // IncomingTransfer
sphere.on('transfer:updated', handler); // TransferResult (read status/deliveryPending)
sphere.on('transfer:attention', handler); // { transferId, code, detail? }
sphere.on('inventory:updated', handler); // {}
sphere.on('history:updated', handler); // HistoryEntry (the recorded entry)
sphere.on('payment_request:incoming', handler); // PaymentRequestView
sphere.on('payment_request:updated', handler); // { id, status }
sphere.on('connection:status', handler); // { status: 'connected'|'degraded'|'offline' }
sphere.on('message:dm', handler);
sphere.on('message:broadcast', handler);
sphere.on('connection:changed', handler);
sphere.on('nametag:registered', handler);
sphere.on('nametag:recovered', handler);
sphere.on('identity:changed', handler);
// Unsubscribe
const unsubscribe = sphere.on('transfer:incoming', handler);
unsubscribe(); // Stop listening
Payment Requests¶
Request payments over the wallet-api rail (sphere.payments.requests):
// Send a payment request (memo travels in an encrypted envelope)
const result = await sphere.payments.requests.create('@bob', {
coinId: 'UCT',
amount: '1000000',
memo: 'Order #1234',
});
// Track outgoing request status
sphere.on('payment_request:updated', ({ id, status }) => {
// 'pending' | 'settling' | 'paid' | 'rejected' | 'expired'
console.log(`Request ${id}: ${status}`);
});
// Handle incoming requests
sphere.on('payment_request:incoming', async (request) => {
console.log(`${request.senderNametag} requests ${request.amount} ${request.symbol}`);
await sphere.payments.requests.pay(request.id); // pay...
// await sphere.payments.requests.decline(request.id); // ...or decline
});
// Current views + housekeeping
const requests = sphere.payments.requests.list();
sphere.payments.requests.dismissProcessed();
Paying is crash-safe: a request is durably
settlingbefore any possibly-committed error can surface, so a restart never double-pays.
Transaction History¶
History is a server read-through, paged:
const page = await sphere.payments.history({ limit: 50 });
for (const entry of page.entries) {
console.log(entry.type, entry.amount, entry.symbol, new Date(entry.timestamp));
}
if (page.more) {
const older = await sphere.payments.history({ before: page.cursor!, limit: 50 });
}
Error Handling¶
import { isSphereError } from '@unicitylabs/sphere-sdk';
try {
const result = await sphere.payments.send({
coinId: 'UCT',
amount: '1000000',
recipient: '@alice',
});
console.log('Sent:', result.id, result.status);
} catch (error) {
if (isSphereError(error)) {
switch (error.code) {
case 'INSUFFICIENT_BALANCE':
console.error('Not enough funds');
break;
case 'INVALID_RECIPIENT':
console.error('Recipient not found');
break;
case 'TRANSPORT_ERROR':
console.error('Network issue, try again');
break;
default:
console.error('Transfer failed:', error.message);
}
} else {
console.error('Unexpected error:', error);
}
}
TypeScript Support¶
Full TypeScript support with exported types:
import type {
Identity,
FullIdentity,
StorageProvider,
TransportProvider,
OracleProvider,
ProviderStatus,
SphereEventType,
} from '@unicitylabs/sphere-sdk';
Custom CLI Example¶
Build your own CLI tool using the SDK:
#!/usr/bin/env node
import { Sphere } from '@unicitylabs/sphere-sdk';
import { createNodeProviders } from '@unicitylabs/sphere-sdk/impl/nodejs';
import { createWalletApiProviders } from '@unicitylabs/sphere-sdk/impl/shared/wallet-api';
async function main() {
const base = createNodeProviders({
network: 'testnet',
dataDir: './my-wallet',
oracle: {
apiKey: 'sk_ddc3cfcc001e4a28ac3fad7407f99590',
},
});
const providers = createWalletApiProviders(base, {
baseUrl: 'https://wallet-api.unicity.network',
network: 'testnet2',
deviceId: 'my-device-id',
});
const { sphere, created, generatedMnemonic } = await Sphere.init({
...providers,
network: 'testnet2',
autoGenerate: true,
});
if (created) {
console.log('\n=== NEW WALLET CREATED ===');
console.log('Mnemonic (SAVE THIS!):', generatedMnemonic);
console.log('==========================\n');
}
console.log('Direct Address:', sphere.identity?.directAddress);
console.log('Nametag:', sphere.identity?.nametag || '(not registered)');
// Listen for incoming transfers (handler receives the IncomingTransfer payload)
sphere.on('transfer:incoming', (transfer) => {
console.log('\nIncoming transfer!');
for (const token of transfer.tokens) {
console.log('Amount:', token.amount, token.symbol);
}
console.log('From:', transfer.senderNametag ?? transfer.senderPubkey);
});
// Keep running
console.log('\nListening for transfers... Press Ctrl+C to exit');
process.on('SIGINT', async () => {
console.log('\nShutting down...');
await sphere.destroy();
process.exit(0);
});
}
main().catch(console.error);
Troubleshooting¶
"Cannot find module 'ws'"¶
"Failed to connect to relay"¶
Check network connectivity and relay URLs:
const base = createNodeProviders({
network: 'testnet',
transport: {
debug: true, // Enable debug logging
timeout: 10000, // Increase timeout
},
});
"Trustbase not found"¶
Download or specify trustbase path:
Data not persisting¶
Ensure the data directory exists and is writable:
Debug Logging¶
Enable SDK debug logging to diagnose issues:
import { logger } from '@unicitylabs/sphere-sdk';
// Enable all debug logging
logger.configure({ debug: true });
// Enable only specific modules
logger.setTagDebug('Nostr', true); // Transport logs
logger.setTagDebug('Payments', true); // Payment logs
// Custom log handler (e.g., write to file)
logger.configure({
debug: true,
handler: (level, tag, message, ...args) => {
fs.appendFileSync('sdk.log', `[${level}] [${tag}] ${message}\n`);
},
});
Next Steps¶
- API Reference - Full API documentation
- Integration Guide - Advanced integration patterns, and Upgrading to 0.15.0
- Connect Protocol - dApp ↔ wallet RPC (protocol version
2.1) - Parallel token verification - The opt-in worker pool
- Browser Quick Start - For web applications