Skip to content

wardhq/ward-app

Repository files navigation

Ward

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.

What lives where

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-serverwithX402(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

Two-actor model

  1. Treasurer (a wallet). Owns a Vault PDA seeded by their pubkey and the asset mint. Spawns one AgentCard PDA per agent with a policy. Pauses, rotates admin, and revokes cards. All vault and card admin lives on base devnet.
  2. Agent (a keypair, usually headless). Holds the keypair that matches the AgentCard PDA seed. Calls ward.spend(...) directly, or — for paid APIs — hands ward.x402Signer({ ... }) to callX402 from @ward/x402-client and 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.

Quick start

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/ward

The dashboard:

  1. Connect a devnet wallet → vault PDA derived from your pubkey
  2. Create vault → on-chain initialize_vault (USDC by default)
  3. Top up → idempotently creates the vault's ATA and runs top_up. Form takes atomic units (USDC = 6 decimals, so 20 USDC = 20000000)
  4. Delegate to ER → delegates the treasurer ATA to the MagicBlock devnet validator via delegateSpl. The vault-PDA-owned ATA needs a program-side delegate_vault_ata ix and is surfaced as pending.
  5. Spawn agent card → policy form: caps, allowlist, expiry, day boundary
  6. Edit / Revoke per card
  7. Live SpendEventsward.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.

Test the agent flow end to end

solana-keygen new --outfile ~/agent.json --no-bip39-passphrase --silent
solana-keygen new --outfile ~/merchant.json --no-bip39-passphrase --silent

Spawn 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:spend

scripts/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.

Plug ward into x402

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.

Common commands

Root

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)

Web app

bun run app:dev
bun run app:build            # builds @ward/sdk first, then Next.js
bun run app:start
bun run app:lint

Anchor

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

IDL sync

bun run idl:sync

Copies target/idl/*.json and target/types/*.ts into packages/idl/src/. Runs automatically as part of anchor:build.

Local Solana / Anchor testing

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.

Deployment

  • Web app: Vercel via app/vercel.json. Set the Vercel project Root Directory to app. Build command runs sdk:build before the Next.js build so the dashboard always sees the latest types.
  • Anchor program: deployed to devnet at 5WyDfSJvrSY8g6fiJeSAFMfEFwdribkoVMnZEK2MCHeA. Use solana program deploy --use-rpc target/deploy/ward.so for upgrades — anchor deploy hits a TPU-client timeout against public devnet.

Tooling

  • Package manager: bun@1.3.2 (linker pinned to hoisted via bunfig.toml)
  • Anchor: 0.32.1 (Solana 3.1.12)
  • Rust: 1.89.0 (rust-toolchain.toml)
  • TypeScript: ^5
  • Turbo: ^2.9 for workspace package builds
  • MagicBlock ER SDK: @magicblock-labs/ephemeral-rollups-sdk@^0.13

About

Resources

Stars

Watchers

Forks

Contributors

Languages