Skip to content

feat(gamification): pay rank periodic bonuses on the house edge - #372

Open
okapitula wants to merge 1 commit into
devfrom
feat/rank-bonus-house-edge
Open

okapitula wants to merge 1 commit into
devfrom
feat/rank-bonus-house-edge

Conversation

@okapitula

@okapitula okapitula commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • Rank daily, weekly and monthly bonuses can pay a tier percent of the house edge: min(own-money stake × (1 − rtp) × percent / 100, tier cap), behind promo.rankPeriodic.basis: 'margin'. The default flat is unchanged. The edge is priced by the shared houseEdge from feat(gamification): rakeback on the house edge and claimable rakeback #365, so a missing, zero or malformed rtp adds nothing.
  • Tiers gain dailyBonusPercent, weeklyBonusPercent and monthlyBonusPercent, set through the rank ladder admin.
  • A ladder save is refused when a tier's rakeback and bonus percents total more than 100, in the input and by a database check.
  • On the margin rakeback basis a streak boost is trimmed so rakeback, the boost and the margin bonuses pay at most the bet's edge.
  • A bet rollback removes its round's proportional share of the recorded margin.
  • The ladder, the player's rank, promo.rank.changed and its notification params carry the percents and periodicBasis.

Why

A flat periodic bonus costs the operator the same whatever the player gave up to the house, so it is hard to price on low-edge games. Rakeback already pays on the edge (ADR-0048); the periodic bonus now can too. ADR-0049 records the decision.

Diagram

sequenceDiagram
  participant W as Wallet
  participant R as RankService
  participant P as Period counter
  participant RW as Round wager
  participant J as Payout job
  W->>R: recordWager(realAmount, rtp)
  R->>P: wagered += stake, margin += stake × (1 − rtp)
  R->>RW: stake, margin for the round
  W->>R: reverseWager(round, realAmount)
  R->>RW: lock, take proportional share
  R->>P: margin -= share (clamped at 0)
  J->>P: read margin for the closed period
  J->>J: min(margin × percent / 100, cap)
Loading

Alternatives considered

  • Pricing a rollback from the rolled-back bet's rtp, carried through the wallet: dropped. The round row already holds the converted margin, which also survives rate changes and several RTPs in one round.
  • A separate margin counter table: the existing period counter takes a margin column in the same upsert instead.

Risks

  • One migration, gamification 0022_rank_periodic_margin: new nullable percent columns, a margin column defaulting to 0, the promo_rank_round_wager table with a created_at index, and CHECKs. An existing tier whose percents already total more than 100 cannot exist yet, since the percents are new.
  • promo_rank_round_wager gets a row for every staked round under either basis. The daily prune deletes rows created more than 35 days ago, whether or not any periodic payout has run, and a rollback of a pruned round takes no margin back.
  • Under margin a provider that reports no rtp earns its players no periodic bonus.
  • The 100% cap keeps the payout within the edge only while rakeback runs on its margin basis. On stake rakeback is a share of turnover and can exceed the edge.
  • Stacked on feat(gamification): rakeback on the house edge and claimable rakeback #365; retarget to dev after it merges.

@okapitula
okapitula force-pushed the feat/rank-bonus-house-edge branch from 933389e to 4534819 Compare October 30, 2026 08:26
@okapitula
okapitula force-pushed the feat/rank-bonus-house-edge branch 3 times, most recently from 72fa8fa to e41aada Compare October 30, 2026 14:08
@okapitula
okapitula force-pushed the feat/rank-bonus-house-edge branch from e41aada to bfa823a Compare November 2, 2026 16:18
@okapitula
okapitula enabled stacked auto-merge November 2, 2026 16:33
@okapitula
okapitula force-pushed the feat/rank-bonus-house-edge branch 2 times, most recently from 36698b6 to 6ce8bd8 Compare November 3, 2026 12:12
stacked auto-merge was automatically disabled November 4, 2026 11:17

Pull Request is not mergeable

stacked auto-merge was automatically disabled November 4, 2026 11:24

Pull Request is not mergeable

@jakubfilinger-b jakubfilinger-b left a comment

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.

