Skip to content

feat(core): gaming, sportsbook and bonus read tools for the agent kernel - #406

Open
mp-blurify wants to merge 6 commits into
feat/agent-compliance-read-toolsfrom
feat/agent-gaming-sportsbook-bonus-read-tools
Open

mp-blurify wants to merge 6 commits into
feat/agent-compliance-read-toolsfrom
feat/agent-gaming-sportsbook-bonus-read-tools

Conversation

@mp-blurify

Copy link
Copy Markdown
Collaborator

Summary

  • Add gaming.top_games (gaming, report:view): the games ranked per currency by their completed rounds over a range - volume, revenue, rounds and distinct players - as the back office game performance report counts them, with each currency's totals over every game played. sortBy and sortDir pick the figure and the order, so the biggest losers are one call away.
  • Add sportsbook.turnover_exposure (betting, sportsbook:view): operator-wide stakes, payouts and GGR per currency over a range, by the per-player sportsbook summary's rules, plus the open bets now - their count, stakes and potential payout - and the largest open bets by player id.
  • Add bonus.grants_summary (bonus, bonus:view): per currency, the grants funded in a range, how grants ended in it (converted with the bonus paid out as cash, expired, forfeited with its reason, cancelled), split by source, and what the live grants hold now - bonus balance, wagering still required and what expires within 7 days.
  • Move the whole-UTC-day range the analytics tools take into the shared reporting contract, and share the per-player sportsbook summary's SQL with the new tool, so both count bets the same way.
  • Add partial indexes on sportsbook_bet.settled_at (settled bets) and on promo_grant.activated_at and closed_at, which the operator-wide reads range over.
  • All three covered end to end through the MCP kernel, and recorded in ADR-0042 and the gaming and sportsbook module docs.

Why

An operator's assistant answers questions about the operation from the read tools an admin holds. "Which games drive the volume this month", "what does the sportsbook stand to pay out on open bets" and "how much bonus did we hand out, and how much of it is still a liability" had no tool: the casino, sportsbook and bonus modules served nothing to the kernel.

Each tool reports per currency and never sums across currencies, bounds its range (92 days for games and sportsbook, 366 for bonuses) and caps every list (20 currencies; 50 games, 20 open bets). Only the sportsbook tool names a player, by player id under playerId; none returns a bet's selections, events or markets, a forfeit note or an admin.

Alternatives considered

  • Wrapping ADMIN_GAME_REPORTING.listGamePerformance: without a currency it sums amounts across currencies unconverted, and it lists every game in the catalogue. The tool ranks per currency in one query over the same rounds, so its figures match the report's.
  • Building bonus.grants_summary from the bonus ledger alone: a ledger range read has no index by entry type, and a forfeit entry does not say whether it closed a grant. The tool reads the grants funded and ended in the range by their own timestamps, and takes from the ledger only the closing entry of each ended grant, by grant id.
  • Building the betting services for the tool: they need the wallet's and the provider's ports, and the kernel builds every tool's handler when it starts. The tool reads through its own service over DRIZZLE.

Risks

  • Three new indexes. sportsbook_bet_settled_at_idx and the two promo_grant ones are built without CONCURRENTLY, as every generated migration is, so a large table locks writes while they build.
  • sportsbook.turnover_exposure's stake side still reads every bet accepted, or reserved, in the range: on a busy book a 92-day range is a large read, served by the existing accepted_at and status, created_at indexes.
  • bonus.grants_summary counts a grant's conversion by its closing convert entry; a conversion cap's surplus is a separate forfeit entry and is not in converted or forfeited.

Mateusz Paś and others added 6 commits November 6, 2026 11:53
…ools

The analytics read tools resolved and bounded their whole UTC days with
helpers private to analytics. The reporting contract now holds them -
resolveDayRange, checkDayRange and dayRangeInstants - so every module's
report tool takes the same dateFrom and dateTo, defaults the same way
and covers the same instants.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Mateusz Paś <mateusz.pas@studiosoftware.com>
gaming.top_games, under report:view, ranks the games by their completed
rounds over 1 to 92 whole UTC days, separately per currency: volume,
revenue, rounds played and distinct players, as the back office game
performance report counts them, with each currency's totals over every
game played. sortBy and sortDir pick the figure and the order, limit
takes 1 to 50 games, gameType and currency narrow it, and at most 20
currencies are listed. It names no player.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Mateusz Paś <mateusz.pas@studiosoftware.com>
sportsbook.turnover_exposure, under sportsbook:view, totals every
player's bets per currency over 1 to 92 whole UTC days by the player
summary's rules, now one shared query: stakes of the bets accepted or
still reserved in range, payouts of those settled in it, and GGR. It
adds the open bets now - their count, stakes and potential payout - and
the 0 to 20 largest open bets by potential payout, each by player id
with its stake, payout, number of legs and when its last leg starts,
never its selections.

The tool builds its reader from DRIZZLE alone, since the kernel builds
every handler when it starts. A partial index on settled_at serves the
settlement side, which no longer reads one player's bets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Mateusz Paś <mateusz.pas@studiosoftware.com>
bonus.grants_summary, under bonus:view, reports per currency over 1 to
366 whole UTC days: the grants funded in range and the bonus they
credited; the grants that ended in it - converted with the bonus paid
out as cash, expired and forfeited with the balance they took away and
the forfeit reasons, cancelled - split by source; and what the live
grants hold now: their bonus balance, the wagering still required and
those expiring within 7 days. It names no player and returns no note.

Partial indexes on activated_at and closed_at serve the range reads;
the closing ledger entries are read by grant id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Mateusz Paś <mateusz.pas@studiosoftware.com>
…to end through the mcp kernel

The kernel suite expects the three tools under their owners and calls
each against rows seeded in a currency of its own: exactly the
allow-listed keys, the game ranked with its figures, the open bet by
player id without its event, the grant and the liability it holds, no
user id in what is served, invalid_input for a range out of bounds,
and forbidden - audited once - for an admin whose role lacks the
tool's grant. The transport suite lists them under their model names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Mateusz Paś <mateusz.pas@studiosoftware.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Mateusz Paś <mateusz.pas@studiosoftware.com>
* game performance report reads (ADMIN_GAME_REPORTING): stakes net of reversed stakes, wins, and
* who played.
*/
export class TopGamesService {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a separate service for the agent? Can't we reuse listGamePerformance? It already has these figures and the sort; a per-currency method on ADMIN_GAME_REPORTING (like rankAllGamesByRounds) would also keep the tool in line when an operator rebinds the port.

'stands now and as the per-player sportsbook summary counts them: bets and stake (cash and ' +
'promotion-funded) of the bets accepted, or still reserved, in range; payout (cash and ' +
'promotion-funded, a void returning its stake included) of the bets settled in range; and ' +
'GGR, cash stake minus payout. The open figures are now, whatever the range: openBets, ' +

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ggr comes out different from financial.summary's sportsbook GGR for the same range, for example after a reopen. Drop it and point to financial.summary, or say in the description that it's a different figure.

.select({
...totals.columns,
openBets: sql<number>`count(*) filter (where ${openBet})`.mapWith(Number),
openPotentialPayout: sql<string>`coalesce(sum(${bet.potentialPayout}) filter (where ${openBet}), 0)::numeric(38, 18)`,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is sumOf from bet-query.service.ts:103 inlined, and grants-summary and top-games each declare their own too. Can we export one money-sum helper and use it in all four places?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants