Warda Protocol Developer guide ← wardaprotocol.com · v0.4.2 · testnet-10

Building on Warda

Give an agent a budget the network enforces.

A grant is a UTXO locked by a covenant. It names an agent key, a total budget, a per-spend cap, a rate limit, a validity window and an allowlist of payees. The agent spends from it directly — no co-signing, no relay, no custody — and consensus rejects anything outside those limits.

Steal the agent key and you inherit exactly the same limits. That is the point.

Grant · live authority check

budget total1,000.00000 KAS
spent to date100.00000
reserved for children0.00000
per-spend cap200.00000
this epoch · limit 50050.00000 used
coin in the UTXO898.00000

The same six checks the covenant runs, in the same order. On chain these are consensus rules, not advice: a transaction failing one is not rejected by a server, it is invalid.

Before you start

One prerequisite, and there is now a way round it.

You need a Kaspa testnet-10 node you can reach. Everything in this SDK builds and signs transactions locally — no server sees a key — but building one means reading the UTXO set, and reading the UTXO set means talking to a node.

To try it, borrow ours. It is a laptop behind a tunnel, forwarding four methods and refusing the rest by name; /start says exactly what it is and when it is down.

# enough to finish the quickstart. Not enough to build on. export WARDA_RPC_JSON=wss://warda-node.tailc0c0ec.ts.net

To rely on it, run your own — and if you are building anything real, do:

BASH
kaspad --testnet --netsuffix=10 --utxoindex \
      --rpclisten-json=127.0.0.1:18210

Both flags matter. Without --utxoindex a node reports every grant as spent. The JSON wRPC port is separate from the Borsh one (18210 versus 17210) and only listens when that flag is given — note the =, which the flag requires.

WARDA_RESOLVER is supported, and here is the honest note it needs. A Kaspa Resolver picks a public node for you, and there are sixteen public instances — named hosts under kaspa.stream, .red, .green and .blue, listed in rusty-kaspa's Resolvers.toml. This page previously said none existed; that was wrong. What is true is narrower and worse: they serve borsh, and this SDK speaks JSON. Measured on 10 September 2026 — 404 for /wrpc/json, 200 for /wrpc/borsh, on mainnet and testnet both. So the resolver path finds you nothing today, and running a node is not one option of two: it is the option. The SDK still compiles no resolver host in, because naming one is a decision about who you trust to answer "which node", and that belongs to you.

There is also a floor under how small a payment can be. Kaspa charges storage mass for the small outputs a transaction creates, and a payment of 0.01 KAS is refused by consensus — massing 1,000,000 against a ceiling of 500,000 — with the grant, the budget and the fee all perfectly fine. In practice nothing under about 0.02 KAS can be broadcast, so per-request micropayments below that are not a thing this chain does.

tools/check-node.ts interrogates whichever node you point at before you build anything against it. Each thing it checks fails by returning a plausible wrong answer that reads as a problem with your grant rather than with the node.

Install

Five packages. Nothing to compile.

Compiled JavaScript with TypeScript declarations. Node 20+, no build step, no Rust toolchain and no Silverscript compiler — address derivation splices values into a compiled covenant template that ships inside the package.

# the command — the whole lifecycle, without writing code npm install -g @warda_protocol/cli # the seller's half — take an agent's money, verified on chain npm install @warda_protocol/vendor # the SDK — build, sign and verify grants npm install @warda_protocol/kaspa # the MCP server — hand an agent framework a Warda tool npx @warda_protocol/mcp # the x402 adapter — pay metered APIs from a grant npm install @warda_protocol/x402

Create a grant

TYPESCRIPT
import { buildGenesis, attachGenesisSignature, signDigest, RecipientSet } from "@warda_protocol/kaspa";
import template from "@warda_protocol/kaspa/covenant-template.json" with { type: "json" };

// Who this agent may pay, committed as a Merkle root.
const payees = new RecipientSet([vendorA, vendorB]);

