Safe spending rails for AI agents on Solana.
WARD is a non-custodial treasury primitive built in Pinocchio on Solana. A single funded Vault governs many per-agent Cards — every payment is policy-checked on-chain before it settles. Confidential payments keep amounts and counterparties off the public chain, while the owner reconstructs the full ledger off-chain via a view key.
When AI agents spend money today, two things break at once:
- No isolation — each agent holds its own wallet keys, so any bug, bad prompt, or rogue task can drain everything.
- No privacy — every payment is public, leaking the buyer's identity, balance history, and counterparties to anyone watching the chain.
On-chain accounts, PDAs, and the instruction set.
flowchart TD
subgraph PROGRAM["WARD program"]
subgraph VAULT["Vault account — PDA: [b'vault', owner]"]
V1["owner: Pubkey · admin_signer: Pubkey\npaused: bool · bump: u8"]
SPL["SPL token account\nAuthority: vault PDA · asset: USDC"]
V1 --> SPL
end
subgraph CARD["Card account — PDA: [b'card', vault, agent]"]
C1["vault: Pubkey · agent_authority: Pubkey\ncap_per_tx: u64 · cap_per_day: u64\ndaily_spent: u64 · day_start_ts: i64\nexpiry: i64 · revoked: bool\nallowlist: [Pubkey;8] · bump: u8"]
end
VAULT -->|spawns| CARD
end
subgraph INSTR["Instructions"]
I1["initialize_vault\nSigners: owner, admin"]
I2["create_card\nSigner: owner"]
I3["revoke_card\nSigner: owner"]
I4["spend\nSigner: agent"]
I5["private_spend\nSigner: agent + CPI"]
I6["pause_vault\nSigner: owner"]
I7["rotate_admin\nSigner: owner"]
end
PROGRAM --> INSTR
INSTR --> TRUST["Trust boundary\nAgent authority signs on-chain\nLLM decision stays off-chain, keys never exposed to model"]
TRUST --> VIEWKEY["View key\nDerived from owner keypair · decrypts sealed SpendEvent\npayloads off-chain · full ledger reconstruction"]
All paths, error states, cap rollover, and settlement.
flowchart TD
AGENT["Agent authority\nSigns spend instruction"] --> ROLLOVER
ROLLOVER["Daily cap rollover\nclock_ts - day_start_ts >= 86400 -> reset daily_spent"] --> POLICY
subgraph POLICY["On-chain policy check"]
P1["amount <= cap_per_tx"]
P2["daily_spent + amount <= cap_per_day"]
P3["payee in allowlist · !expired · !revoked · !paused"]
P4["card.vault == vault.key"]
end
POLICY -->|deny| ERRORS
POLICY -->|approve: spend| SPL2
POLICY -->|approve: private_spend| MB
subgraph ERRORS["Errors"]
E1["ExceedsTxCap · ExceedsDailyCap"]
E2["PayeeNotAllowed · CardExpired"]
E3["CardRevoked · VaultPaused"]
E4["InvalidVault · Unauthorized"]
end
SPL2["CPI -> spl_token\ninvoke_signed with vault PDA seeds\nCleartext on-chain"]
subgraph MB["CPI -> MagicBlock"]
MB1["ephemeral-spl-token\nShuttle PDA delegated to MB validator"]
MB2["Encrypted disbursement queued on validator"]
MB1 --> MB2
MB2 --> MB3["Sealed settlement\nAmount + payee encrypted"]
end
SPL2 --> EVENT
MB3 --> EVENT
EVENT["SpendEvent (sol_log_data)\nclient_ref_id · card · vault · amount (cleartext)\nsealed_payload: Option[u8;96] · timestamp"]
EVENT --> VIEWKEY["Owner view key decrypts sealed_payload -> full ledger"]
| Account | PDA seeds | Description |
|---|---|---|
| Vault | ["vault", owner] |
Funded treasury, governed by owner + admin signer |
| Card | ["card", vault, agent] |
Per-agent policy account |
| Field | Purpose |
|---|---|
cap_per_tx |
Max amount allowed per transaction |
cap_per_day |
Max daily spend, auto-resets every 86,400 seconds |
allowlist |
Up to 8 approved payee addresses |
expiry |
Unix timestamp after which the card stops working |
revoked |
Kill switch — owner can flip at any time |
| Instruction | Signer | Description |
|---|---|---|
initialize_vault |
owner + admin | Create and fund a vault |
create_card |
owner | Spawn a scoped card for an agent |
spend |
agent | Cleartext SPL transfer, policy-checked |
private_spend |
agent | Confidential transfer via MagicBlock — amount + payee sealed |
revoke_card |
owner | Instantly kill a card |
pause_vault |
owner | Pause all spending from the vault |
rotate_admin |
owner + new admin | Rotate the admin signer |
ExceedsTxCap · ExceedsDailyCap · PayeeNotAllowed · CardExpired · CardRevoked · VaultPaused · InvalidVault · Unauthorized
For private_spend, the client encrypts (payee pubkey + amount) with the owner keypair before building the transaction. The sealed 96-byte payload is stored in SpendEvent. The owner later runs decryptPayload against emitted events to reconstruct the full ledger — counterparties and amounts never touch the public chain.
- Rust + Cargo
- Solana CLI
- Node.js 18+
cargo build-sbfsolana program deploy target/deploy/ward.socd client
npm install
npm run buildcd client
PROGRAM_ID=<your-program-id> npx ts-node scripts/devnet-admin.ts
PROGRAM_ID=<your-program-id> npx ts-node scripts/devnet-spend.ts
PROGRAM_ID=<your-program-id> MAGICBLOCK_PROGRAM_ID=<magicblock-id> npx ts-node scripts/devnet-private-spend.ts
PROGRAM_ID=<your-program-id> npx ts-node scripts/owner-console.tsDevnet: A6ak7ZnX3BioPejqTHGrfgQu9RYQ8mfHQJ3CDbxKn7Lf
- Pinocchio — zero-dependency Solana program framework
- MagicBlock ephemeral-spl-token — confidential payment rail
- Solana devnet