bot (bitview-bot) — Rust backend
The always-on backend service. Tracks Twitch chat presence, runs the accrual loop, builds the merkle trees, exposes the REST API consumed by both public-web and admin-web, and returns base64-encoded unsigned transactions for every on-chain action the product takes.
It is not an on-chain signer. The backend never holds Solana keys. Every on-chain write is signed by a streamer's, viewer's, or admin's wallet through one of the web apps. The backend's only on-chain posture is read-only (RPC reads + tx submission monitoring).
Responsibilities
| Area | Responsibility |
|---|---|
| Twitch presence | Maintains IRC connections per channel with active distributions; tracks who is in chat in real time; reconciles presence on bot restart from a MongoDB-persisted view. |
| Accrual | Per-tick loop (default 60s) credits linked viewers across all Active distributions. Catches up missed ticks on resume. Polls Twitch Helix for stream-online state so accrual auto-pauses when the streamer goes offline. |
| Identity | Direct Twitch OAuth callback exchange; ed25519 wallet signature verification; bridges Twitch handle ↔ Solana wallet ↔ Keycloak attribute. |
| REST API | Typed, OpenAPI/Swagger-documented surface (port 4477). 20+ namespaced scopes. |
| Unsigned-tx builders | Hand-rolled CPI clients for merkle-distributor, streamer-vault, launchpad-vault, Meteora DLMM v2, Meteora DAMM v2, Token-2022, Metaplex Token Metadata, Streamflow. Every product action returns a base64-encoded unsigned tx + signers + a request_id the client uses for status polling. |
| Tx lifecycle | Background watcher polls Solana RPC, advances tx_log state machine (built → submitted → confirmed → finalized), surfaces errors. |
| Merkle assembly | Builds a SHA256 merkle tree from the accruals collection on distribution finalize, writes the root on-chain via a streamer-signed init_distributor. |
| Persistence | Owns every MongoDB collection in the system. The bot is the single source of truth for off-chain product state. |
| Billing | Stripe checkout session creation + webhook handler; tier sync writes billing_tiers and propagates the streamer role to Keycloak. |
| Observability | Prometheus metrics (port 9100), structured tracing logs, bot-admin-api introspection endpoints. |
Tech stack
| Layer | Library |
|---|---|
| Async runtime | tokio 1.38 (full features) |
| HTTP server | actix-web 4.8 |
| Twitch IRC | twitch-irc 5.0 |
| Database | mongodb 3.2 (official driver) |
| OpenAPI / Swagger | utoipa + utoipa-swagger-ui + utoipa-rapidoc |
| Metrics | Prometheus exporter on :9100 |
| Crypto (wallet linking) | ed25519-dalek 2.x |
| Solana | solana-sdk / solana-program 3.0 (matches distributor workspace) |
| SPL | spl-token 9 + spl-token-2022 10 + spl-associated-token-account 8 |
| Build | Cargo Chef multi-stage Docker, Debian Bookworm slim runtime |
Compile target: Rust 1.75+. Single static binary. Cargo workspace with
23 crates under crates/.
Crate map
| Crate | Purpose |
|---|---|
bitview-bot | Composition root binary; boots config → metrics → db → solana RPC → accrual loop → HTTP server. |
bitview-http | actix-web routes + OpenAPI registration. ~20 route modules grouped by namespace. |
bitview-config | dotenv loading + process-wide Config singleton. |
bitview-db | MongoDB client + 18 repository types + index setup + Repositories singleton. |
bitview-model | Wire shapes (User, Distribution, Accrual, …) with utoipa derives. |
bitview-core | Shared types (Pubkey, TokenAmount, signatures). |
bitview-auth | Twitch OAuth, Keycloak OIDC, wallet signature validation. |
bitview-twitch | IRC listener oracle; in-memory DashMap<channel_login, TwitchChannel> of present users. |
bitview-accrual | Per-tick accrual loop spawned at boot. |
bitview-snapshot | Merkle tree builder + distribution finalize pipeline. |
bitview-merkle | SHA256 merkle tree implementation + proof generation. Matches the off-chain shape the distributor program verifies. |
bitview-solana | RPC client, blockhash service, transaction builder, confirmation watcher. |
bitview-distributor | CPI client for merkle-distributor (init_distributor, init_nft_distributor, claim, claim_nft_core, claim_nft_compressed, clawback, set_enable_slot, set_admin, accept_admin, set_clawback_receiver). |
bitview-streamer-vault | CPI client for streamer-vault (initialize_vault, withdraw_for_distribution, withdraw_for_pool_seeding, withdraw_for_damm_v2_launch, close_vault). |
bitview-launchpad-vault | CPI client for launchpad-vault (initialize, deposit, add_allowlist, disable_allowlist, close_allowlist, withdraw_for_launch_seeding, withdraw_for_damm_v2_launch). |
bitview-amm | Meteora DLMM v2 CPI client (initialize_lb_pair, initialize_bin_array, initialize_position, add_liquidity_by_strategy2, swap2). |
bitview-launchpad | Token-2022 mint creation + Metaplex metadata + revoke authority + extension allocation. |
bitview-vesting | Streamflow stream builders (identity reserve, BTV genesis, protocol allocation). |
bitview-treasury | Treasury deposit/payout/report builders (stub). |
bitview-nft | Metaplex badge mint (Phase 5 stub → 501). |
bitview-marketplace | Sponsorship + orders CRUD. |
bitview-swap-router | Jupiter Lite v6 quote + build_swap; viewer cash-out path. |
bitview-metrics | Prometheus exporter. |
Each crate is a pure library with a typed public API. The bitview-bot
binary initializes singletons in order and binds nothing else. Crates
pull dependencies from process-wide singletons (get_config,
get_repos, get_rpc) rather than threading state through every
function.
How accrual works
loop every accrual_tick_seconds (default 60s):
for each distribution with status == Active:
1. snapshot present viewers from the in-memory map
2. resolve their wallets via the users collection
3. compute the per-tick share:
floor( pool_tokens / (duration / tick_seconds) / max(1, viewer_count) )
4. if gap_since_last_tick > 1.5 × interval:
catch up min(gap_ticks, MAX_CATCHUP_TICKS) at once
5. upsert accrual rows (+amount, +1 tick) keyed by (distribution_id, twitch_login)
6. update AccrualLiveSnapshot rollup per distribution
7. skip viewers above max_per_viewer
8. stop crediting when the pool is exhausted
A separate watcher polls Twitch Helix /streams every minute for
channels with active distributions and auto-finalizes the distribution
when the streamer goes offline (3-minute hold-off to absorb stream
hiccups).
No tokens move on-chain during a stream. The merkle root published at finalize is the on-chain reflection of every tick the loop ran.
REST surface
The exact request/response shapes are in API reference
and at https://api.bitview.club/swagger-ui/ (live). Endpoints are
grouped into namespaced scopes:
Viewer + identity
/bitview-api/*— Wallet ↔ Twitch linking (POST /user/link), by-login / by-wallet lookup, viewer accrual reads./twitch-api/*— Channel list, per-channel presence snapshot, idempotent listener connect./auth-api/*— Direct Twitch OAuth callback / unlink / link-via-handle; Keycloak link-twitch admin helper./viewer-api/*— Viewer session list, viewer preferences (read/write).
Distribution lifecycle
/distributions-api/*— Distribution CRUD, register, finalize (admin-gated), by-channel / by-event listings, debug tree builders./claims-api/*— Claim summary, proof lookup (SPL + NFT)./distributor-tx-api/*— Unsigned tx builders for the merkle-distributor program:build-init,build-init-nft,build-claim,build-claim-nft,build-clawback,build-set-enable-slot,build-fund-vault. Returns base64 unsigned tx + signers +request_id./vault-api/*— Unsigned tx builders for thestreamer-vaultprogram (init, withdraw-for-distribution, withdraw-for-pool-seeding, withdraw-for-damm-v2-launch, close, bundled fund-distribution, bundled fund-pool-seeding) + PDA lookup./launchpad-vault-api/*— Unsigned tx builders forlaunchpad-vault(init, deposit, add/disable/close allowlist, withdraw for launch seeding, withdraw for DAMM v2, snapshot, bundled fund-launch-pool-seeding).
Streamer + tokens
/streamer-api/*— Streamer profile, KYC application submission, streamer-token CRUD (token,tokens/drafts,tokens/live,PATCH /token/{mint}/progress), NFT collection registration./launchpad-api/*— Token-2022 mint creation + Metaplex metadata- revoke authorities + extension allocation. Unsigned-tx builders.
/metadata-api/*— Token-image + JSON upload (size-capped, rate-limited); CDN-style fetch by id./vesting-api/*— Streamflow stream builders (identity reserve, BTV genesis, protocol allocation).
Pools + swaps
/amm-api/*and/meteora-api/*— Meteora DLMM v2 pool lifecycle (init, bin-array, position, add-liquidity); pool snapshot + pool list; quote + build-swap (direct, no aggregator);active_idfrom price helper./swap-api/*— Jupiter aggregator quote + build-swap; default viewer cash-out path.
Marketplace + billing
/marketplace-api/*— Orders + sponsorships (offer, accept, streamer list, all listings)./billing-api/*— Stripe checkout session + webhook + tier status.
Tx lifecycle
/tx-api/*—POST /notify(frontend reports signed signature);GET /{request_id}polls status:built → submitted → confirmed → finalized(orfailed). Background watcher reconciles every 8s.
Operator
/admin-api/*— App config (read + patch with cluster-switch confirmation), vesting stream registry, system health, fraud overview/slashing/log (stubbed), audit-log query (stubbed). Gated byX-Admin-Api-KeyOR Keycloak bearer (bitview-opsrealm withadmin/ops/supportrole)./bot-admin-api/*— Read-only operational introspection (version, listeners, accrual status per distribution, recent accruals)./metrics-api/*— Dashboard KPI feed (leaderboard, distributed totals, KPI snapshot)./public-api/network— Read-only public network metadata (RPC URL, explorer templates, mint addresses).
Other
/solana-rpc— JSON-RPC proxy (wallet adapters can't send session cookies, so this is intentionally unauthenticated; rate-limited)./health— Liveness probe./swagger-ui/and/rapidoc— OpenAPI docs.
MongoDB collections
The bot owns every collection in the system. Schemas live in
bitview-model with utoipa derives so they round-trip into the
OpenAPI document.
| Collection | Purpose |
|---|---|
users | BitView users: twitch_login, twitch_user_id, solana_wallet, linked_at |
distributions | Distribution lifecycle docs (status, root, mint, pool params) |
accruals | Per-(distribution, twitch_login) accrued amount + tick count |
merkle_snapshots | Finalized merkle root + leaf count per distribution |
merkle_snapshot_nodes | Per-leaf proof nodes |
channels | Channels the bot has ever joined |
tx_log | Unsigned-tx lifecycle: request_id, signature, status, timestamps |
viewer_preferences | Per-wallet email + Twitch link + notification opts |
billing_tiers | Stripe-driven tier (Free / Pro / Plus) per streamer wallet |
streamer_applications | KYC submissions, review state |
app_config | Singleton network/RPC/program-id/mint config (PATCH bumps version → pod restart) |
streamer_tokens | Streamer-launched Token-2022 mints + launch_progress |
nft_collections | Streamer Metaplex Core collections |
vesting_streams | Streamflow stream registry |
metadata_blobs | Inline-stored token image + JSON blobs |
bot_listener_states | Per-channel listener heartbeat + uptime |
accrual_live_snapshots | Per-distribution rolling rollup of total accrued + distinct earners |
viewer_presence | Persistent (channel_login, twitch_login, present) — used to hydrate the in-memory presence map on bot restart |
Status
| Subsystem | State |
|---|---|
| Config + env + app_config singleton | ✅ Built |
| Twitch IRC oracle (per-channel listeners + presence reconciliation) | ✅ Built |
| Accrual loop (with catch-up + stream-online polling) | ✅ Built |
| MongoDB persistence (18 collections + index management) | ✅ Built |
| REST + Swagger + RapiDoc | ✅ Built |
| Twitch OAuth callback + ed25519 wallet linking | ✅ Built |
| Keycloak bearer verification (JWKS-cached, role-gated) | ✅ Built |
| Prometheus metrics | ✅ Built |
| Merkle-tree assembly + finalize pipeline | ✅ Built |
Unsigned-tx builders: /distributor-tx-api/* | ✅ Built |
Unsigned-tx builders: /launchpad-api/* (Token-2022 + metadata) | ✅ Built |
Unsigned-tx builders: /vault-api/* (streamer-vault) | ✅ Built |
Unsigned-tx builders: /launchpad-vault-api/* | ✅ Built |
Unsigned-tx builders: /amm-api/* + /meteora-api/* (Meteora DLMM v2) | ✅ Built |
Unsigned-tx builders: /swap-api/* (Jupiter Lite v6) | ✅ Built |
Unsigned-tx builders: /vesting-api/* (Streamflow) | ✅ Built |
| Stripe checkout + webhook + tier sync | ✅ Built |
| Marketplace / sponsorships CRUD | ✅ Built |
Tx lifecycle (tx_log, /tx-api/*, confirmation watcher) | ✅ Built |
bot-admin-api/* operational introspection | ✅ Built |
| Admin app_config PATCH + cluster-switch trigger | ✅ Built |
| Anti-fraud engine (sybil detection layers) | 🔴 Routes stubbed; engine planned |
| Admin audit-log persistence | 🔴 Endpoint stubbed |
| Admin SSE event stream | 🔴 Sends keepalives only |
| NFT badge minting | 🔴 Phase 5 stub → 501 |
| Treasury program integration | 🔴 Stub |
| Subscription-tier accrual weighting | 🔴 Tier exists in billing_tiers; not factored into share math yet |
| Streamflow auto-unlock tracking | 🔴 Builders ship streams; no auto-claim loop yet |
Configuration
Bootstrap from env on first boot; thereafter the singleton
app_config MongoDB doc is the source of truth. Admin can PATCH
/admin-api/app-config; the bot's background watcher exits on version
bump (Kubernetes restarts with new config).
Required (first boot)
| Var | Purpose |
|---|---|
MONGODB_URL | MongoDB connection string |
MONGODB_DB | Database name |
SOLANA_RPC_URL | Solana RPC endpoint |
DISTRIBUTOR_PROGRAM_ID | Merkle distributor program ID |
BTV_MINT | BitView token mint |
USDC_MINT | USDC mint |
Optional
| Var | Default / Purpose |
|---|---|
HTTP_HOST / HTTP_PORT | 0.0.0.0 / 4477 |
METRICS_PORT | 9100 |
ACCRUAL_TICK_SECONDS | 60 |
TWITCH_CLIENT_ID, TWITCH_CLIENT_SECRET, TWITCH_REDIRECT_URI | OAuth callback |
ADMIN_API_KEY | Legacy back-channel for admin endpoints |
KEYCLOAK_ISSUERS | Comma-separated; bearer verification |
KEYCLOAK_AUDIENCE, KEYCLOAK_JWKS_CACHE_TTL_SECONDS | JWKS gate |
STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET | Billing (no-op if unset) |
NETWORK | mainnet / devnet (seeds initial app_config) |
CORS_ALLOWED_ORIGINS | * by default |
Local development
cd bitview-bot
cp .env.example .env
$EDITOR .env
cargo run --release
# Verify
curl http://localhost:4477/health
open http://localhost:4477/swagger-ui/
curl http://localhost:9100/metrics
See Operations for the full local + production guide.
Deployment
Dockerfile builds with cargo-chef for layer caching. Runtime is
Debian Bookworm slim + libssl3 + an unprivileged user (UID 1001).
Exposes 4477 (HTTP) and 9100 (Prometheus). CI:
.github/workflows/build-and-deploy.yml builds + pushes to
registry.bitview.club on main and triggers GitOps sync.
Going deeper
For the internal engineering view — per-crate responsibilities,
data-model schemas, performance budgets, the anti-sybil layer plan,
and per-route operational runbooks — see the internal
_internal/technical/bitview-bot-detail.md doc.
Cross-references
- Architecture — one-screen visual
- API reference — exact request/response shapes
- On-chain program — what the merkle root commits to
- distributor — the on-chain side
- public-web — primary consumer of the REST API
- admin-web — proxies into the bot's admin scopes