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:
withdraw_for_distribution— moves tokens into a merkle-distributortoken_vault.withdraw_for_pool_seeding— moves tokens into a Meteora DLMMlb_pair.reserve_xorreserve_y.
Everything else (transferring to a personal wallet, depositing to a random ATA, swapping outside the canonical pool) is rejected by the program.
PDAs
| PDA | Seeds | Purpose |
|---|---|---|
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).
| Account | Mut / signer | Notes |
|---|---|---|
streamer | mut, signer | Pays rent + signs the transfer |
mint | readonly | Token-2022 mint |
vault | mut, init | PDA created here |
vault_ata | mut, init_if_needed | Owned by vault |
streamer_ata | mut | Source of funds; pre-existing |
token_program | readonly | Token-2022 |
associated_token_program | readonly | |
system_program | readonly |
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: u64 — base and version are the seeds Anchor uses
to re-derive the distributor PDA on-chain. Three independent purpose
checks run before signing the transfer:
- On-chain PDA derivation —
distributor_pdais 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. - Account-owner inspection — the destination token account's
authorityfield must equal the PDA derived above. This rejects attackers who try to point at a plain wallet ATA they control. - 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_distributoron the merkle-distributor program, and itstoken_vaultaccount slot (account index 3) must match thedestinationof 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.
| Route | Returns |
|---|---|
POST /vault-api/build-initialize | unsigned tx (streamer signs) |
POST /vault-api/build-withdraw-for-distribution | unsigned tx — caller must append matching merkle-distributor ix |
POST /vault-api/build-withdraw-for-pool-seeding | unsigned tx — caller must append matching Meteora add_liquidity_by_strategy2 ix |
POST /vault-api/build-close | unsigned 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:
| Constant | Source ix | Sibling program |
|---|---|---|
IX_DISCRIMINATOR_INIT_DISTRIBUTOR | init_distributor | merkle-distributor |
IX_DISCRIMINATOR_ADD_LIQUIDITY_BY_STRATEGY2 | add_liquidity_by_strategy2 | Meteora 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
| Cluster | Program id | Deploy tx |
|---|---|---|
| Devnet | 2qsNccNeHgqY9fsLobmpwKJXmZZKqemNdUsQ12w3ydTr | 4hYLrySS2fud6ThEaQypLPLqWVz3KwAKHVsK2KD7s5JDExKRN1fJM3CTiZzZQ7PbMR2BdfeHjB6jT4gGrx88fK37 |
| 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
| Flow | Status |
|---|---|
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 vault | Builder 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.