Governed agent spend on Solana. A treasurer creates a vault, spawns an
AgentCard per agent with per-tx and per-day caps and a payee allowlist, and
the agent then spends through ward.spend — the program enforces every gate
on-chain, emits a SpendEvent, and the rollup reflects it live.
Live on devnet at https://wardhq.xyz.
| Directory | What it contains | Start here |
|---|---|---|
app/ |
Next.js treasurer dashboard (vault + cards + top-up + delegate + spend feed) | app/README.md |
programs/ward/ |
Anchor program — vault, agent cards, spend, private_spend | programs/ward/README.md |
sdk/ward-ts/ |
@ward/sdk — TS client with x402 signer + wallet-adapter helpers |
sdk/ward-ts/README.md |
packages/idl/ |
@ward/idl — synced Anchor IDL JSON + generated types |
packages/idl/README.md |
packages/x402/ |
@ward/x402 — type-only x402 v2 protocol shapes (SVM) |
packages/x402/README.md |
packages/x402-client/ |
@ward/x402-client — HTTP client + walletSigner for x402 endpoints |
source |
packages/x402-server/ |
@ward/x402-server — withX402(spec, handler) facilitator with ward-aware verification |
packages/x402-server/README.md |
packages/view-key/ |
@ward/view-key — view-key derivation + decrypt of private spends |
source |
services/ |
Headless services: runner, webhook-dispatcher, mcp-ward, the four x402-* demo endpoints |
each service/README.md |
tests/ |
Anchor + bankrun + devnet smoke tests | tests/ward.ts |
scripts/ |
Repo automation — IDL sync, ER spike, x402 bootstrap, agent-spend smoke | scripts/agent-spend.ts |
- Treasurer (a wallet). Owns a
VaultPDA seeded by their pubkey and the asset mint. Spawns oneAgentCardPDA per agent with a policy. Pauses, rotates admin, and revokes cards. All vault and card admin lives on base devnet. - Agent (a keypair, usually headless). Holds the keypair that matches
the AgentCard PDA seed. Calls
ward.spend(...)directly, or — for paid APIs — handsward.x402Signer({ ... })tocallX402from@ward/x402-clientand the same policy gates apply.
Vaults are single-asset. Top up the vault's ATA, spawn agent cards, then any agent in the allowlist + caps can spend until you revoke.
bun install # workspace symlinks via bunfig.toml's hoisted linker
bun run anchor:build # anchor keys sync && anchor build && idl:sync
bun run sdk:build # tsc → sdk/ward-ts/dist
bun run app:dev # Next.js dashboard at http://localhost:3000/wardThe dashboard:
- Connect a devnet wallet → vault PDA derived from your pubkey
- Create vault → on-chain
initialize_vault(USDC by default) - Top up → idempotently creates the vault's ATA and runs
top_up. Form takes atomic units (USDC = 6 decimals, so 20 USDC =20000000) - Delegate to ER → delegates the treasurer ATA to the MagicBlock devnet
validator via
delegateSpl. The vault-PDA-owned ATA needs a program-sidedelegate_vault_ataix and is surfaced as pending. - Spawn agent card → policy form: caps, allowlist, expiry, day boundary
- Edit / Revoke per card
- Live SpendEvents —
ward.tailLedger({ vault })over the ER ws endpoint
Reads target the ephemeral validator (single-account fetches clone on-demand;
list-cards falls back to base because getProgramAccounts doesn't trigger
cloning). Writes always target devnet base.
solana-keygen new --outfile ~/agent.json --no-bip39-passphrase --silent
solana-keygen new --outfile ~/merchant.json --no-bip39-passphrase --silentSpawn a card in the dashboard with the agent pubkey + merchant pubkey in the allowlist. Then:
AGENT_KEYPAIR=~/agent.json \
VAULT_OWNER=<your treasurer pubkey> \
MERCHANT=<merchant pubkey> \
bun run agent:spendscripts/agent-spend.ts auto-airdrops the agent, idempotently creates the
merchant ATA, probes the vault balance up front, and sends ward.spend. Set
AMOUNT=999999 etc. to exercise cap_per_tx / cap_per_day / pause /
revoke boundaries. Each landed spend appears in the dashboard's spend feed
within a few seconds.
import { Ward } from "@ward/sdk";
import { callX402 } from "@ward/x402-client";
const ward = new Ward({ connection, wallet: agent });
const signer = ward.x402Signer({ vaultOwner, agent });
const { body, settlement } = await callX402("https://api.example.com/paid", {
signer,
});The signer builds a ward.spend ix instead of a raw SPL transfer, signs
with the agent, and returns the payload x402 expects. @ward/x402-server's
withX402(spec, handler) recognises the ward spend ix on the verify path
by default — facilitators that ship this server accept ward agent cards out
of the box. Full walkthrough in sdk/ward-ts/README.md.
bun run lint
bun run lint:fix
bun run build:packages # turbo run build for packages/*
bun run sdk:build
bun run agent:spend # smoke-test a card on devnet
bun run spike:spend # legacy ER reference flow (mints + delegates ATAs)bun run app:dev
bun run app:build # builds @ward/sdk first, then Next.js
bun run app:start
bun run app:lintbun run anchor:build # keys sync + build + IDL sync
bun run anchor:test:localnet # boots a local validator, deploys, runs tests
bun run anchor:test:devnet # against devnet (program already deployed)anchor:test:devnet uses real devnet USDC at
4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU and your provider wallet's
balance. Top up at https://faucet.circle.com/ if needed.
bun run idl:syncCopies target/idl/*.json and target/types/*.ts into packages/idl/src/.
Runs automatically as part of anchor:build.
anchor:test:localnet boots a local validator, deploys ward, and runs the
suite in tests/. The validator clones MagicBlock's delegation and
permission programs from mainnet (configured under [test.validator] in
Anchor.toml) so the ER paths exercise locally.
- Web app: Vercel via
app/vercel.json. Set the Vercel project Root Directory toapp. Build command runssdk:buildbefore the Next.js build so the dashboard always sees the latest types. - Anchor program: deployed to devnet at
5WyDfSJvrSY8g6fiJeSAFMfEFwdribkoVMnZEK2MCHeA. Usesolana program deploy --use-rpc target/deploy/ward.sofor upgrades —anchor deployhits a TPU-client timeout against public devnet.
- Package manager:
bun@1.3.2(linker pinned tohoistedviabunfig.toml) - Anchor:
0.32.1(Solana3.1.12) - Rust:
1.89.0(rust-toolchain.toml) - TypeScript:
^5 - Turbo:
^2.9for workspace package builds - MagicBlock ER SDK:
@magicblock-labs/ephemeral-rollups-sdk@^0.13