Kaspalytics Learn is under (heavy) construction. Check back regularly!
Updated: Aug 4, 2026

Addresses

Kaspa address format - prefixes, version bytes, payloads, and encoding - plus how to encode, decode, and validate one in Rust, JavaScript, and Python.

A Kaspa address encodes three things: a network prefix, a version byte, and a payload (a public key or script hash). Example:

kaspa:qz0k2...
└─┬─┘ └──┬──┘
prefix   base32-encoded version + payload + checksum

Network prefixes

NetworkPrefix
Mainnetkaspa:
Testnetkaspatest:
Simnetkaspasim:
Devnetkaspadev:

The prefix is part of the address (including the :) and is covered by the checksum - a mainnet address can’t be accidentally used on testnet.

Version bytes & payloads

VersionNamePayloadMeaning
0PubKey32 bytesSchnorr x-only public key (the default)
1PubKeyECDSA33 bytesCompressed ECDSA public key
8ScriptHash32 bytesblake2b-256 hash of a script (P2SH)

When a wallet sends to an address, it decodes it back into a script_public_key for the transaction output: version 0 becomes a pay-to-pubkey script (OpData32 <pubkey> OpCheckSig-style), version 8 becomes the P2SH template.

Encoding

The version + payload are encoded in cashaddr-style base32 with a polymod checksum computed over the prefix and data (the same family of encoding as Bitcoin Cash addresses, not base58). There is no case-mixing: addresses are lowercase.

Encoding an address

You almost never build the base32 yourself — you hand a key to the SDK and it assembles prefix, version, and payload for you. Every example below uses the same public key, so the address strings can be compared line for line.

use kaspa_addresses::{Address, Prefix, Version};
use secp256k1::PublicKey;

let public_key: PublicKey =
    "02dff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659".parse()?;

// Version 0 takes the 32-byte x-only key, not the 33-byte compressed one.
let (x_only, _parity) = public_key.x_only_public_key();
let address = Address::new(Prefix::Mainnet, Version::PubKey, &x_only.serialize());

println!("{address}");
// kaspa:qr0lr4ml9fn3chekrqmjdkergxl93l4wrk3dankcgvjq776s9wn9jkdskewva
const { PublicKey } = kaspa;

const publicKey = new PublicKey(
	'02dff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659'
);

// Schnorr (version 0) — the default for every wallet.
console.log(publicKey.toAddress('mainnet').toString());
// kaspa:qr0lr4ml9fn3chekrqmjdkergxl93l4wrk3dankcgvjq776s9wn9jkdskewva

// ECDSA (version 1), from the same key.
console.log(publicKey.toAddressECDSA('mainnet').toString());
// kaspa:qypdluwh0u4xw8zlxcvrwfkmydqmuk874cw69hkwmppjgrmm2q46vkgl9zsrch2

// The network decides the prefix; nothing else about the address changes.
console.log(publicKey.toAddress('testnet').toString());
// kaspatest:qr0lr4ml9fn3chekrqmjdkergxl93l4wrk3dankcgvjq776s9wn9jhtkdksae
from kaspa import PublicKey

public_key = PublicKey(
    "02dff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659"
)

# Schnorr (version 0) — the default for every wallet.
print(public_key.to_address("mainnet").to_string())
# kaspa:qr0lr4ml9fn3chekrqmjdkergxl93l4wrk3dankcgvjq776s9wn9jkdskewva

# ECDSA (version 1), from the same key.
print(public_key.to_address_ecdsa("mainnet").to_string())
# kaspa:qypdluwh0u4xw8zlxcvrwfkmydqmuk874cw69hkwmppjgrmm2q46vkgl9zsrch2

# The network decides the prefix; nothing else about the address changes.
print(public_key.to_address("testnet").to_string())
# kaspatest:qr0lr4ml9fn3chekrqmjdkergxl93l4wrk3dankcgvjq776s9wn9jhtkdksae

The WASM and Python SDKs also go straight from a private key, which is what most examples in the wild start from:

// Rust has no one-call equivalent — derive the public key first, then use
// Address::new as above.
use secp256k1::{Secp256k1, SecretKey};

