The Wallet SDK is the set of primitives the managed Wallet is built on — and the toolkit you reach for when you don’t want a managed wallet at all: custom signers, indexers, watch-only flows, hot-path services that keep everything in memory.
Five pieces, in the order you’d use them:
| Piece | What it does |
|---|---|
| Mnemonic / XPrv | BIP-39 phrase to seed to extended private key. |
| Key generators | BIP-44 derivation — turn an XPrv into receive and change keys. |
| UtxoProcessor | The engine. Opens a node listener and dispatches UTXO events. |
| UtxoContext | Per-address-set UTXO tracking on top of a processor. |
| Generator | Coin selection, mass and fee calculation, signing, submission. |
Key material
A mnemonic gives you a seed, which gives you an extended private key:
use kaspa_bip32::{Language, Mnemonic, SecretKey, WordCount, ExtendedPrivateKey};
let mnemonic = Mnemonic::random(WordCount::Words24, Language::English)?;
let seed = mnemonic.to_seed(""); // "" or a BIP-39 passphrase
let xprv = ExtendedPrivateKey::<SecretKey>::new(seed)?; const { Mnemonic, XPrv } = kaspa;
const mnemonic = Mnemonic.random(24); // or new Mnemonic(existingPhrase)
const xprv = new XPrv(mnemonic.toSeed()); from kaspa import Mnemonic, XPrv
mnemonic = Mnemonic.random() # or Mnemonic(existing_phrase)
xprv = XPrv(mnemonic.to_seed())Treat the phrase and the seed as the wallet. Anyone with either controls every key derived from them — see the security notes in the Python SDK docs for handling guidance.
Derivation
Kaspa’s BIP-44 coin type is 111111. The full path for the first receive address of account 0 is m/44'/111111'/0'/0/0; change addresses use .../1/i.
The SDKs give you two ways there — walk the path yourself, or use a key generator that knows the layout:
// Explicit path
const key = xprv.derivePath("m/44'/111111'/0'/0/0").toPrivateKey();
const address = key.toAddress(NetworkType.Mainnet);
// Or a generator that knows the BIP-44 layout
const keys = new PrivateKeyGenerator(xprv, false, 0n);
const receiveKey = keys.receiveKey(0);
const changeKey = keys.changeKey(0); from kaspa import NetworkType, PrivateKeyGenerator
# Explicit path
key = xprv.derive_path("m/44'/111111'/0'/0/0").to_private_key()
address = key.to_address(NetworkType.Mainnet)
# Or a generator that knows the BIP-44 layout
keys = PrivateKeyGenerator(xprv=xprv, is_multisig=False, account_index=0)
receive_key = keys.receive_key(0)
change_key = keys.change_key(0)There’s a matching public key generator for watch-only flows: derive addresses to monitor without the private keys ever being present.
Addresses are network-specific. The same key under mainnet and testnet produces different addresses — see Networks.
UTXO processor and context
For a one-shot script, fetching UTXOs over RPC on demand is fine. For anything long-running it isn’t: you’d be re-fetching state the node is already willing to push to you, and you’d have no way to know when an output matured.
The processor opens that notification stream. A context sits on top and tracks a specific address set, maintaining per-state balances (mature, pending, outgoing) and the mature UTXO set the coin selector draws from.
const processor = new UtxoProcessor({ rpc, networkId: 'mainnet' });
await processor.start();
const context = new UtxoContext({ processor });
await context.trackAddresses([receiveAddress]);
processor.addEventListener('balance', (event) => {
console.log(event.data.balance.mature);
}); from kaspa import UtxoContext, UtxoProcessor
processor = UtxoProcessor(client, network_id)
await processor.start()
context = UtxoContext(processor)
await context.track_addresses([receive_address])
processor.add_event_listener("balance", lambda event: print(event))Tracking an address seeds the mature set from the node and subscribes to changes for it. Until that first load completes, the context legitimately reports an empty UTXO set — gate on the processor’s synced state before trusting a balance.
The transaction generator
The generator is the coin selector and fee calculator. Hand it UTXOs, a change address, and the outputs you want; it picks inputs, computes mass and fees, and yields one or more pending transactions ready to sign and submit.
In a long-running process, pass the context rather than a UTXO list — the generator pulls the current mature set on iteration, so there’s no snapshot to go stale:
const generator = new Generator({
entries: context, // a UtxoContext, not an array
outputs: [{ address: recipient, amount: kaspaToSompi('1.0') }],
changeAddress: myAddress,
priorityFee: 0n
});
let pending;
while ((pending = await generator.next())) {
await pending.sign([key]);
console.log('submitted:', await pending.submit(rpc));
}
console.log(generator.summary()); from kaspa import Generator, PaymentOutput
gen = Generator(
network_id=network_id,
entries=context, # a UtxoContext, not a list
change_address=my_address,
outputs=[PaymentOutput(recipient, 100_000_000)], # 1 KAS
)
for pending in gen:
pending.sign([key])
print("submitted:", await pending.submit(client))
print(gen.summary().fees, gen.summary().transactions)The generator is iterable because a large input set can exceed one transaction’s mass budget. In that case it yields a chain of consolidating transactions followed by the final payment — loop and submit each in order.
Beyond the basics, the constructor takes a payload, a priority fee, priority UTXOs to consume first, sig_op_count and minimum_signatures for multisig mass estimation, and an explicit fee rate override. See Transactions for what each one does to the resulting transaction.
End to end, without a managed wallet
The canonical primitives-only flow — a mnemonic, a derived address, live UTXO tracking, and a signed-and-submitted send. No wallet file, no persistence:
import asyncio
from kaspa import (
Generator, Mnemonic, NetworkId, NetworkType, PaymentOutput,
PrivateKeyGenerator, Resolver, RpcClient, UtxoContext, UtxoProcessor,
XPrv,
)
async def main():
network = NetworkId("testnet-10")
# 1. Key material
xprv = XPrv(Mnemonic("<24-word phrase>").to_seed())
keys = PrivateKeyGenerator(xprv=xprv, is_multisig=False, account_index=0)
receive_key = keys.receive_key(0)
receive_addr = receive_key.to_address(NetworkType.Testnet)
# 2. Connect and start a processor
client = RpcClient(resolver=Resolver(), network_id="testnet-10")
await client.connect()
try:
processor = UtxoProcessor(client, network)
await processor.start()
# 3. Track the address — seeds the mature set and subscribes
context = UtxoContext(processor)
await context.track_addresses([receive_addr])
# 4. Build, sign, submit
gen = Generator(
network_id=network,
entries=context,
change_address=receive_addr,
outputs=[PaymentOutput(receive_addr, 100_000_000)], # 1 KAS
)
for pending in gen:
pending.sign([receive_key])
print("submitted:", await pending.submit(client))
await processor.stop()
finally:
await client.disconnect()
asyncio.run(main()) The WASM SDK ships the same flow in transactions/utxo-context-generator.js inside the release zip. In Rust the equivalent is UtxoProcessor / UtxoContext from kaspa-wallet-core, feeding GeneratorSettings::try_new_with_context.
Where to next
- Transactions — the generator’s output, and the manual path underneath it.
- Wallet — what this all looks like with accounts and persistence bolted on.
- Python SDK Wallet SDK docs — per-method reference for key management, derivation, the processor, and the context.