← Back|CROWDAQ+PulseDaqArchitecture
Technical Architecture

How It Works

A full walkthrough of how CROWDAQ and PulseDaq make decisions — from live game event to ad placement, fan message, and revenue tick.

System Overview


┌─────────────────────────────────────────────────────────────┐
│                    CROWDAQ + PulseDaq                        │
│                  Real-Time Decision Graph                    │
└─────────────────────────────────────────────────────────────┘

  ┌──────────────────────┐
  │   LIVE SPORTS DATA   │  API-Football / Opta / Nadwell
  │  (Goals, Cards, VAR) │  Orchestrator (production)
  └──────────┬───────────┘
             │ WebSocket / Supabase Realtime
             ▼
  ┌──────────────────────────────────────────────────────┐
  │               DECISION ENGINE  (server)              │
  │                                                      │
  │   ┌──────────────┐  ┌────────────────┐              │
  │   │  Excitement  │  │  Display Mode  │              │
  │   │    Score     │  │   Selector     │              │
  │   │  Calculator  │  │ LIVE/STATS/AD  │              │
  │   └──────┬───────┘  └───────┬────────┘              │
  │          │                  │                        │
  │   ┌──────▼──────────────────▼────────┐              │
  │   │       Ad Rotation Engine         │              │
  │   │  Slot A (30s) · B (45s) · C (60s)│              │
  │   │  Revenue tracking per impression  │              │
  │   └──────────────────┬───────────────┘              │
  │                      │                              │
  │   ┌──────────────────▼───────────────┐              │
  │   │    Message Generator (Claude AI) │              │
  │   │    Novice / Casual / Expert      │              │
  │   │    Cached per event_id + tier    │              │
  │   └──────────────────────────────────┘              │
  └──────────────────────────────────────────────────────┘
             │                        │
             ▼                        ▼
  ┌──────────────────┐     ┌──────────────────────┐
  │  CROWDAQ Screen  │     │  PulseDaq Fan App    │
  │  (bar TV)        │     │  (fan's phone)       │
  │  · Scoreboard    │     │  · Score chip        │
  │  · Excitement    │     │  · Pulse bar         │
  │  · Event / Stats │     │  · Commentary msg    │
  │  · Ad slots      │     │  · Badge unlocks     │
  │  · QR code       │     │  · [PLAY-BY-PLAY /   │
  └──────────────────┘     │    COLOR] tag        │
                           └──────────────────────┘
Bar pays monthly
Basic / Partner / Premium tiers for CROWDAQ screen license
Fans pay nothing
PulseDaq is free — acquired through QR scan during the match
Advertisers pay per scan
Verified venue-anchored impressions tracked across bar network

Event Pipeline

Phase 1 (POC — Simulation)

1
Static event array
15 pre-scripted match events (KICKOFF → FULLTIME) stored as TypeScript constants with all message variants pre-written.
2
Interval-based tick engine
A pure-function tick() runs every 1000ms ÷ speed. Every 8 ticks it dequeues the next event and updates state.
3
React state dispatch
useSimulation hook calls tick() inside setInterval, updating SimulationState immutably on each tick.
4
Component re-render
Bar screen and phone components re-render from state. No WebSocket needed in Phase 1.

Phase 2 (Production — Live)

1
API-Football webhook
API-Football fires a POST to our Supabase Edge Function on every match event (goal, card, VAR, etc.).
2
Nadwell orchestrator
Atabong's pipeline normalizes event format, computes excitement delta, and inserts into match_events table.
3
Supabase Realtime broadcast
INSERT on match_events triggers a Realtime broadcast. All connected clients (bar screens + phones) receive the payload instantly.
4
Client state update
Both CROWDAQ (Next.js) and PulseDaq (React Native) subscribe to the match channel and update UI from the broadcast payload.
Game Event Schema
interface GameEvent {
  matchId:   string          // "WC2026-USAMEX-GC"
  matchMin:  number          // 23
  type:      EventType       // "GOAL" | "NEARLYGOAL" | "YELLOWCARD" | "REDCARD"
                             // | "VAR" | "GOALCONFIRMED" | "SUBSTITUTION"
                             // | "HALFTIME" | "SECONDHALF" | "FULLTIME" | "KICKOFF"
  team:      string          // "USA"
  player:    string          // "Pulisic"
  score:     { home: 1, away: 0 }
  excitement: number         // 0–100 (computed by engine)
  description: string        // "Pulisic slots home from close range. 1-0 USA."
  timestamp: string          // ISO 8601
}

Display Mode Engine

The bar screen cycles through three display modes between game events — mimicking a televised broadcast. Each mode is triggered by how many ticks have elapsed since the last game event fired.

LIVE0–2 ticks
Event Display
Shown immediately when an event fires. Full-screen event card with type badge (GOAL / VAR / REDCARD etc.), event text, excitement bar. Phones receive play-by-play messages.
Phone commentary: PLAY-BY-PLAY
STATS3–5 ticks
Stats Overlay
Transitions to game stats between events: possession bar (USA vs MEX), shots, shots on target, corners, saves, yellow cards. Same scoreboard stays visible above.
Phone commentary: COLOR
AD CENTER6+ ticks
Sponsored Break
Slot A ad expands to fill the main display with a "Brought to you by" full-bleed treatment and brand glow. Slots B and C compress in the sidebar. Halftime triggers this immediately.
Phone commentary: COLOR
Mode Transition Logic
function resolveDisplayMode(ticksSinceEvent: number, eventType?: EventType): DisplayMode {
  // Halftime jumps straight to AD_CENTER — no LIVE or STATS phase
  if (eventType === 'HALFTIME') return 'AD_CENTER';

  if (ticksSinceEvent >= 6) return 'AD_CENTER';  // cut to commercial
  if (ticksSinceEvent >= 3) return 'STATS';       // broadcast stats
  return 'LIVE';                                  // event card
}

// In the tick loop:
ticksSinceLastEvent++;
const newMode = resolveDisplayMode(ticksSinceLastEvent);

if (newMode !== currentMode && (newMode === 'STATS' || newMode === 'AD_CENTER')) {
  // Push downtime (color) messages to all connected phones
  pushToPhones({ novice: event.noviceDowntime, casual: event.casualDowntime,
                 expert: event.expertDowntime, commentaryType: 'color' });
  logEntry('MODE', `Display → ${newMode} · color commentary dispatched`);
}

Bar Push Notifications

The bar can push real-time notifications directly to the phones of fans who are actively watching via PulseDaq. These appear as iOS-style toast banners sliding in from below the Dynamic Island. Pushes are triggered two ways: automatically by game events, or as ambient venue specials on a match-minute schedule.

Event-Triggered Pushes

On KICKOFF
⚽ USA vs Mexico LIVE! Show this screen for $1 off your first drink.
On GOAL (USA)
🎉 USA SCORES! Free shot with any pitcher — next 10 min only!
On GOAL (MEX)
😤 Mexico ties it! $2 off all Mexican beers while supplies last.
On HALFTIME
⏱ HALFTIME! $3 drafts + $5 margs for 15 minutes. Go!
On REDCARD
🟥 10 vs 11! Mexico down a man. Get another round in!
On VAR
📺 VAR = perfect time to order. Flag your server now!
On FULLTIME
🏆 USA WINS 2-1!! $2 off ALL beers for 30 min. Cheers!

Ambient Venue Pushes

Fired when the simulation crosses a match-minute threshold and no higher-priority event push is active. Bars configure these from their dashboard — or they're pre-set from CROWDAQ templates.

After 8'
🍺 World Cup Pint Special — Guinness + nachos $12. Scan to claim.
After 28'
📱 Loving PulseDaq? Share the QR with someone at the bar — it's free.
After 52'
⚽ Next USA match in 3 days. Book your table now — DM us @ryansbarKC.
After 71'
🏆 Knockout stage this weekend — we're screening every USA match.
Priority Logic
// Event push takes priority over ambient
if (EVENT_PUSHES[eventType]) {
  push = eventType === 'GOAL'
    ? (usaScored ? USA_GOAL_PUSH : MEX_GOAL_PUSH)
    : EVENT_PUSHES[eventType];
  barPushTicksRemaining = 7; // ~7 seconds at 1×
}

// Ambient only fires when no push is active
if (!currentBarPush && matchMin >= ambientPushes[nextIdx].afterMin) {
  push = ambientPushes[nextIdx];
  ambientPushesShown++;
}
Why This Matters for Ryan
Zero staff effort
Pushes fire automatically based on game events. No one has to watch the clock or manually send anything.
Verified audience
These fans scanned Ryan's QR. They are physically in the bar, right now, watching this game. Not a cold email list.
Geo push (Phase 2)
Fans who scanned in the last 30 days can be reached before the next match: "Tonight at 8pm — USA is back. Tables open."

Fan Companion Screen (PulseDaq)

The fan app has three tabs, each surfacing a different layer of the data. All three update in real-time from the same event stream. The tab state is local to each device — a fan can be on Stats while another fan next to them is watching match messages.

⚽ Match
The default view. Shows the Pulse excitement bar, a commentary tag (PLAY-BY-PLAY or COLOR), the calibrated message for the fan's depth level, and badge unlocks.
Data source
Game event stream + Claude AI message generation
📊 Stats
Live match statistics: possession split bar (USA vs MEX), shots, shots on target, corners, saves, yellow cards. Updates every time a game event fires with new stat snapshot.
Data source
GameStats snapshot attached to each event in the event pipeline
🌍 Games
Four other World Cup matches running simultaneously. Each shows live score, match minute, group, venue, and a mini excitement bar. Games evolve in lockstep with the USA match timeline.
Data source
Simulated in POC via getOtherGames(matchMin). Production: API-Football multi-match polling.
Other Games — Evolution Logic
// lib/data/otherGames.ts
// Each of the 4 parallel games has a scoring timeline and a minute offset
// relative to USA's current match minute.

// England vs Iran — kicked off 30 min before USA
const engMin = Math.min(usaMatchMin + 30, 98);

// Spain vs Germany — kicks off 45 min AFTER USA (shows as "Upcoming" early on)
const spaRawMin = usaMatchMin - 45;
const spaMin = Math.max(0, spaRawMin);
const spaStatus = spaRawMin < 0 ? 'pre' : spaMin >= 91 ? 'ft' : 'live';

// Score computed from a timeline of goal moments
function scoreAt(min, events) {
  let home = 0, away = 0;
  for (const e of events) {
    if (min >= e.min) { home = e.home; away = e.away; }
  }
  return { home, away };
}
// e.g. Brazil vs Serbia goals: [20' Serbia 0-1] [30' Brazil 1-1] [58' Brazil 2-1] [73' Brazil 3-1]
Phone Chrome Design
  • ·Dynamic Island (black pill, live dot inside)
  • ·Side buttons — volume (left ×3), power (right)
  • ·Home indicator bar (bottom of screen)
  • ·Screen border-radius: 36px inner, 42px chassis
  • ·Bar push toast slides from behind Dynamic Island
  • ·Depth sticker label appears above the phone
  • ·Tab bar color matches the fan's depth accent
  • ·Chassis gradient: #222228 → #111116

Ad Rotation Engine

Three independent ad slots run on separate countdown timers. Each slot holds a queue of ads that rotate automatically. Revenue is tracked per impression, with a multiplier when the display is in AD_CENTER mode (the ad occupies the full main panel).

Ad Slot A — rotates every 30s
$0.07–$0.09 / impression
Beer brands (Guinness, Modelo, Heineken, Bud Light)
Featured in AD_CENTER
Ad Slot B — rotates every 45s
$0.10–$0.12 / impression
Bar's own promotions + PulseDaq app install CTA
Secondary in AD_CENTER
Ad Slot C — rotates every 60s
$0.06–$0.11 / impression
Broadcast sponsors + ticketing (Fox Sports, MLS, WC2026)
Secondary in AD_CENTER
Revenue Calculation Logic
function rotateAd(state, slot) {
  const ad = ADS[slot][nextIndex];

  // AD_CENTER multiplier: ad fills the main screen = 1.5× impressions
  const multiplier = state.displayMode === 'AD_CENTER' ? 1.5 : 1.0;

  // Revenue = rate × active users in venue × display multiplier
  const earned = ad.rate × activeUsers × multiplier;

  totalRevenue += earned;
  // e.g. Guinness at $0.08 × 3 users × 1.5 (AD_CENTER) = $0.36 per rotation
}

// Ad selection decision tree (production):
// 1. If excitement > 80 (GOAL just fired) → prioritize beer/celebration ads
// 2. If displayMode === 'HALFTIME'         → bar specials (Ryan's Happy Hour)
// 3. If excitement < 40 (quiet period)    → highest-CPM ad available
// 4. Default                              → round-robin by slot timer
Revenue at Scale
Venue SizeActive UsersSessions/GameRev/Game (est.)Rev/Month (est.)
Small bar10–203 games/wk$8–$18$90–$200
Ryan's Bar (KC)50–1008 games/wk$40–$90$450–$1,000
10-bar network500–1,0008 games/wk$400–$900$4,500–$10,000
50-bar network2,500–5,0008 games/wk$2,000–$4,500$22k–$50k

AI Message Generation

For each game event, three commentary messages are generated — one per fan tier. Messages are cached by event_id + tier so Claude is only called once per event type, regardless of how many fans are connected. Two commentary types serve different purposes in the broadcast cadence.

PLAY-BY-PLAY
Immediate event reaction
Sent the moment an event fires (LIVE mode). Describes what just happened in the language and depth appropriate to the fan tier. Reactive, present-tense, emotionally charged.
Novice — 23' GOAL
🎉🎉🎉 GOAL!!! USA SCORES!!! Pulisic put it in the net! 1-0! The whole bar is going crazy!!!
Casual — 23' GOAL
Pulisic with his 4th World Cup goal! Neat one-two with Reyna, clinical finish bottom corner. 1-0 USA.
COLOR COMMENTARY
Tactical analysis between events
Sent during STATS and AD_CENTER modes (downtime between action). Provides context, history, and tactical insight. This is what keeps fans engaged during the quiet periods — the "color" that a TV analyst provides between plays.
Expert — 23' COLOR
Goal came from the exact vulnerability the Álvarez booking created. Reyna exploited the gap between lines. xG map now USA 0.89 / MEX 0.14. Mexico must switch shape.
Novice — 23' COLOR
🔥 Pulisic is one of the best American soccer players ever — he plays for Chelsea in England. He's been waiting for this moment.
Claude API Call — Production Pattern
// lib/messages/generate.ts
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();

// System prompt is cached — only sent once, subsequent calls are cache hits
const SYSTEM_PROMPT = `You are a live soccer commentary engine for CROWDAQ/PulseDaq.
Generate calibrated commentary for three fan tiers from a single game event.

NOVICE:  No jargon. Emojis encouraged. Explain WHY it matters. ≤60 words.
CASUAL:  Player names, basic stats, assume they know the rules. ≤80 words.
EXPERT:  Tactics, xG, formations, pressing intensity. No hand-holding. ≤100 words.

Always return JSON: { novice, casual, expert, noviceType, casualType, expertType }
Types: "play-by-play" (immediate reaction) or "color" (tactical analysis between events).`;

export async function generateMessages(event: GameEvent, commentaryContext: 'live' | 'downtime') {
  // Cache key: skip Claude if we already generated for this event + context
  const cacheKey = `${event.matchId}:${event.matchMin}:${event.type}:${commentaryContext}`;
  const cached = await messageCache.get(cacheKey);
  if (cached) return cached;

  const response = await client.messages.create({
    model: "claude-haiku-4-5-20251001",  // fastest + cheapest for speed-critical push
    max_tokens: 500,
    system: [
      {
        type: "text",
        text: SYSTEM_PROMPT,
        cache_control: { type: "ephemeral" },  // cache the system prompt — 5 min TTL
      }
    ],
    messages: [{
      role: "user",
      content: `Event: ${event.type} at ${event.matchMin}' — ${event.description}
Score: ${event.score.home}–${event.score.away}
Context: ${commentaryContext === 'live' ? 'Event just happened' : 'Between events — provide insight/analysis'}`,
    }],
  });

  const result = JSON.parse(response.content[0].text);
  await messageCache.set(cacheKey, result, { ttl: 3600 }); // cache 1 hour
  return result;
}
// Cost estimate: Haiku at $0.00025/1K input tokens
// Cached system prompt: ~$0.000025 per call after first (90% savings)
// Per game (~15 events × 2 contexts × 3 tiers): ~$0.02 total
Novice
  • · No jargon
  • · Emojis encouraged
  • · Explain why it matters
  • · ≤60 words
  • · Examples and analogies
Casual
  • · Player names OK
  • · Basic stats fine
  • · Assume they know rules
  • · ≤80 words
  • · Team context
Expert
  • · Tactics + formations
  • · xG and pressing %
  • · No hand-holding
  • · ≤100 words
  • · Analysis only

Excitement Score Engine

The excitement score (0–100) is a composite signal computed from incoming game events. It drives the PULSE bar on both the CROWDAQ screen and each PulseDaq phone, and determines when to trigger certain ad categories (high excitement = beer celebration ads).

Event Weights
FULLTIME (decisive)+99
GOAL+30
REDCARD+20
FULLTIME+20
NEARLYGOAL+15
GOALCONFIRMED (VAR)+24
VAR Review+10
YELLOWCARD+5
SUBSTITUTION−2
HALFTIME−10
Decay (per tick)−0.5
Floor20 min
Score Formula
function computeExcitement(prev: number, event: GameEvent): number {
  const deltas: Record<EventType, number> = {
    GOAL:          30,
    NEARLYGOAL:    15,
    REDCARD:       20,
    YELLOWCARD:    5,
    VAR:           10,
    GOALCONFIRMED: 24,  // VAR confirms + relief
    SUBSTITUTION:  -2,
    HALFTIME:      -10,
    SECONDHALF:    5,
    FULLTIME:      20,
    KICKOFF:       0,
  };

  const raw = prev + (deltas[event.type] ?? 0);
  return Math.min(100, Math.max(20, raw));  // clamp [20, 100]
}

// Decay between events (called each tick):
function decayExcitement(current: number): number {
  return Math.max(20, current - 0.5);
}
Score → UI Behavior
80–100
CriticalOrange glow pulses. Ads suppressed during LIVE phase.
60–79
HighFull excitement bar. PLAY-BY-PLAY tags active.
40–59
MediumNormal broadcast cadence. Stats shown between events.
20–39
LowAD_CENTER mode triggered faster. CPM-optimal ads served.

Platform Optimization Engines

Three independent engines run on every tick to keep the broadcast experience tight, the ad inventory working at full efficiency, and the fan app content feeling live even during quiet periods. All three are pure functions operating on simulation state — no side effects, easy to test.

Content Cadence Engine
Detects quiet periods (no game event for 20+ ticks in non-LIVE mode) and injects contextual fill messages. Messages are selected from a pool of 25 calibrated fills keyed to match minute, score state, and stats.
  • ·25 fill messages across 5 time pools
  • ·Score-context injection every 3rd fill
  • ·Stats-triggered fill when SOT > 4
  • ·Resets counter on any event or downtime push
Excitement Context Engine
Applies a game-state multiplier to the base excitement score. A 0-0 game at 38' reads differently than 0-0 at 88'. Late-game tension, score closeness, and rivalry floor are all factored.
  • ·+12 bonus: minute 75+, score within 1
  • ·+8 bonus: minute 85+, score within 1
  • ·Floor: 42 minimum for USA vs Mexico
  • ·Updates every tick (not just on events)
Ad Timing Intelligence
Pauses all three ad rotation timers when contextual excitement exceeds 80. This prevents a sponsor slot from rotating during a goal celebration — protecting advertiser quality and fan experience simultaneously.
  • ·Pause trigger: contextualExcitement ≥ 80
  • ·Pause duration: 12 ticks (~12 seconds)
  • ·All 3 slots frozen simultaneously
  • ·Resumes automatically after countdown
Push Frequency Management

A fourth engine caps bar push notification frequency at 15 ticks between pushes — preventing notification fatigue during event-dense sequences. High-priority events (GOAL, FULLTIME) bypass the cap and always fire immediately.

Frequency cap15 ticks (~15 seconds at 1×)
Cap bypass eventsGOAL + FULLTIME only
Ambient push gateAD_CENTER mode + cap satisfied
Push duration7 ticks visible, then auto-dismiss
// lib/simulation/SimulationEngine.ts

// Push frequency cap: min ticks between bar pushes
// (exceptions: GOAL, FULLTIME always fire immediately)
const PUSH_FREQUENCY_CAP = 15;

function applyBarPush(state, ev) {
  const isHighPriority =
    ev.type === 'GOAL' || ev.type === 'FULLTIME';

  if (!isHighPriority &&
      state.ticksSinceLastPush < PUSH_FREQUENCY_CAP) {
    return state; // Too soon — skip push
  }

  return {
    ...state,
    currentBarPush: push,
    barPushTicksRemaining: PUSH_DURATION_TICKS,
    ticksSinceLastPush: 0,   // reset cap
  };
}

// Content cadence — fires fill message in quiet periods
const CADENCE_THRESHOLD = 20;

if (ticksSinceLastMessage > CADENCE_THRESHOLD
    && displayMode !== 'LIVE') {
  const fill = getCadenceFill(
    matchMin, scoreUSA, scoreMEX,
    gameStats, contextualExcitement, cadenceFillIndex
  );
  // → updates novice/casual/expert messages
  // → resets ticksSinceLastMessage to 0
  cadenceFillIndex++;
}
Content Cadence Fill — Message Selection Logic
// lib/data/cadenceContent.ts

// 5 time-windowed message pools (25 fills total)
const FILLS_0_20:   CadenceFill[] = [ ...rivalry, pulisic, group, atmosphere, tactics ]
const FILLS_20_45:  CadenceFill[] = [ ...halftime-approaching, possession, historical, stats, engagement ]
const FILLS_45_60:  CadenceFill[] = [ ...second-half, tactical-adjust, tournament-stakes, pressing, wc-context ]
const FILLS_60_75:  CadenceFill[] = [ ...late-tension, substitutions, set-pieces, rivalry-history, crowd ]
const FILLS_75_94:  CadenceFill[] = [ ...late-tension, desperate-late, injury-time, advance-r16, peak-drama ]

// Score-context messages injected every 3rd fill
function getScoreContextFill(matchMin, scoreUSA, scoreMEX) {
  if (scoreDiff === 0 && matchMin >= 70)  return TIED_LATE_MESSAGES;
  if (diff > 0 && matchMin >= 60)        return USA_PROTECTING_MESSAGES;
  if (diff < 0 && matchMin >= 60)        return USA_CHASING_MESSAGES;
  return null; // fall through to time-pool
}

// Stats-triggered fill (injected every 4th fill when SOT > 4)
if (shotsOnTarget > 4 && fillIndex % 4 === 1) return STATS_LIVE_FILL;

Bar Owner Portal

The bar owner portal gives Ryan a real-time view of his CROWDAQ deployment — revenue, fan activity, push campaigns, and match schedule. It scales from a free Basic tier through Partner and Premium tiers, each unlocking new capabilities as the business relationship deepens.

📊 Overview
KPI strip: revenue tonight, peak fans connected, total impressions, campaigns sent. Fan scan bar chart by day. Active match summary with score, QR scans, and claimed specials.
📺 Ad Slots
Live view of all 3 ad rotation slots — current creative, next ad in queue, impressions tonight, revenue per slot. Partner+ can edit the Slot B queue with custom bar specials.
📱 Push Campaigns
All push campaigns sent tonight with timestamps and open rates. Template library for one-click campaign setup: Happy Hour, Goal Celebration, Halftime Special, Next Match Promo.
🗓 Match Schedule
Upcoming matches Ryan can screen at his bar. One-click "Screen this match" to activate CROWDAQ for that game. Past results with scores and scan counts.
📈 Analytics
Weekly revenue chart, total scans, session length, push open rate, revenue per fan. Premium tier adds full advertiser breakdown by brand and category with export.
⚙️ Account
Venue profile, active tier, billing date, CROWDAQ rep contact. Interactive tier selector shows all 3 tiers side-by-side with feature comparison and live switching for the demo.
BasicFree
  • CROWDAQ screen + QR code
  • Standard branding
  • Event-triggered pushes
  • Basic scan count
  • Custom branding
  • Ad revenue share
  • Push campaign builder
  • Analytics dashboard
Partner$50/mo
  • Everything in Basic
  • Custom bar branding
  • Slot B ad queue (owned)
  • Push campaign builder
  • 70% revenue share
  • Full analytics dashboard
  • Geo push window
  • Advertiser breakdown
Premium$100/mo
  • Everything in Partner
  • Full analytics dashboard
  • Advertiser breakdown
  • Geo push (30-day)
  • Priority ad matching
The Bar Owner Value Equation
What Ryan gets for free

A TV screen that runs itself. Live World Cup data on display. A QR code that brings fans to their phones and keeps them in his bar longer. No staff training required.

What Partner tier adds

His Guinness poster goes digital. His happy hour special goes to every fan who scanned in the last 30 minutes — at the exact moment they might leave. He controls Slot B.

What Premium tier adds

He can pitch local advertisers with real data: "327 fans in my bar last Tuesday. Average session 41 minutes. $0.54 revenue per fan per game. Here is the CPM."

QR Scan → Fan Onboarding Flow

The QR code on the CROWDAQ bar screen is permanent — it never rotates and requires no staff management. A fan can scan at any moment during the match and be live within 2 taps.

1
Fan scans QR code
QR encodes a permanent deep link: pulsedaqs://scan?venue=ryans-bar-kc. Works from any camera app — no prior app install required (PWA).
2
App loads pre-filled
The deep link carries the venueId. PulseDaq queries Supabase for the venue's activeMatchId and loads that game's current state instantly.
3
One-question onboarding
"How much do you follow soccer?" → slider from 0–100. Under 33 = Novice. 33–66 = Casual. 67+ = Expert. Skippable (defaults to Casual).
4
Fan is live
UserProfile created with { userId, venueId, depth, scanTime, activeMatchId }. Fan starts receiving calibrated messages and badge unlocks immediately.
5
Venue gets attribution
The scan is logged against the venue. Bar can see: 'Tonight: 43 scans from Ryan's Bar.' This is the audience inventory sold to advertisers.
Why permanent QR matters
Rotating QR (do not use)
  • Requires staff to update display
  • Fan scans stale code at wrong moment
  • Codes expire mid-match
  • Staff error = acquisition loss
Permanent QR (CROWDAQ approach)
  • Zero staff involvement ever
  • Scan at kickoff, halftime, or minute 87
  • QR printed once, lasts the season
  • Venue onboards itself

Revenue Model

Bar License
BasicFree
CROWDAQ screen, QR code, standard branding
Partner$50/mo/screen
Custom branding, ad rotation, push notifications
Premium$100/mo/screen
All above + analytics dashboard
Ad Network Revenue

Third-party advertisers (Guinness, local sponsors) pay per impression across the bar network.

$0.06–$0.12 per impression

Revenue = rate × active users × display multiplier

AD_CENTER mode: 1.5× multiplier (full-screen placement)

Platform takes 30% of third-party ad revenue. Bar keeps 70%.

Geo Push (Phase 2)

Fans who scanned a bar's QR code in the last 30 days can be reached with geo-targeted push notifications.

Bar pushes specials before game day

"Tonight: $3 drafts for PulseDaq users. USA game kicks off at 8pm."

Priced per push. Bar pays platform fee.

The Core Insight

A fan who scans a QR code at Ryan's Bar during the USA vs Mexico match is not just a “user.” They are a verified, venue-anchored, emotionally engaged sports fan with a known location, a known team, and a timestamped emotional context (excitement score at time of scan). That is the inventory unit advertisers are buying — not a cookie, not a demographic guess. A real person, in a real bar, watching a real game, who just felt something.