Kaspalytics Learn is a work in progress.
Updated: Aug 2, 2026

Wallet SDK

Kaspa wallet primitives - key management, BIP-44 derivation, the UTXO processor and context, and the transaction generator.

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:

PieceWhat it does
Mnemonic / XPrvBIP-39 phrase to seed to extended private key.
Key generatorsBIP-44 derivation — turn an XPrv into receive and change keys.
UtxoProcessorThe engine. Opens a node listener and dispatches UTXO events.
UtxoContextPer-address-set UTXO tracking on top of a processor.
GeneratorCoin 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.
Kaspalytics strives to provide accurate data - our highest level of effort is given to data validation and maintenance. However, we cannot guarantee 100% accuracy. Data is subject to inaccuracies and change.

Contact | © 2026 Kaspalytics