Skip to content

Subscriptions

Subscriptions let RpcClient receive a live feed of node events. The node pushes events as they happen; the client invokes the callbacks you registered.

The two-step pattern

Every subscription has two parts:

  1. A listener — a Python callback registered via add_event_listener("<event>", callback).
  2. A subscriptionawait client.subscribe_<event>(...) tells the node to start streaming.

Both halves are required. A listener with no subscription receives nothing; a subscription with no listener silently drops events.

def on_utxo_change(event):
    print("UTXO change:", event)

client.add_event_listener("utxos-changed", on_utxo_change)
await client.subscribe_utxos_changed([Address("kaspa:qz...")])

Available events

Each subscribe_* has a matching unsubscribe_* with the same argument shape.

Event name Subscribe call Arguments Event payload
utxos-changed subscribe_utxos_changed addresses: list[Address] UtxosChangedEvent
block-added subscribe_block_added BlockAddedEvent
virtual-chain-changed subscribe_virtual_chain_changed include_accepted_transaction_ids: bool VirtualChainChangedEvent
virtual-daa-score-changed subscribe_virtual_daa_score_changed VirtualDaaScoreChangedEvent
sink-blue-score-changed subscribe_sink_blue_score_changed SinkBlueScoreChangedEvent
finality-conflict subscribe_finality_conflict FinalityConflictEvent
finality-conflict-resolved subscribe_finality_conflict_resolved FinalityConflictResolvedEvent
new-block-template subscribe_new_block_template NewBlockTemplateEvent
pruning-point-utxo-set-override subscribe_pruning_point_utxo_set_override PruningPointUtxoSetOverrideEvent

Event names also map to the NotificationEvent enum if you prefer typed variants over kebab-case strings.

The client also emits two control events that don't require a subscribe_* call — just register a listener:

Event name Fires when Event payload
connect The WebSocket has connected (including after a reconnect). ConnectEvent
disconnect The WebSocket has dropped. DisconnectEvent

Use these to track connection state without polling client.is_connected.

Listening to all events

Pass the special "all" event name (or NotificationEvent.All) to register one callback for every notification — node-pushed events plus connect / disconnect. You still need subscribe_* for any node-pushed event you want to receive; "all" only multiplexes delivery, it doesn't subscribe on your behalf.

def on_any(event):
    print(event["type"], event)

client.add_event_listener("all", on_any)
await client.subscribe_block_added()
await client.subscribe_virtual_daa_score_changed()

Event payload shape

Every callback receives a dict with a "type" key naming the event. The remaining keys depend on the event.

utxos-changed

UtxosChangedEvent is the only event that does not nest its body under "data" — it's flattened so callbacks can read event["added"] directly. The "added" and "removed" lists hold RpcUtxosByAddressesEntry items.

The "type" value is the PascalCase variant name ("UtxosChanged", "BlockAdded", …) — the kebab-case form ("utxos-changed", "block-added", …) is only used when registering listeners.

{
    "type": "UtxosChanged",
    "added": [
        {
            "address": "kaspa:qz...",
            "outpoint": {"transactionId": "...", "index": 0},
            "utxoEntry": {
                "amount": 100000000,
                "scriptPublicKey": {"version": 0, "script": "..."},
                "blockDaaScore": 123456789,
                "isCoinbase": False,
            },
        },
    ],
    "removed": [],
}

All other node-pushed events

A "data" key holds the notification body. Each event has a wrapper TypedDict (e.g. BlockAddedEvent) and a body TypedDict (e.g. RpcBlockAddedNotification). See the Available events table for the full list. For example, a virtual-daa-score-changed callback receives:

{
    "type": "VirtualDaaScoreChanged",
    "data": {
        "virtualDaaScore": 123456789,
    },
}

connect / disconnect

ConnectEvent and DisconnectEvent carry a single "rpc" key holding the node URL as a string:

{"type": "connect", "rpc": "wss://node.example.com:17110"}

The bundled Notification class wraps notifications internally; for callback type hints, use the per-event TypedDicts above.

Examples

Watching addresses for UTXO changes

Pass Address instances (or strings parsed by Address(...)):

from kaspa import Address

addresses = [Address("kaspa:qz...")]

def on_change(event):
    for added in event.get("added", []):
        print("+", added["utxoEntry"]["amount"])
    for removed in event.get("removed", []):
        print("-", removed["utxoEntry"]["amount"])

client.add_event_listener("utxos-changed", on_change)
await client.subscribe_utxos_changed(addresses)

# ... later ...
await client.unsubscribe_utxos_changed(addresses)

To watch a managed-wallet account instead of raw addresses, use the Wallet's Balance and Maturity events — see Wallet → Events.

Block events

def on_block(event):
    print("new block:", event["data"]["block"]["header"]["hash"])

client.add_event_listener("block-added", on_block)
await client.subscribe_block_added()

Virtual chain progression

def on_chain(event):
    data = event["data"]
    print("added:", data["addedChainBlockHashes"])
    print("removed:", data["removedChainBlockHashes"])

client.add_event_listener("virtual-chain-changed", on_chain)
await client.subscribe_virtual_chain_changed(include_accepted_transaction_ids=True)

With include_accepted_transaction_ids=True, the payload doubles as a confirmation feed — every accepted transaction id appears in event["data"]["acceptedTransactionIds"]. For the one-shot equivalent, see get_virtual_chain_from_block.

Connection state

def on_connect(event):
    print("connected to", event["rpc"])

def on_disconnect(event):
    print("disconnected from", event["rpc"])

client.add_event_listener("connect", on_connect)
client.add_event_listener("disconnect", on_disconnect)

No subscribe_* is needed — the client emits these itself when the WebSocket transitions.

Listener bookkeeping

client.add_event_listener("block-added", callback)             # add
client.add_event_listener("block-added", callback, extra)      # forward extra arg

client.remove_event_listener("block-added", callback)          # remove specific
client.remove_event_listener("block-added")                    # remove all for event
client.remove_all_event_listeners()                            # remove all globally

Listeners outlive a single subscription cycle. Re-subscribing after an unsubscribe does not re-fire previously delivered events. To catch up, do a one-shot get_utxos_by_addresses (or equivalent) before re-subscribing.

Where to next