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
| Network | Prefix |
|---|---|
| Mainnet | kaspa: |
| Testnet | kaspatest: |
| Simnet | kaspasim: |
| Devnet | kaspadev: |
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
| Version | Name | Payload | Meaning |
|---|---|---|---|
| 0 | PubKey | 32 bytes | Schnorr x-only public key (the default) |
| 1 | PubKeyECDSA | 33 bytes | Compressed ECDSA public key |
| 8 | ScriptHash | 32 bytes | blake2b-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:qr0lr4ml9fn3chekrqmjdkergxl93l4wrk3dankcgvjq776s9wn9jhtkdksaeThe 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:qr0lr4ml9fn3chekrqmjdkergxl93l4wrk3dankcgvjq776s9wn9jkdskewvaDecoding 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:qr0lr4ml9fn3chekrqmjdkergxl93l4wrk3dankcgvjq776s9wn9jkdskewvaVersion-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 ScriptHashNote 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.