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 directory | Source files |
|---|---|
apps | 303 |
packages | 159 |
programs | 61 |
tests | 22 |
formal_verification | 3 |
(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.
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 {- Line 424
AuthorityMode::Direct => CpiContext::new(token_program.key(), cpi_accounts),: Makes a cross-program invocation into another program. - 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. - Line 429
token_interface::transfer_checked(cpi_ctx, reward, mint_decimals)?;: A checked token transfer, which also verifies the mint and decimals. - Line 434
.checked_add(reward): Uses checked arithmetic so overflow fails instead of wrapping.
programs/tributary/src/instructions/initialize.rs L1–24 @ 4b0a32e · MIT · originaluse 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>,- Line 7
pub authority: Signer<'info>,: Requires this account to have signed the transaction. - Line 13
init,: Creates and pays for a new account. Space and payer must be specified. - Line 16
seeds = [CONFIG_SEED],: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match. - Line 17
bump: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
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 {:?}",- Line 15
seeds = [CONFIG_SEED],: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match. - Line 16
bump = config.bump,: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve. - Line 30
emit!(ProgramAuthorityChanged {: Emits an Anchor event that clients can parse from the logs. - Line 35
msg!(: Logs to the program log, which is visible in explorers and the test output.
programs/tributary/src/instructions/gateway/create_payment_gateway.rs L1–24 @ 4b0a32e · MIT · originaluse 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(),- Line 7
pub admin: Signer<'info>,: Requires this account to have signed the transaction. - Line 13
init,: Creates and pays for a new account. Space and payer must be specified. - 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. - 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.
| Directory | Command | Runs |
|---|---|---|
packages/tokens-client | npm run build | tsc |
packages/tokens-client | npm run test | npx tsx src/devnetFallback.test.ts && npx tsx src/client.test.ts |
packages/tokens-client | npm run lint | eslint . |
packages/payments | npm run build | tsc |
packages/payments | npm run test | jest --testPathIgnorePatterns='verification.e2e.test.ts' |
packages/payments | npm run lint | eslint . |
packages/ui | npm run build | pnpm run clean && tsup |
packages/ui | npm run test | exit 0 |
packages/ui | npm run lint | eslint . |
packages/sdk-x402 | npm run build | rm -rf dist && tsc -b |
packages/sdk-x402 | npm run test | node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage |
packages/sdk-x402 | npm run lint | eslint . |
packages/sdk | npm run build | npx tsup |
packages/sdk | npm run test | npx tsx --test src/__tests__/**/*.test.ts |
packages/sdk | npm run lint | eslint . |
packages/forward-builders | npm run build | npx tsup |
What you could build from it
- Fork the SuperStack copy and extend it within the Payments gaps listed on the idea page.
- Learn the pattern first: Solana Pay
- Learn the pattern first: SPL Tokens: mints, accounts and transfers
- Learn the pattern first: Blinks and Solana Actions