let secp = Secp256k1::new();
let secret_key: SecretKey =
    "b7e151628aed2a6abf7158809cf4f3c762e7160f38b4da56a784d9045190cfef".parse()?;
let (x_only, _parity) = secret_key.x_only_public_key(&secp);
const { PrivateKey } = kaspa;

const privateKey = new PrivateKey(
	'b7e151628aed2a6abf7158809cf4f3c762e7160f38b4da56a784d9045190cfef'
);

console.log(privateKey.toAddress('mainnet').toString());
// kaspa:qr0lr4ml9fn3chekrqmjdkergxl93l4wrk3dankcgvjq776s9wn9jkdskewva
from kaspa import PrivateKey

private_key = PrivateKey(
    "b7e151628aed2a6abf7158809cf4f3c762e7160f38b4da56a784d9045190cfef"
)

print(private_key.to_address("mainnet").to_string())
# kaspa:qr0lr4ml9fn3chekrqmjdkergxl93l4wrk3dankcgvjq776s9wn9jkdskewva

Decoding an address

Parsing gives you back the three fields the string was built from.

use kaspa_addresses::{Address, Prefix, Version};

let address =
    Address::try_from("kaspa:qr0lr4ml9fn3chekrqmjdkergxl93l4wrk3dankcgvjq776s9wn9jkdskewva")?;

assert_eq!(address.prefix, Prefix::Mainnet);
assert_eq!(address.version, Version::PubKey);
assert_eq!(address.payload.len(), 32);          // raw bytes, already decoded

// TryFrom is the validity check — there is no separate validate().
let ok = Address::try_from("kaspa:notanaddress").is_ok();   // false
const { Address } = kaspa;

// Validate before constructing: the constructor is a Rust panic on bad input,
// not a JavaScript exception, and a panic leaves the WASM instance unusable.
const text = 'kaspa:qr0lr4ml9fn3chekrqmjdkergxl93l4wrk3dankcgvjq776s9wn9jkdskewva';
if (!Address.validate(text)) throw new Error('bad address');

const address = new Address(text);

console.log(address.prefix); // "kaspa"
console.log(address.version); // "PubKey"
console.log(address.payload); // "qr0lr4ml...kdskewva" — still base32, not bytes
console.log(address.short(6)); // "kaspa:qr0lr4....skewva"
from kaspa import Address

text = "kaspa:qr0lr4ml9fn3chekrqmjdkergxl93l4wrk3dankcgvjq776s9wn9jkdskewva"

# Raises on bad input; Address.validate() is the non-raising form.
address = Address(text)
print(Address.validate("kaspa:notanaddress"))  # False

print(address.prefix)    # "kaspa"
print(address.version)   # "PubKey"
print(address.payload)   # "qr0lr4ml...kdskewva" — still base32, not bytes
print(address.short(6))  # "kaspa:qr0lr4....skewva"

payload means two different things. In Rust it is the decoded byte vector — 32 bytes for a version-0 key. In the WASM and Python SDKs the payload accessor returns the base32 text after the colon. If you are porting code between them, that is the line that will bite.

Addresses and script public keys

An address is a transport format for humans. What actually goes on chain is a script_public_key, and the two convert both ways — this is the pair of calls a block explorer, an indexer, or anything reading UTXOs ends up needing.

use kaspa_addresses::Prefix;
use kaspa_txscript::standard::{extract_script_pub_key_address, pay_to_address_script};

let spk = pay_to_address_script(&address);
// version 0, script 20dff1d7...ba659ac
//   20 = OpData32 (push the next 32 bytes), ac = OpCheckSig

let recovered = extract_script_pub_key_address(&spk, Prefix::Mainnet)?;
assert_eq!(recovered, address);
const { payToAddressScript, addressFromScriptPublicKey } = kaspa;

const spk = payToAddressScript(address);
console.log(spk.version, spk.script);
// 0 20dff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659ac
//   20 = OpData32 (push the next 32 bytes), ac = OpCheckSig

