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 one prerequisite
A covenant spend has to be built against live chain state, so this is the one thing that
cannot be faked. Until today the only answer here was "run kaspad yourself",
which is a fair ask of an operator and an unreasonable one of somebody trying this for
ten minutes. So there is a testnet-10 node you can point at:
Read what that is before you trust it. It is a laptop behind a tunnel,
running testnet-10, and it will be down sometimes — this page says so when it is,
from a check that runs every fifteen minutes and asks the node rather than the door. It
forwards exactly four methods: getInfo, getBlockDagInfo,
getUtxosByAddresses and submitTransaction. Everything else,
including shutdown and ban, is refused by name, because kaspad's
RPC has no authentication and publishing it raw would hand every reader of this page the
ability to stop it.
A node you do not run is a node you are trusting. It answers "what is in the UTXO set" and receives what you broadcast, so a dishonest one could deny you service or misreport whether something settled. What it 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. On testnet none of this costs anything. For anything real, run your own.
Why there is no public one to fall back on. The public
resolvers are real and there are sixteen of them, listed in rusty-kaspa's own
Resolvers.toml. They serve borsh. This SDK speaks the JSON
wRPC encoding, and kaspad only listens for it when started with
--rpclisten-json= — borsh is the default, so that is what public
operators run. Asked for a JSON node on 10 September 2026,
eric.kaspa.stream answered 404 for json and
200 for borsh, on mainnet and testnet alike. The resolver is up,
its pool is full, and it holds nothing we can talk to.
npm install -g @warda_protocol/cli
export WARDA_RPC_JSON=wss://warda-node.tailc0c0ec.ts.net
warda node --grant kaspatest:prw9hklems02v8apxlx5m6y0d90e0j6657ztr3c3cjqf0wsnwsxz2fs9n0jxr
That endpoint is ours, and it is a laptop behind a tunnel — fine for this page,
not something to build a business on. To use your own instead, put its URL in
WARDA_RPC_JSON and run the same command.
The --grant is not
decoration. Three of the four checks a node can fail 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.
It does not merely connect. It asks whether the node is worth believing — synced, utxo-indexed, on the network it claims — because each of those failures returns 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.
If it will not connect
— the JSON wRPC port is a different port from the Borsh one (testnet 18210 vs 17210),
and it only listens if kaspad was started with --rpclisten-json=host:port. Note
the =; the flag rejects a space.
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 needs a node, because a covenant spend has to be built against live chain state. 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 ten 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