Full lesson
cascade
hourly payment platform based on solana
What it does
From the project README:
Cascade is an hourly payment platform designed to pay employees on an hourly basis. It leverages the Solana blockchain to provide fast, transparent, and reliable payments for workers, ensuring that employees are compensated in real-time for the hours they work.
- Original
- praxzy/cascade
- SuperStack fork
- Blockchains/cascade @
015b43e - Stars
- 1
- Last push (original)
- 2026-01-23
- Languages
- TypeScript, Rust, CSS, Just, JavaScript, Shell
- Detected stack
- AnchorNext.js
- Categories
- Payments
- Licence
- Apache-2.0
Architecture
284 source files. By extension: .tsx ×166, .ts ×104, .rs ×11, .js ×3.
| Top-level directory | Source files |
|---|---|
src | 247 |
anchor | 32 |
(root) | 5 |
On-chain programs: anchor/programs/cascade
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/cascade/. 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/cascade/src/instructions/employer_emergency_withdraw.rs L13–36 @ 015b43e · Apache-2.0 · original ); // Check if 30 days have passed since employee's last activity let employee_inactive_duration = clock.unix_timestamp - stream.employee_last_activity_at; let thirty_days: i64 = 30 * 24 * 60 * 60; require!( employee_inactive_duration >= thirty_days, ErrorCode::EmployeeStillActive ); let remaining_balance = stream .total_deposited .checked_sub(stream.withdrawn_amount) .ok_or(ErrorCode::InsufficientBalance)?; // Transfer remaining balance back to employer let employer_key = stream.employer.key(); let employee_key = stream.employee.key(); let seeds = &[ b"stream", employer_key.as_ref(), employee_key.as_ref(), &[stream.bump],- Line 16
let employee_inactive_duration = clock.unix_timestamp - stream.employee_last_activity_at;: Reads on-chain time from the Clock sysvar. - Line 19
require!(: An explicit validation that aborts the instruction with an error. - Line 26
.checked_sub(stream.withdrawn_amount): Uses checked arithmetic so overflow fails instead of wrapping. - Line 32
let seeds = &[: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match. - Line 36
&[stream.bump],: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
anchor/programs/cascade/src/instructions/withdraw.rs L37–60 @ 015b43e · Apache-2.0 · original // Transfer tokens from vault to employee using PDA signer let employer_key = stream.employer.key(); let employee_key = stream.employee.key(); let seeds = &[ b"stream", employer_key.as_ref(), employee_key.as_ref(), &[stream.bump], ]; let signer = &[&seeds[..]]; let cpi_accounts = Transfer { from: ctx.accounts.vault.to_account_info(), to: ctx.accounts.employee_token_account.to_account_info(), authority: stream.to_account_info(), }; let cpi_program = ctx.accounts.token_program.to_account_info(); let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts).with_signer(signer); token::transfer(cpi_ctx, amount)?; // Update stream state stream.withdrawn_amount = stream .withdrawn_amount .checked_add(amount)- Line 40
let seeds = &[: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match. - Line 44
&[stream.bump],: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve. - Line 54
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts).with_signer(signer);: Makes a cross-program invocation into another program. - Line 55
token::transfer(cpi_ctx, amount)?;: An SPL token transfer through the Token Program. - Line 60
.checked_add(amount): Uses checked arithmetic so overflow fails instead of wrapping.
anchor/programs/cascade/src/instructions/close_stream.rs L13–36 @ 015b43e · Apache-2.0 · original Ok(())} #[derive(Accounts)]pub struct CloseStream<'info> { #[account(mut)] pub employer: Signer<'info>, #[account( mut, close = employer, seeds = [b"stream", employer.key().as_ref(), stream.employee.as_ref()], bump = stream.bump, has_one = employer )] pub stream: Account<'info, PaymentStream>, #[account( mut, close = employer, seeds = [b"vault", stream.key().as_ref()], bump, )] pub vault: Account<'info, TokenAccount>,- Line 19
pub employer: Signer<'info>,: Requires this account to have signed the transaction. - Line 23
close = employer,: Closes the account at the end of the instruction and refunds its lamports. - Line 24
seeds = [b"stream", employer.key().as_ref(), stream.employee.as_ref()],: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match. - Line 25
bump = stream.bump,: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve. - Line 26
has_one = employer: Checks that a stored pubkey field matches the passed account.
anchor/programs/cascade/src/instructions/top_up_stream.rs L25–47 @ 015b43e · Apache-2.0 · original stream.total_deposited = stream .total_deposited .checked_add(additional_amount) .ok_or(ErrorCode::MathOverflow)?; Ok(())} #[derive(Accounts)]pub struct TopUpStream<'info> { #[account(mut)] pub employer: Signer<'info>, #[account( mut, seeds = [b"stream", employer.key().as_ref(), stream.employee.as_ref()], bump = stream.bump, has_one = vault )] pub stream: Account<'info, PaymentStream>, #[account(mut)] pub vault: Account<'info, TokenAccount>,- Line 27
.checked_add(additional_amount): Uses checked arithmetic so overflow fails instead of wrapping. - Line 36
pub employer: Signer<'info>,: Requires this account to have signed the transaction. - Line 40
seeds = [b"stream", employer.key().as_ref(), stream.employee.as_ref()],: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match. - Line 41
bump = stream.bump,: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve. - Line 42
has_one = vault: 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 dev | next dev |
. | npm run start | next start |
. | npm run build | next build |
. | npm run lint | eslint |
anchor | anchor build | compile programs and generate IDLs |
anchor | anchor test | local validator + test suite |
anchor | anchor run test | pnpm exec vitest |
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