const grant = {
  authority: { principalKey, revocationKey },
  state: {
    agentKey,
    budgetTotal:  1_000_00000000n,  // 1000 KAS, ever
    maxPerSpend:    200_00000000n,  // 200 KAS per payment
    epochLimit:     500_00000000n,  // 500 KAS ...
    epochLength:            1000n,  // ... per 1000 blocks
    recipientsRoot: payees.rootHex,
    notBefore, expiresAt,
    delegationDepth: 2n,
    /* accounting starts at zero */
  },
};

const unsigned = buildGenesis({ template, grant, funding, changeScriptPublicKey, fee });
const tx = attachGenesisSignature(unsigned, signDigest(unsigned.sighash, principalSecret));

Paying a metered API

TYPESCRIPT
import { WardaPayer, wardaFetch } from "@warda_protocol/x402";

const payer = new WardaPayer({
  grant: { template, authority, state, recipients },
  node, sign: agentSecret,             // or a remote signer function
});

// A paid endpoint, called like a free one. The 402, the covenant
// spend and the X-PAYMENT proof are handled underneath.
const res = await wardaFetch("https://vendor.example/compute", {
  method: "POST",
  body: JSON.stringify({ prompt: "explain GHOSTDAG" }),
}, { payer });

It never pays twice. A second 402 means the payment is still settling, so the same proof is re-presented rather than a new one bought — and if the server never settles, it says the money is spent instead of paying again.

The agent spends, unattended

TYPESCRIPT
import { signSpend } from "@warda_protocol/kaspa";

const { tx } = signSpend({
  template, authority, state,
  utxo,                             // the grant's current coin
  amount:   50_00000000n,
  recipient: vendorA,
  proof:     payees.proof(vendorA), // proves the payee is allowed
  claimedDaa, fee, computeBudget: 16,
}, agentSecret);                    // the principal is not involved

The SDK deliberately does not decide whether a spend is allowed — the covenant does, on chain, and it is the only thing that can. If the SDK reimplemented the rules, a divergence would fail by wrongly permitting: it says yes, and a budget drains on a spend nobody authorised. Reimplementing assembly fails the other way — a mistake produces a transaction the network rejects, which is loud.

The distinction it all rests on

Where does the limit actually live?

In your process

The usual pattern: fund a hot wallet with "only what the agent should spend", and keep a running total in the code that pays.

The x402 documentation describes the standard control: the client "applies a $1 USD spend cap unless you override spendControls" — a default in the process that pays.

So the cap resets when the process does. It is bypassed by a crash, a redeploy, a second instance, or anyone who reads the key out of env. The wallet balance is the only true limit.

In consensus

The budget, the caps, the window and the payee allowlist are part of the script that unlocks the coin.

A spend exceeding any of them is not refused by a server. It is an invalid transaction — no node relays it, no miner can include it.

Restarting changes nothing. Nor does compromising the agent. The limits are not enforced by the code holding the key.

Anatomy

Six constraints, three keys.

A grant is state encoded into an address. Change any field and it is a different address — which is why a grant is never edited, only spent forward.

FieldWhat it binds
budgetTotalThe most this grant can ever spend, across its life and every child it delegates to.
maxPerSpendA ceiling on any single payment. Caps the blast radius of one bad decision.
epochLimitA rate: how much per epochLength blocks. Bounds how fast a compromise drains.
notBefore / expiresAtA validity window. After expiry the principal may sweep the remainder home.
recipientsRootA Merkle root over allowed payees. Every spend carries an inclusion proof; there is no way to pay anyone else.
delegationDepthHow many further levels of sub-agent this grant may create. Zero makes it a leaf.
KeyPowerHeld by
agentKeySpend within the limits; delegate to sub-agents.The agent, online, on the machine doing the work.
revocationKeyStop the grant. Sweeps the balance to the principal — it can never redirect it elsewhere.A monitor or kill-switch you do not have to trust with the money.
principalKeyReceive. Reclaim the remainder after expiry.The owner. Signs once at creation, then offline.

