Skip to main content

API reference

The BitView backend exposes a single REST API in four scopes. Every endpoint speaks JSON, every response is a stable schema documented both here and via OpenAPI.

ScopePurpose
/twitch-apiTwitch channel listening + read-only chat presence
/bitview-apiViewer wallet linking, profile lookup, accrual reads
/distributions-apiStreamer / admin distribution lifecycle
/claims-apiClaim summaries and merkle-proof proxy

Live OpenAPI

When the backend is running, full schemas are available at:

  • Swagger UI — https://api.bitview.so/swagger-ui/
  • RapiDoc — https://api.bitview.so/rapidoc
  • Raw JSON — https://api.bitview.so/api-docs/openapi.json

This page gives you the conceptual model, examples, and the contract guarantees that aren't expressible in OpenAPI.

Authentication

SchemeHeaderUsed by
twitch_oauthAuthorization: Bearer <twitch user-access token>POST /bitview-api/user/link
admin_api_keyx-admin-api-key: <ADMIN_API_KEY env value>distribution register / finalize
(none)All public read endpoints

Twitch tokens are validated against https://api.twitch.tv/helix/users on every request. Tokens that fail Helix verification are rejected with 401.

The admin API key is a simple shared secret used until Phase 4 introduces per-streamer signed-action authorization. It is rotated quarterly.

Base URL

Production: https://api.bitview.so

Local dev (default): http://localhost:4477

Common response shapes

All errors follow:

{
"error": "human-readable message"
}

HTTP status codes map to:

StatusMeaning
200OK
201Created (registration endpoints)
400Validation error (invalid pubkey, missing param, etc.)
401Authentication missing or invalid
403Authenticated but not authorized for this resource
404Resource not found
409Conflict (e.g., duplicate link)
429Rate-limited (see rate limits)
500Internal server error (please report — see security disclosure)
502Upstream gateway error (Solana RPC, distributor proof API)

Rate limits

Endpoint classAnonymousAuthenticatedAdmin
Read endpoints (GET)60 req/min per IP300 req/min per token1200 req/min
Write endpoints (POST)10 req/min per IP60 req/min per token600 req/min
Auth endpoints (/user/link)5 req/min per IPn/an/a
Proof endpoints30 req/min per IP300 req/min per tokenn/a

Limits are intentionally generous for legitimate use and tightened algorithmically against patterns matching abuse signatures. Contact us via security@bitview.so if your integration needs a higher allowance.

Idempotency

All POST endpoints are idempotent on their primary key:

  • /bitview-api/user/link — keyed on Twitch user_id. Re-posting with the same identity updates the link.
  • /distributions-api/register — keyed on distributor_pda. Re-posting the same PDA is rejected with 409.
  • /distributions-api/{id}/finalize — keyed on distribution id. Re-posting after snapshotting is a no-op.

Because the on-chain state is the source of truth for distributions, the backend reconciles automatically if a transaction succeeds on-chain but the API call fails — the next read will reflect chain state.

Endpoints

GET /health

Public. Returns process health status.

$ curl https://api.bitview.so/health
{"status": "ok"}

Twitch scope

GET /twitch-api/channel/list

List channels the bot is currently listening to.

$ curl https://api.bitview.so/twitch-api/channel/list
["pewpiediepie", "lirik", "shroud", "gamesdonequick"]

GET /twitch-api/channel/user_list/{channel_login}

List logins currently present in chat for channel_login.

$ curl https://api.bitview.so/twitch-api/channel/user_list/pewpiediepie
["alice42", "bob_streams", "charlie", "..."]

Note: this endpoint reflects the in-memory presence map, not historical chat. It updates in real time as JOIN/PART events arrive.

GET /twitch-api/channel/connect/{channel_login}

Idempotently connect (or no-op) to a channel's chat.

$ curl https://api.bitview.so/twitch-api/channel/connect/pewpiediepie
{"channel": "pewpiediepie", "newly_started": true}

newly_started: false means the listener was already running.

BitView scope (viewer)

POST /bitview-api/user/link

Link a Twitch identity to a Solana wallet. Requires both auth headers described in authentication — wait, only the Twitch bearer; the wallet signature is in the request body and is verified server-side.

