Disclaimer: NKaspa is alpha software (0.1.11-alpha) and has not been security-audited. It is a community project, not an official Kaspa project. This tutorial runs on Kaspa testnet-10 only. Use test coins and small amounts. Do not use it for real funds. Public nodes are run by third parties and are not guaranteed to be available.
This guide shows how to use Kaspa from C# with NKaspa, a community .NET library on NuGet. You will connect to a testnet-10 node over wRPC, check the node, read UTXOs, build and sign a transaction, verify it locally, and submit it.
Flow: KaspaRpcClient.Create → ConnectAsync → GetServerInfoAsync → GetUtxosByAddressesAsync → TransactionBuilder → tx.Verify → SubmitTransactionAsync.
wRPC, not gRPC
NKaspa does not include a gRPC client. It talks to kaspad over wRPC on a WebSocket, using either JSON or Borsh encoding. If you need gRPC, use the official Kaspa proto files instead (see docs.kaspa.org).
Prerequisites
- .NET 8 SDK (the repo's
global.jsonpins 8.0.427 withrollForward: latestFeature). - The package:
dotnet add package NKaspa --version 0.1.11-alpha 0.1.11-alpha is on nuget.org and restores into a new net8.0 console project.
- A testnet-10 wRPC node. Run your own kaspad, or use one of the two testnet-10 fallback nodes listed in the
KaspaResolversource (src/NKaspa/RPC/KaspaResolver.cs):-
wss://vector-10.kaspa.green/kaspa/testnet-10/wrpc/json(or/borsh) -
wss://muon-10.kaspa.blue/kaspa/testnet-10/wrpc/json(or/borsh)
-
These are third-party nodes, not run by NKaspa, and they are not guaranteed to be available. At the time of writing, vector-10 answered and muon-10 did not.
- Testnet KAS from the testnet-10 faucet listed in the official Kaspa Wiki: https://faucet-tn10.kaspanet.io/ . Open it in a browser; it asks for a human check (captcha).
Ports: JSON or Borsh?
| Network | gRPC (rejected) | wRPC Borsh | wRPC JSON |
|---|---|---|---|
| mainnet | 16110 | 17110 | 18110 |
| testnet-10 | 16210 | 17210 | 18210 |
| simnet | 16510 | 17510 | 18510 |
| devnet | 16610 | 17610 | 18610 |
With KaspaRpcEncoding.Auto (the default), the URL path wins over the port: a path with /wrpc/borsh selects Borsh and /wrpc/json selects JSON. Otherwise the default wRPC port decides. If neither the path nor the port is recognized (for example wss on 443 with no hint), Create throws; it does not guess. A gRPC port always throws ArgumentException with a message pointing you to the wRPC ports.
If the server sends the wrong frame type (a JSON client gets binary, or a Borsh client gets text), pending requests fail with KaspaRpcEncodingMismatchException right away. The client does not wait for a timeout and does not reconnect.
The two encodings do the same job. Pick whichever your node exposes.
Connect to the node
using NKaspa; using NKaspa.RPC; await using var rpc = KaspaRpcClient.Create(url); // returns a KaspaRpcConnection; does NOT connect await rpc.ConnectAsync(cancellationToken); The signature is:
public static KaspaRpcConnection Create(string url, KaspaRpcEncoding encoding = KaspaRpcEncoding.Auto, KaspaWebSocketOptions? options = null) KaspaRpcConnection implements IKaspaRpcClient and IAsyncDisposable, so dispose it with await using. KaspaWebSocketOptions controls the connect timeout (default 15 s), request timeout (default 30 s), AutoReconnect, and MaxMessageBytes (default 16 MiB).
Check the node first
var info = await rpc.GetServerInfoAsync(cancellationToken); Console.WriteLine($"network={info.NetworkId} synced={info.IsSynced} utxoindex={info.HasUtxoIndex}"); if (!info.IsSynced || !info.HasUtxoIndex) return; // stop here Stop if the node is not synced or does not have the UTXO index; you cannot list UTXOs without it. On testnet-10, NetworkId is testnet-10.
Derive a testnet address from a mnemonic
Read the mnemonic from an environment variable. Never put it in source code, logs, or screenshots.
var words = Environment.GetEnvironmentVariable("NKASPA_MNEMONIC"); var mnemonic = new Mnemonic(words, Wordlist.English); if (!mnemonic.IsValidChecksum) return; using var master = mnemonic.DeriveExtKey(); using var receive = master.Derive(KeyPath.KaspaReceive(0)); using var changeKey = master.Derive(KeyPath.KaspaChange(0)); using var spendKey = receive.PrivateKey; var network = Network.Testnet; var own = spendKey.GetAddress(network); // kaspatest:... var changeAddress = changeKey.PrivateKey.GetAddress(network); Always pass Network.Testnet explicitly. In 0.1.11-alpha, ExtPubKey.Parse cannot tell a mainnet extended public key from a testnet one, so the network comes from what you pass to GetAddress. Testnet addresses start with kaspatest:.
See also: docs/tutorials/generate-kaspa-address-csharp.md in the repo.
Read UTXOs and turn them into coins
var listed = await rpc.GetUtxosByAddressesAsync(new[] { own.ToString() }, cancellationToken); if (listed.Count == 0) { Console.WriteLine($"No UTXO on {own}. Nothing was submitted."); return; } var coins = new Coin[listed.Count]; for (var i = 0; i < listed.Count; i++) coins[i] = new Coin(listed[i].Outpoint, listed[i].Entry); Other read calls: GetBalanceByAddressAsync, GetBalancesByAddressesAsync, GetFeeEstimateAsync.
Build and sign the transaction
var payee = KaspaAddress.Parse(to, network); var amount = Money.Coins(0.1m); // small amount var builder = new TransactionBuilder(network).AddKeys(spendKey).AddCoins(coins); var tx = builder .Send(payee, amount) .SetChange(changeAddress) .SendMinimumRelayFee() .BuildTransaction(true); // true = sign Notes:
-
SendEstimatedFees(FeeRate)is an alternative to the minimum relay fee. -
SetVersion(1)builds a version-1 transaction. - Change below
MinimumChange(default 0.02 KAS) is folded into the fee. Checkbuilder.ChangeFoldedIntoFeeto see how much. - Reference numbers from the repo's offline sample (no network): one input pays 203,600 sompi (0.002036 KAS) at relay mass 2,036. These are offline sample values, not a promise of what a node will charge.
Verify locally before you broadcast
Do not submit unless all of these pass:
- One output pays exactly
amountto the payee's script. - Sum of inputs ≥ sum of outputs.
-
tx.Verify(entries)returns true (entries= theUtxoEntryof each input, in input order). You can also check each input withScript.VerifyScript. - The fee equals
MassCalculator.MinimumRelayFee(MassCalculator.Calculate(tx, entries)).
if (!paid || outputSum > inputSum || !tx.Verify(entries)) return; // nothing submitted var fee = inputSum - outputSum; var minimum = MassCalculator.MinimumRelayFee(MassCalculator.Calculate(tx, entries)); if (fee != minimum) return; The full loop that builds entries, inputSum, outputSum and paid is in samples/Tutorials/SendTutorial.cs.
Submit to testnet
uint256 id = await rpc.SubmitTransactionAsync(tx, false, cancellationToken); Console.WriteLine($"submitted: {id}"); Signature: Task<uint256> SubmitTransactionAsync(Transaction transaction, bool allowOrphan = false, CancellationToken cancellationToken = default). It returns the transaction id.
Example output:
network: testnet fee-sompi: <fee> submitted: <txid> The complete method is SendTutorial.SendOnTestnetAsync in samples/Tutorials. It defaults to Network.Testnet and refuses to run unless NKASPA_MNEMONIC, NKASPA_WRPC_URL, NKASPA_TO and NKASPA_AMOUNT are set. It also refuses mainnet by default. The sample's Main does not call it.
Not tested end to end; example code. The connect and query steps above (
ConnectAsync,GetServerInfoAsync, balance and UTXO reads) were run successfully against thevector-10testnet node over both JSON and Borsh. The build, sign and submit steps have not been run against a live node. Verify them in your own environment with test coins.
samples/Tutorials only runs offline checks and does not send. To try the send path, create a console project that references NKaspa 0.1.11-alpha, copy in SendTutorial.cs, and call it yourself:
return await NKaspa.Samples.Tutorials.SendTutorial.SendOnTestnetAsync(Console.Out, Console.Error); export NKASPA_MNEMONIC='<throwaway testnet mnemonic>' export NKASPA_WRPC_URL='wss://vector-10.kaspa.green/kaspa/testnet-10/wrpc/borsh' export NKASPA_TO='<another kaspatest: address>' export NKASPA_AMOUNT='0.1' dotnet run Reconnects and subscriptions
Set AutoReconnect in KaspaWebSocketOptions to reconnect after a drop. Subscriptions are not restored automatically. After a reconnect, call NotifyUtxosChangedAsync (and any other Notify*) again and re-query the balance so you don't miss changes.
KaspaRpcConnection exposes two events: Reconnected (Action, raised after a dropped socket opens again; not raised by the first ConnectAsync) and StateChanged (Action<KaspaWebSocketState>). State is a read-only property, not an event.
rpc.StateChanged += state => Console.WriteLine($"state: {state}"); rpc.Reconnected += () => { // Subscriptions are gone: subscribe again with NotifyUtxosChangedAsync // and re-read the balance (do async work off this handler). }; For a full watch-only example, see samples/AspNetCheckout (on main only, added after the 0.1.11-alpha release; it is a sample, not a library API).
Troubleshooting
-
ArgumentExceptionmentioning gRPC — you used 16x10. Use 17210 (Borsh) or 18210 (JSON) on testnet-10. -
Createthrows on a 443 URL — add/wrpc/jsonor/wrpc/borshto the path, or pass the encoding explicitly. -
KaspaRpcEncodingMismatchException— the endpoint speaks the other encoding. Switch the path or encoding. - No UTXOs — fund the address from a faucet and wait; also confirm the node has the UTXO index.
- Node rejects the fee — a fee computed at the pre-Toccata rate (1 sompi/gram) is below the post-Toccata floor (100 sompi/gram). Use
SendMinimumRelayFee()or a fee estimate for the current network. - Change "disappeared" — it was below
MinimumChangeand folded into the fee; checkChangeFoldedIntoFee.
FAQ
Is there a Kaspa SDK for C#? Yes, there is a community library, NKaspa (NuGet NKaspa, 0.1.11-alpha, unaudited). It covers addresses, HD derivation, a wRPC client (JSON and Borsh), and transaction building and signing. It is not an official Kaspa project.
Does NKaspa support gRPC? No. It uses wRPC over WebSocket only. Passing a 16x10 gRPC port to KaspaRpcClient.Create throws and points you to 17x10 (Borsh) or 18x10 (JSON).
Should I use JSON or Borsh? Either. Both expose the same calls. KaspaRpcEncoding.Auto picks one from the URL path or port, so use whatever your node offers.
Does KaspaRpcClient.Create connect automatically? No. It returns a KaspaRpcConnection; call ConnectAsync, and dispose it with await using.
How do I send a Kaspa transaction in C#? Read UTXOs, build and sign with TransactionBuilder, verify locally with tx.Verify, then call SubmitTransactionAsync. This tutorial only does this on testnet.
Do subscriptions survive a disconnect? No. With AutoReconnect the connection comes back, but you must subscribe again.
How do I make sure I'm on testnet? Pass Network.Testnet explicitly and check that addresses start with kaspatest:. Don't rely on ExtPubKey.Parse to tell you the network.
Can I sign offline? Yes. Building and signing need no connection. BuildPSKT produces a partially signed transaction.
Can I use it with real funds? Not recommended. NKaspa is alpha and unaudited. Test on testnet with small amounts and judge the risk yourself.
Next steps
- Accept payments:
samples/AspNetCheckout(main only) and the checkout tutorial (link after publication). - Generate addresses:
docs/tutorials/generate-kaspa-address-csharp.md. - Send tutorial in the NKaspa repo:
docs/tutorials/send-kaspa-transaction-dotnet.md. - Multisig with PSKT:
samples/PsktMultisig. Node reads:samples/NodeQuery. Spending:samples/Spend. - NBitcoin users:
docs/NBitcoin-Mapping.en.md. - NKaspaDoc (API reference and guides, GitHub repo): https://github.com/lifeme2027/NKaspaDoc
If this helped, a star on GitHub is welcome: github.com/lifeme2027/NKaspa. Try dotnet add package NKaspa --version 0.1.11-alpha on testnet and open an issue if you hit a problem.
Disclaimer: NKaspa is alpha software (0.1.11-alpha) and has not been security-audited. It is a community project, not an official Kaspa project. This tutorial runs on Kaspa testnet-10 only. Use test coins and small amounts. Do not use it for real funds. Public nodes are run by third parties and are not guaranteed to be available.