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.

Install

Three 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 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

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

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

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.

A real agent-payments library shipping today caps spending with an environment variable, and its own docs say the server "refuses further calls until restarted."

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:

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".