Sol.College

Full lesson

X402Guard

Solana-native spending guardrails for AI agents. Built for Colosseum Frontier 2026.

What it does

From the project README:

[](LICENSE) [](https://colosseum.com/frontier) [](https://x402guard.acedata.cloud/.well-known/x402guard)
Original
AceDataCloud/X402Guard
SuperStack fork
Blockchains/X402Guard @ 159b9c0
Stars
0
Last push (original)
2026-06-06
Languages
Python, TypeScript, Vue, Rust, Shell, Dockerfile
Detected stack
Anchorsolana-pyweb3.js
Categories
AI & AgentsPayments
Licence
MIT
Homepage
https://x402guard.acedata.cloud

Architecture

51 source files. By extension: .py ×28, .ts ×15, .rs ×7, .js ×1.

Top-level directorySource files
api27
web13
programs7
tests3
scripts1

On-chain programs: programs/agent_vault

Dapp: devnet deploy pending

⏳ Devnet deploy pending — goes live when the devnet wallet is topped up. This fork's own dapp is not deployed yet, so there is no program address or transaction to show. It is queued in dapp-queue.txt; once devnet SOL is available it is deployed, exercised with real devnet transactions, and this section switches to the live dapp at /dapps/x402guard/. Until then, the lesson and step-by-step guide below are complete, and you can run it locally with the commands below.

Key code, explained

Excerpts chosen automatically where the code uses the most Solana concepts, pinned to the fork's commit.

AceDataCloud/X402Guard · programs/agent_vault/src/instructions/spend.rs L49–72 @ 159b9c0 · MIT · original
    #[account(        mut,        seeds = [SEED_POLICY, vault.key().as_ref()],        bump = policy.bump,        has_one = vault,    )]    pub policy: Box<Account<'info, Policy>>,     #[account(address = vault.usdc_mint)]    pub usdc_mint: Box<Account<'info, Mint>>,     #[account(        mut,        associated_token::mint = usdc_mint,        associated_token::authority = vault,    )]    pub vault_usdc_ata: Box<Account<'info, TokenAccount>>,     /// Recipient's USDC ATA. Must already exist (the upstream x402 facilitator    /// always has one — for now we don't auto-create here to keep the spend    /// instruction lean. Backend tx builder pre-flights this and adds an    /// `init_if_needed` ix in the same tx if necessary.)    #[account(        mut,
  1. Line 51 seeds = [SEED_POLICY, vault.key().as_ref()],: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match.
  2. Line 52 bump = policy.bump,: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
  3. Line 53 has_one = vault,: Checks that a stored pubkey field matches the passed account.
  4. Line 67 /// Recipient's USDC ATA. Must already exist (the upstream x402 facilitator: Uses x402, an HTTP 402 pattern where an agent pays per request with an on-chain payment.
  5. Line 70 /// `init_if_needed` ix in the same tx if necessary.): Creates the account only if it doesn't exist. Watch for re-initialisation bugs.
AceDataCloud/X402Guard · programs/agent_vault/src/instructions/owner_ops.rs L25–48 @ 159b9c0 · MIT · original
        has_one = owner,    )]    pub vault: Box<Account<'info, Vault>>,     #[account(        mut,        seeds = [SEED_POLICY, vault.key().as_ref()],        bump = policy.bump,        has_one = vault,    )]    pub policy: Box<Account<'info, Policy>>,} // ─── pause / resume ─────────────────────────────────────────────────────── pub fn pause(ctx: Context<OwnerOnly>) -> Result<()> {    let policy = &mut ctx.accounts.policy;    policy.paused = true;    emit!(VaultPaused {        vault: ctx.accounts.vault.key(),        ts: Clock::get()?.unix_timestamp,    });    Ok(())}
  1. Line 25 has_one = owner,: Checks that a stored pubkey field matches the passed account.
  2. Line 31 seeds = [SEED_POLICY, vault.key().as_ref()],: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match.
  3. Line 32 bump = policy.bump,: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
  4. Line 43 emit!(VaultPaused {: Emits an Anchor event that clients can parse from the logs.
  5. Line 45 ts: Clock::get()?.unix_timestamp,: Reads on-chain time from the Clock sysvar.
AceDataCloud/X402Guard · programs/agent_vault/src/instructions/create_vault.rs L25–47 @ 159b9c0 · MIT · original
    pub per_call_cap: u64,     /// `sha256(host)` of each permitted endpoint. Up to `MAX_ALLOWLIST` (8) entries.    pub endpoint_allowlist: Vec<[u8; 32]>,     /// Unix timestamp (seconds). Must be strictly in the future at submit time.    pub expires_at: i64,} #[derive(Accounts)]#[instruction(args: CreateVaultArgs)]pub struct CreateVault<'info> {    #[account(mut)]    pub owner: Signer<'info>,     #[account(        init,        payer = owner,        space = Vault::SIZE,        seeds = [SEED_VAULT, owner.key().as_ref(), &args.agent_id],        bump,    )]    pub vault: Box<Account<'info, Vault>>,
  1. Line 38 pub owner: Signer<'info>,: Requires this account to have signed the transaction.
  2. Line 41 init,: Creates and pays for a new account. Space and payer must be specified.
  3. Line 44 seeds = [SEED_VAULT, owner.key().as_ref(), &args.agent_id],: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match.
  4. Line 45 bump,: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
AceDataCloud/X402Guard · programs/agent_vault/src/state.rs L25–48 @ 159b9c0 · MIT · original
    /// User who owns this vault. Only this signer can pause / resume / clawback / update policy.    pub owner: Pubkey,    /// Application-defined agent identifier, supplied by the owner at creation.    /// Lets one user own multiple distinct vaults (e.g. one per agent persona).    pub agent_id: [u8; 32],    /// USDC mint this vault holds. Locked at creation so the policy can't be evaded    /// by swapping in a different SPL mint later.    pub usdc_mint: Pubkey,    pub bump: u8,    pub created_at: i64,} impl Vault {    /// 8-byte discriminator + struct fields.    pub const SIZE: usize = 8 + 32 + 32 + 32 + 1 + 8;} #[account]#[derive(Debug)]pub struct Policy {    pub vault: Pubkey,    /// Ed25519 public key allowed to authorise `spend` ixs. Held by the x402guard    /// backend session for the hackathon; v2 will move this to user-side WebAuthn.    pub delegation_key: Pubkey,
  1. Line 33 pub bump: u8,: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
  2. Line 46 /// Ed25519 public key allowed to authorise `spend` ixs. Held by the x402guard: Uses x402, an HTTP 402 pattern where an agent pays per request with an on-chain payment.

How to run, build and test

These commands come from the repo's own manifests (package.json scripts, Anchor.toml, Cargo, go.mod, Makefile). Check the README for prerequisites and environment variables.

DirectoryCommandRuns
.npm run testts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts
webnpm run devvite
webnpm run buildvue-tsc --noEmit --skipLibCheck && vite build
webnpm run linteslint --ext .ts,.vue src
.anchor buildcompile programs and generate IDLs
.anchor testlocal validator + test suite
.anchor run testyarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts
.cargo buildRust workspace build
.cargo testRust tests

What you could build from it