The signed message must contain the Twitch user_id (returned by Helix) to bind the signature to the identity. This prevents replay across different users.

$ TWITCH_TOKEN="oauth:xxx"
$ WALLET="Hxxx..."
$ MSG="BitView wallet link. Twitch user_id=12345, timestamp=2026-05-10T15:00:00Z"
$ SIG="$(...your wallet signs $MSG with ed25519, encodes base58...)"

$ curl -X POST https://api.bitview.so/bitview-api/user/link \
-H "Authorization: Bearer $TWITCH_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"solana_wallet": "'"$WALLET"'",
"signature": "'"$SIG"'",
"signed_message": "'"$MSG"'"
}'
{
"twitch_login": "alice42",
"solana_wallet": "Hxxx..."
}

Possible errors:

  • 400 — invalid wallet pubkey, signed_message missing user_id
  • 401 — Twitch token invalid or signature verification failed
  • 403 — wallet sanctions screen hit
  • 409 — Twitch user_id already linked to a different wallet (use the relink flow with cooling-off period)

GET /bitview-api/user/by-login/{twitch_login}

Fetch a linked viewer by Twitch login. Public read.

$ curl https://api.bitview.so/bitview-api/user/by-login/alice42
{
"twitch_login": "alice42",
"twitch_user_id": "12345",
"twitch_display_name": "Alice42",
"solana_wallet": "Hxxx...",
"created_at": "2026-05-10T15:00:00Z",
"last_update": "2026-05-12T18:30:00Z"
}

404 if not found.

GET /bitview-api/user/by-wallet/{wallet}

Same shape as above, looked up by Solana wallet pubkey. Public read.

GET /bitview-api/user/list

List linked users (capped at 200; the production limit will be paginated via ?cursor=). Public.

GET /bitview-api/viewer/{wallet}/accruals

List all accruals for a wallet across all distributions. Public.

$ curl https://api.bitview.so/bitview-api/viewer/Hxxx.../accruals
[
{
"distribution_id": "Dxxx...",
"twitch_login": "alice42",
"solana_wallet": "Hxxx...",
"amount": 4523,
"tick_count": 42
},
{
"distribution_id": "Dyyy...",
"twitch_login": "alice42",
"solana_wallet": "Hxxx...",
"amount": 800,
"tick_count": 18
}
]

amount is in the smallest token unit of the distribution's mint.

Distributions scope (streamer / admin)

POST /distributions-api/register

Register an on-chain distribution with the backend. Requires x-admin-api-key during Phase 1–3; replaced by streamer-signed authorization in Phase 4+.

$ curl -X POST https://api.bitview.so/distributions-api/register \
-H "x-admin-api-key: $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{
"distributor_pda": "Dxxx...",
"channel_login": "pewpiediepie",
"streamer_wallet": "Sxxx...",
"mint": "EFsATdhojBVLTbQtrAWncbmFHng4U4jyEiekCptKTtuE",
"title": "Charity Stream Reward Pool",
"total_token": 200000000000,
"periodicity_seconds": 60,
"duration_seconds": 14400,
"max_per_viewer": 5000000000,
"start_at": "2026-05-15T20:00:00Z"
}'

Returns 201 Created with the full distribution document on success.

total_token, max_per_viewer, and accrual amounts are all in the mint's smallest unit (e.g., 200000000000 for 200K tokens at 6 decimals).

GET /distributions-api/by-channel/{channel}

List distributions for a channel, newest first. Public read.

GET /distributions-api/{id}

Get a single distribution by distributor PDA. Public read.

$ curl https://api.bitview.so/distributions-api/Dxxx...
{
"id": "Dxxx...",
"channel_login": "pewpiediepie",
"streamer_wallet": "Sxxx...",
"mint": "EFsATdho...",
"title": "Charity Stream Reward Pool",
"total_token": 200000000000,
"periodicity_seconds": 60,
"duration_seconds": 14400,
"max_per_viewer": 5000000000,
"status": "active",
"merkle_root": null,
"start_at": "2026-05-15T20:00:00Z",
"end_at": "2026-05-16T00:00:00Z",
"created_at": "2026-05-15T19:55:00Z",
"last_update": "2026-05-15T22:30:00Z"
}

