AI agent for game analytics
Most analytics tools answer the questions someone set up in advance. Everything else becomes a request to whoever writes the queries, and the answer arrives after the decision was made.
Rawbbit takes a different route. Your events sit in a database you own, and the AI agent your team already uses connects to it directly. You ask in plain language, the agent writes and runs the SQL, and you get the answer while the question still matters.
Ask a question, get the answer
Everything below is a real exchange against a demo dataset: a match-3 mobile title with 3.8 million events from 25,797 players, collected over ten weeks. The data is synthetic, but the queries and the numbers are exactly what came back.
Did any of our onboarding experiments actually improve tutorial completion?
Let me check completion by experiment variant.
run_readonly_sqlSELECT JSONExtractString(event_params_json, 'experiment_variant') AS variant,
countIf(event_name = 'tutorial_started') AS started,
countIf(event_name = 'tutorial_completed') AS completed,
round(100 * completed / nullIf(started, 0), 1) AS completion_pct
FROM analytics.events
WHERE app_id = 'match3.mobile.game'
AND event_name IN ('tutorial_started', 'tutorial_completed')
GROUP BY variant
ORDER BY completion_pct DESC
| variant | started | completed | completion |
|---|---|---|---|
| softer_early_levels | 435 | 134 | 30.8% |
| control | 761 | 228 | 30.0% |
| fewer_steps | 176 | 51 | 29.0% |
| booster_assist | 476 | 134 | 28.2% |
| free_lives_offer | 131 | 36 | 27.5% |
| bonus_boosters | 162 | 41 | 25.3% |
No. Control sits at 30.0% and no variant beats it by more than a point, while two land below it. Across all variants 2,193 players abandoned the tutorial against 944 who finished it.
The tutorial is not the lever here. Every version loses roughly the same seven players in ten, which points at something before or around it rather than at the steps themselves.
What the agent is actually connected to
The Rawbbit MCP server gives the agent a read-only connection to your ClickHouse events table. Not a chat wrapper around a dashboard, and not a fixed set of reports: the agent can write and run queries against your data, and the results come straight from the database.
That is the whole point. There is no list of supported questions. If something can be expressed in SQL against your events, you can ask it, and you can keep asking follow-ups until you have the answer you came for. Ask about a cohort nobody defined, a metric nobody set up, a week nobody looked at.
A few shortcuts ship with the server for things every studio asks constantly, like daily actives and funnels, so the agent does not have to rebuild them each time. They save a step. They are not the boundary.
Where players actually leave
If onboarding is not the lever, the next question is whether players return at all.
How many players come back for a second session?
Counting distinct players by session index.
run_readonly_sqlSELECT JSONExtractInt(event_params_json, 'session_index') AS session_no,
uniqExact(user_pseudo_id) AS players
FROM analytics.events
WHERE app_id = 'match3.mobile.game'
AND event_name = 'session_started'
AND session_no BETWEEN 1 AND 6
GROUP BY session_no
ORDER BY session_no
| session | players |
|---|---|
| 1 | 21,901 |
| 2 | 8,868 |
| 3 | 4,799 |
| 4 | 2,862 |
| 5 | 1,665 |
| 6 | 943 |
Sixty percent of players never open the game a second time. After that the curve is ordinary: roughly half carry over between each session.
The first gap is where the game is won or lost, and it is far larger than every later one combined.
Connecting the agent you already use
The server speaks MCP over HTTP, so any MCP client can use it: Codex, OpenCode, OpenClaw, Claude, or whatever your team standardises on next year. You point the client at your endpoint with a bearer token, and that is the whole setup.
The agent runs under your own account with your own provider. Rawbbit is not reselling model access and is not sitting between your team and your data. If you already pay for an agent, you already have this.
Ruling things out
The first-session gap makes level difficulty the next place to look.
Which levels are hardest?
Comparing starts to completions per level.
run_readonly_sqlSELECT JSONExtractInt(event_params_json, 'level_id') AS level,
countIf(event_name = 'level_started') AS starts,
countIf(event_name = 'level_completed') AS completes,
round(100 * completes / nullIf(starts, 0), 1) AS completion_pct
FROM analytics.events
WHERE app_id = 'match3.mobile.game'
AND event_name IN ('level_started', 'level_completed')
GROUP BY level
HAVING starts > 300
ORDER BY completion_pct ASC
LIMIT 6
| level | starts | completes | completion |
|---|---|---|---|
| 3 | 1,596 | 1,324 | 83.0% |
| 6 | 3,265 | 2,709 | 83.0% |
| 1 | 433 | 360 | 83.1% |
| 2 | 1,016 | 845 | 83.2% |
| 5 | 2,937 | 2,446 | 83.3% |
| 8 | 3,880 | 3,254 | 83.9% |
Nothing stands out. Every level sits between 83 and 86 percent, including the very first one.
That is a useful negative result. Players are not leaving because a level blocked them, so difficulty tuning is not where the retention problem lives.
With no unusual level spike, failure reasons can narrow the mechanic involved.
Why do players fail levels?
Grouping failures by reason.
run_readonly_sqlSELECT JSONExtractString(event_params_json, 'fail_reason') AS reason,
count() AS fails,
uniqExact(user_pseudo_id) AS players
FROM analytics.events
WHERE app_id = 'match3.mobile.game'
AND event_name = 'level_failed'
GROUP BY reason
ORDER BY fails DESC
| reason | fails | players |
|---|---|---|
| out_of_moves | 7,530 | 5,547 |
One reason accounts for effectively all of it. Nobody is running out of time or hitting a blocker they cannot clear: they simply run out of moves.
That narrows a vague question about difficulty to a single number in the level config.
What it can and cannot do
The surface is read-only. The agent can query the events table and read container logs, and it cannot write, drop, or alter anything.
It also has no path into the ingestion side of the pipeline. In a production deployment ingestion and analytics run on separate machines, with the raw Parquet layer as the boundary between them, so an agent working on the analytics side cannot reach the collector or the stream.
Code base access is separate and optional. Give the agent read access to your game's code, scoped to a system or a level pack rather than the whole project, and it can connect an event to the mechanic that produced it. Without it, the agent works on analytics data alone.
Who your players actually are
The next view is how those behaviours differ across player cohorts.
Break players into cohorts and show me what each one does.
Using the journey cohort already on the events.
run_readonly_sqlSELECT JSONExtractString(event_params_json, 'journey_cohort') AS cohort,
uniqExact(user_pseudo_id) AS players,
countIf(event_name = 'iap_completed') AS purchases,
countIf(event_name = 'ad_completed') AS ads_watched,
round(countIf(event_name = 'level_completed') / uniqExact(user_pseudo_id), 1) AS levels_per_player
FROM analytics.events
WHERE app_id = 'match3.mobile.game'
AND cohort != ''
GROUP BY cohort
ORDER BY players DESC
| cohort | players | purchases | ads | levels per player |
|---|---|---|---|---|
| casual_returner | 4,953 | 0 | 0 | 1.4 |
| ad_monetized | 3,751 | 0 | 4,688 | 2.9 |
| day0_casual | 3,430 | 0 | 0 | 0.8 |
| one_event_bounce | 3,390 | 0 | 0 | 0.0 |
| stuck_churner | 3,018 | 0 | 698 | 1.3 |
| engaged_progressor | 2,848 | 0 | 1,366 | 5.5 |
| onboarding_dropoff | 2,804 | 0 | 0 | 0.0 |
| payer | 1,138 | 1,579 | 0 | 4.9 |
Payers are 1,138 players out of 25,797, and they complete about five levels each. The engaged group that never pays completes more, 5.5, and watches ads instead.
Two cohorts, 6,194 players between them, produce no level completions at all. They arrive and leave without playing.
When the answer is not in the analytics data
Analytics can locate a break precisely, but not every cause lives in the event table.
Ad revenue dropped on the 25th. What happened?
Comparing ad starts, completions and failures across that week.
run_readonly_sqlSELECT event_date,
countIf(event_name = 'ad_started') AS started,
countIf(event_name = 'ad_completed') AS completed,
countIf(event_name = 'ad_failed') AS failed,
round(100 * failed / nullIf(started, 0), 1) AS fail_pct
FROM analytics.events
WHERE app_id = 'match3.mobile.game'
AND event_name IN ('ad_started', 'ad_completed', 'ad_failed')
AND event_date BETWEEN '2026-08-22' AND '2026-08-28'
GROUP BY event_date
ORDER BY event_date
| date | started | completed | failed | fail rate |
|---|---|---|---|---|
| 2026-08-22 | 88 | 88 | 0 | 0.0% |
| 2026-08-23 | 107 | 107 | 0 | 0.0% |
| 2026-08-24 | 75 | 75 | 0 | 0.0% |
| 2026-08-25 | 64 | 7 | 57 | 89.1% |
| 2026-08-26 | 78 | 78 | 0 | 0.0% |
| 2026-08-27 | 87 | 87 | 0 | 0.0% |
| 2026-08-28 | 83 | 83 | 0 | 0.0% |
On the 25th, 89 percent of rewarded ad plays failed. Every other day that week is at zero. Players still started ads at a normal rate, so this is not a demand problem: the ads were being requested and not delivered.
Revenue for that day was not lost to player behaviour. It was lost to something in the delivery path, on one day, and it recovered by itself.
Sometimes the numbers show that something broke without showing what. Container logs are reachable over MCP too, so the agent that found the drop can also look at what the services were doing when it happened.
That is a property of owning the stack rather than a feature bolted onto it. The logs are yours, the database is yours, and the agent has read access to both, so a question that starts in analytics does not have to stop at the edge of the analytics data.
Ready to wire this up? The setup takes a config file and a token.
Want this on your own game data?