If you hold KAS and use a self-custody tool like Kaspa Safe, you probably trust that the service knows the real state of your on-chain vault. But *how* does it know? Does it run its own node, or does it ask someone else's API and hope the answer is honest?
For us, the answer is straightforward: every Kaspa Forge service — vault monitoring, escrow deal lifecycle, payment confirmation — reads chain state from a kaspad instance we operate ourselves, connected via gRPC. This article explains why, how it works under the hood, and what it actually costs.
Why not just use someone else's API?
There's nothing wrong with public APIs for block explorers, portfolio trackers, or casual queries. But the moment your software acts on that data — triggers a vault alarm, releases escrow funds, or marks a payment as confirmed — you've introduced a trust assumption that contradicts the point of a decentralized ledger.
A public API can return stale UTXOs during congestion. It can go down during a critical window. Its operator can filter responses. For a service where the cost of a wrong answer is someone's covenant funds, "trust us, we checked" isn't enough. You need the answer to come from a node that participates in consensus — and you need to run it yourself.
How a Kaspa node works under the hood
Kaspa doesn't use a single chain. Its consensus is a blockDAG — a directed acyclic graph where multiple blocks can be produced simultaneously and merged into a consistent view. The GHOSTDAG protocol orders these blocks into a "selected chain" by having each block vote on a selected parent; blocks well-connected to this chain are blue (rewarded), while poorly connected blocks are red (accepted but not rewarded). The Kaspa wiki describes the full algorithm, including the difficulty adjustment window of 2,641 blocks.
GHOSTDAG — Kaspa's ordering protocol for the blockDAG. Each block selects a parent; the resulting path determines transaction order. Blue blocks are well-connected and receive mining rewards; red blocks are merged but not rewarded. This allows high block rates (currently 10 BPS on mainnet) without sacrificing security.
A running kaspad node maintains:
- The full DAG — every validated block, from genesis to the current *virtual block* (the node's aggregate view of all tips).
- The UTXO set — the current set of unspent transaction outputs, the authoritative source for "does this address have funds?"
- An optional
utxoindex— a reverse mapping from addresses to their UTXOs. Without it, finding an address's balance means scanning the entire UTXO set. With it, a single gRPC call returns every UTXO belonging to an address. This index must be enabled before IBD starts; enabling it later requires a resync.
The node exposes all of this through a gRPC interface over TCP. Clients send typed requests — *get UTXOs by addresses*, *submit a transaction*, *query block data* — and receive structured binary responses. No JSON overhead, no REST indirection. As the wiki notes, Kaspa's network communication is TCP-based, and your gRPC client connects alongside the node's peers as a first-class participant.
Confirmations in a blockDAG
At 10 blocks per second, a transaction included in a block is confirmed within seconds. The confirmation depth is measured in blue score — the count of blue blocks in the DAG's past from the accepting block's perspective. Once the blue score gap exceeds a configured threshold, the transaction is final. For Kaspa Forge's payment gateway, we set this threshold high enough to cover the unlikely case of a deep reorg, but low enough that buyers don't wait long. A few seconds of headroom is typically sufficient.
Our node setup: architecture, not credentials
We run kaspad on a dedicated machine, separate from the web server that serves kaspaforge.org. This separation is deliberate: the node is a stateful consensus participant requiring stable uptime, fast disk I/O for the DAG, and bandwidth for its peer connections (the default is 8 outbound peers). Sharing a machine with nginx and application processes would create resource contention.
The application-server connection
Our backend — a single Rust process handling all Kaspa Forge services (Kaspa Safe, Kaspa Escrow, payments, marketplace) — opens a persistent gRPC connection to the node at startup. A reconnect loop wraps the connection: if the node restarts for an upgrade or machine reboot, the server detects the drop and retries with exponential backoff. During reconnection, watchers pause. On-chain contracts continue operating on the blockDAG; the watchers catch up on the next successful poll.
Watchers: the 10-second heartbeat
The core polling loop runs every 10 seconds. For each registered vault or escrow contract, the watcher:
loop every 10s:
for each registered_address:
utxos = grpc.getUtxosByAddresses([address])
if utxos != snapshot[address]:
diff = analyze(snapshot[address], utxos)
trigger_alerts(diff) # Telegram, email
trigger_actions(diff) # auto-release, auto-inherit
snapshot[address] = utxos
The diff analysis determines what happened: a new UTXO appeared (deposit), a UTXO was consumed (withdrawal initiated), a vault mode changed (cancellation or completion). Each change type maps to a specific action — send a Telegram alarm about an initiation, email the heir about inactivity, broadcast a keyless complete transaction once the delay expires.
We chose polling over subscription-based alternatives for a practical reason: gRPC stream subscriptions can drop silently on network hiccups, and recovering the missed events requires its own reconciliation logic. Polling with persistent snapshots is idempotent — miss a cycle, and the next one catches everything. The trade-off is up to 10 seconds of latency, which is fine for vault alarms (the covenant's time delay gives you hours) and acceptable for escrow and payments.
Transaction submission
When a user signs a vault initiation, escrow release, or wallet send in the browser, the signed transaction flows through our backend to the node via gRPC submitTransaction. The node validates it against consensus rules — including the Silverscript covenant constraints — adds it to its mempool, and broadcasts it to peers. We don't relay transactions ourselves.
Public node front for recovery
Non-custodial means the service must be recoverable without us. Our open-source vaultctl CLI connects to any Kaspa v2+ node to manage vaults from a terminal. We also expose a public gRPC endpoint behind an nginx stream proxy that forwards TCP connections to the same kaspad instance, hiding the node's direct address.
The proxy includes rate limiting — per-IP connection caps, global concurrency limits, slowloris timeouts — because the node serves production traffic and must stay responsive for our watchers. This is a pragmatic middle ground: a fully public node is a DoS target, but a locked-down proxy gives recovery tools a reliable endpoint when you need it.
Every Kaspa Safe vault and Kaspa Escrow deal is monitored by our own node — no third-party API sits between your on-chain contract and the service that watches it. If you want to explore the tools, you can create a vault or read how the vault works to see the full non-custodial architecture.
What the node serves in production
| Service | What it queries | Interval | Why own node |
|---|---|---|---|
| Kaspa Safe | UTXOs at vault address | 10s per vault | Detect deposits, initiations, completions; fire alarms |
| Kaspa Escrow | UTXOs at escrow address | 10s per deal | Detect funding, auto-release after dispute window, handle timeouts |
| Payments | UTXOs at HD-derived address | 10s per pending order | Confirm payment with configurable blue-score threshold |
| Transaction relay | Submit signed tx | On demand | Broadcast user-signed transactions to the network |
One thing the node does *not* do well is transaction history. The utxoindex tracks current state, not a chronological ledger. For the Desk wallet's transaction list, we proxy requests to an external indexer — the client contacts only our backend, so its IP never reaches the indexer. The node is a consensus engine, not a block explorer. This is an honest architectural boundary.
Hardware, bandwidth, and practical costs
Running kaspad for production services isn't exotic, but it's not a Raspberry Pi project either:
- Storage. The UTXO set and DAG metadata grow with the chain. With
utxoindexenabled, plan for more than a bare-minimum install. SSD is strongly recommended — UTXO lookups are I/O-bound. - RAM. kaspad is written in Go and is reasonably memory-efficient, but
utxoindexadds overhead. Several GB of working memory is realistic. - Bandwidth. Steady-state traffic is moderate with 8 outbound peers. Initial Block Download and peer discovery are burstier.
- IBD. Syncing from genesis takes time. The node downloads and validates every block in the DAG's history. Until IBD completes,
utxoindexqueries won't return current data — your services can't go live until the node is fully synced. - Upgrades. Protocol changes (like the Toccata hardfork that brought covenants to mainnet) require coordinated node updates. We test on testnet first and schedule mainnet upgrades during low-traffic windows.
Trade-offs and honest limitations
Single point of failure. We run one kaspad instance for production. If it goes down, watchers stop. On-chain funds are safe — covenants enforce their rules regardless — but alerts and auto-actions pause until the node recovers. We mitigate this with process supervision and monitoring, but accept the limitation. A multi-node setup would add complexity we haven't needed yet.
Polling latency. Ten seconds between on-chain events and our reaction. For vault alarms, this is a rounding error against a 24-hour withdrawal delay. For payments, the buyer sees "confirmed" a few seconds after the network confirms. We could reduce the interval, but it would multiply gRPC calls without meaningful benefit.
External dependency for history. Transaction-history queries depend on a separate indexer we don't operate. If it goes down, the Desk wallet shows stale transaction logs while balances (from our node) and signing (in the browser) continue working. This is a real trade-off we've accepted rather than building and maintaining our own historical index.
Resource commitment. Someone has to handle node upgrades, disk growth, and the occasional peer connectivity issue. For a solo developer, a well-maintained public node might be more practical. We chose to run our own because our services monitor contracts that hold real funds — the cost of trusting the wrong API is too high.
The takeaway
If you build on Kaspa — custody tools, payment rails, anything that touches other people's money — run your own node. The protocol gives you a gRPC interface, a UTXO index, and direct participation in consensus. The kaspad source is open, the hardware requirements are manageable, and the wiki's developer knowledge base covers the internals.
The alternative is trusting someone else's API for your source of truth. For Kaspa Forge, that was a non-starter.
FAQ
What is gRPC and why does Kaspa use it?
gRPC is a high-performance binary RPC framework. Kaspa nodes expose gRPC endpoints over TCP for querying chain state (blocks, UTXOs, balances) and submitting transactions. It's more efficient than REST/JSON for the kind of high-frequency polling that wallet and service software performs.
Can I run a Kaspa node myself?
Yes. Kaspa's codebase is open source — you run kaspad, optionally enable utxoindex for address-based lookups, and wait for Initial Block Download to complete. The Kaspa wiki's developer knowledge base covers the network internals.
Does running your own node mean Kaspa Forge can see my private keys?
No. Our node sees only public addresses and UTXOs. Private keys are generated and held exclusively in your browser via WASM, and backed up in your encrypted .age key-file. The node has zero access to signing material — how the vault works explains this separation in detail.
What happens to my vault or escrow if your node goes offline?
On-chain contracts are unaffected — funds sit in covenants on the blockDAG, enforced by consensus rules, not by any server. What pauses are our watchers: alerts, auto-completions, and payment confirmations resume once the node reconnects. You can always recover independently using vaultctl against any public Kaspa node.
Why not use a public API or hosted indexer for everything?
Public APIs can go down, return stale data, or censor responses. For services that monitor on-chain vaults and escrow contracts, the cost of a wrong or delayed answer is someone's funds. Direct gRPC access to your own node's UTXO set eliminates that trust assumption.
What's the difference between UTXO state and transaction history?
The node's utxoindex tracks current unspent outputs by address — what you need for balance checks and payment detection. A chronological list of past transactions requires a separate block-indexing service, which is why Kaspa Forge proxies tx-history requests to an external indexer while keeping balance queries on our own node.
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