That split is load-bearing, and it took two real bugs to make it true. One version let the revocation path burn the balance to fees; a later one let it sweep a delegated child anywhere it liked. Both turned a stop capability into a destroy or take one. Both are fixed, and both now have an attack test that fails if they return.

Lifecycle

Six operations, each with its own signer.

Every one is an entrypoint in the covenant. The address changes at each step, because the address is the state.

Principal signs · once

Genesis

An ordinary payment that happens to pay into a covenant address. From here the principal can go offline permanently.

Agent signs

Spend

Pay an allowlisted recipient within every cap. The remainder continues to a new address encoding the updated accounting.

Agent signs

Delegate

Hand a sub-agent its own narrower grant, without asking the principal and without custody. The parent reserves exactly what the child receives, so authority is subdivided and never created. A child may narrow the payee list, the caps, the rate and the window — never widen any of them.

Agent + revocation key

Settle

Take a finished child back in. Its remaining coin returns to the parent and what it spent is charged against the parent's budget. Without this, delegation would be one-way: a grant that subdivided itself would shrink permanently.

Revocation key

Revoke

Stop the grant at any time. The balance goes to the principal and nowhere else — the covenant checks destination and amount, so a monitor holding this key can halt an agent without being able to take or burn its money.

Principal signs

Reclaim

After expiry, sweep the remainder home. This is a right that opens at expiry, not a prohibition that closes: a UTXO covenant can say "not before X", never "must be spent before X".

Proof of work, in the literal sense

One agent, one paid API call, on testnet-10.

A real x402 endpoint priced at 0.2 KAS, paid by a covenant spend, with the vendor verifying against the UTXO set before serving. Reproduce it with x402/demo/testnet-demo.ts.

── 1. a payee the grant never committed to ────────────────── refused, nothing spent: kaspatest:qrxvenxven… is not on this grant's allowlist, so no inclusion proof places it in the recipients tree. There is no valid transaction that pays them — not one the network would reject, none at all. ── 2. paying the vendor, on chain ────────────────────────── broadcast 267e1bac1270fde34d9719d676b378745fb57007062cd1b6de52f2d2a4af433e settling, retrying in 1106ms (same proof) 200 in 1.1s ── 3. what moved ─────────────────────────────────────────── grant was at : kaspatest:pqckh2ay4rxxtpuv3g9snaclza47tz0f0uxmfwsaflr0uupmg4yx6eddrwck5 grant is now : kaspatest:pp985pts8r297cpl8u28e6hpztxj4mss9fpnvcjzla4p79rrl7glw5apwk4dg spent total : 0 → 20000000 headroom left : 50000000 sompi

The settling retry is the part worth watching. The vendor answered 402 a second time because the coin was not yet in the UTXO set, and the client re-presented the same proof rather than buying a second call — the one failure mode in this design capable of draining a budget through nobody's fault.

Checking it yourself

The receipt was written by the process that made the payment, which is precisely the party not to take it from. So every claim in it is re-derived from the UTXO set:

BASH
node --experimental-strip-types x402/demo/verify-receipt.ts

  ok   the vendor was paid 20000000 sompi by 267e1bac1270fde3…
  ok   the address the grant spent FROM is now empty
  ok   the successor address holds exactly one coin
  ok   the coin balances: 300000000 in, 20000000 paid, 2000000 fee, 278000000 left
  ok   spentTotal advanced by exactly the amount paid (20000000)
  ok   the payment was within the per-spend cap (50000000)

the chain agrees with this receipt.

One thing the first live run taught, and the code now knows. Kaspa prices by mass, and a covenant spend carries the whole 5.7 KB redeem script in its signature script — so it is ~6 KB on the wire and costs roughly 1,511,400 sompi, not the 1,000,000 that covers a plain transfer. v3 fitted under the old default; v4 does not, because settlement and the subset witness made the script bigger. A node will accept the signature and still refuse to relay. The adapter now defaults higher and translates that rejection into the exact figure the node asked for.

