Full lesson
Fitr
Fitness commitment app for Solana Seeker: stake SKR on your daily workout, on-device AI counts every rep, live challenges and an onchain vault (Squads 2-of-3) settle it. No-shows fund the people who show up.
What it does
From the project README:
A fitness commitment app for the Solana Seeker: stake your own SKR on a daily workout goal, let on-device AI count every rep, and settle the day onchain.
- Original
- NetLayerLabs/Fitr
- SuperStack fork
- Blockchains/Fitr @
79b3dae - Stars
- 0
- Last push (original)
- 2026-10-12
- Languages
- TypeScript, Rust, CSS, JavaScript, Kotlin, Shell
- Detected stack
- @solana/kitAnchorNext.js
- Categories
- AI & AgentsDeFi
- Licence
- MIT
- Homepage
- https://fitr.day
Architecture
717 source files. By extension: .ts ×485, .tsx ×214, .rs ×16, .js ×1, .py ×1.
| Top-level directory | Source files |
|---|---|
src | 660 |
video | 37 |
anchor | 16 |
(root) | 1 |
scripts | 1 |
public | 1 |
ops | 1 |
On-chain programs: anchor/programs/fitr-vault
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/fitr/. 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.
anchor/programs/fitr-vault/src/lib.rs L733–756 @ 79b3dae · MIT · original pool.reserve = pool.reserve.checked_add(amount).ok_or(VaultError::MathOverflow)?; emit!(ReserveFunded { pool: pool.key(), funder: ctx.accounts.funder.key(), amount, crew_id: pool.crew_id, reserve: pool.reserve, daily_sponsor_amount: pool.daily_sponsor_amount, }); Ok(()) } /// Authority can only ever withdraw accrued fees - never principal or the reserve directly. pub fn withdraw_treasury(ctx: Context<WithdrawTreasury>, amount: u64) -> Result<()> { require!(amount <= ctx.accounts.pool.treasury_accrued, VaultError::InsufficientFunds); let mint_key = ctx.accounts.mint.key(); let crew = ctx.accounts.pool.crew_seed(); let bump = [ctx.accounts.pool.bump]; let signer_seeds: &[&[&[u8]]] = &[&[POOL_SEED, mint_key.as_ref(), &crew, &bump]]; token_interface::transfer_checked( CpiContext::new_with_signer( ctx.accounts.token_program.to_account_info(), TransferChecked { from: ctx.accounts.vault.to_account_info(),- Line 733
pool.reserve = pool.reserve.checked_add(amount).ok_or(VaultError::MathOverflow)?;: Uses checked arithmetic so overflow fails instead of wrapping. - Line 734
emit!(ReserveFunded {: Emits an Anchor event that clients can parse from the logs. - Line 747
require!(amount <= ctx.accounts.pool.treasury_accrued, VaultError::InsufficientFunds);: An explicit validation that aborts the instruction with an error. - Line 750
let bump = [ctx.accounts.pool.bump];: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve. - Line 752
token_interface::transfer_checked(: A checked token transfer, which also verifies the mint and decimals. - Line 753
CpiContext::new_with_signer(: Makes a CPI signed by a PDA. The signer seeds prove the program controls that address.
anchor/programs/fitr-vault/src/contexts.rs L97–120 @ 79b3dae · MIT · original #[account(mut)] pub creator: Signer<'info>, /// The global pool (crew_id 0) of `mint`: the config source. #[account( seeds = [POOL_SEED, mint.key().as_ref(), &GLOBAL_CREW_SEED], bump = global_pool.bump, has_one = mint )] pub global_pool: Box<Account<'info, Pool>>, #[account(mint::token_program = token_program)] pub mint: Box<InterfaceAccount<'info, Mint>>, /// The crew pool. `init` fails if this crew_id already exists. #[account( init, payer = creator, space = 8 + Pool::INIT_SPACE, seeds = [POOL_SEED, mint.key().as_ref(), &crew_id.to_le_bytes()], bump )] pub pool: Box<Account<'info, Pool>>, #[account( init, payer = creator, seeds = [VAULT_SEED, pool.key().as_ref()],- Line 98
pub creator: Signer<'info>,: Requires this account to have signed the transaction. - Line 101
seeds = [POOL_SEED, mint.key().as_ref(), &GLOBAL_CREW_SEED],: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match. - Line 102
bump = global_pool.bump,: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve. - Line 103
has_one = mint: Checks that a stored pubkey field matches the passed account. - Line 110
init,: Creates and pays for a new account. Space and payer must be specified.
anchor/programs/fitr-vault/src/math.rs L13–36 @ 79b3dae · MIT · original} /// Inclusive start of day `day`: day * len + offset.pub fn day_start(day: u32, offset: i64, len: i64) -> Result<i64> { (day as i64) .checked_mul(len) .and_then(|v| v.checked_add(offset)) .ok_or(error!(VaultError::MathOverflow))} /// Exclusive end of the day window (== start of next day).pub fn day_end(day: u32, offset: i64, len: i64) -> Result<i64> { day_start(day.checked_add(1).ok_or(VaultError::MathOverflow)?, offset, len)} /// Largest allowed commit cutoff: len - len/24 (23 h for a 24 h day), so at least/// 1/24 of the day always remains to do the workout after the last possible commit.pub fn max_commit_cutoff(len: i64) -> i64 { len - len / 24} /// Hard stake ceiling in base units: MAX_STAKE_CEILING_TOKENS * 10^decimals.pub fn stake_ceiling(decimals: u8) -> Result<u64> { require!(decimals <= MAX_MINT_DECIMALS, VaultError::UnsupportedMintDecimals);- Line 18
.checked_mul(len): Uses checked arithmetic so overflow fails instead of wrapping. - Line 34
/// Hard stake ceiling in base units: MAX_STAKE_CEILING_TOKENS * 10^decimals.: Touches staking state: the amount locked and when it was locked. - Line 36
require!(decimals <= MAX_MINT_DECIMALS, VaultError::UnsupportedMintDecimals);: An explicit validation that aborts the instruction with an error.
src/lib/actions/spec.ts L1–24 @ 79b3dae · MIT · original/** * Solana Actions (Blinks) wire format and HTTP helpers. Pure: no network, importable anywhere. * * Spec: https://solana.com/docs/advanced/actions (Actions spec v2.x). * - GET → ActionGetResponse (metadata: icon, title, description, label, links.actions) * - POST {account} → ActionPostResponse {type: "transaction", transaction (base64, unsigned, * fee payer = account), message} * - OPTIONS → CORS preflight. Every response (including errors) carries the CORS headers plus * X-Action-Version / X-Blockchain-Ids so blink clients (dial.to, wallets) accept it. * - errors → 4xx/5xx with ActionError {message} (shown to the user verbatim). * - /actions.json at the domain root maps website paths to action API paths. */import type { SolanaCluster } from "@/lib/solana/config"; export const ACTION_VERSION = "2.4"; /** CAIP-2 ids: solana:<first 32 chars of the genesis hash>. */export const BLOCKCHAIN_IDS: Readonly<Record<SolanaCluster, string>> = Object.freeze({ mainnet: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", devnet: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",}); export function actionsCorsHeaders(cluster: SolanaCluster): Record<string, string> { return {- Line 5
* - GET → ActionGetResponse (metadata: icon, title, description, label, links.actions): The metadata a Blink client renders (title, icon, buttons). - Line 6
* - POST {account} → ActionPostResponse {type: "transaction", transaction (base64, unsigne: Returns the serialized transaction the wallet will sign.
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 | next dev |
. | npm run start | next start |
. | npm run build | next build |
. | npm run test | vitest run |
. | npm run lint | eslint |
anchor | anchor build | compile programs and generate IDLs |
anchor | anchor test | local validator + test suite |
anchor | anchor run test | cd programs/fitr-vault && CARGO_RESOLVER_INCOMPATIBLE_RUST_VERSIONS=fallback cargo test |
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: Building and sending transactions from a client
- Learn the pattern first: Blinks and Solana Actions