Sol.College

Full lesson

tributary

Bringing Web2's subscription simplicity to Web3. Tributary enables any Solana-based business to offer truly automated recurring payments

What it does

From the project README:

Automated recurring payments on Solana using token delegation. Web2 subscription UX with Web3 transparency — non-custodial, permissionless, and composable.
Original
tributary-so/tributary
SuperStack fork
Blockchains/tributary @ 4b0a32e
Stars
12
Last push (original)
2026-09-28
Languages
TypeScript, Rust, Lean, CSS, HTML, JavaScript
Detected stack
@solana/kitAnchorMetaplexweb3.js
Categories
Payments
Licence
MIT
Homepage
https://tributary.so

Architecture

550 source files. By extension: .ts ×324, .tsx ×132, .rs ×62, .js ×29, .py ×3.

Top-level directorySource files
apps303
packages159
programs61
tests22
formal_verification3
(root)2

On-chain programs: programs/tributary

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/tributary/. 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.

tributary-so/tributary · programs/tributary/src/shared/referral.rs L421–444 @ 4b0a32e · MIT · original
        authority: authority_info.clone(),    };    let cpi_ctx = match authority_mode {        AuthorityMode::Direct => CpiContext::new(token_program.key(), cpi_accounts),        AuthorityMode::PdaSigner(seeds) => {            CpiContext::new_with_signer(token_program.key(), cpi_accounts, seeds)        }    };    token_interface::transfer_checked(cpi_ctx, reward, mint_decimals)?;     let mut referral_account = referral_loader.load_mut()?;    referral_account.total_earned = referral_account        .total_earned        .checked_add(reward)        .ok_or(TributaryError::ArithmeticOverflow)?;     Ok(())} // ──────────────────────────────────────────────────────────────────// C-02: referral chain topology validation// ──────────────────────────────────────────────────────────────────#[cfg(test)]mod tests {
  1. Line 424 AuthorityMode::Direct => CpiContext::new(token_program.key(), cpi_accounts),: Makes a cross-program invocation into another program.
  2. Line 426 CpiContext::new_with_signer(token_program.key(), cpi_accounts, seeds): Makes a CPI signed by a PDA. The signer seeds prove the program controls that address.
  3. Line 429 token_interface::transfer_checked(cpi_ctx, reward, mint_decimals)?;: A checked token transfer, which also verifies the mint and decimals.
  4. Line 434 .checked_add(reward): Uses checked arithmetic so overflow fails instead of wrapping.
tributary-so/tributary · programs/tributary/src/instructions/initialize.rs L1–24 @ 4b0a32e · MIT · original
use crate::{error::TributaryError, state::*, CONFIG_SEED};use anchor_lang::prelude::*; #[derive(Accounts)]pub struct Initialize<'info> {    #[account(mut)]    pub authority: Signer<'info>,     #[account(mut)]    pub admin: Signer<'info>,     #[account(        init,        payer = admin,        space = ProgramConfig::SIZE,        seeds = [CONFIG_SEED],        bump    )]    pub config: Account<'info, ProgramConfig>,     /// CHECK: Program data account containing upgrade authority info.    /// Enforces that only the upgrade authority can initialize the protocol.    #[account(constraint = program_data.upgrade_authority_address == Some(authority.key()) @ TributaryError::UnauthorizedInitializer)]    pub program_data: Account<'info, ProgramData>,
  1. Line 7 pub authority: Signer<'info>,: Requires this account to have signed the transaction.
  2. Line 13 init,: Creates and pays for a new account. Space and payer must be specified.
  3. Line 16 seeds = [CONFIG_SEED],: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match.
  4. Line 17 bump: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
tributary-so/tributary · programs/tributary/src/instructions/change_program_authority.rs L13–36 @ 4b0a32e · MIT · original
    #[account(        mut,        seeds = [CONFIG_SEED],        bump = config.bump,        constraint = config.admin == admin.key() @ TributaryError::Unauthorized    )]    pub config: Account<'info, ProgramConfig>,} impl<'info> ChangeProgramAuthority<'info> {    /// Rotate the protocol admin. Existing admin must sign; new admin is    /// recorded for future admin-gated calls. `fee_recipient` is untouched.    pub fn handler_change_program_authority(ctx: Context<ChangeProgramAuthority>) -> Result<()> {        let config = &mut ctx.accounts.config;        let old_admin = config.admin;        config.admin = ctx.accounts.new_admin.key();         emit!(ProgramAuthorityChanged {            old_admin,            new_admin: config.admin,        });         msg!(            "Program authority rotated from {:?} to {:?}",
  1. Line 15 seeds = [CONFIG_SEED],: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match.
  2. Line 16 bump = config.bump,: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
  3. Line 30 emit!(ProgramAuthorityChanged {: Emits an Anchor event that clients can parse from the logs.
  4. Line 35 msg!(: Logs to the program log, which is visible in explorers and the test output.
tributary-so/tributary · programs/tributary/src/instructions/gateway/create_payment_gateway.rs L1–24 @ 4b0a32e · MIT · original
use crate::{constants::*, error::TributaryError, state::*};use anchor_lang::prelude::*; #[derive(Accounts)]pub struct CreatePaymentGateway<'info> {    #[account(mut)]    pub admin: Signer<'info>,     /// CHECK: The authority that will own the gateway    pub authority: UncheckedAccount<'info>,     #[account(        init,        payer = admin,        space = PaymentGateway::SIZE,        seeds = [GATEWAY_SEED, authority.key().as_ref()],        bump    )]    pub gateway: Account<'info, PaymentGateway>,     #[account(        seeds = [CONFIG_SEED],        bump = config.bump,        constraint = config.admin == admin.key(),
  1. Line 7 pub admin: Signer<'info>,: Requires this account to have signed the transaction.
  2. Line 13 init,: Creates and pays for a new account. Space and payer must be specified.
  3. Line 16 seeds = [GATEWAY_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 17 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
packages/tokens-clientnpm run buildtsc
packages/tokens-clientnpm run testnpx tsx src/devnetFallback.test.ts && npx tsx src/client.test.ts
packages/tokens-clientnpm run linteslint .
packages/paymentsnpm run buildtsc
packages/paymentsnpm run testjest --testPathIgnorePatterns='verification.e2e.test.ts'
packages/paymentsnpm run linteslint .
packages/uinpm run buildpnpm run clean && tsup
packages/uinpm run testexit 0
packages/uinpm run linteslint .
packages/sdk-x402npm run buildrm -rf dist && tsc -b
packages/sdk-x402npm run testnode --experimental-vm-modules node_modules/jest/bin/jest.js --coverage
packages/sdk-x402npm run linteslint .
packages/sdknpm run buildnpx tsup
packages/sdknpm run testnpx tsx --test src/__tests__/**/*.test.ts
packages/sdknpm run linteslint .
packages/forward-buildersnpm run buildnpx tsup

What you could build from it

Used in lessons