Evidence

Fourteen attacks, run against the real script engine.

Golden vectors prove two implementations agree about a transaction meant to work. They cannot catch a covenant that permits too much. These do the other half — and every attack sits beside its nearest legitimate twin, because a covenant that refuses everything would pass a suite made only of refusals.

ProbeEngineWhat it establishes
epoch-rewindrefusedThe agent picks the block height it claims. Claiming an earlier epoch once reset the allowance to zero, repeatably. The rate cap limited nothing at all.
epoch-forwardacceptedA genuinely later epoch still gets its fresh allowance. The fix must not block the honest case.
past-expiryrefusedSpending after the window closed. The spend path had no expiry check whatsoever.
revoke-burnrefusedRevoke paying 1 sompi to the principal and burning the rest to fees. The exits checked where coin went and never how much.
settle-stealrefusedThe revocation key sweeping a delegated child to an address of its choosing — the same bug as above, in a path written after that one was fixed.
reabsorb-live-grandchildrefusedSettling a child that still funds grandchildren, releasing the parent's reserve while that coin sits outside anyone's accounting.
narrowed-pays-outsiderefusedA sub-agent paying someone on its parent's allowlist but not its own. Without this, narrowing would be decoration.
narrowed-pays-insideacceptedThe same sub-agent paying its own vendor. The twin that proves the check discriminates.

Eight of fourteen shown. Six vulnerabilities have been found in this covenant; none was found by reading the code, and each is now pinned by a probe that fails if it returns.

Integrations

A grant is a funding source with rules attached.

Which makes it a drop-in wherever an agent currently needs a private key.

Available now

Model Context Protocol

@warda_protocol/mcp exposes four tools over stdio: read a grant's authority, check whether a spend is permitted and why, check a delegation, and build an unsigned spend. It never holds a key and never enforces — it explains, and the covenant decides.

Available now

x402 / HTTP 402

@warda_protocol/x402 is a drop-in fetch. Call a paid endpoint as you would a free one; the 402, the covenant spend and the proof happen underneath. x402 answers how to pay for one call; Warda answers what may be spent, and to whom — a marketplace's validated vendor list is the allowlist.

Built in

Kaspa nodes

The SDK speaks wRPC directly — four calls, no proxy. It refuses to route covenant transactions through third-party REST services, which silently drop the covenant binding and produce a signature that cannot verify.

Any language

The vectors ship with it

Golden genesis, spend and delegation vectors travel in the package, so a second implementation in any language can check itself against the same bytes rather than against a prose spec.

Limits

What the chain does not enforce.

Published as prominently as the guarantees, because a limit you discover in production is a bug and a limit you read first is a design constraint.

  • The epoch cap bounds one grant's rate, not a subtree's. A parent limited to 500 KAS per epoch can delegate to ten children at 500 each. The total is still bounded exactly by budgetTotal — the reserve accounting is conservative — but the rate limit degrades toward the budget limit under delegation.
  • Expiry opens a reclaim right; it does not close the spend path. A UTXO covenant can express "not before", never "must be spent before". After expiry an agent keeps spending until someone reclaims. A grant past its term is unattended, not dormant.
  • A grant is one UTXO, so its payments are serial. The second spend's input is the first spend's output. Delegation is the concurrency primitive: N children are N independent UTXOs and therefore N parallel lanes.
  • Narrowing a payee list follows the Merkle tree. A child's allowlist must be a subtree of its parent's — a contiguous, power-of-two-aligned run. So the order of an allowlist is a design decision: payees delegated together should sit together.
  • Settlement is last-in-first-out. The reserve is a hash chain, so the most recently delegated child settles first. That is the price of one hash and no proof instead of a second Merkle fold.
  • Nothing here cancels an in-flight spend. Revocation makes the balance unreachable from the next block on; a transaction already in the mempool may still land. No UTXO covenant can say "and cancel anything outstanding".