Streaming & Betting Integration Guide
Last Updated: March 23, 2026PR: #1065 - Internal bet sync feed and renderer health
Overview
Hyperscape provides an authenticated internal betting feed that makes the streaming duel lifecycle authoritative for betting market synchronization. This guide covers the betting feed API, renderer health monitoring, and integration patterns for betting consumers.Architecture
Source of Truth
Hyperscape is the authoritative source for:- Duel lifecycle events (announcement, countdown, fight start, resolution)
- Agent state (HP, equipment, position)
- Arena state (positions, phase transitions)
- Renderer health (ready vs degraded states)
Components
-
DuelBettingBridge (
packages/server/src/systems/DuelScheduler/DuelBettingBridge.ts)- Listens to streaming duel events
- Creates/syncs/resolves markets with Solana operator
- Publishes sequence-aware SSE feed
-
Betting Feed Routes (
packages/server/src/routes/streaming-betting-routes.ts)- Bootstrap endpoint:
GET /api/internal/bet-sync/state - SSE feed:
GET /api/internal/bet-sync/events - Authentication, rate limiting, CORS
- Bootstrap endpoint:
-
Renderer Health (
packages/server/src/routes/streaming-betting-health.ts)- Derives health from streaming guardrails + external RTMP status
- Detects degraded states (loading, invalid positions, etc.)
-
Streaming Guardrails (
packages/shared/src/utils/rendering/streamingGuardrails.ts)- Shared validation logic (client + server)
- Agent snapshot validation
- Arena position sanity checks
API Reference
Bootstrap Endpoint
Endpoint:GET /api/internal/bet-sync/state
Authentication: Required
SSE Events Feed
Endpoint:GET /api/internal/bet-sync/events
Authentication: Required (Bearer header or ?streamToken= query param)
since=<sequence>- Resume from specific sequence number (optional)limit=<number>- Max frames in initial replay (default: 100, max: 2048)
"bootstrap"- Full replay buffer (client is behind or first connection)"incremental"- Frames sincesincesequence (client is caught up)"reset"- Client sequence is ahead of server (server restarted)
Max Clients: 32 concurrent connections (configurable)
Heartbeat: Every 15 seconds
Payload Structure
Renderer Health
Health States
Healthy (ready: true, degradedReason: null):
- All agents present with valid HP
- Arena positions are sane (no overlaps)
- Loading overlay dismissed
- Camera locked to target (if required)
- Active phase (ANNOUNCEMENT, COUNTDOWN, FIGHTING)
ready: false, degradedReason: <string>):
Surface-Level Reasons (client/server not ready):
"socket_disconnected"- WebSocket connection lost"world_not_ready"- 3D world not initialized"terrain_not_ready"- Terrain system not loaded"camera_target_unresolved"- Camera hasn’t locked to target"initialization_failed"- World init error"renderer_unavailable"- WebGPU not available
"agent1_invalid"- Agent 1 missing or invalid HP"agent2_invalid"- Agent 2 missing or invalid HP"arena_positions_invalid"- Positions overlapping or missing
"loading_overlay_active"- Loading screen still visible"initializing"- Waiting for duel data (IDLE phase)"waiting_for_duel_data"- No streaming state yet
Client-Side Health Monitoring
Window Globals (exposed for capture pipeline):packages/client/src/screens/StreamingMode.tsx):
Server-Side Health Monitoring
Derivation (packages/server/src/routes/streaming-betting-health.ts):
packages/server/src/routes/streaming-external-status.ts):
DuelBettingBridge Lifecycle
State Machine
Event Handlers
Announcement (handleStreamingAnnouncement):
handleStreamingFightStart):
handleStreamingResolution):
handleStreamingAbort):
Reconciliation Loop
Runs every 1 second to ensure market state stays aligned with streaming lifecycle:Security
Authentication
Timing-Safe Token Comparison (packages/server/src/routes/streaming-betting-auth.ts):
CORS Configuration
Token Handling Best Practices
Server-Side:- Store
BETTING_FEED_ACCESS_TOKENin environment variables or secret manager - Use strong random tokens:
openssl rand -base64 32 - Rotate tokens periodically
- Never log tokens in access logs (use
redactStreamingSecretsFromUrl)
- Pass tokens in URL hash fragments (not query params):
#streamToken=... - Scrub tokens immediately via
history.replaceState - Use
getStreamingAccessToken()to retrieve cached token - Never send tokens in
Refererheaders (use<meta name="referrer" content="same-origin">)
Renderer Health Monitoring
Health Derivation
Client (packages/client/src/screens/StreamingMode.tsx):
packages/server/src/routes/streaming-betting-health.ts):
Capture Pipeline Integration
Renderer Health Probe (packages/server/scripts/stream-to-rtmp.ts):
packages/server/src/streaming/captureBrowserPolicy.ts):
Configuration
Environment Variables
Server (packages/server/.env):
packages/client/.env):
Rate Limits
Bootstrap Endpoint:- 240 requests/minute per IP
- No concurrent connection limit (stateless)
- 60 requests/minute per IP
- Max 32 concurrent connections (configurable via
BETTING_SSE_MAX_CLIENTS) - Slow clients evicted when
writableLength > STREAMING_SSE_MAX_PENDING_BYTES
Integration Patterns
Betting Consumer (Hyperbet)
Bootstrap on Startup:Troubleshooting
Authentication Failures
Symptom: 401 Unauthorized on betting feed endpoints Solutions:- Verify
BETTING_FEED_ACCESS_TOKENis set in server.env - Check token is passed correctly (Bearer header or
?streamToken=) - Ensure token matches exactly (no extra whitespace)
- Check server logs for “Betting feed auth failed” warnings
Renderer Health Always Degraded
Symptom:rendererHealth.ready is always false
Solutions:
- Check
degradedReasonfor specific issue - Verify streaming state is present (
hasStreamingState: true) - Check agent snapshots have valid HP (
agent.hp <= agent.maxHp) - Verify arena positions are not overlapping
- Check loading overlay has dismissed (
loadingDismissed: true) - Verify camera has locked to target (if
needsCameraLock: true)
SSE Connection Drops
Symptom: EventSource closes unexpectedly Solutions:- Check server logs for “Slow client evicted” warnings
- Verify client is consuming frames fast enough
- Increase
STREAMING_SSE_MAX_PENDING_BYTESif needed - Check network stability (SSE requires persistent connection)
- Implement reconnection logic with
?since=parameter
Sequence Gaps
Symptom: Missing sequence numbers in SSE feed Solutions:- Check
sourceEpoch- if changed, server restarted (sequence reset) - Use bootstrap endpoint to get full replay buffer
- Implement gap detection and re-bootstrap logic
- Check
modefield in SSE delivery:"bootstrap"- Full buffer (client is behind)"incremental"- Frames sincesince(client is caught up)"reset"- Client is ahead (server restarted)
Testing
Unit Tests
Betting Feed Auth (packages/server/src/routes/__tests__/streaming-betting-auth.test.ts):
- Token extraction from Bearer header and query params
- Timing-safe comparison
- Development bypass logic
- Production fail-closed behavior
packages/server/src/routes/__tests__/streaming-betting-feed.test.ts):
- Payload construction
- Replay delivery modes (bootstrap, incremental, reset)
- Frame ordering and deduplication
packages/server/src/systems/DuelScheduler/__tests__/DuelBettingBridge.test.ts):
- Lifecycle transitions (announcement → fight → resolution)
- Reconciliation loop
- Abort handling
- Error recovery
packages/shared/src/utils/rendering/__tests__/streamingGuardrails.test.ts):
- Agent snapshot validation
- Arena position validation
- Phase-specific requirements
Integration Tests
StreamingMode Component (packages/client/tests/unit/screens/StreamingMode.component.test.tsx):
- Loading overlay dismissal
- Renderer health globals
- Token passing to WebSocket
- Init error handling
packages/client/tests/unit/screens/StreamingMode.test.ts):
- Surface-level blocking reasons
- Streaming guardrail reasons
- Loading overlay state
- Arena position validation
Migration Guide
From Public Spectator Polling to Internal Feed
Old Pattern (polling public endpoint):- Real-time updates (no polling delay)
- Renderer health signals (prevent betting on degraded frames)
- Sequence-aware (idempotent deduplication via
phaseVersion) - Replay buffer (reconnection support)
- Lower server load (push vs pull)
Breaking Changes
Authentication Required:- All internal betting endpoints now require
BETTING_FEED_ACCESS_TOKEN - Public spectator endpoints remain unauthenticated
- Set
BETTING_FEED_ACCESS_TOKENin server.envbefore deploying
- Internal endpoints restrict CORS to
INTERNAL_BET_SYNC_ALLOWED_ORIGIN - Public endpoints remain open (wildcard CORS)
- Set
INTERNAL_BET_SYNC_ALLOWED_ORIGINto your betting frontend domain
- Sequence numbers reset on server restart (new
sourceEpoch) - Clients must detect
sourceEpochchanges and re-bootstrap - Replay buffer limited to 2048 frames (older frames are trimmed)
References
- PR #1065: Internal bet sync feed and renderer health
- Hyperbet Consumer PR: HyperscapeAI/hyperbet#28
- Streaming Guardrails:
packages/shared/src/utils/rendering/streamingGuardrails.ts - DuelBettingBridge:
packages/server/src/systems/DuelScheduler/DuelBettingBridge.ts - Betting Feed Routes:
packages/server/src/routes/streaming-betting-routes.ts