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 directory | Source files |
|---|---|
apps | 32 |
packages | 28 |
programs | 22 |
scripts | 2 |
tests | 1 |
examples | 1 |
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.
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]);- Line 52
require!(clock.unix_timestamp < intent.expires_at, GuardError::ActionExpired);: An explicit validation that aborts the instruction with an error. - Line 52
require!(clock.unix_timestamp < intent.expires_at, GuardError::ActionExpired);: Reads on-chain time from the Clock sysvar. - Line 63
record.bump = ctx.bumps.approval_record;: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve. - Line 65
emit!(IntentApproved {: Emits an Anchor event that clients can parse from the logs. - Line 71
msg!("Intent approved: {:?}", &intent_id[..8]);: Logs to the program log, which is visible in explorers and the test output.
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;- 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. - Line 19
bump = action_intent.bump,: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve. - Line 31
pub authority: Signer<'info>,: Requires this account to have signed the transaction. - Line 35
let clock = Clock::get()?;: Reads on-chain time from the Clock sysvar.
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;- Line 61
require!(clock.unix_timestamp < intent.expires_at, GuardError::ActionExpired);: An explicit validation that aborts the instruction with an error. - Line 61
require!(clock.unix_timestamp < intent.expires_at, GuardError::ActionExpired);: Reads on-chain time from the Clock sysvar. - 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. - Line 81
token::transfer(transfer_ctx, intent.amount)?;: An SPL token transfer through the Token Program.
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(())}- Line 25
let clock = Clock::get()?;: Reads on-chain time from the Clock sysvar. - Line 29
require!(: An explicit validation that aborts the instruction with an error. - Line 38
emit!(IntentExpired {: Emits an Anchor event that clients can parse from the logs. - 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.
| Directory | Command | Runs |
|---|---|---|
. | npm run dev | turbo run dev |
. | npm run build | turbo run build |
. | npm run test | turbo run test |
. | npm run lint | turbo run lint |
tests/litesvm | npm run test | tsx index.test.ts |
apps/dashboard | npm run dev | next dev -p 3009 |
apps/dashboard | npm run start | next start -p ${PORT:-3009} |
apps/dashboard | npm run build | next build |
apps/mcp-server | npm run dev | tsx watch src/index.ts |
apps/mcp-server | npm run start | node dist/index.js |
apps/mcp-server | npm run build | tsc |
apps/mcp-server | npm run test | vitest run --passWithNoTests |
apps/guard-api | npm run dev | nest start --watch |
apps/guard-api | npm run start | nest start |
apps/guard-api | npm run build | nest build |
apps/guard-api | npm run test | jest --passWithNoTests |
What you could build from it
- Fork the SuperStack copy and extend it within the AI & Agents gaps listed on the idea page.
- Learn the pattern first: AI agents on Solana
- Learn the pattern first: Building and sending transactions from a client