On-chain policy enforcement for AI agent wallets on Solana
Built by Trion Labs
Website · Source · ClawHub · Android APK
Caution
Unaudited, do not use with real funds.** This project is in early stages of development, has not undergone a security audit, and was built with significant AI tooling assistance. It is provided as-is for educational and experimental purposes only. Use at your own risk.
- What Is This?
- High-Level Architecture
- Core Concepts
- The 10-Step Policy Check Chain
- Account Architecture
- PDA Seeds Reference
- Instructions Reference
- SDK Quick Start
- Factory Flow
- Spending Limits Deep Dive
- Tier 1 vs Tier 2
- Mobile
- Project Structure
- Development
- Program IDs & Deployment
- Design Decisions
- Security Model
- Error Codes
An AI agent needs a wallet. But you don't trust it with unlimited access.
Maestro lets vault owners define exactly what an agent can and can't do. Which tokens, which programs, which recipients, how much per day, all enforced on-chain. The agent operates autonomously within those boundaries, and the owner always has a kill switch.
The system is split into two programs:
- Agent Policy Engine: The brain. Manages vaults, access control lists, session keys, spending limits, and executes policy-checked CPI calls on behalf of the agent.
- Vault Factory: The registry. Provides vault creation with fee collection, vault discovery by owner, and labeling.
graph TB
Owner["<b>Owner</b><br/>Human / Multisig"]
Agent["<b>AI Agent</b><br/>Autonomous wallet operator"]
Vault["<b>Vault PDA</b><br/>Holds SOL & tokens"]
Config["<b>VaultConfig</b><br/>Spending limits & time controls"]
Lists["<b>Green / White / Blacklist</b><br/>Access control entries"]
SessionKey["<b>Session Key</b><br/>Time-bound agent credential"]
PE["<b>Policy Engine</b><br/>10-step policy check"]
Target["<b>Target Program</b><br/>Jupiter, Token, etc."]
Factory["<b>Vault Factory</b><br/>Registry & fee collection"]
Registry["<b>VaultRegistry</b><br/>Discovery & labels"]
Owner -->|creates & configures| Vault
Owner -->|sets policies| Config
Owner -->|manages| Lists
Owner -->|issues| SessionKey
Agent -->|execute_action| PE
PE -->|enforces| Config
PE -->|checks| Lists
PE -->|validates| SessionKey
PE -->|CPI passthrough| Target
Factory -->|creates vaults via CPI| PE
Factory -->|registers| Registry
style Owner fill:#4A9CFF,color:#fff
style Agent fill:#FF6B6B,color:#fff
style Vault fill:#FFD93D,color:#333
style PE fill:#6BCB77,color:#fff
style Factory fill:#9B59B6,color:#fff
PDA-controlled wallets. One owner can have multiple vaults (via vault_index). The vault PDA is the signer for all CPI calls, so it can hold SOL, SPL tokens, and interact with any Solana program — Jupiter swaps, token transfers, DeFi protocols, you name it.
Seeds: ["vault", owner, vault_index_le_bytes]
Access control uses PDA existence. If the account exists, the entry is on the list. No vectors, no reallocs, O(1) lookup.
graph LR
BL["<b>Blacklist</b><br/>Absolute deny"]
GL["<b>Greenlist</b><br/>Tokens & programs"]
WL["<b>Whitelist</b><br/>Recipients"]
BL -->|"overrides"| GL
BL -->|"overrides"| WL
GL -->|"required for"| TokenProgram["Token & program access"]
WL -->|"required for"| Recipients["Recipient approval"]
style BL fill:#FF4444,color:#fff
style GL fill:#44BB44,color:#fff
style WL fill:#4488FF,color:#fff
- Greenlist: Tokens and programs the agent can interact with. If a program isn't greenlisted, the agent can't call it.
- Whitelist: Approved recipient addresses for transfers. Optional:
require_cosign_new_recipientforces owner co-sign for new recipients. - Blacklist: Overrides everything. A blacklisted address cannot be a target program, token mint, or recipient regardless of greenlist/whitelist status.
Time-bounded, spending-capped credentials issued to agents. Each session key has:
valid_after/valid_until- time windowspending_limit_usdc- max USDC the agent can spend during this sessionnonce- tied to the vault'sglobal_session_nonce. Incrementing the nonce instantly invalidates all active session keys (bulk revocation).
| Tier 1: Autonomous | Tier 2: Co-signed | |
|---|---|---|
| Signers | Agent only | Agent + Owner |
| Policy checks | Full 10-step chain | Skips tier2 threshold and whitelist |
| Use case | Routine operations | Large transfers, new recipients |
| Instruction | execute_action |
execute_action_cosigned |
Every execute_action call passes through this chain. Any failure rejects the transaction.
flowchart TD
Start(["Agent calls execute_action"]) --> TrackerCheck{"Tracker matches<br/>current day?"}
TrackerCheck -->|No| R0["TrackerDayMismatch"]
TrackerCheck -->|Yes| S1
S1{"1. Vault frozen?"} -->|Yes| R1["VaultFrozen"]
S1 -->|No| S2
S2{"2. Session key valid?<br/>(not revoked, nonce matches,<br/>agent matches, within time window)"} -->|No| R2["SessionKey error<br/>(5 possible codes)"]
S2 -->|Yes| S3
S3{"3. Within operating<br/>hours?"} -->|No| R3["OutsideOperatingHours"]
S3 -->|Yes| S4
S4{"4. Cooldown<br/>elapsed?"} -->|No| R4["CooldownActive"]
S4 -->|Yes| S5
S5{"5. Target program<br/>blacklisted?"} -->|Yes| R5["AddressBlacklisted"]
S5 -->|No| S6
S6{"6. Target program<br/>greenlisted?"} -->|No| R6["ProgramNotGreenlisted"]
S6 -->|Yes| S7
S7{"7. Token & recipient<br/>checks pass?<br/>(blacklist, greenlist,<br/>whitelist)"} -->|No| R7["Token/Recipient error<br/>(4 possible codes)"]
S7 -->|Yes| S8
S8{"8. USDC limits OK?<br/>(per-tx, daily, session,<br/>tier2, amount verified)"} -->|No| R8["Limit error<br/>(4 possible codes)"]
S8 -->|Yes| S9
S9["9. Execute CPI<br/>via invoke_signed"] --> S10
S10["10. Emit ActionExecuted<br/>event"]
style Start fill:#4A9CFF,color:#fff
style S9 fill:#6BCB77,color:#fff
style S10 fill:#6BCB77,color:#fff
style R0 fill:#FF4444,color:#fff
style R1 fill:#FF4444,color:#fff
style R2 fill:#FF4444,color:#fff
style R3 fill:#FF4444,color:#fff
style R4 fill:#FF4444,color:#fff
style R5 fill:#FF4444,color:#fff
style R6 fill:#FF4444,color:#fff
style R7 fill:#FF4444,color:#fff
style R8 fill:#FF4444,color:#fff
erDiagram
Vault ||--|| VaultConfig : "has config"
Vault ||--o{ GreenlistEntry : "approved tokens & programs"
Vault ||--o{ WhitelistEntry : "approved recipients"
Vault ||--o{ BlacklistEntry : "denied addresses"
Vault ||--o{ SessionKey : "agent credentials"
Vault ||--o{ SpendingTracker : "daily spending"
FactoryState ||--o{ VaultRegistry : "registered vaults"
Owner ||--o{ OwnerIndex : "vault count"
Vault {
Pubkey owner
Pubkey usdc_mint
u64 vault_index
u64 global_session_nonce
bool is_frozen
u64 session_counter
u8 bump
}
VaultConfig {
Pubkey vault
u64 per_tx_limit_usdc
u64 daily_limit_usdc
u32 operating_hours_start
u32 operating_hours_end
u32 cooldown_seconds
u64 cooldown_threshold_usdc
i64 last_cooldown_trigger
u64 tier2_threshold_usdc
bool require_cosign_new_recipient
u8 bump
}
SessionKey {
Pubkey vault
Pubkey agent
u64 nonce
i64 valid_after
i64 valid_until
u64 spending_limit_usdc
u64 amount_spent_usdc
bool is_revoked
u8 bump
}
SpendingTracker {
Pubkey vault
u64 day_epoch
u64 total_spent_usdc
u8 bump
}
| Account | Size (bytes) | Program |
|---|---|---|
| Vault | 98 | Policy Engine |
| VaultConfig | 94 | Policy Engine |
| GreenlistEntry | 73 | Policy Engine |
| WhitelistEntry | 73 | Policy Engine |
| BlacklistEntry | 73 | Policy Engine |
| SessionKey | 114 | Policy Engine |
| SpendingTracker | 57 | Policy Engine |
| FactoryState | 154 | Factory |
| VaultRegistry | 122 | Factory |
| OwnerIndex | 49 | Factory |
| Account | Seeds |
|---|---|
| Vault | ["vault", owner, vault_index_le_bytes] |
| VaultConfig | ["vault_config", vault] |
| GreenlistEntry | ["greenlist", vault, pubkey] |
| WhitelistEntry | ["whitelist", vault, address] |
| BlacklistEntry | ["blacklist", vault, address] |
| SessionKey | ["session", vault, agent, counter_le_bytes] |
| SpendingTracker | ["tracker", vault, day_epoch_le_bytes] |
| Account | Seeds |
|---|---|
| FactoryState | ["factory"] |
| VaultRegistry | ["registry", vault_pubkey] |
| OwnerIndex | ["owner_index", owner] |
| Instruction | Parameters | Description |
|---|---|---|
create_vault |
vault_index: u64, usdc_mint: Pubkey |
Create a new vault with config |
close_vault |
— | Close vault and reclaim rent |
update_vault_config |
UpdateVaultConfigParams |
Update spending limits, time controls |
| Instruction | Parameters | Description |
|---|---|---|
add_greenlist |
entry_pubkey: Pubkey |
Approve a token or program |
remove_greenlist |
entry_pubkey: Pubkey |
Remove token/program approval |
add_whitelist |
address: Pubkey |
Approve a recipient |
remove_whitelist |
address: Pubkey |
Remove recipient approval |
add_blacklist |
address: Pubkey |
Block an address (overrides all) |
remove_blacklist |
address: Pubkey |
Unblock an address |
| Instruction | Parameters | Description |
|---|---|---|
create_session_key |
agent, valid_after, valid_until, spending_limit_usdc |
Issue a time-bound credential |
revoke_session_key |
— | Revoke a single session key |
revoke_all_sessions |
— | Increment nonce, invalidate all keys |
| Instruction | Parameters | Description |
|---|---|---|
freeze_vault |
— | Block all agent operations |
unfreeze_vault |
— | Resume agent operations |
withdraw_sol |
amount: u64 |
Owner withdraws SOL from vault |
withdraw_spl |
amount: u64 |
Owner withdraws SPL tokens from vault |
| Instruction | Parameters | Description |
|---|---|---|
execute_action |
ExecuteActionParams |
Tier 1: autonomous, full policy check |
execute_action_cosigned |
ExecuteActionParams |
Tier 2: owner co-signs, relaxed checks |
| Instruction | Parameters | Description |
|---|---|---|
init_tracker |
day_epoch: u64 |
Initialize daily spending tracker |
close_spent_tracker |
— | Close expired tracker, reclaim rent |
| Instruction | Parameters | Description |
|---|---|---|
initialize_factory |
treasury, creation_fee_lamports, policy_engine_program |
Bootstrap factory state |
update_factory_config |
UpdateFactoryConfigParams |
Update fees, treasury, pause |
transfer_admin |
new_admin: Pubkey |
Initiate admin transfer |
accept_admin |
— | New admin accepts transfer |
| Instruction | Parameters | Description |
|---|---|---|
create_vault_via_factory |
vault_index, usdc_mint, label |
Create vault with fee, register it |
register_existing_vault |
label |
Register a pre-existing vault |
deregister_vault |
— | Remove vault from registry |
update_vault_label |
label |
Update vault display label |
npm install @trionlabs/agent-policy-engineimport { Program, AnchorProvider, BN } from "@coral-xyz/anchor";
import { OwnerClient, AgentWallet, PROGRAM_ID } from "@trionlabs/agent-policy-engine";
import type { AgentPolicyEngine } from "@trionlabs/agent-policy-engine";
const provider = AnchorProvider.env();
const program = new Program<AgentPolicyEngine>(idl, PROGRAM_ID, provider);
// Owner: set up vault and policies
const ownerClient = new OwnerClient(program, provider.wallet.publicKey);
await ownerClient.createVault(usdcMint);
await ownerClient.updateVaultConfig({
perTxLimitUsdc: new BN(100_000_000), // 100 USDC
dailyLimitUsdc: new BN(500_000_000), // 500 USDC
tier2ThresholdUsdc: new BN(200_000_000), // 200 USDC requires co-sign
});
// Greenlist tokens/programs the agent can use
await ownerClient.addGreenlist(usdcMint);
await ownerClient.addGreenlist(TOKEN_PROGRAM_ID);
// Whitelist approved recipients
await ownerClient.addWhitelist(recipientAddress);
// Issue a 24-hour session key with 200 USDC spending cap
const { sessionKeyPda } = await ownerClient.createSessionKey(
agentPubkey,
new BN(Math.floor(Date.now() / 1000) - 60),
new BN(Math.floor(Date.now() / 1000) + 86400),
new BN(200_000_000),
);
// Agent: execute actions within policies
const agentClient = new AgentWallet(program, agentPubkey, ownerPubkey);
await agentClient.initTracker();
await agentClient.executeAction(
{
instructionData: Buffer.from([3, ...amountLE]),
usdcAmount: new BN(50_000_000),
recipient: recipientAddress,
tokenMint: usdcMint,
},
sessionKeyPda,
trackerPda,
TOKEN_PROGRAM_ID,
{
tokenMint: usdcMint,
recipient: recipientAddress,
cpiAccounts: [
{ pubkey: vaultTokenAccount, isWritable: true, isSigner: false },
{ pubkey: destTokenAccount, isWritable: true, isSigner: false },
{ pubkey: vaultPda, isWritable: false, isSigner: false },
],
},
);See the sdk/ directory for the full API.
sequenceDiagram
participant User
participant Factory
participant PolicyEngine
User->>Factory: createVaultViaFactory(index, mint, label)
Factory->>Factory: Check not paused
Factory->>Factory: Collect creation fee (SOL-> treasury)
Factory->>PolicyEngine: Manual CPI -> create_vault
PolicyEngine->>PolicyEngine: Init Vault PDA + VaultConfig PDA
Factory->>Factory: Init VaultRegistry PDA
Factory->>Factory: Init/update OwnerIndex PDA
Factory-->>User: Vault created & registered
The Factory uses manual CPI (
solana_program::program::invoke) instead of Anchor's cross-program feature because Anchor'sfeatures = ["cpi"]dependency between programs fails IDL generation.
Four stacking limits protect against overspending. All amounts are in USDC (6 decimals).
Example scenario: Vault configured with per-tx $100, daily $500, session $1000, tier2 $200. Agent tries to spend $80 USDC.
flowchart TD
TX["Agent: spend $80 USDC"] --> S1
S1{"Per-tx limit<br/>$80 <= $100?"} -->|"No: exceeds"| R1["PerTxLimitExceeded"]
S1 -->|"Yes: within limit"| S2
S2{"Daily limit<br/>spent_today + $80 <= $500?"} -->|"No: exceeds"| R2["DailyLimitExceeded"]
S2 -->|"Yes: within limit"| S3
S3{"Session limit<br/>session_spent + $80 <= $1000?"} -->|"No: exceeds"| R3["SessionLimitExceeded"]
S3 -->|"Yes: within limit"| S4
S4{"Tier 2 threshold<br/>$80 > $200?"} -->|"Yes: needs co-sign"| R4["Tier2ThresholdExceeded"]
S4 -->|"No: autonomous OK"| PASS["Execute CPI"]
style TX fill:#4A9CFF,color:#fff
style PASS fill:#6BCB77,color:#fff
style R1 fill:#FF4444,color:#fff
style R2 fill:#FF4444,color:#fff
style R3 fill:#FF4444,color:#fff
style R4 fill:#FF4444,color:#fff
| Limit | Config Field | Scope | Purpose |
|---|---|---|---|
| Per-transaction | per_tx_limit_usdc |
Single CPI call | Cap individual transaction size |
| Daily | daily_limit_usdc |
UTC day (via SpendingTracker) | Cap cumulative daily spending |
| Session | spending_limit_usdc |
Session key lifetime | Cap total spend per credential |
| Tier 2 threshold | tier2_threshold_usdc |
Single CPI call | Force owner co-sign above amount |
Set any limit to 0 to disable it.
When the target program is SPL Token Program and the token mint is the vault's configured USDC mint, the engine parses the instruction data to verify that the actual transfer amount matches the declared usdc_amount parameter. This prevents an agent from underreporting spending to bypass limits.
- Covers:
Transfer(discriminator 3) andTransferChecked(discriminator 12) - Skips: Non-Token-Program targets, non-USDC token mints, and non-transfer instructions
- Limitation: Only verifies direct SPL Token Program calls. Wrapped transfers via DEX aggregators or other programs are not parsed (they are controlled by greenlist membership instead)
flowchart LR
subgraph Tier1["<b>Tier 1: Autonomous</b>"]
direction TB
A1["Agent signs alone"] --> A2["Full 10-step policy check"]
A2 --> A3["Whitelist enforced"]
A3 --> A4["Tier 2 threshold enforced"]
end
subgraph Tier2["<b>Tier 2: Co-signed</b>"]
direction TB
B1["Agent + Owner sign"] --> B2["Policy check with exceptions"]
B2 --> B3["Whitelist SKIPPED"]
B3 --> B4["Tier 2 threshold SKIPPED"]
end
style Tier1 fill:#E8F5E9
style Tier2 fill:#FFF3E0
Co-signing allows the owner to explicitly approve operations that would normally be blocked (large transfers, new recipients, etc.), while still enforcing blacklist, greenlist, operating hours, and cooldown.
Set the limits. Watch them hold. Manage vaults, policies, and session keys from your phone.
| Feature | Description |
|---|---|
| Dashboard | SOL and USDC at a glance. Daily spending bar shows where you stand against your limit. |
| Policies | Recipients, blacklist, greenlist, time windows, spending caps, security — all configurable from one screen. |
| Session Keys | Time-bound agent authorization from one hour to one year. Monitor or revoke instantly. |
| Emergency | Vault locks immediately. No confirmation chain, no waiting period. You stay in control. |
Download the Android APK from Releases.
| Tool | Version | Install |
|---|---|---|
| Solana CLI | Agave 2.2.20 | agave-install init 2.2.20 |
| Anchor CLI | 0.32.1 | avm install 0.32.1 && avm use 0.32.1 |
| Rust | 1.89.0 | rustup default 1.89.0 |
| Node.js | 18+ | - |
# Ensure toolchain is in PATH
export PATH="$HOME/.local/share/solana/install/releases/2.2.20/solana-release/bin:$HOME/.avm/bin:$PATH"
# Build both programs with IDL generation
anchor build
# Sync generated IDL/types to SDK
./scripts/sync-idl.sh
# Build SDK
cd sdk && npm run build# macOS: prevent ._ metadata files from corrupting genesis
COPYFILE_DISABLE=1 anchor test
# Skip rebuild if already built
COPYFILE_DISABLE=1 anchor test --skip-build- blake3 pin: After
cargo update, runcargo update -p blake3 --precise 1.7.0(blake3 1.8+ needs edition2024, incompatible with platform-tools Cargo 1.84.0) - cfg warnings:
custom-heap,custom-panic,anchor-debugwarnings from Anchor macros are harmless
What this system protects against:
- Agent spending more than allowed (per-tx, daily, session limits)
- Agent interacting with unauthorized programs or tokens (greenlist)
- Agent sending to unauthorized recipients (whitelist)
- Agent accessing explicitly banned addresses (blacklist overrides all)
- Compromised session keys (time-bound, individually revocable, bulk revocable via nonce)
- Agent underreporting USDC transfer amounts to bypass spending limits (transfer amount verification)
Owner escape hatches:
freeze_vault, instantly blocks all agent operationsrevoke_all_sessions, atomic invalidation of every session keywithdraw_sol/withdraw_spl, owner can always pull funds
| Code | Name | Description |
|---|---|---|
| 6000 | VaultFrozen |
Vault is frozen, all agent operations blocked |
| 6001 | SessionKeyRevoked |
Session key has been revoked |
| 6002 | SessionKeyNonceMismatch |
Session key nonce doesn't match vault nonce |
| 6003 | SessionKeyAgentMismatch |
Signer doesn't match the agent on the key |
| 6004 | SessionKeyNotYetValid |
Session key not yet valid (future valid_after) |
| 6005 | SessionKeyExpired |
Session key has expired |
| 6006 | OutsideOperatingHours |
Operation outside allowed hours |
| 6007 | CooldownActive |
Vault in cooldown after large transaction |
| 6008 | AddressBlacklisted |
Address is blacklisted |
| 6009 | ProgramNotGreenlisted |
Target program not on greenlist |
| 6010 | TokenNotGreenlisted |
Token mint not on greenlist |
| 6011 | RecipientNotWhitelisted |
Recipient not on whitelist |
| 6012 | RecipientRequiresCosign |
Non-whitelisted recipient needs co-sign |
| 6013 | PerTxLimitExceeded |
USDC exceeds per-transaction limit |
| 6014 | DailyLimitExceeded |
USDC would exceed daily limit |
| 6015 | SessionLimitExceeded |
USDC would exceed session key limit |
| 6016 | Tier2ThresholdExceeded |
USDC exceeds tier 2, co-sign required |
| 6017 | ArithmeticOverflow |
Overflow in spending calculation |
| 6018 | InvalidDayEpoch |
day_epoch doesn't match current UTC day |
| 6019 | TrackerDayMismatch |
Tracker day doesn't match current day |
| 6020 | TrackerNotExpired |
Tracker hasn't expired yet |
| 6021 | InvalidSessionKeyWindow |
valid_until must be after valid_after |
| 6022 | InvalidSessionKeySpendingLimit |
Spending limit must be > 0 |
| 6023 | InvalidOperatingHoursStart |
Start must be < 86400 |
| 6024 | InvalidOperatingHoursEnd |
End must be < 86400 |
| 6025 | UsdcAmountMismatch |
Declared USDC amount doesn't match Token Program transfer amount |
MIT