Sol.College

Full lesson

three.ws

Give your AI agent a 3D body: text or a photo to a rigged GLB avatar, an LLM brain, a Solana wallet and a one-tag embed. Keyless MCP server for Claude, Cursor and Codex. 50k+ models forged.

What it does

From the project README:

**Website** · **Docs** · **Changelog** · **X / Twitter** · **GitHub** · **$THREE on pump.fun**
Original
nirholas/three.ws
SuperStack fork
Blockchains/three.ws @ ab4b79e
Stars
266
Last push (original)
2026-11-03
Languages
JavaScript, HTML, Markdown, TypeScript, CSS, Solidity
Detected stack
@solana/kitAnchorMetaplexSolana Agent KitSolana Payweb3.js
Categories
AI & AgentsDeFi
Licence
Apache-2.0
Homepage
https://three.ws

Architecture

12246 source files. By extension: .js ×9770, .ts ×1175, .sol ×655, .py ×366, .tsx ×184, .jsx ×69.

Top-level directorySource files
api3144
tests2687
src1712
packages1057
contracts921
apps612
robinhood451
workers392

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/three-ws/. 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.

nirholas/three.ws · contracts/event-markets-stake/src/lib.rs L145–168 @ ab4b79e · Apache-2.0 · original
        p.max_pool = max_pool;        p.lock_ts = lock_ts;        p.void_after_ts = void_after_ts;        p.status = STATUS_OPEN;        p.winning_outcome = 0;        p.totals = [0; MAX_OUTCOMES];        p.total_staked = 0;        p.fee_paid = 0;        p.paid_out = 0;        p.bump = ctx.bumps.pool;        emit!(PoolCreated { pool: p.key(), pool_id, mint: p.mint, lock_ts, void_after_ts });        Ok(())    }     pub fn stake(ctx: Context<Stake>, outcome: u8, amount: u64) -> Result<()> {        require!(!ctx.accounts.config.paused, StakeError::Paused);        let now = Clock::get()?.unix_timestamp;        let pool = &mut ctx.accounts.pool;        require!(pool.status == STATUS_OPEN, StakeError::NotOpen);        require!(now < pool.lock_ts, StakeError::Locked);        require!(outcome < pool.outcome_count, StakeError::BadOutcome);        require!(amount >= pool.min_stake, StakeError::BelowMinimum);         let pos = &mut ctx.accounts.position;
  1. Line 154 p.bump = ctx.bumps.pool;: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
  2. Line 155 emit!(PoolCreated { pool: p.key(), pool_id, mint: p.mint, lock_ts, void_after_ts });: Emits an Anchor event that clients can parse from the logs.
  3. Line 159 pub fn stake(ctx: Context<Stake>, outcome: u8, amount: u64) -> Result<()> {: Touches staking state: the amount locked and when it was locked.
  4. Line 160 require!(!ctx.accounts.config.paused, StakeError::Paused);: An explicit validation that aborts the instruction with an error.
  5. Line 161 let now = Clock::get()?.unix_timestamp;: Reads on-chain time from the Clock sysvar.
nirholas/three.ws · contracts/skill-license/src/lib.rs L169–192 @ ab4b79e · Apache-2.0 · original
    /// longer access-bearing.    pub fn revoke_skill_license(ctx: Context<RevokeSkillLicense>) -> Result<()> {        require!(            ctx.accounts.skill_license.revoked_at == 0,            SkillLicenseError::AlreadyRevoked        );         let marketplace_bump = ctx.accounts.marketplace.bump;        let signer_seeds: &[&[&[u8]]] = &[&[MARKETPLACE_SEED, &[marketplace_bump]]];         token::freeze_account(CpiContext::new_with_signer(            ctx.accounts.token_program.to_account_info(),            FreezeAccount {                account: ctx.accounts.owner_token_account.to_account_info(),                mint: ctx.accounts.nft_mint.to_account_info(),                authority: ctx.accounts.marketplace.to_account_info(),            },            signer_seeds,        ))?;         let now = Clock::get()?.unix_timestamp;        ctx.accounts.skill_license.revoked_at = now;         emit!(SkillLicenseRevoked {
  1. Line 171 require!(: An explicit validation that aborts the instruction with an error.
  2. Line 176 let marketplace_bump = ctx.accounts.marketplace.bump;: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
  3. Line 179 token::freeze_account(CpiContext::new_with_signer(: Makes a CPI signed by a PDA. The signer seeds prove the program controls that address.
  4. Line 189 let now = Clock::get()?.unix_timestamp;: Reads on-chain time from the Clock sysvar.
  5. Line 192 emit!(SkillLicenseRevoked {: Emits an Anchor event that clients can parse from the logs.
nirholas/three.ws · contracts/knock-escrow/src/lib.rs L169–192 @ ab4b79e · Apache-2.0 · original
        if let Some(o) = open {            door.open = o;        }        Ok(())    }     /// Pay the door's price into escrow and record one knock.    ///    /// `nonce` lets one sender knock the same door repeatedly; `message_hash`    /// is the SHA-256 of the message body, which stays off-chain.    pub fn knock(ctx: Context<Knock>, nonce: u64, message_hash: [u8; 32]) -> Result<()> {        let door = &ctx.accounts.door;        require!(door.open, KnockError::DoorClosed);        require_keys_eq!(door.mint, ctx.accounts.mint.key(), KnockError::WrongMint);         let price = door.price;        let now = Clock::get()?.unix_timestamp;        let expires_at = now            .checked_add(door.reply_window)            .ok_or(KnockError::MathOverflow)?;         token_interface::transfer_checked(            CpiContext::new(                ctx.accounts.token_program.to_account_info(),
  1. Line 181 require!(door.open, KnockError::DoorClosed);: An explicit validation that aborts the instruction with an error.
  2. Line 185 let now = Clock::get()?.unix_timestamp;: Reads on-chain time from the Clock sysvar.
  3. Line 187 .checked_add(door.reply_window): Uses checked arithmetic so overflow fails instead of wrapping.
  4. Line 190 token_interface::transfer_checked(: A checked token transfer, which also verifies the mint and decimals.
  5. Line 191 CpiContext::new(: Makes a cross-program invocation into another program.
nirholas/three.ws · contracts/agent-invocation/src/lib.rs L61–84 @ ab4b79e · Apache-2.0 · original
        );         emit!(SkillInvoked {            invoker_agent,            target_agent,            invoker_authority: ctx.accounts.invoker_authority.key(),            skill_name,            parameters,            timestamp: Clock::get()?.unix_timestamp,        });         Ok(())    }} #[derive(Accounts)]#[instruction(skill_name: String, parameters: String)]pub struct InvokeSkill<'info> {    /// The invoking agent's program-derived identity. Constrained to the PDA    /// for `invoker_authority`, binding the on-chain agent to the signer.    /// CHECK: validated by the `seeds`/`bump` PDA constraint against this    /// program; never read or written, only its key is recorded.    #[account(        seeds = [AGENT_SEED, invoker_authority.key().as_ref()],
  1. Line 63 emit!(SkillInvoked {: Emits an Anchor event that clients can parse from the logs.
  2. Line 69 timestamp: Clock::get()?.unix_timestamp,: Reads on-chain time from the Clock sysvar.
  3. Line 81 /// CHECK: validated by the `seeds`/`bump` PDA constraint against this: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
  4. Line 84 seeds = [AGENT_SEED, invoker_authority.key().as_ref()],: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match.

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 devvite --port 3000
.npm run buildNODE_OPTIONS='--no-deprecation --max-old-space-size=6144' vite build && node scripts/strip-sw-from-embeds.mjs && node sc
.npm run testvitest run && playwright test
.npm run linteslint .
.npm run deploynpm run deploy:gcp:full
third_party/threeuinpm run devvite
third_party/threeuinpm run buildnpm run test && npm run build:site && npm run build:lib
third_party/threeuinpm run testnode --test scripts/community-sanitizers.test.mjs scripts/public-boundary.test.mjs scripts/library-package.test.mjs scri
third_party/image-blasternpm run devbun --cwd=app run dev
third_party/image-blasternpm run buildbun --cwd=app run build
third_party/image-blasternpm run testbun --cwd=app run test
tour-sdknpm run buildnode build.mjs
tour-sdknpm run testvitest run
chatnpm run devvite
chatnpm run buildvite build
agent-protocol-sdknpm run buildtsc

What you could build from it

Used in lessons