Distribution modes & asset selection
Every distribution event is shaped by two orthogonal choices:
- Mode — who funds the rewards (Native, Identity, Sponsored).
- Assets — what viewers actually earn (BTV, the streamer's own token, NFT collections, or any combination).
The wizard at /studio/distributions/new infers the mode from the
streamer's billing tier and the assets they pick — there is no
separate tier-picker screen. Sponsored events originate elsewhere
(brands initiate them at /sponsor); the streamer-side wizard only
ever produces Native and Identity distributions.
Tier matrix
| Plan | BTV | Identity token | NFT collection / airdrop | Sponsorship |
|---|---|---|---|---|
| Free | ✓ | ✗ | ✗ | ✗ |
| Pro | ✓ | ✓ | ✓ | ✗ |
| Plus | ✓ | ✓ | ✓ | ✓ |
Tier is enforced as a hard gate, not a preview: a Free wallet doesn't see the Token or NFT pages in the studio at all. The asset selector only renders the rows the streamer's plan unlocks.
The three modes
Native
Available on every plan. Funded by the streamer in USDC or SOL.
The streamer swaps USDC or SOL into BTV inside the wizard, then funds the merkle distributor with BTV. Viewers earn BTV that they can claim and trade. This is the canonical mode and the one the wizard defaults to.
Touch points:
- Step 1: pick Native (default).
- Step 2: parameters (channel, title, periodicity, duration, pool size) — and now the asset selector.
- Step 3: swap USDC/SOL → BTV via Jupiter (
SwapPanel). - Step 4: sign one tx per distribution. The wizard creates a Metaplex
Core asset that anchors the event, then POSTs to
/distributions-api/register.
Identity
Pro and Plus only. Distribute your own streamer token.
The streamer first launches a fungible token via /studio/token. The
launchpad sequence runs on the backend's /launchpad-api/* routes —
create mint, mint full supply, attach Metaplex metadata, then revoke
mint authority (the supply-cap invariant). The wallet signs four
transactions and one backend registration.
One streamer token per wallet. The backend enforces this with a
unique Mongo index on streamer_tokens.streamer_wallet. A duplicate
registration returns 409 Conflict. Once the token is registered, the
creation form is hidden — /studio/token flips to the detail view
showing mint address, supply, decimals, and explorer links.
The token then becomes selectable in the distribution wizard. Identity-mode distributions match-fund the STREAM/BTV pool with BTV so viewers can cash out — see streamer-token.
Sponsored
Plus only. A brand pays the rewards through the marketplace.
The streamer accepts a brand's sponsorship offer; the brand's USDC funds the vault directly (no streamer-side swap step). The streamer earns a 5% co-share on every accepted sponsorship. The asset selector still works — the brand can pay rewards in BTV, the streamer's token, or NFT airdrops.
Asset selection
Step 2 of the wizard renders an Asset selector with one row per selectable asset:
- BTV — always on, always first. Amount mirrors the main pool slider. Cannot be removed (every event must distribute at least one asset, and BTV is the canonical one).
- Streamer token — visible iff the streamer has launched one. The row carries the token's symbol and lets the streamer set an independent amount.
- NFT collections — one row per collection registered at
/studio/collections. Amount = number of NFTs to airdrop.
If the streamer selects multiple assets, the wizard spawns N
distributions at step 4 — one per asset — and groups them under a
shared event_id. There is no on-chain bundling: each asset has its
own merkle tree, its own claimable PDA, and its own claim window.
That keeps the on-chain surface simple and lets viewers claim each
asset independently.
Asset registry
Two new Mongo collections back this:
streamer_tokens— one document per streamer wallet. Unique index onstreamer_walletis the hard guarantee of one-per-streamer.nft_collections— many documents per streamer wallet. Indexed bystreamer_wallet + created_atfor the studio list view.
REST surface under /streamer-api/:
GET /streamer-api/token/{wallet}—200with the token doc, or404when none is registered.POST /streamer-api/token— register a freshly-minted token. Returns409if the streamer already has one.GET /streamer-api/collections/{wallet}— list registered collections.POST /streamer-api/collections— register a newly-created Metaplex Core collection.GET /streamer-api/profile/{wallet}— now includesstreamer_tokenandnft_collectionsso the admin dashboard reflects launch state alongside KYC, tier, and distribution counts.
NFT collections use Metaplex Core (mpl-core) and are created
client-side via Umi — the backend only persists the resulting
collection address. The fungible token launch uses Token-2022 and
runs through the existing /launchpad-api/* builders.
Admin reflection
The admin app at bitview-admin surfaces both registries on the
streamer detail page (/streamers/[wallet]):
- Streamer token card — name, symbol, mint, decimals, total supply. Only renders when the wallet has a registered token.
- NFT collections card — list of every collection the streamer has registered, with cap and short address.
The admin can use these to verify a streamer's launchpad state when investigating fraud flags or sponsorship eligibility.
Funding model
The wizard does not fund the merkle distributor vault directly. For BTV and streamer-token distributions the flow is:
- Wizard (
/studio/distributions/new) anchors the event by creating a Metaplex Core asset per selected asset and posting aDistributionrow per asset to/distributions-api/register. - Accrual loop in the bot tracks per-viewer earnings against
each
Distributionrow. - Snapshot phase (admin-triggered or scheduled) takes the final
accrual state, builds the merkle tree, and computes the
(max_total_claim, max_num_nodes, root)needed by the on-chaininit_distributorinstruction. /distributor-tx-api/build-fund-vaultreturns two unsigned transactions (init + fund) that, when signed by the streamer, atomically create the distributor PDA + vault ATA and transfer the snapshot'smax_total_claimfrom the streamer's ATA to the vault.
Because step 4 is parameterised on mint, BTV and streamer-token
distributions use the exact same code path — they only differ in
which SPL ATA the streamer is transferring from. No per-asset funding
UI is needed in the wizard.
NFT distribution (on-chain merkle distributor)
The merkle-distributor program already ships
init_nft_distributor + claim_nft_core
(see merkle-distributor/src/instructions/init_nft_distributor.rs
and claim_nft_core.rs). The handler verifies the leaf proof, flips
a bitmap, and CPIs into mpl-core::CreateV2 to mint the asset
directly to the claimant — the bot is bypassed entirely at claim
time. The bot only ever builds the unsigned transactions; the
streamer and viewers sign in their browsers.
- Collection creation (
/studio/collections/new) creates a Metaplex Core collection with the streamer asupdate_authority. - Accrual loop counts NFT slots a viewer earned, exactly the
same way it counts SPL token amounts.
asset_kind = "nft-collection"on theDistributionrow,mintcarries the collection address. - Snapshot phase materialises the merkle tree via
bitview-merkle::build_nft_treeand writes one node per claimant tomerkle_snapshot_nodes. - Streamer init (one signature for the whole event) —
lib/nftDistributor.ts::initNftDistributororchestrates:- mpl-core
UpdateV2transferring the collection'supdate_authorityto the (deterministic) NFT distributor PDA, POST /distributor-tx-api/build-init-nftreturning theinit_nft_distributortx (streamer + ephemeralbaseKeypair co-sign).
- mpl-core
- Viewer claim (per claim, viewer signs) —
lib/claim.ts::claimNftDistribution:GET /claims-api/proof/{distribution_id}/{wallet}returns(index, metadata_uri, proof),POST /distributor-tx-api/build-claim-nftreturns theclaim_nft_coretx (viewer + fresh asset Keypair co-sign).
After step 5 the viewer holds a freshly-minted Core asset with a
Royalties plugin pointing at the streamer's wallet at the
configured royalty_bps. Trustless on-chain, no backend signer.
Streamer-token launchpad (whitepaper 10 / 5 / 85)
The full launchpad sequence runs from /studio/token and matches
streamer-token.md §Supply allocation exactly:
| Step | What | Signature path |
|---|---|---|
| 1 | create-mint (Token-2022) | Streamer + ephemeral mint Keypair |
| 2 | mint-supply (100% to streamer ATA) | Streamer |
| 3 | create-metadata (Metaplex) | Streamer |
| 4 | revoke-mint-authority | Streamer |
| 5 | Streamer reserve Streamflow stream — 10% of supply, 24-month linear, no cliff | Streamer + reserve metadata Keypair |
| 6 | BitView protocol allocation Streamflow stream — 5% of supply, 48-month linear, no cliff | Streamer + protocol metadata Keypair |
| 7 | Register with backend | (no signature, REST POST) |
After step 7 the streamer's ATA holds the remaining 85% — pool
seeding + distributions draw from this float. Steps 5 and 6 lock the
whitepaper-mandated portions in Streamflow streams so the streamer
cannot dump their full supply on day one. Both streams are
uncancellable, non-transferable, non-pausable per
identity_reserve_params / protocol_allocation_params.
Step 6 is skipped when NEXT_PUBLIC_BITVIEW_TREASURY is unset (dev
environments without a treasury wallet). The streamer can re-run it
later from /studio/token once the operator publishes a treasury
pubkey.
What's intentionally out of Phase 3
- On-chain multi-asset distributors. The Anchor program still
takes one mint per distributor — multi-asset is orchestrated at the
UI layer with N independent distributors sharing an
event_id. - Batch-packed NFT mints. Phase 3 sends one tx per NFT to keep
the UX predictable; optimised packing (3-4
creates per tx) is a follow-up. - Cross-tab race on token creation. The unique Mongo index
guarantees only one row ever lands, but the second tab will see a
409rather than the existing token. The studio refresh fixes that on the next load.