Sol.College

Full lesson

agentchain-guard-solana

AgentChain Guard for Solana

What it does

From the project README:

Warrant (signed spend permission) lives in `packages/warrant` (`@agentguard/warrant`). All hackathon work stays in this folder; CI pushes it to the public AgentGuard repo via subtree sync.
Original
humbleaf/agentchain-guard-solana
SuperStack fork
Blockchains/agentchain-guard-solana @ f917e9b
Stars
0
Last push (original)
2026-10-17
Languages
TypeScript, CSS, Rust, Shell, JavaScript, Makefile
Detected stack
AnchorNext.jsweb3.js
Categories
AI & Agents
Licence
MIT
Homepage
https://agentchain-guard-solana-dashboard.vercel.app

Architecture

86 source files. By extension: .ts ×60, .rs ×22, .tsx ×3, .js ×1.

Top-level directorySource files
apps32
packages28
programs22
scripts2
tests1
examples1

On-chain programs: programs/agentchain-guard

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/agentchain-guard-solana/. 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.

humbleaf/agentchain-guard-solana · programs/agentchain-guard/src/instructions/approve_action.rs L49–71 @ f917e9b · MIT · original
    let intent = &mut ctx.accounts.action_intent;     // Check expiry    require!(clock.unix_timestamp < intent.expires_at, GuardError::ActionExpired);     // Update intent status    intent.status = status::APPROVED_READY;     // Write approval record    let record = &mut ctx.accounts.approval_record;    record.intent = intent_key;    record.intent_id = intent_id;    record.approver = ctx.accounts.approver.key();    record.approved_at = clock.unix_timestamp;    record.bump = ctx.bumps.approval_record;     emit!(IntentApproved {        intent_id,        approver: ctx.accounts.approver.key(),        timestamp: clock.unix_timestamp,    });     msg!("Intent approved: {:?}", &intent_id[..8]);
  1. Line 52 require!(clock.unix_timestamp < intent.expires_at, GuardError::ActionExpired);: An explicit validation that aborts the instruction with an error.
  2. Line 52 require!(clock.unix_timestamp < intent.expires_at, GuardError::ActionExpired);: Reads on-chain time from the Clock sysvar.
  3. Line 63 record.bump = ctx.bumps.approval_record;: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
  4. Line 65 emit!(IntentApproved {: Emits an Anchor event that clients can parse from the logs.
  5. Line 71 msg!("Intent approved: {:?}", &intent_id[..8]);: Logs to the program log, which is visible in explorers and the test output.
humbleaf/agentchain-guard-solana · programs/agentchain-guard/src/instructions/deny_action.rs L13–36 @ f917e9b · MIT · original
    )]    pub guard_config: Account<'info, GuardConfig>,     #[account(        mut,        seeds = [INTENT_SEED, action_intent.id.as_ref()],        bump = action_intent.bump,        constraint = action_intent.status == status::PENDING_APPROVAL || action_intent.status == status::PROPOSED            @ GuardError::InvalidApprovalState,    )]    pub action_intent: Account<'info, ActionIntent>,     /// Must be the guard authority or approval_signer    #[account(        constraint = authority.key() == guard_config.authority            || authority.key() == guard_config.approval_signer            @ GuardError::Unauthorized,    )]    pub authority: Signer<'info>,} pub fn handler(ctx: Context<DenyAction>) -> Result<()> {    let clock = Clock::get()?;    let intent = &mut ctx.accounts.action_intent;
  1. Line 18 seeds = [INTENT_SEED, action_intent.id.as_ref()],: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match.
  2. Line 19 bump = action_intent.bump,: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
  3. Line 31 pub authority: Signer<'info>,: Requires this account to have signed the transaction.
  4. Line 35 let clock = Clock::get()?;: Reads on-chain time from the Clock sysvar.
humbleaf/agentchain-guard-solana · programs/agentchain-guard/src/instructions/settle_action.rs L61–84 @ f917e9b · MIT · original
    require!(clock.unix_timestamp < intent.expires_at, GuardError::ActionExpired);     // ── Transfer from vault to recipient ──    let mint_key = ctx.accounts.guard_config.treasury_mint;    let vault_bump = ctx.bumps.vault_token_account;    let signer_seeds: &[&[&[u8]]] = &[&[        VAULT_SEED,        mint_key.as_ref(),        &[vault_bump],    ]];     let transfer_ctx = CpiContext::new_with_signer(        ctx.accounts.token_program.to_account_info(),        Transfer {            from: ctx.accounts.vault_token_account.to_account_info(),            to: ctx.accounts.recipient_token_account.to_account_info(),            authority: ctx.accounts.vault_token_account.to_account_info(),        },        signer_seeds,    );    token::transfer(transfer_ctx, intent.amount)?;     // ── Update intent state ──    let intent = &mut ctx.accounts.action_intent;
  1. Line 61 require!(clock.unix_timestamp < intent.expires_at, GuardError::ActionExpired);: An explicit validation that aborts the instruction with an error.
  2. Line 61 require!(clock.unix_timestamp < intent.expires_at, GuardError::ActionExpired);: Reads on-chain time from the Clock sysvar.
  3. Line 72 let transfer_ctx = CpiContext::new_with_signer(: Makes a CPI signed by a PDA. The signer seeds prove the program controls that address.
  4. Line 81 token::transfer(transfer_ctx, intent.amount)?;: An SPL token transfer through the Token Program.
humbleaf/agentchain-guard-solana · programs/agentchain-guard/src/instructions/sweep_expired.rs L25–46 @ f917e9b · MIT · original
    let clock = Clock::get()?;    let intent = &ctx.accounts.action_intent;     // Must actually be expired    require!(        clock.unix_timestamp >= intent.expires_at,        GuardError::ActionNotExpired,    );     // Transition to expired — funds can then be refunded    let intent = &mut ctx.accounts.action_intent;    intent.status = status::EXPIRED;     emit!(IntentExpired {        intent_id: intent.id,        timestamp: clock.unix_timestamp,    });     msg!("Intent expired: {:?}", &intent.id[..8]);     Ok(())}
  1. Line 25 let clock = Clock::get()?;: Reads on-chain time from the Clock sysvar.
  2. Line 29 require!(: An explicit validation that aborts the instruction with an error.
  3. Line 38 emit!(IntentExpired {: Emits an Anchor event that clients can parse from the logs.
  4. Line 43 msg!("Intent expired: {:?}", &intent.id[..8]);: Logs to the program log, which is visible in explorers and the test output.

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 devturbo run dev
.npm run buildturbo run build
.npm run testturbo run test
.npm run lintturbo run lint
tests/litesvmnpm run testtsx index.test.ts
apps/dashboardnpm run devnext dev -p 3009
apps/dashboardnpm run startnext start -p ${PORT:-3009}
apps/dashboardnpm run buildnext build
apps/mcp-servernpm run devtsx watch src/index.ts
apps/mcp-servernpm run startnode dist/index.js
apps/mcp-servernpm run buildtsc
apps/mcp-servernpm run testvitest run --passWithNoTests
apps/guard-apinpm run devnest start --watch
apps/guard-apinpm run startnest start
apps/guard-apinpm run buildnest build
apps/guard-apinpm run testjest --passWithNoTests

What you could build from it

Used in lessons