documentation.

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.

Getting Started

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

  • 1. Deploy a vault (PDA) on Solana
  • 2. Create sub-accounts for each agent
  • 3. Set spending rules, whitelist, time windows
  • 4. Agents transact through the vault — rules enforced on-chain

Architecture

The MPP Vault system consists of four on-chain components:

  • Solana program — built with Anchor framework, holds all logic
  • Vault PDA — program-derived address that holds all funds (USDC as primary currency)
  • Sub-account PDAs — one per agent, derived from vault + agent ID
  • Rules as account data — spending limits, whitelist, time rules stored on each sub-account

Vault owner can:

  • Create / pause / close sub-accounts
  • Set and modify rules per sub-account
  • Add / remove from whitelist
  • Deposit / withdraw funds
  • Set multi-sig threshold
  • Emergency stop — pause an agent instantly

Agents can:

  • View own balance and rules
  • Make payments within rules
  • View own transaction history

Payment Flow

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.

1

Agent wants to pay an MPP service

2

Service responds HTTP 402 with payment requirement

3

Agent sends a transaction to the Vault program (not a direct payment)

4

Program checks: has budget? Whitelisted? Within time window? Under max?

5

If all checks pass, program sends payment from agent’s sub-account to service

6

Transaction logged on-chain — fully auditable

7

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.


Create a Vault

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.

create_vault.rs
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>,
}
TypeScript SDK
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...f9mP

Sub-Accounts

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

create_sub_account.rs
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>,
}
TypeScript SDK
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

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:

  • Max per transaction — rejects any single transaction above the limit.
  • Max per day — rolling 24-hour spending cap. Resets based on Solana clock timestamps.
set_spending_rules.rs
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(())
}
TypeScript SDK
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`);

Whitelist

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.

whitelist.rs
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(())
}
TypeScript SDK
// 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

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.

time_rules.rs
#[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(())
}
TypeScript SDK
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 false

Auto Top-Up

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

auto_topup.rs
#[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(())
}
TypeScript SDK
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);

Multi-Sig

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.

multisig.rs
#[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(())
}
TypeScript SDK
// 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();

API Reference

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.

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

Install (coming soon)
npm install @mpp/vault-sdk @solana/web3.js

# or
yarn add @mpp/vault-sdk @solana/web3.js