Skip to main content

Streamer-token vault program

Purpose-gated escrow for the 85% of streamer-token supply that would otherwise sit liquid in the streamer's hot wallet at launch. The remaining 15% follows the whitepaper splits (10% streamer reserve

  • 5% BitView protocol allocation), both via Streamflow streams that already existed before the vault.
  • Program path: distributor/streamer-vault/
  • Anchor: 1.0.2
  • Solana: 3.0
  • Token program: Token-2022 (extends to classic SPL via a v2 follow-up; today's streamer launches mint Token-2022 only).

Why a separate program

Pre-vault, the wizard handed 85% of supply directly to the streamer wallet at launch. That has two real problems:

  • Hot-wallet blast radius. A compromised streamer key drains the entire token supply — including tokens earmarked for future distributions and pool seeding.
  • Trust gradient. Viewers earning the token have no on-chain signal that anything beyond the 15% Streamflow-locked tranche is committed to the project. Tokens parked in the streamer's wallet can flow anywhere.

The vault fixes both by enforcing only two outflows on-chain:

  1. withdraw_for_distribution — moves tokens into a merkle-distributor token_vault.
  2. withdraw_for_pool_seeding — moves tokens into a Meteora DLMM lb_pair.reserve_x or reserve_y.

Everything else (transferring to a personal wallet, depositing to a random ATA, swapping outside the canonical pool) is rejected by the program.

PDAs

PDASeedsPurpose
vault[b"bitview-vault", streamer, mint]Holds bookkeeping (streamer + mint + balance counters). Signs every withdraw via the bump seed.
vault_ata(Associated Token Account, owner = vault)Holds the actual Token-2022 balance.

One vault per (streamer, mint). A second initialize_vault call fails with Anchor's AccountInit error — by design.

Instructions

initialize_vault(amount: u64)
withdraw_for_distribution(amount: u64)
withdraw_for_pool_seeding(amount: u64)
close_vault()

initialize_vault

Called once during the streamer launch wizard, after revoke_mint_authority. Creates the vault PDA, creates the vault ATA, and pulls amount raw units from the streamer's source ATA into the vault ATA via transfer_checked (Token-2022).

AccountMut / signerNotes
streamermut, signerPays rent + signs the transfer
mintreadonlyToken-2022 mint
vaultmut, initPDA created here
vault_atamut, init_if_neededOwned by vault
streamer_atamutSource of funds; pre-existing
token_programreadonlyToken-2022
associated_token_programreadonly
system_programreadonly

withdraw_for_distribution(amount)

Moves amount raw units from the vault ATA into a merkle-distributor token_vault account. The handler takes amount: u64, base: Pubkey, and version: u64base and version are the seeds Anchor uses to re-derive the distributor PDA on-chain. Three independent purpose checks run before signing the transfer:

  1. On-chain PDA derivationdistributor_pda is derived from [b"Distributor", base, mint, &version.to_le_bytes()] against the hardcoded merkle-distributor program id. Anchor refuses the ix unless the supplied account matches the derivation, so the caller cannot pass an arbitrary pubkey.
  2. Account-owner inspection — the destination token account's authority field must equal the PDA derived above. This rejects attackers who try to point at a plain wallet ATA they control.
  3. Sibling-instruction introspection — the bot's instructions sysvar is read so the program can look at the next instruction in the same transaction. That ix must be init_distributor on the merkle-distributor program, and its token_vault account slot (account index 3) must match the destination of the withdraw. This closes the loophole where a real distributor's vault could be funded by accident (the bot would have to actually intend to fund THAT distributor in THIS tx for the check to pass).

After all three checks pass, the vault PDA signs a transfer_checked into destination. No vault-side position counter is maintained — the merkle-distributor lifecycle lives entirely on the distributor program with no callback path back into the vault, so close_vault gates on the vault ATA balance alone.

withdraw_for_pool_seeding(amount)

Same shape as withdraw_for_distribution, but the target program is Meteora DLMM instead of the merkle-distributor, and the sibling ix must be add_liquidity_by_strategy2 referencing the destination at either reserve_x (account index 5) or reserve_y (account index 6).

No counter increments — Meteora positions are owned by the streamer directly, so the vault doesn't need to track them.

close_vault

Frees the vault PDA + the vault ATA, refunding rent to the streamer. Requires:

  • vault ATA balance == 0 (run withdraw paths first)

The merkle-distributor lifecycle (claims, clawback) happens entirely on the distributor program with no callback path back into the vault, so the vault carries no in-flight position counter — the ATA balance is the authoritative drain gate.

Off-chain client

bitview-streamer-vault mirrors each ix with a build_*_tx function that returns an UnsignedTransaction. The bot's /vault-api/* HTTP routes are thin wrappers around those builders.

RouteReturns
POST /vault-api/build-initializeunsigned tx (streamer signs)
POST /vault-api/build-withdraw-for-distributionunsigned tx — caller must append matching merkle-distributor ix
POST /vault-api/build-withdraw-for-pool-seedingunsigned tx — caller must append matching Meteora add_liquidity_by_strategy2 ix
POST /vault-api/build-closeunsigned tx
GET /vault-api/pda/{streamer}/{mint}{ vault, vault_ata }

Discriminator pinning

The on-chain purpose check matches the sibling ix's 8-byte Anchor discriminator against two pinned constants:

ConstantSource ixSibling program
IX_DISCRIMINATOR_INIT_DISTRIBUTORinit_distributormerkle-distributor
IX_DISCRIMINATOR_ADD_LIQUIDITY_BY_STRATEGY2add_liquidity_by_strategy2Meteora DLMM

These constants are unit-tested against sha256("global:<name>")[..8] on every build. If either program renames an instruction, the build fails before the vault could silently reject every withdraw.

Deployment

ClusterProgram idDeploy tx
Devnet2qsNccNeHgqY9fsLobmpwKJXmZZKqemNdUsQ12w3ydTr4hYLrySS2fud6ThEaQypLPLqWVz3KwAKHVsK2KD7s5JDExKRN1fJM3CTiZzZQ7PbMR2BdfeHjB6jT4gGrx88fK37
Mainnet— pending audit + redeploy

Rent cost: ~1.78 SOL per deploy (255 KB program size; Anchor 1.0.2 debug symbols stripped at release-profile compile time).

Wired-in flows

FlowStatus
Launch wizard deposit (initialize_vault as step 5 of the public-web token-launch wizard)✅ Live
Fund-distribution withdraw bundled with init_distributor via POST /vault-api/build-fund-distribution✅ Live
Pool-seeding withdraw bundled with add_liquidity_by_strategy2 via POST /vault-api/build-fund-pool-seeding✅ Live
DAMM v2 launch withdraw (withdraw_for_damm_v2_launch) bundled with the DAMM v2 add-liquidity ix✅ Live
Close vaultBuilder exists; surfaced when a wizard explicitly opts in

All bundled endpoints return a single VersionedTransaction containing the vault withdraw + the matching sibling instruction in the same tx, so the on-chain sibling-ix introspection check succeeds.

Out of scope for v1

  • Classic SPL Token mints (streamer launchpad creates Token-2022 only).
  • Multi-streamer vault sharing.
  • Program-upgrade governance — the program-id is pre-allocated and becomes immutable after first deploy.
  • An external audit; v1 ships on devnet for integration, mainnet needs a follow-up review.