Full lesson
Apis
What it does
From the project README:
Pay USDC, get IPFS results, settled on-chain via an open Anchor program. No accounts, no middleman, no vendor lock-in. Buyers post jobs, registered workers pick them up, escrow releases on proof of completion.
- Original
- hu-oscar/Apis
- SuperStack fork
- Blockchains/Apis @
468247b - Stars
- 0
- Last push (original)
- 2026-07-21
- Languages
- TypeScript, Python, Rust, CSS, JavaScript, HTML
- Detected stack
- @solana/kitAnchorNext.jssolana-pyweb3.js
- Categories
- Other
- Licence
- Apache-2.0
- Homepage
- https://apis-web-five.vercel.app
Architecture
192 source files. By extension: .ts ×121, .tsx ×25, .rs ×24, .py ×21, .js ×1.
| Top-level directory | Source files |
|---|---|
packages | 192 |
On-chain programs: packages/program/programs/apis_program
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/apis/. 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.
packages/program/programs/apis_program/src/instructions/confirm_completion.rs L145–168 @ 468247b · Apache-2.0 · original .checked_sub(fee) .ok_or(ApisError::ArithmeticOverflow)?; // Pre-CPI invariant — vault must hold exactly the locked price. require!( ctx.accounts.escrow_vault.amount == price, ApisError::VaultAmountMismatch ); // Build PDA signer seeds for the vault transfers + close. let buyer_key = ctx.accounts.job.buyer; let id_le = ctx.accounts.job.id.to_le_bytes(); let job_bump = ctx.accounts.job.bump; let job_seeds: &[&[u8]] = &[b"job", buyer_key.as_ref(), &id_le, &[job_bump]]; let signer_seeds = &[job_seeds]; let token_program = ctx.accounts.token_program.key(); let mint_decimals = ctx.accounts.usdc_mint.decimals; // Payout to provider (omit if 0 — happens only at fee_bps == 10_000). if payout > 0 { let cpi_accounts = TransferChecked { from: ctx.accounts.escrow_vault.to_account_info(), mint: ctx.accounts.usdc_mint.to_account_info(),- Line 145
.checked_sub(fee): Uses checked arithmetic so overflow fails instead of wrapping. - Line 149
require!(: An explicit validation that aborts the instruction with an error. - Line 157
let job_bump = ctx.accounts.job.bump;: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve. - Line 159
let signer_seeds = &[job_seeds];: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match. - Line 166
let cpi_accounts = TransferChecked {: A checked token transfer, which also verifies the mint and decimals.
packages/program/programs/apis_program/src/instructions/create_job.rs L109–132 @ 468247b · Apache-2.0 · original to: ctx.accounts.escrow_vault.to_account_info(), authority: ctx.accounts.buyer.to_account_info(), }; let cpi_ctx = CpiContext::new(ctx.accounts.token_program.key(), cpi_accounts); transfer_checked(cpi_ctx, price_lamports_usdc, ctx.accounts.usdc_mint.decimals)?; // Re-read the vault after the CPI; assert the exact amount we asked // to transfer landed (catches partial-fill edge cases under // Token-2022 transfer hooks, etc.). ctx.accounts.escrow_vault.reload()?; require!( ctx.accounts.escrow_vault.amount == price_lamports_usdc, ApisError::VaultAmountMismatch ); let now = Clock::get()?.unix_timestamp; let deadline = now .checked_add(deadline_offset_secs) .ok_or(ApisError::InvalidDeadline)?; let job = &mut ctx.accounts.job; job.set_inner(Job { id, buyer: ctx.accounts.buyer.key(),- Line 112
let cpi_ctx = CpiContext::new(ctx.accounts.token_program.key(), cpi_accounts);: Makes a cross-program invocation into another program. - Line 113
transfer_checked(cpi_ctx, price_lamports_usdc, ctx.accounts.usdc_mint.decimals)?;: A checked token transfer, which also verifies the mint and decimals. - Line 119
require!(: An explicit validation that aborts the instruction with an error. - Line 124
let now = Clock::get()?.unix_timestamp;: Reads on-chain time from the Clock sysvar. - Line 126
.checked_add(deadline_offset_secs): Uses checked arithmetic so overflow fails instead of wrapping.
packages/program/programs/apis_program/src/instructions/cancel_job.rs L25–47 @ 468247b · Apache-2.0 · original /// Buyer cancelling. Receives the USDC refund + vault/Job rent. #[account(mut)] pub buyer: Signer<'info>, /// Singleton config — only consulted for `usdc_mint`. #[account(seeds = [b"config"], bump = config.bump)] pub config: Account<'info, GlobalConfig>, /// Job to cancel. Must be the buyer's, Funded, and gets closed /// (rent → buyer) via Anchor's `close = buyer`. #[account( mut, close = buyer, seeds = [b"job", buyer.key().as_ref(), job.id.to_le_bytes().as_ref()], bump = job.bump, has_one = buyer @ ApisError::WrongBuyer, constraint = job.status == JobStatus::Funded @ ApisError::JobNotFunded, )] pub job: Account<'info, Job>, /// USDC mint — matches config.usdc_mint. #[account(address = config.usdc_mint @ ApisError::WrongMint)] pub usdc_mint: Account<'info, Mint>,- Line 27
pub buyer: Signer<'info>,: Requires this account to have signed the transaction. - Line 30
#[account(seeds = [b"config"], bump = config.bump)]: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match. - Line 30
#[account(seeds = [b"config"], bump = config.bump)]: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve. - Line 34
/// (rent → buyer) via Anchor's `close = buyer`.: Closes the account at the end of the instruction and refunds its lamports. - Line 40
has_one = buyer @ ApisError::WrongBuyer,: Checks that a stored pubkey field matches the passed account.
packages/program/programs/apis_program/src/instructions/accept_job.rs L13–36 @ 468247b · Apache-2.0 · original/// - `job.provider == provider.key()` (the job was targeted to this/// provider, not someone else)/// - `job.status == Funded` (no double-accept; no jumping a non-funded/// job)#[derive(Accounts)]pub struct AcceptJob<'info> { /// Provider's authority — must match `provider.authority`. pub authority: Signer<'info>, /// Provider PDA the signer owns. #[account( seeds = [b"provider", authority.key().as_ref()], bump = provider.bump, has_one = authority @ ApisError::WrongProviderAuthority, constraint = provider.status == ProviderStatus::Active @ ApisError::ProviderNotActive, )] pub provider: Account<'info, Provider>, /// Job to accept. Must target this provider and be in `Funded` /// status. We rederive the PDA from `job.buyer` / `job.id` to make /// sure the caller hasn't substituted a fake account at the same /// type — Anchor will deserialise + then verify seeds match. #[account(- Line 20
pub authority: Signer<'info>,: Requires this account to have signed the transaction. - Line 24
seeds = [b"provider", authority.key().as_ref()],: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match. - Line 25
bump = provider.bump,: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve. - Line 26
has_one = authority @ ApisError::WrongProviderAuthority,: Checks that a stored pubkey field matches the passed account.
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 build | pnpm -r --parallel build |
. | npm run test | pnpm -r --parallel test |
. | npm run lint | pnpm -r --parallel lint |
packages/program | npm run lint | prettier */*.js "*/**/*{.js,.ts}" --check |
packages/web | npm run dev | next dev |
packages/web | npm run start | next start |
packages/web | npm run build | next build |
packages/web | npm run test | echo "web: tests will be added in W2" && exit 0 |
packages/web | npm run lint | eslint |
packages/mcp | npm run dev | tsx watch src/index.ts |
packages/mcp | npm run start | tsx src/index.ts |
packages/mcp | npm run build | tsc |
packages/mcp | npm run test | echo "mcp: no tests yet" && exit 0 |
packages/apis-provider | npm run dev | vite |
packages/apis-provider | npm run build | tsc && vite build |
packages/agent | npm run build | tsc |
What you could build from it
- Fork the SuperStack copy and extend it within the Other gaps listed on the idea page.
- Learn the pattern first: Program Derived Addresses (PDAs)
- Learn the pattern first: Cross-Program Invocations (CPIs)