Sol.College

Full lesson

agent-escrow

Trustless escrow and verifiable settlement for autonomous AI agents on Solana.

What it does

From the project README:

▶ Watch the 2-minute video on YouTube, with sound (or the MP4). Recorded from the real product on Solana devnet: every arrow is a real transaction (jobs paid and rejected). Re-create it with pnpm promo on a local validator; the devnet take needs funded demo wallets and a GitHub token, see Recording the promo video.
Original
aralroca/agent-escrow
SuperStack fork
Blockchains/agent-escrow @ c1508a2
Stars
0
Last push (original)
2026-10-03
Languages
TypeScript, Rust, CSS, JavaScript, HTML
Detected stack
@solana/kitAnchor
Categories
AI & Agents
Licence
MIT
Homepage
https://aralroca.github.io/agent-escrow

Architecture

123 source files. By extension: .ts ×91, .tsx ×18, .rs ×14.

Top-level directorySource files
packages55
apps42
programs14
e2e10
(root)2

On-chain programs: programs/agent_escrow

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/agent-escrow/. 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.

aralroca/agent-escrow · programs/agent_escrow/src/instructions/deliver.rs L1–24 @ c1508a2 · MIT · original
use anchor_lang::prelude::*; use crate::{    constants::*,    error::ErrorCode,    state::{AgentProfile, Job, JobStatus},}; #[derive(Accounts)]pub struct Accept<'info> {    pub provider: Signer<'info>,    #[account(mut, has_one = provider @ ErrorCode::Unauthorized)]    pub job: Account<'info, Job>,    /// Only registered agents can take jobs, so every settlement has a profile to update.    #[account(seeds = [AGENT_SEED, provider.key().as_ref()], bump = provider_profile.bump)]    pub provider_profile: Account<'info, AgentProfile>,} pub fn handle_accept(ctx: Context<Accept>) -> Result<()> {    let job = &mut ctx.accounts.job;     require!(job.status == JobStatus::Funded, ErrorCode::InvalidStatus);    require!(        Clock::get()?.unix_timestamp < job.deadline,
  1. Line 11 pub provider: Signer<'info>,: Requires this account to have signed the transaction.
  2. Line 12 #[account(mut, has_one = provider @ ErrorCode::Unauthorized)]: Checks that a stored pubkey field matches the passed account.
  3. Line 15 #[account(seeds = [AGENT_SEED, provider.key().as_ref()], bump = provider_profile.bump)]: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match.
  4. Line 15 #[account(seeds = [AGENT_SEED, provider.key().as_ref()], bump = provider_profile.bump)]: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
  5. Line 22 require!(job.status == JobStatus::Funded, ErrorCode::InvalidStatus);: An explicit validation that aborts the instruction with an error.
  6. Line 24 Clock::get()?.unix_timestamp < job.deadline,: Reads on-chain time from the Clock sysvar.
aralroca/agent-escrow · programs/agent_escrow/src/instructions/register_agent.rs L1–24 @ c1508a2 · MIT · original
use anchor_lang::prelude::*; use crate::{constants::*, error::ErrorCode, state::AgentProfile}; #[derive(Accounts)]pub struct RegisterAgent<'info> {    #[account(mut)]    pub authority: Signer<'info>,    #[account(        init_if_needed,        payer = authority,        space = 8 + AgentProfile::INIT_SPACE,        seeds = [AGENT_SEED, authority.key().as_ref()],        bump,    )]    pub profile: Account<'info, AgentProfile>,    pub system_program: Program<'info, System>,} /// Creates the agent profile, or updates its public fields without touching the counters.pub fn handle_register_agent(    ctx: Context<RegisterAgent>,    name: String,    capabilities: String,
  1. Line 8 pub authority: Signer<'info>,: Requires this account to have signed the transaction.
  2. Line 10 init_if_needed,: Creates the account only if it doesn't exist. Watch for re-initialisation bugs.
  3. Line 13 seeds = [AGENT_SEED, authority.key().as_ref()],: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match.
  4. Line 14 bump,: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
aralroca/agent-escrow · programs/agent_escrow/src/instructions/release.rs L13–36 @ c1508a2 · MIT · original
 /// Accounts for paying the provider: by evaluator approval or by review timeout.#[derive(Accounts)]pub struct Release<'info> {    #[account(mut)]    pub authority: Signer<'info>,    #[account(mut, has_one = client, has_one = provider, has_one = mint)]    pub job: Account<'info, Job>,    /// CHECK: bound to the job by `has_one`; only receives the vault rent it paid.    #[account(mut)]    pub client: UncheckedAccount<'info>,    /// CHECK: bound to the job by `has_one`; only used to derive the accounts below.    pub provider: UncheckedAccount<'info>,    pub mint: InterfaceAccount<'info, Mint>,    #[account(        mut,        seeds = [VAULT_SEED, job.key().as_ref()],        bump = job.vault_bump,        token::token_program = token_program,    )]    pub vault: InterfaceAccount<'info, TokenAccount>,    #[account(        mut,        token::mint = mint,
  1. Line 18 pub authority: Signer<'info>,: Requires this account to have signed the transaction.
  2. Line 19 #[account(mut, has_one = client, has_one = provider, has_one = mint)]: Checks that a stored pubkey field matches the passed account.
  3. Line 29 seeds = [VAULT_SEED, job.key().as_ref()],: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match.
  4. Line 30 bump = job.vault_bump,: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
aralroca/agent-escrow · programs/agent_escrow/src/instructions/return_funds.rs L1–24 @ c1508a2 · MIT · original
use anchor_lang::prelude::*;use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; use crate::{    constants::*,    error::ErrorCode,    state::{AgentProfile, Job, JobStatus},    vault::Vault,}; /// Accounts for sending the payment back to the client: rejection, cancellation or expiry.#[derive(Accounts)]pub struct ReturnFunds<'info> {    pub authority: Signer<'info>,    #[account(mut, has_one = client, has_one = mint)]    pub job: Account<'info, Job>,    /// CHECK: bound to the job by `has_one`; only receives the vault rent it paid.    #[account(mut)]    pub client: UncheckedAccount<'info>,    pub mint: InterfaceAccount<'info, Mint>,    #[account(        mut,        seeds = [VAULT_SEED, job.key().as_ref()],        bump = job.vault_bump,
  1. Line 14 pub authority: Signer<'info>,: Requires this account to have signed the transaction.
  2. Line 15 #[account(mut, has_one = client, has_one = mint)]: Checks that a stored pubkey field matches the passed account.
  3. Line 23 seeds = [VAULT_SEED, job.key().as_ref()],: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match.
  4. Line 24 bump = job.vault_bump,: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.

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 buildpnpm --filter @agent-escrow/web build && pnpm --filter agent-escrow-mcp build
.npm run testvitest run
.npm run lintbiome check .
apps/webnpm run devvite
apps/webnpm run buildvite build
packages/mcpnpm run buildnode scripts/build.mjs
.anchor buildcompile programs and generate IDLs
.anchor testlocal validator + test suite
.anchor run testcargo test
.cargo buildRust workspace build
.cargo testRust tests

What you could build from it

Used in lessons