# RoChart > Live product analytics for Roblox games. A Luau module on the game server sends joins, leaves, server heartbeats, purchases and custom events, so DAU, playtime, concurrent players, D1/D7/D30 retention, join sources and Robux revenue are live (including today so far) instead of a day or two behind like the Creator Dashboard. Also: insights (trends, funnels, retention, paths), players (persons, cohorts, groups), feature flags and Bayesian A/B experiments evaluated from the game server, alerts, destinations and SQL. Humans use the web UI; agents get exactly the same capabilities over MCP, a CLI and a REST API. Not affiliated with Roblox Corporation. This instance: https://app.rochart.co Full documentation: https://docs.rochart.co/llms.txt (index) · https://docs.rochart.co/llms-full.txt (everything in one file) ## Authenticate Create a project API key in the UI (Project settings → API keys & MCP). Keys look like `rc_sk_…`, belong to one project and are read-only or read+write. Send it as `Authorization: Bearer rc_sk_…`. Every change made with a key is attributed to it in the project's activity log. The public project token (`rc_…`) is different: it only sends events and reads feature flags, and goes in the game's `RoChart.init` (or a website). ## Use it - [MCP server](https://app.rochart.co/mcp): streamable HTTP, stateless. Claude Code: `claude mcp add --transport http rochart https://app.rochart.co/mcp --header "Authorization: Bearer rc_sk_…"`. 66 tools; tools for products switched off in the project are hidden, read-only keys see only read tools. - [CLI](https://app.rochart.co/cli/install.sh): `curl -fsSL https://app.rochart.co/cli/install.sh | sh`, then `rochart login --host https://app.rochart.co --key rc_sk_…` (or env ROCHART_HOST / ROCHART_API_KEY). `rochart overview`, `rochart tools`, `rochart query ''`, `rochart sql ""`, `rochart call --arg value`. - REST: `https://app.rochart.co/api/projects//…` with the same Bearer header. Errors are `{ "error", "message", "hint" }`. ## Work like this 1. Orient: `project_overview` (enabled products, top events, cohorts, running experiments, recent activity), then `{"kind":"game_live"}` for right now and today so far. 2. Learn the real schema before querying: `get_schema` (every event with 30-day volume, trend, people, how many saved things use it and look-alike names; or person/group properties with coverage, top values and where they're set from), `get_event_usage` (one event's properties and everything that uses it — check before renaming or hiding), `get_event`, `list_properties`. Never guess event or property names; when two events look alike, prefer the verified or more-used one. 3. Analyse with `query` (one JSON language, see below). Every result starts with a plain-text summary. 4. Segment with cohorts; compare segments with a cohort breakdown. 5. Test changes: `create_experiment` (hypothesis + primary metric + segment cohorts) → `launch_experiment` → `get_experiment_results` → `conclude_experiment` with your reasoning. Leave `create_annotation` notes for humans. ## Dashboards Build one in a single call: MCP `create_dashboard` with `sections` (or `set_dashboard_layout` for an existing one; REST `PUT /api/projects//dashboards//layout`). A section is `{ "heading": "Revenue", "rows": [[{ "tile": {…} }, { "tileId": 12, "size": "large" }]] }`; rows fill the 12-column width and share one height, so there are no gaps or overlaps. Lead with 3–4 headline metrics (`format`: currency | percent | duration, `goodWhen: "down"` for churn), then trends, then breakdowns and funnels (`display`: line | area | bar | stacked_bar | stacked_area | pie | donut | table | number). `get_dashboard` prints the layout outline and any layout issues; `set_dashboard_layout` without sections tidies it. ## Roblox events Player events use distinct_id = tostring(UserId) and carry $session_id, $server_id, $place_id, $device_type (Computer | Phone | Tablet | Console | VR | Unknown), $geoip_country_code, $locale. Server events use "$server:" and never create a person. - `$session_start` (join: $join_source = link | referral | friend | teleport | discovery, $campaign, $account_age_days, $premium, $server_type, $place_version), `$session_end` ($duration s, $end_reason), `$server_heartbeat` (every 60 s: $players, $max_players, $place_version, health: $fps, $memory_mb, $heartbeat_ms, $ping_ms), `$server_shutdown`, `$purchase` ($product_type developer_product | game_pass, $product_id, $product_name, $robux), `$error` ($message, $stack, $script, $side server | client, $fingerprint, $count), `$economy` ($flow source | sink, $currency, $amount, $balance, $transaction_type, $item_sku), `$funnel_step` ($funnel, $step, $step_name), `$feedback`, `$feature_flag_called`, plus the game's own events. A new $place_version is a published update; a server that stops heartbeating without $server_shutdown crashed. - "discovery" lumps home, search, charts and ads together (Roblox doesn't tell the game which). There is no age or gender data. New users = first session since the SDK was installed. ## Query language (POST /api/projects//query, MCP `query`, CLI `rochart query`) - Game analytics: `game_engagement` (DAU, new vs returning, playtime, sessions, MAU, peak CCU), `game_acquisition` (new players by join source / campaign / country / platform with their retention and revenue), `game_retention` (D1/D7/D30 per cohort day, stickiness), `game_monetization` (Robux revenue, payers, ARPDAU, ARPPU, top products), `game_live` (players in game now, servers, today so far vs yesterday at the same time), `game_updates` (every published place version vs the one before: playtime, session length, D1, errors per 1k sessions, crash rate, Robux per player — use it to judge an update), `game_health` (server FPS, memory, ping, errors, crashes over time), `game_errors` (script errors grouped by fingerprint), `game_servers` (live servers, outdated versions), `game_economy` (currency sources vs sinks), `game_funnels` (RoChart.funnelStep funnels), `game_player` (one player's summary), `game_metric` (one number: playing_now, robux today, errors in the last N minutes…). They take `dateRange` and `filters` / `breakdown` by player dimension (platform, country, locale, join_source, campaign, new_vs_returning, premium, server_type, place_version); read the `query` tool schema for the current fields. - `{"kind":"trends","series":[{"event":"$session_start","math":"unique_persons"}],"interval":"day","dateRange":{"from":"-30d"}}` math: total | unique_persons | unique_sessions | unique_groups (+groupTypeIndex) | sum | avg | min | max | median | p90 | p99 (+mathProperty) - `{"kind":"funnel","steps":[{"event":"$session_start"},{"event":"tutorial_completed"},{"event":"$purchase"}],"windowDays":7,"breakdown":{"type":"event","key":"$device_type"}}` - `{"kind":"retention","startEvent":{"event":"tutorial_completed"},"returnEvent":{"event":"$session_start"},"period":"day","periods":8}` - `{"kind":"paths","include":"events","startPoint":"$session_start","steps":5,"per":"session"}` (include: events | pageviews | both; endPoint shows what leads to an event) - `{"kind":"lifecycle","event":{"event":"$session_start"},"interval":"week","dateRange":{"from":"-8w"}}` → new / returning / resurrecting / dormant per period - `{"kind":"stickiness","series":[{"event":"match_ended"}],"interval":"day","dateRange":{"from":"-30d"}}` → people by number of active days - `{"kind":"events","event":"$purchase","limit":50}` · `{"kind":"sql","sql":"SELECT event, count() FROM events GROUP BY event"}` (see SQL below) - Filters (insight queries): `{"type":"event"|"person"|"group","key":"platform","operator":"eq","value":"Phone"}`, `{"type":"cohort","value":,"operator":"in"}`; operators: eq neq contains not_contains regex gt gte lt lte is_set is_not_set in not_in - Breakdowns: `{"type":"event"|"person"|"group","key":"…"}` or `{"type":"cohort","cohortIds":[…]}` - Date ranges: "-30min", "-1h", "-3h", "-24h", "-7d", "-12w", "-6m" (months), "mtd", "ytd", "all", or ISO dates/datetimes (`{"from":"2026-11-02T09:00","to":"2026-11-02T17:30"}`, project timezone). Intervals: minute | 5min | 10min | hour | day | week | month. ## Live events MCP `tail_events` (REST `GET /api/projects//live/tail?seconds=60`): events as they are ingested, seconds old. Pass the returned `cursor` to get only newer events next time. Filters: `event` (repeatable), `pageviews=0`, `autocapture=0`, `properties=` (event properties), `person`, `search`. `GET /api/projects//live` is the same stream as Server-Sent Events (what the Live view uses). ## SQL (MCP `sql` / `sql_schema` / `save_query`, CLI `rochart sql`) Read-only ClickHouse SQL, already scoped to this project; timestamps are in the project's timezone. Tables: - events(uuid, event, distinct_id, person_id, session_id, timestamp, properties, current_url, pathname, host, referrer, referrer_domain, utm_source, utm_medium, utm_campaign, browser, os, device_type, country, lib, group_0..group_4). person_id is already resolved through identity merges, so uniq(person_id) counts real people. - persons(id, properties, is_identified, created_at) - sessions(session_id, person_id, distinct_id, started_at, ended_at, duration_seconds, has_ended, event_count, platform, country, locale, join_source, campaign, premium, server_type, server_id, place_version, robux, purchases) — one row per Roblox session ($session_start … $session_end) - servers(server_id, place_id, place_version, server_type, started_at, last_heartbeat_at, shut_down, crashed, peak_players, avg_fps, peak_memory_mb) — one row per game server, from heartbeats - groups(group_type_index, group_key, properties, created_at) - every saved query, by name (e.g. SELECT * FROM active_accounts). Shorthands: properties.plan → the event/person/group property as a string (also properties['my key']); person.properties.email on events → the person's property; toFloat64OrNull(properties.amount) for numbers; inCohort(person_id, 3) → is the person in cohort 3. Save a query with `save_query` (snake_case name) to define a reusable table such as paying_players; every person and agent can then SELECT FROM it. Up to 20 s and 10,000 rows per run. ## Send data - Roblox (Luau, server only): ModuleScript `RoChart` in ServerScriptService, Game Settings → Security → Allow HTTP Requests on. `local RoChart = require(game.ServerScriptService.RoChart)`; `RoChart.init({ token = "", host = "https://app.rochart.co" })`; `RoChart.track(player, "event", props)`, `RoChart.trackServer("event", props)`, `RoChart.setUserProperties(player, props)`, `RoChart.processReceipt(handler)` (instead of MarketplaceService.ProcessReceipt), `RoChart.getFlag(player, key)`, `RoChart.isFeatureEnabled(player, key)`. - Browser SDK (websites): `` or the ESM build https://app.rochart.co/sdk/browser.js — `rochart.init('', { host: 'https://app.rochart.co' })`, `identify`, `group`, `capture`, `getFeatureFlag`. - Node/Bun SDK (bots, backends): https://app.rochart.co/sdk/node.js — `new RoChart({ token, host })`, `capture`, `identify`, `groupIdentify`, `getFeatureFlag`, `shutdown`. - HTTP: `POST https://app.rochart.co/i/batch` `{ "token", "batch": [{ "uuid", "event", "distinct_id", "timestamp", "properties", "$set"? }] }`. ## Products - Product analytics (core): Events, insights, dashboards, people, cohorts and groups. - Game analytics: Live engagement, acquisition, retention and Robux revenue for your Roblox game. - Feature flags: Rollouts and targeting. - Experiments: A/B tests with Bayesian results. - Alerts: Get told in Slack or by webhook when a number crosses a line or moves sharply. - Destinations: Forward events as they arrive to a webhook or Slack. - Feedback: Store feedback from your own UI and triage it by person, cohort or group.