No blockers. Two points to decide before this is enabled:

  1. promo_rank_round_wager gets an upsert in every bet's transaction and a row for every staked round, also under the default flat basis and for ineligible products, and rows leave only when a round is fully reversed. That is the hottest table in the feature, and it grows with play for operators who never use margin. Could the round rows be written only under basis: 'margin' (the period counter margin can stay counted under both), or could the retention decision get a deadline?

  2. The "rakeback and the bonus percents of a tier add up to at most 100" check reads as a guarantee that the payout stays inside the house edge. The ADR says that holds only while rakeback runs on its margin basis with no streak boost, and a null cap is uncapped. With rakeback on turnover or a streak boost, the same ladder can pay more than the margin. Either say so in the error text and admin docs, or tie the check to the rakeback basis.

This sits on #365, so it lands after it.

@okapitula
okapitula force-pushed the feat/rank-bonus-house-edge branch from 9b3895a to f881dd7 Compare November 4, 2026 13:48
@okapitula

Copy link
Copy Markdown
Collaborator Author
  1. Round rows are now pruned by the daily period prune once they are 35 days older than the earliest periodic watermark, so the table holds about two months of rounds (f881dd7). Ineligible stakes stay in the row so a mixed round still reverses at its blended margin.

  2. A streak boost on the margin rakeback basis is now trimmed so rakeback, the boost and the margin bonuses pay at most the bet's whole edge (f881dd7). On the stake basis rakeback is a share of turnover, and the docs say it can exceed the edge.

@okapitula
okapitula force-pushed the feat/rank-bonus-house-edge branch from f881dd7 to 90e0531 Compare November 4, 2026 14:05
Base automatically changed from feat/rakeback-margin-claim to dev November 4, 2026 14:47
@okapitula
okapitula force-pushed the feat/rank-bonus-house-edge branch from 90e0531 to 40041ca Compare November 4, 2026 14:47

@jakubfilinger-b jakubfilinger-b left a comment

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.

Re-reviewed after f881dd7. The rakeback-plus-bonus limit is now sound: the boost is trimmed on the margin basis and the docs say the stake basis can exceed the edge. Thanks.

One gap left in the round-row retention:

pruneSettledRoundWagers (rank-payout.service.ts) returns 0 until all three kinds (daily, weekly, monthly) have a paidThrough watermark. Only markPaid sets one, and settingsFor returns null, so markPaid never runs, for a kind without rewards[kind]. The seed does not set rewards. So an operator that does not configure all three periodic rewards, or whose payout job is not running, never prunes promo_rank_round_wager, while every staked round still upserts a row in the bet's transaction, also under flat. That is the unbounded growth from my first comment, just for the common default case. Could rows be pruned by age alone for kinds with no terms (or the row only be written under basis: 'margin')?

Smaller, non-blocking:

  • The prune is one DELETE ... RETURNING id; returning only a count avoids loading every id of a busy day into memory.
  • ADR-0049 here clashes with 0049-per-process-job-worker-selection.md in #298. Whichever lands second needs a renumber.

@okapitula
okapitula force-pushed the feat/rank-bonus-house-edge branch 2 times, most recently from b922384 to 51e2533 Compare November 5, 2026 16:58
@okapitula

Copy link
Copy Markdown
Collaborator Author

Round rows are now pruned by age alone, 35 days after they are written, whatever the payout watermarks say, because a rollback only lowers the open period's counter (51e2533). The prune now uses rowCount instead of returning ids. I'll renumber ADR-0049 if #298 lands first.

promo.rankPeriodic.basis 'margin' pays each tier's daily, weekly and
monthly bonus percent of the period's own-money stake x (1 - rtp),
capped by the tier's flat amount; the default 'flat' is unchanged.

- recordWager counts the margin on the period counter under both bases
  and keeps each round's stake and converted margin in
  promo_rank_round_wager, so a rollback removes its proportional share.
- A ladder save is refused when rakeback and the three bonus percents
  of a tier total more than 100, in the input and by a database check.
- The ladder, the player's rank, promo.rank.changed and its
  notification params carry the percents and periodicBasis.
- ADR-0047 records the decision.
@okapitula
okapitula force-pushed the feat/rank-bonus-house-edge branch from 51e2533 to 1f5fe0b Compare November 6, 2026 11:32
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.

3 participants