Everything you need to deploy and manage smart wallet infrastructure for AI agents on Solana. On-chain rules, sub-accounts, whitelist, multi-sig — all trustless.
MPP Vault is smart wallet infrastructure for autonomous AI agents on Solana, built on top of Machine Payments Protocol (MPP).
The problem: AI agents that spend money via MPP have no real control today. Solutions like MPP Gate run budgets on a server — if the server dies, there are no rules. And each agent needs its own wallet, funded manually.
The solution: MPP Vault is a single Solana program (smart contract) that acts as a central vault. All rules are enforced on-chain. Trustless — no server, no middleman. You deploy one vault, create sub-accounts for each agent, and every spending rule is stored as on-chain account data.
Core concept
One vault, many agents. You deploy one vault on Solana. Inside the vault you create sub-accounts — one per agent. Each sub-account has its own rules. All funds live in the vault, but each agent can only access its own sub-account within the rules you've set.
Quick start
The MPP Vault system consists of four on-chain components:
Vault owner can:
Agents can:
When an agent wants to pay for a service via MPP, the payment flows through the vault program instead of a direct transfer. Every step is verified on-chain.
Agent wants to pay an MPP service
Service responds HTTP 402 with payment requirement
Agent sends a transaction to the Vault program (not a direct payment)
Program checks: has budget? Whitelisted? Within time window? Under max?
If all checks pass, program sends payment from agent’s sub-account to service
Transaction logged on-chain — fully auditable
Agent retries with payment credential and gets the resource
Currency
USDC (SPL token) on Solana is the primary currency. Can be extended to other SPL tokens. Works with any MPP-compatible service — API endpoints, MCP servers, or any HTTP-addressable service that accepts MPP payments.
A vault is the root account for all agent activity. When you create a vault, the program derives a PDA from your wallet address and a nonce, allocates on-chain storage, and transfers initial funds. The vault owner retains full administrative control — adding sub-accounts, modifying rules, withdrawing funds.
use anchor_lang::prelude::*;
use mpp_vault::program::MppVault;
use mpp_vault::state::Vault;
pub fn create_vault(
ctx: Context<CreateVault>,
name: String,
initial_deposit: u64,
) -> Result<()> {
let vault = &mut ctx.accounts.vault;
vault.authority = ctx.accounts.authority.key();
vault.name = name;
vault.balance = initial_deposit;
vault.created_at = Clock::get()?.unix_timestamp;
vault.bump = ctx.bumps.vault;
// Transfer SOL from authority to vault PDA
anchor_lang::system_program::transfer(
CpiContext::new(
ctx.accounts.system_program.to_account_info(),
anchor_lang::system_program::Transfer {
from: ctx.accounts.authority.to_account_info(),
to: ctx.accounts.vault.to_account_info(),
},
),
initial_deposit,
)?;
msg!("Vault '{}' created with {} lamports", vault.name, initial_deposit);
Ok(())
}
#[derive(Accounts)]
#[instruction(name: String)]
pub struct CreateVault<'info> {
#[account(
init,
payer = authority,
space = Vault::LEN,
seeds = [b"vault", authority.key().as_ref()],
bump
)]
pub vault: Account<'info, Vault>,
#[account(mut)]
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
}import { MppVault } from "@mpp/vault-sdk";
import { Connection, Keypair } from "@solana/web3.js";
const connection = new Connection("https://api.mainnet-beta.solana.com");
const wallet = Keypair.fromSecretKey(/* ... */);
const vault = await MppVault.create(connection, {
authority: wallet.publicKey,
name: "production-vault",
initialDeposit: 10_000_000_000, // 10 SOL in lamports
});
console.log("Vault PDA:", vault.address.toBase58());
// => Vault PDA: 7xKm...f9mPEach AI agent operates through its own sub-account — an isolated partition within the vault. Sub-accounts have their own balance, spending rules, and whitelist. The vault owner can create, pause, or close any sub-account at any time. Budget allocation determines how much of the vault's total pool each agent can access.
Sub-accounts are derived as PDAs from the vault address and an agent identifier (typically the agent's public key or a human-readable label). This means sub-account addresses are deterministic and can be computed off-chain.
pub fn create_sub_account(
ctx: Context<CreateSubAccount>,
agent_id: String,
budget: u64,
) -> Result<()> {
let sub = &mut ctx.accounts.sub_account;
sub.vault = ctx.accounts.vault.key();
sub.agent_id = agent_id;
sub.budget = budget;
sub.spent = 0;
sub.active = true;
sub.bump = ctx.bumps.sub_account;
msg!("Sub-account '{}' created with budget {}", sub.agent_id, budget);
Ok(())
}
#[derive(Accounts)]
#[instruction(agent_id: String)]
pub struct CreateSubAccount<'info> {
#[account(
init,
payer = authority,
space = SubAccount::LEN,
seeds = [b"sub", vault.key().as_ref(), agent_id.as_bytes()],
bump
)]
pub sub_account: Account<'info, SubAccount>,
#[account(mut, has_one = authority)]
pub vault: Account<'info, Vault>,
#[account(mut)]
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
}const subAccount = await vault.createSubAccount({
agentId: "research_agent",
budget: 2_000_000_000, // 2 SOL
});
// List all sub-accounts
const accounts = await vault.listSubAccounts();
accounts.forEach((a) => {
console.log(`${a.agentId}: ${a.budget / 1e9} SOL (spent: ${a.spent / 1e9})`);
});Spending rules define hard limits on what an agent can spend. They are stored as on-chain account data attached to a sub-account and enforced by the Solana runtime — the agent cannot bypass them. Two primary rule types:
pub fn set_spending_rules(
ctx: Context<SetSpendingRules>,
max_per_tx: u64,
max_per_day: u64,
) -> Result<()> {
let rules = &mut ctx.accounts.spending_rules;
rules.sub_account = ctx.accounts.sub_account.key();
rules.max_per_tx = max_per_tx;
rules.max_per_day = max_per_day;
rules.spent_today = 0;
rules.day_start = Clock::get()?.unix_timestamp;
msg!(
"Rules set: max/tx={}, max/day={}",
max_per_tx,
max_per_day
);
Ok(())
}
// Enforcement during payment
pub fn execute_payment(ctx: Context<ExecutePayment>, amount: u64) -> Result<()> {
let rules = &ctx.accounts.spending_rules;
let clock = Clock::get()?;
require!(amount <= rules.max_per_tx, VaultError::ExceedsMaxPerTx);
let elapsed = clock.unix_timestamp - rules.day_start;
let spent = if elapsed >= 86_400 { 0 } else { rules.spent_today };
require!(spent + amount <= rules.max_per_day, VaultError::ExceedsMaxPerDay);
// ... transfer logic
Ok(())
}await subAccount.setSpendingRules({
maxPerTx: 500_000_000, // 0.5 SOL per transaction
maxPerDay: 2_000_000_000, // 2 SOL per day
});
// Check current usage
const rules = await subAccount.getSpendingRules();
console.log(`Spent today: ${rules.spentToday / 1e9} SOL`);
console.log(`Remaining: ${(rules.maxPerDay - rules.spentToday) / 1e9} SOL`);The whitelist restricts which addresses an agent can pay. Every outgoing transaction is checked against the whitelist — if the destination is not on the list, the transaction is rejected. This prevents agents from sending funds to unauthorized addresses, even if they are compromised.
Whitelist entries are stored in a dedicated PDA per sub-account. The vault owner can add or remove entries at any time. Each entry can optionally include a label for human-readable identification.
pub fn add_to_whitelist(
ctx: Context<ModifyWhitelist>,
address: Pubkey,
label: String,
) -> Result<()> {
let whitelist = &mut ctx.accounts.whitelist;
require!(
!whitelist.entries.iter().any(|e| e.address == address),
VaultError::AlreadyWhitelisted
);
whitelist.entries.push(WhitelistEntry {
address,
label,
added_at: Clock::get()?.unix_timestamp,
});
msg!("Address {} added to whitelist", address);
Ok(())
}
// Checked during payment execution
pub fn verify_whitelist(
whitelist: &Whitelist,
destination: &Pubkey,
) -> Result<()> {
require!(
whitelist.entries.iter().any(|e| e.address == *destination),
VaultError::NotWhitelisted
);
Ok(())
}// Add addresses to the whitelist
await subAccount.addToWhitelist([
{ address: "4zMMC9...openai", label: "OpenAI API" },
{ address: "7xKm3...claude", label: "Claude API" },
{ address: "9pRt7...market", label: "Market Data" },
]);
// Remove an address
await subAccount.removeFromWhitelist("4zMMC9...openai");
// List all whitelisted addresses
const entries = await subAccount.getWhitelist();
entries.forEach((e) => console.log(`${e.label}: ${e.address}`));Time rules restrict when an agent is allowed to spend. You define time windows in UTC — for example, 09:00 to 17:00 on weekdays only. The Solana clock (on-chain Clock::get()) is used as the source of truth, so enforcement works even if the agent's server is offline or tampered with.
Multiple time windows can be configured per sub-account. Outside of all defined windows, transactions are rejected.
#[account]
pub struct TimeRule {
pub sub_account: Pubkey,
pub windows: Vec<TimeWindow>,
pub bump: u8,
}
#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
pub struct TimeWindow {
pub start_hour: u8, // 0-23 UTC
pub end_hour: u8, // 0-23 UTC
pub days: Vec<u8>, // 0=Sun, 1=Mon, ..., 6=Sat
}
pub fn check_time_rules(time_rule: &TimeRule) -> Result<()> {
let clock = Clock::get()?;
let ts = clock.unix_timestamp;
let hour = ((ts % 86_400) / 3_600) as u8;
let day = ((ts / 86_400 + 4) % 7) as u8; // Unix epoch was Thursday
let allowed = time_rule.windows.iter().any(|w| {
w.days.contains(&day) && hour >= w.start_hour && hour < w.end_hour
});
require!(allowed, VaultError::OutsideTimeWindow);
Ok(())
}await subAccount.setTimeRules({
windows: [
{
startHour: 9,
endHour: 17,
days: [1, 2, 3, 4, 5], // Mon–Fri
},
],
});
// Check if the agent can transact right now
const canSpend = await subAccount.isWithinTimeWindow();
console.log("Can spend:", canSpend); // => true or falseAuto top-up ensures sub-accounts never run dry. You set a minimum balance threshold — when the sub-account drops below that threshold, the vault automatically refills it from the main pool up to a predefined target balance. This runs as a permissionless crank: anyone can invoke the top-up instruction, but the rules are enforced on-chain.
#[account]
pub struct AutoTopUp {
pub sub_account: Pubkey,
pub min_balance: u64, // trigger threshold
pub target_balance: u64, // refill to this amount
pub enabled: bool,
pub last_topup: i64,
pub bump: u8,
}
pub fn execute_topup(ctx: Context<ExecuteTopUp>) -> Result<()> {
let config = &ctx.accounts.auto_topup;
let sub = &mut ctx.accounts.sub_account;
let vault = &mut ctx.accounts.vault;
require!(config.enabled, VaultError::TopUpDisabled);
require!(sub.balance() < config.min_balance, VaultError::AboveThreshold);
let refill = config.target_balance - sub.balance();
require!(vault.balance >= refill, VaultError::InsufficientVaultFunds);
vault.balance -= refill;
sub.credit(refill);
msg!("Top-up: {} lamports → {}", refill, sub.agent_id);
Ok(())
}await subAccount.configureAutoTopUp({
minBalance: 500_000_000, // trigger when below 0.5 SOL
targetBalance: 2_000_000_000, // refill to 2 SOL
enabled: true,
});
// Run the crank (permissionless — anyone can call this)
await vault.crankAutoTopUp(subAccount.address);For high-value operations, you can require multiple signatures before a transaction is executed. Multi-sig is configured at the vault level and applies to operations above a defined threshold. Common configurations include 2-of-3 and 3-of-5. Signers are defined on-chain and can be updated by the vault authority.
When a multi-sig payment is initiated, a proposal account is created. Each required signer submits an approval. Once the threshold is met, anyone can finalize the transaction. Proposals expire after a configurable TTL.
#[account]
pub struct MultisigConfig {
pub vault: Pubkey,
pub signers: Vec<Pubkey>,
pub threshold: u8, // e.g. 2 for 2-of-3
pub amount_threshold: u64, // multi-sig required above this amount
pub proposal_ttl: i64, // seconds until proposal expires
pub bump: u8,
}
#[account]
pub struct Proposal {
pub vault: Pubkey,
pub destination: Pubkey,
pub amount: u64,
pub approvals: Vec<Pubkey>,
pub created_at: i64,
pub executed: bool,
pub bump: u8,
}
pub fn approve_proposal(ctx: Context<ApproveProposal>) -> Result<()> {
let proposal = &mut ctx.accounts.proposal;
let config = &ctx.accounts.multisig_config;
let signer = ctx.accounts.signer.key();
require!(
config.signers.contains(&signer),
VaultError::NotAuthorizedSigner
);
require!(
!proposal.approvals.contains(&signer),
VaultError::AlreadyApproved
);
proposal.approvals.push(signer);
if proposal.approvals.len() >= config.threshold as usize {
msg!("Threshold met — proposal ready for execution");
}
Ok(())
}// Configure multi-sig
await vault.configureMultisig({
signers: [wallet1.publicKey, wallet2.publicKey, wallet3.publicKey],
threshold: 2, // 2-of-3
amountThreshold: 5_000_000_000, // require multi-sig above 5 SOL
proposalTtl: 86_400, // 24 hour expiry
});
// Create a proposal
const proposal = await vault.createProposal({
destination: "9pRt7...vendor",
amount: 10_000_000_000, // 10 SOL
});
// Each signer approves
await proposal.approve(wallet1);
await proposal.approve(wallet2); // threshold met
// Execute
await proposal.execute();The MPP Vault TypeScript SDK provides a high-level interface over the on-chain program. It handles PDA derivation, account serialization, transaction building, and confirmation — so you can interact with vaults using clean, typed method calls.
| Method | Description |
|---|---|
| MppVault.create() | Deploy a new vault PDA |
| vault.createSubAccount() | Create an agent sub-account |
| subAccount.setSpendingRules() | Set max per tx / per day limits |
| subAccount.addToWhitelist() | Whitelist destination addresses |
| subAccount.setTimeRules() | Configure time window restrictions |
| subAccount.configureAutoTopUp() | Set auto-refill thresholds |
| vault.configureMultisig() | Set up multi-sig approval flow |
| vault.createProposal() | Initiate a multi-sig payment |
| proposal.approve() | Submit a signer approval |
| proposal.execute() | Finalize an approved proposal |
SDK coming soon
The TypeScript SDK is currently in private beta. The examples on this page reflect the target API surface. Join the waitlist or reach out on Twitter for early access.
npm install @mpp/vault-sdk @solana/web3.js
# or
yarn add @mpp/vault-sdk @solana/web3.js