GET /distributions-api/active

List all currently-accruing distributions across all channels.

POST /distributions-api/{id}/finalize

Move a distribution into snapshotting status. Admin-gated.

$ curl -X POST https://api.bitview.so/distributions-api/Dxxx.../finalize \
-H "x-admin-api-key: $ADMIN_KEY"
{"distribution_id": "Dxxx...", "status": "snapshotting"}

After finalize, an offline tree-builder job constructs the merkle tree from MongoDB accruals, then an on-chain set_enable_slot call publishes the root and flips status to claimable.

Claims scope (viewer)

GET /claims-api/proof/{wallet}

Proxy for the distributor merkle-proof API. Returns the proof a viewer needs to call new_claim on-chain.

$ curl https://api.bitview.so/claims-api/proof/Hxxx...
{
"merkle_tree": "Mxxx...",
"amount": 4523000000,
"proof": [
"QXJiaXRyYXJ5SGFzaDM=",
"QXJpdGFyeUhhc2g0",
"..."
]
}

merkle_tree is the distributor PDA (which holds the on-chain root). proof is an array of base58-encoded 32-byte hashes.

GET /claims-api/summary/{wallet}

Combined view of every distribution where this wallet has accruals, with claimability status. Drives the rewards page.

$ curl https://api.bitview.so/claims-api/summary/Hxxx...
{
"wallet": "Hxxx...",
"claimable": [
{
"distribution_id": "Dxxx...",
"mint": "EFsATdho...",
"amount": 4523000000,
"status": "claimable"
},
{
"distribution_id": "Dyyy...",
"mint": "EFsATdho...",
"amount": 800000000,
"status": "active"
}
]
}

status: "active" entries are not yet claimable (distribution still running). status: "claimable" is what the frontend lights up.

TypeScript example — complete claim flow

import { Connection, PublicKey, Transaction } from '@solana/web3.js';
import { AnchorProvider, Program, BN } from '@coral-xyz/anchor';
import idl from './merkle_distributor.json';

const BACKEND = 'https://api.bitview.so';
const RPC = 'https://api.mainnet-beta.solana.com';
const PROGRAM_ID = new PublicKey('4ffj6hEnx6cqp4ToMALExqk6QwPNSbZyr8ro9yW1Qvok');

async function claimAll(wallet: any) {
const summary = await fetch(
`${BACKEND}/claims-api/summary/${wallet.publicKey}`
).then((r) => r.json());

const claimable = summary.claimable.filter((c: any) => c.status === 'claimable');

const conn = new Connection(RPC);
const provider = new AnchorProvider(conn, wallet, {});
const program = new Program(idl, PROGRAM_ID, provider);

for (const entry of claimable) {
const proof = await fetch(
`${BACKEND}/claims-api/proof/${wallet.publicKey}`
).then((r) => r.json());

const tx = await program.methods
.newClaim(
new BN(proof.amount),
new BN(0),
proof.proof.map((p: string) => Array.from(Buffer.from(p, 'base64')))
)
.accounts({
distributor: new PublicKey(proof.merkle_tree),
// ... other accounts derived per IDL
})
.transaction();

await provider.sendAndConfirm(tx);
}
}

What's not in the API today

These are deliberately out of the current API surface:

  • Streamer-signed authorizationx-admin-api-key is a Phase 1 shortcut; we move to per-streamer signed actions in Phase 4.
  • Webhooks — Pro/Plus subscription tier feature in Phase 3+.
  • Cursor-based pagination — current limit is 200; cursor pagination ships when listing endpoints exceed practical limits.
  • GraphQL — REST suffices for the surface area today.
  • NFT drops API — ships in Phase 3 with the NFT drops feature.
  • Sponsorship marketplace API — ships in Phase 4.
  • Swap router API — ships in Phase 2 (with BTV launch).

These are tracked on the roadmap.

Versioning

The current API is unversioned during MVP. When we hit Phase 2 we introduce Accept: application/vnd.bitview.v1+json versioning with backward-compat for 12 months on any breaking change. Notable schema changes are publicly announced.