Eight steps, about ten minutes, and no prior knowledge of Kaspa. At the end you will have made a real HTTP 402 purchase on Kaspa testnet-10, out of a grant that could not have paid anyone else or paid more, and you will have the transaction id to check it with.
Everything here is testnet. The coins are free and worth nothing, which is the correct amount to risk on a protocol that has not been audited.
The whole thing first
$ warda key --out wallet.key
address kaspatest:qr7z53sv56te4ya8r378y2xwcrj3vqr3p5gzl6kkx205v7anh6u0zefx77c4m
fund it from the testnet-10 faucet, then list who the agent may pay
$ echo kaspatest:qq7xj0mpl0p46875mnkzhwatdy478pjkum745srhaey44l9jx566zefjaam3e > payees.txt
$ warda grant --key wallet.key --payees payees.txt --budget 10 --max-per-spend 1
✔ grant.json 10 KAS budget · 1 KAS per payment · 1 payee, fixed at creation
✔ agent.key the agent's whole authority, and it is the line above
$ warda pay https://warda-demo-api.vercel.app/fact
✔ 0.04 KAS · 200 · fa7ad66b6b17…
$ warda pay https://somewhere-else.example/thing
✗ that address is not on this grant's allowlist. There is nothing to sign.That is the entire surface. The rest of this page is the same nine lines with the reasons attached — why the funder is not the agent, why the allowlist can never change, and what each failure means when you meet it.
The last line is the one worth stopping on. It is not a check the CLI performed and it is not a rule the endpoint applied: the address is committed into the script that unlocks the coin, so there is no valid transaction to build — not one the network would reject, none at all.
Do you need to know Kaspa?
01
An ordinary key holding ordinary money. Nothing bounds it, which is exactly the problem with handing one to a program. This is what funds a grant, and it is the one thing the agent never sees.
02
Money sitting at an address whose rules — a budget, a cap per payment, a fixed list of who may be paid — are part of what the network counts as a valid transaction. Not a setting. Not a server check. A condition of the money moving at all.
03
It can spend inside the rules and it cannot edit them, and neither can you once the grant exists. That is why publishing an agent's key, as we do on /attack, is not a stunt.
Two words you will meet below and can safely skim past. A UTXO is a coin — a discrete lump of money with its own rules, rather than a balance in a ledger. A covenant is rules attached to that coin that every node enforces. Kaspa activated covenants in June 2026, which is the only reason any of this can exist.
Before step 0 · the two keys
These are different things and the difference is the whole model. Your wallet is an ordinary Kaspa key: nothing bounds it, it can pay anyone, and it is what pays for the grant. The grant is an address whose unlocking script carries the limits — that is what the agent is given, and it is all the agent is given.
warda wallet
address kaspatest:qr7z53sv56te4ya8r378y2xwcrj3vqr3p5gzl6kkx205v7anh6u0zefx77c4m
holds 12.4 KAS across 7 coins
largest 3.1 KAS
Watch largest, not
holds. Creating a grant takes exactly ONE input, so the biggest
grant a key can fund is bounded by its biggest single coin. Seven faucet coins of 12.4
KAS still only reach 3.1. warda wallet consolidate merges them; it says
when doing so would not be worth it, because coins are also parallelism — a second
grant waits on the first one's change.
The same command reads the other side. An agent that sells is paid to an ordinary address, and that money carries no covenant, because nobody agreed to one. Count it, merge it, then bound it — the key that received it funds a grant directly, so nothing has to be moved anywhere:
warda wallet --key earnings.key
warda wallet consolidate --key earnings.key --submit
warda grant --key earnings.key --payees payees.txt --budget 2Step 0 · the prerequisite that went away
This page used to open with "run kaspad yourself" — a fair ask of an
operator and an unreasonable one of somebody trying this for ten minutes. It was also
wrong, and the reason it was wrong is worth one paragraph, because it is the most useful
thing on this page.
kaspad speaks two encodings, and
almost nobody serves the one we spoke. borsh is the default. JSON
listens only when an operator passes --rpclisten-json=, which is why all
sixteen public resolvers in rusty-kaspa's Resolvers.toml answer
404 for json and 200 for borsh. So
"use a public node" had never worked — not because the nodes were missing, but
because we were asking in a language they do not answer in.
And reading over borsh was not
enough to spend. Borsh is positional: there are no field names on the
wire, so a struct the encoder does not know about is not an unknown field, it is an
absent one, and absence shifts everything after it. The published WASM SDK
predates covenants, so a grant spend through it would have been well-formed, correctly
signed, and bound to nothing — accepted by the encoder, meaningless
on chain. Refusing to submit was the only honest option.
What changed is the premise, not the argument. rusty-kaspa's own bindings have carried
covenants since Toccata and builds from those revisions are published. The node
requirement was never in the protocol. It was in the packaging.
npm install -g @warda_protocol/cli \
@warda_protocol/borsh @kluster/kaspa-wasm
warda node --borsh --grant kaspatest:prw9hklems02v8apxlx5m6y0d90e0j6657ztr3c3cjqf0wsnwsxz2fs9n0jxrYou are not being asked to trust that binary. The covenant binding is part of the sighash — which is exactly why a covenant-blind wallet cannot sign a Warda spend — so any difference between what was signed and what the encoder produced shows up in the transaction id. Every submit computes it both ways and refuses to broadcast unless they match byte for byte. A tampered build, a stale one, one from a revision where a field moved: caught before the network sees anything. The dependency can break this path. It cannot redirect money through it.
It is a flag, not a default. Reaching for a stranger's node because yours is not configured would make the most consequential choice in this tool the one nobody made. A resolver does not remove the question of whose node you believe — it answers it on your behalf, and you should have typed something to allow that.
So warda node does not merely connect. It asks that stranger the same four
questions it would ask yours — synced, utxo-indexed, on the network it claims,
reports covenant ids — because three of those four failures return a
plausible wrong answer rather than an error. A node without a utxo index reports
your grant as empty, which is indistinguishable from a grant that was drained.
The --grant is not
decoration. Three of the four checks need nothing but the node. The fourth
— does it report covenant ids — can only be answered against an address where
one must be present, and it is the only failure that hands you a signed transaction
instead of an error. Without it that line reads UNCHECKED, which is honest
and not the same as passing. The address above is
the published demo grant; a spend moves a grant, so if it reports
nothing there, take a current one from that page.
If you would rather run your
own — and for anything real you should, because a node you do not run is a
node you are trusting — pass --rpc and skip the resolvers. Note that
JSON and borsh are different ports on a kaspad, not different spellings (testnet
18210 vs 17210), and the JSON one listens only with
--rpclisten-json=host:port — the = is required, a space is
rejected. WARDA_RPC_JSON is a JSON url by name, so --borsh
ignores it; point --borsh --rpc at a borsh endpoint instead.
What a node you borrow cannot do is make a grant exceed its limits: the budget,
the per-payment cap and the allowlist are compiled into the script that unlocks the coin,
and no node is asked. It can deny you service or misreport whether something settled. On
testnet none of that costs anything.
Step 1 · a wallet, and coin in it
Three keys matter and they are deliberately separate: a funder that pays for the grant, an agent that spends within it, and a revocation key that can end it. The agent never holds the other two.
warda key --out wallet.keyIt prints the address. Fund it from a testnet-10 faucet, then carry on — the next step will tell you if the balance is short before it creates anything.
Step 2 · who the agent may pay
Not a policy the agent consults — a commitment compiled into the script that unlocks the coin. To pay someone not on it there is no valid transaction: not one the network would reject, none at all. Start with the demo vendor, which is a real 402 endpoint:
echo kaspatest:qqtwdteqxrm7g5gdrfqh8yd8la7v45scvnchamm7uq6lq3f7yxsrx5umtwam4 > payees.txt
That address is covenant/deploy/demo-vendor.key.pub as a pay-to-public-key
address, and it is what warda-demo-api.vercel.app quotes for
/weather, /fact and /inference. Keep this file: a
grant commits to the list's root, and a root cannot produce the proof a spend
needs. Lose it and the grant can be revoked but never spent.
Step 3 · create the grant
warda grant --key wallet.key --payees payees.txt \
--budget 5 --max-per-spend 0.2It checks every precondition first and reports all of them at once — a quickstart that fails five times, each revealing one more prerequisite, is a maze with a progress bar. It creates nothing until they all pass, and it invents nothing: no default resolver, no guessed faucet, no fabricated allowlist.
Out of it comes an agent key, generated fresh and independent of your wallet. That key is the agent's entire authority — and its entire authority is the four limits printed beside it.
Step 4 · spend it
warda pay https://warda-demo-api.vercel.app/factNo flags. Step 3 wrote the grant, the allowlist and the agent key into a config beside them, so the agent it pays as is the one it just made.
The endpoint answers 402 with a price, your agent builds a covenant spend, broadcasts it, and re-presents the same proof while it settles. The vendor does not trust the header: it looks for a UTXO at its own address, from that transaction, for exactly the quoted amount. A vendor that believed the header would accept a fabricated id.
You now have a transaction id. Look it up on any testnet-10 explorer. That is the whole point of this page — nothing above needs to be taken on trust.
What just happened, in plain words. You locked some money at an address with rules. You handed a program a key that opens that address and nothing else. The program bought something, and the payment went through because it obeyed the rules — not because anything checked afterwards, but because a payment breaking them is not a transaction the network has a way to accept. The rules are now unchangeable, by the program, by you, and by us. Someone is welcome to try.
No node required · the fastest way in
Everything above reads the chain, because a covenant spend has to be built against live state — through a resolver or through your own node, but read it must. The MCP server does not. It never reads the chain and never sees a key — you hand it a grant and it tells you what the covenant would allow, or hands back the exact bytes to sign. That makes it the one part of this you can try in a minute, with nothing installed and nothing synced.
{
"mcpServers": {
"warda": {
"command": "npx",
"args": ["-y", "@warda_protocol/mcp"]
}
}
}
Claude Desktop reads claude_desktop_config.json; Cursor reads
.cursor/mcp.json. Restart the client and the agent has eleven tools —
among them what may this agent spend right now, and
would this payment be accepted, and if not, which limit stopped it.
It answers about a grant, not
about a chain. Ask it what an agent may spend and it reports what the grant
document says, which is the covenant's own accounting rather than a balance. That is a
feature for planning a purchase and a trap if you expect it to notice money arriving:
for that you need warda balance, and a node.
Step 5 · from Python, or anything else
The packages are TypeScript. The buyer is also a clean subprocess API: the result on stdout, every diagnostic on stderr, and exit codes that tell you whether money moved.
import json, subprocess
r = subprocess.run(["warda", "pay", URL, "--json"],
capture_output=True, text=True)
if r.returncode == 3:
... # the covenant refused. NOTHING was spent — pick another action
if r.returncode == 4:
... # paid, not served. The money is gone. Do NOT retry: that pays twice
if r.returncode != 0:
raise RuntimeError(r.stderr)
paid = json.loads(r.stdout)
paid["txid"], paid["paidSompi"], paid["body"]
No flags, because warda grant in step 3 wrote the grant, the allowlist and
the agent key into a config file beside them. If you would rather be explicit — or
you are running several agents from one process — every one of those is also a
flag: --grant, --payees, --id, --out.
Four codes, and they are the whole API. 0 bought · 3 refused, nothing spent · 4 paid and unserved · 1 failed, see stderr. They are what a Python, Go or Ruby agent needs, and they are why there is no second implementation of the covenant rules to keep in step with this one.
Exit 4 is the one to handle carefully. It means the transaction settled and the vendor still did not serve. Retrying spends again. The adapter never re-pays on an ambiguous failure, and neither should you.
Step 6 · the part worth doing
Point the same command at an address that is not on your list, or ask for more than the per-payment cap. The refusal arrives before a key is touched, and the reason names the rule. Then read it carefully: the interesting part is not that it was refused, it is that no valid transaction exists to refuse.
When you are done with your own grant, there is a funded one with its private key published. Take it.
Step 7 · when it goes wrong
sdk/tools/follow-grant.ts finds it from the chain.--fee.Next