Kaspa has no memo field. When a transaction lands on-chain, it carries inputs, outputs, a signature — and nothing else. There is no place to stash an order number, a customer email, or an invoice ID.
For a merchant or project accepting KAS, this creates a concrete engineering problem: how do you match an incoming payment to the correct order, without ever touching the buyer's private keys?
The answer is a three-part pattern: derive a unique address per order from an xpub, watch the UTXO set for deposits, and confirm via DAA score. This is the pattern we built into our own payment infrastructure when we added KAS as a payment channel. It requires no custodial wallet, no third-party processor, and no trust beyond the Kaspa node you run yourself.
The Attribution Problem
An extended public key (xpub) is the public half of an HD (hierarchical-deterministic) wallet. It can derive an unlimited sequence of child public keys — and therefore addresses — without exposing any private key. The corresponding private key tree is derived from a single seed, held separately.
In Bitcoin, merchants sometimes rely on a single address and match payments by amount or timing. That breaks down quickly: amounts overlap, wallets batch transactions, and network congestion introduces delays. Kaspa's ten-block-per-second throughput makes timing even less distinctive.
The robust approach is one address per invoice. Since each address is a unique child index of the xpub, the mapping is unambiguous: payment arrives on index 42 → that is order #42.
No private key is needed to generate the address. The xpub alone is sufficient.
Step by Step: The Payment Flow
1. Derive the next address
When a buyer initiates a payment, the server reads a monotonically increasing counter (deriv_index) from its database and derives the next child public key from the xpub. The standard approach follows BIP-44 conventions:
child_pubkey = derive_child(xpub, index)
address = kaspa_p2pk_address(child_pubkey)
The server increments the counter and stores the address alongside the order record. It never needs the private key — it only needs the xpub, which is a read-only artifact.
This is the same class of HD derivation that powers Kaspa Forge's encrypted Desk profile — different domain separation string, same underlying math. For payment addresses a standard xpub path works directly; for tool-specific keys we add a domain prefix to prevent cross-context key reuse.
2. Present the payment page
The response includes the derived address, the expected amount in KAS, and a kaspa: URI:
kaspa:kaspa:qz{...}?amount=328.39
The client renders a QR code from this URI. A countdown timer shows the remaining validity window — we use 20 minutes. The page begins polling a status endpoint on a short interval.
3. Lock the exchange rate
Kaspa trades on open markets, so the KAS/USD rate can drift during the payment window. The server captures the rate at order creation (CoinGecko's free endpoint, with a hardcoded fallback) and adds a volatility buffer — typically 2% — to cover the gap between quote and confirmation.
The expected_sompi is calculated once and frozen:
expected_sompi = ceil($price × 1.02 / rate_usd × 100_000_000)
This number is stored with the order and never recalculated. The buyer sees a fixed KAS amount.
4. Watch the UTXO set
A background process polls the Kaspa node's UTXO index for all actively monitored addresses. The loop is straightforward:
every N seconds:
utxos = node.getUtxosByAddresses(active_addresses)
for each monitored order:
received = sum(utxo.amount for utxo in utxos[order.address])
if received >= order.expected - tolerance:
→ fire signed callback
We chose polling (getUtxosByAddresses with utxoindex enabled) over wRPC subscription. Polling is more resilient to reconnections — if the watcher's connection to the node drops and recovers, the next poll cycle catches up automatically. A subscription can silently miss events during a disconnect window. For a payment system, missing a deposit is worse than the small latency cost of polling.
This is the same data-source pattern that Kaspa Safe's off-chain watcher uses for vault monitoring — different trigger logic, same UTXO index.
5. Count confirmations
Kaspa uses a DAA score rather than a simple block-height counter. The DAA (Difficulty Adjustment Algorithm) operates over a window of 2,641 blocks and produces a monotonically increasing score that accounts for both blue and merged red blocks.
To count confirmations for a payment transaction:
confirmations = virtual_daa_score - tx_accepting_block_daa_score
A configurable threshold gates the callback. At 10 BPS, even a generous confirmation count resolves in seconds. The merchant can tune this — more confirmations for high-value orders, fewer for routine ones.
6. Handle partial payments
Kaspa's UTXO model helps here. A single address can accumulate deposits from multiple transactions, and the watcher sums all unspent outputs. If the buyer sends 200 KAS when 328 was expected:
{
"status": "partial",
"received_sompi": 20000000000,
"expected_sompi": 32839000000,
"shortfall_sompi": 12839000000
}
The payment page displays: "Paid 200 KAS — please send 128.39 more to the same address." Once the total reaches the threshold with sufficient confirmations, the callback fires and the order completes.
A design detail: the exchange rate locks on the first deposit, not on order creation. If the buyer sends a partial payment, the rate is already frozen — the order doesn't expire while they gather the rest. Only orders that receive zero deposits expire by the timer.
7. Fire the HMAC callback
When received ≥ expected − tolerance and confirmations ≥ threshold, the watcher sends an HMAC-signed HTTP POST to the order management service:
POST /api/kaspa/paid
{
"order_id": "...",
"address": "kaspa:qz{...}",
"txid": "abc123...",
"received_sompi": 32839000000,
"daa_score": 12345678
}
Authorization: HMAC-SHA256(body, shared_secret)
The receiving service verifies the HMAC, transitions the order to confirmed, and triggers fulfillment — in our case, issuing a license key and emailing it. The callback is idempotent on order_id; the watcher retries on transient failure but won't double-fire on success.
How Kaspa Forge Uses This Pattern
Our payment architecture splits into two services on two separate machines:
- kaspa-gateway — lives next to the Kaspa node. It holds the xpub, derives addresses, runs the UTXO watcher, and fires signed callbacks. It never sees order details beyond "watch this address for this amount."
- Order service — the order and fulfillment service. It requests addresses, stores orders, and completes them on payment confirmation. It never sees the xpub or any private key.
The separation is deliberate: the gateway is the only service that touches Kaspa-specific crypto logic. If the order service is compromised, the attacker gets no keys and no xpub — only public addresses and HMAC-verified callbacks. If the gateway is compromised, the attacker gets the xpub (which is public-key-only, non-spendable) and can observe incoming payments but cannot move funds.
The seed itself never sits on either server. It was generated offline and the xpub was extracted once. This is the same non-custodial discipline that governs the Kaspa Safe vault: keys are generated and used only where the user (or in this case, the keyholder) controls the device. The vault contract, the Escrow contract, and the payment acceptance layer all share the principle that the party watching the chain never holds signing authority.
The same HD derivation engine that generates payment addresses also powers the Kaspa Safe vault and the encrypted Desk profile — one seed, one derivation scheme, different domain strings. On-chain operations in both Safe and Escrow are free forever; only optional monitoring alerts carry a fee.
Trade-offs and Honest Limitations
Polling adds latency. The watcher doesn't see a deposit until the next poll cycle. At Kaspa's 10 BPS rate, a 10-second poll interval means worst-case ~10 seconds of additional delay after a transaction lands in a block. For a payment page already counting confirmation seconds, this is acceptable. For a real-time trading engine, you'd want a subscription with careful reconnection logic.
DAA score is not intuitive. "3 confirmations" at 10 BPS arrives in under a second, which can feel like "unconfirmed" to someone used to Bitcoin's ten-minute blocks. Your UI should translate the score into a meaningful status — "confirmed by the network" rather than a raw number that looks prematurely low.
Address reuse is possible but inadvisable. Nothing in the protocol prevents depositing to a previously used address, and the watcher will sum all UTXOs regardless. But one address per order keeps accounting clean. Reuse makes it ambiguous which payment belongs to which order after the fact.
No push notifications from the node. Kaspa's current node doesn't offer a reliable subscription API for incoming UTXOs that survives restarts gracefully. The network subsystem relays blocks and mempool transactions between peers, but payment-grade notification of address-specific deposits requires application-level polling. This is the pragmatic choice until richer node APIs mature.
The xpub reveals all derived addresses. Anyone with the xpub can reconstruct the full set of payment addresses and see their on-chain history. For a checkout flow where the address is shown to the buyer anyway, this is rarely a concern. For privacy-sensitive use cases, you'd need to rotate xpubs or limit the derivation window.
No automatic refunds. If a buyer sends 500 KAS when 328 was expected, the excess is not automatically returned. The order completes on the full amount. Refunds, if offered, happen manually outside the protocol. This is a conscious trade-off: automated refund logic would require the payment service to hold spending keys, which breaks the non-custodial model.
---
The xpub-and-watcher pattern is not unique to Kaspa — Bitcoin and other UTXO chains use the same idea. What makes it work well on Kaspa is the speed: ten blocks per second means a payment goes from broadcast to confirmed in seconds rather than minutes, and the UTXO index responds nearly instantly to new deposits. For any project accepting KAS — whether selling software, running a marketplace, or settling OTC trades — the architecture is the same: derive, watch, confirm, callback. No custody required.
FAQ
Why generate a unique Kaspa address per order?
Kaspa transactions carry no memo or comment field. The only reliable way to attribute a payment to an order is to give each buyer a fresh address derived from a public-only extended key (xpub).
Do I need the private seed to derive payment addresses?
No. An extended public key (xpub) lets you derive child public keys — and therefore addresses — without access to any private key. The seed can live on an air-gapped machine or hardware wallet.
How does the watcher know a payment arrived?
The watcher polls the node's UTXO index for all monitored addresses. When the sum of unspent outputs meets or exceeds the expected amount with enough confirmations, it fires a signed callback.
What happens if the buyer sends less than expected?
The watcher reports a shortfall and the UI tells the buyer exactly how much more to send to the same address. Kaspa's UTXO model naturally sums all deposits on a single address.
How are confirmations measured in Kaspa?
Confirmations are counted using the DAA score difference between the transaction's accepting block and the node's virtual tip. Kaspa's 10-block-per-second rate means confirmations arrive in seconds.
Can the watcher steal funds?
No. The watcher holds only public keys. It can report what it sees on-chain, but it cannot sign or move any UTXO. Only the holder of the corresponding private key can spend.
Put your KAS where theft can be cancelled
A covenant vault on Kaspa mainnet: your keys, your rules, our tooling. Free on-chain, forever.
Create a vault