// Returns undefined rather than throwing when the script is non-standard.
console.log(addressFromScriptPublicKey(spk, 'mainnet').toString());
// kaspa:qr0lr4ml9fn3chekrqmjdkergxl93l4wrk3dankcgvjq776s9wn9jkdskewva
from kaspa import pay_to_address_script, address_from_script_public_key

spk = pay_to_address_script(address)
print(spk.version, spk.script)
# 0 20dff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659ac
#   20 = OpData32 (push the next 32 bytes), ac = OpCheckSig

print(address_from_script_public_key(spk, "mainnet").to_string())
# kaspa:qr0lr4ml9fn3chekrqmjdkergxl93l4wrk3dankcgvjq776s9wn9jkdskewva

Version-8 addresses go the same route in reverse: hash a redeem script into a P2SH script public key, then read the address back out of it.

use kaspa_txscript::standard::pay_to_script_hash_script;

let redeem = hex::decode(
    "20dff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659ac",
)?;
let spk = pay_to_script_hash_script(&redeem);
let address = extract_script_pub_key_address(&spk, Prefix::Mainnet)?;
// kaspa:pqm5xlry3zx3lw2nwkklwmtc5y0xrdtedxzudl2ysa6xx8y3yh60g2ukxsly0
const { payToScriptHashScript, addressFromScriptPublicKey } = kaspa;

const redeem = '20dff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659ac';
const spk = payToScriptHashScript(redeem);

const address = addressFromScriptPublicKey(spk, 'mainnet');
console.log(address.toString(), address.version);
// kaspa:pqm5xlry3zx3lw2nwkklwmtc5y0xrdtedxzudl2ysa6xx8y3yh60g2ukxsly0 ScriptHash
from kaspa import pay_to_script_hash_script, address_from_script_public_key

redeem = "20dff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659ac"
spk = pay_to_script_hash_script(redeem)

address = address_from_script_public_key(spk, "mainnet")
print(address.to_string(), address.version)
# kaspa:pqm5xlry3zx3lw2nwkklwmtc5y0xrdtedxzudl2ysa6xx8y3yh60g2ukxsly0 ScriptHash

Note the leading p rather than q in the payload — that is the version byte showing through the base32, and it is how you tell a P2SH address from a key address at a glance.

HD derivation

Kaspa’s registered BIP-44 coin type is 111111, so the standard derivation path for the first receive address is:

m/44'/111111'/0'/0/0

Change addresses use .../1/i. Multisig accounts derive under purpose 45' instead, with a cosigner index between the account and the address type: m/45'/111111'/<account>'/<cosigner>/<type>/<index>.

Deriving a range of addresses from an extended public key — what a wallet does on every rescan — is one call:

// See kaspa_wallet_core::derivation for the account-level helpers; the
// low-level primitives live in kaspa_bip32.
const { PublicKeyGenerator } = kaspa;

const xpub = PublicKeyGenerator.fromMasterXPrv(masterXPrv, false, 0n);

// Ranges are half-open: [start, end).
console.log(xpub.receiveAddressAsStrings('mainnet', 0, 10));
console.log(xpub.changeAddressAsStrings('mainnet', 0, 10));

// Or one at a time.
console.log(xpub.receiveAddress('mainnet', 0).toString());
from kaspa import PublicKeyGenerator

xpub = PublicKeyGenerator.from_master_xprv(master_xprv, False, 0)

# Ranges are half-open: [start, end).
for key in xpub.receive_pubkeys(0, 10):
    print(key.to_string(), key.to_address("mainnet").to_string())
# 02bdcff2b855...e0b469  kaspa:qz7ulu4c25dh7fzec9zjyrmlhnkzrg4wmf89q7gzr3gfrsj3uz6xjellj43pf
# 03a710484954...0a79a69  kaspa:qzn3qjzf2nzyd3zj303nk4sgv0aae42v3ufutk5xsxckfels57dxjjed4qvlx

for key in xpub.change_pubkeys(0, 10):
    print(key.to_string(), key.to_address("mainnet").to_string())

Where to next

  • Units — sompi, KAS, and the conversion helpers that pair with these.
  • P2SH Script Format — what a version-8 address commits to.
  • Transactions — spending to an address once you have one.
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