{"ok":true,"plugins":[{"bonusId":"deposit_match_bonus","version":"2.0.0","description":"Deposit match bonus with tiered deposit ordinal support","requiresFacts":["deposit.count.lifetime"],"ui":{"kind":"bonus","bonusId":"deposit_match_bonus","name":"Deposit Match Bonus","description":"Deposit match bonus with tiered deposit ordinal support","defaults":{"minDeposit":20,"maxBonus":200,"matchRate":0.5,"requirementsMultiplier":10,"onlyFirstDeposit":true,"rewardPlugin":"wallet_cash_bonus","wageringPluginId":"simple_multiplier","tiers":null,"categoryWeights":null,"categoryTagFields":["category"],"selectionCriteria":{"$daysOfWeek":[0,1,2,3,4,5,6]},"selectionCriteriaByEvent":null},"fields":[{"key":"rewardPlugin","label":"Reward plugin","type":"text"},{"key":"tiers","label":"Deposit Tiers","type":"array","help":"Each row matches a range of deposit ordinals (e.g. 1st deposit, 2nd–5th, etc.). Add one row per range you want to reward.","itemFields":[{"key":"minOrdinal","label":"From deposit #","type":"number","step":1,"min":1},{"key":"maxOrdinal","label":"To deposit #","type":"number","step":1,"min":1},{"key":"matchRate","label":"Match rate (1=100%)","type":"number","step":0.01,"min":0,"max":10},{"key":"maxBonus","label":"Max bonus","type":"currency_map","modeKey":"maxBonusCurrencyMode","usedWhen":"when it is paid"},{"key":"minDeposit","label":"Min deposit","type":"currency_map","modeKey":"minDepositCurrencyMode","usedWhen":"when they deposit"},{"key":"requirementsMultiplier","label":"Wagering x","type":"number","step":1,"min":0}],"defaultItem":{"minOrdinal":1,"maxOrdinal":999999,"matchRate":1,"maxBonus":{"USD":"500"},"maxBonusCurrencyMode":"convert","minDeposit":{"USD":"20"},"minDepositCurrencyMode":"convert","requirementsMultiplier":5}},{"key":"wageringPluginId","label":"Wagering contribution (how stake counts toward the requirement)","type":"select","help":"This is separate from “Wagering x” in tiers above (that is the turnover multiplier). Simple = each $1 staked counts $1 toward the requirement. Category-weighted = stake × category weight. Tenant Edit → category-weighted sets defaults; optional fields below override per bonus definition (stored on each granted instance). Ensure plugins.wagering includes category-weighted when using that option.","options":[{"value":"simple_multiplier","label":"Simple — full stake counts (1:1)"},{"value":"category-weighted","label":"Category-weighted — tenant defaults + optional per-bonus overrides"}]},{"key":"categoryWeights","label":"Category weights (this bonus only)","type":"json","showWhen":{"wageringPluginId":"category-weighted"},"required":true,"help":"**Required** when category-weighted is selected. JSON object with at least one category key, merged over tenant category-weighted defaults (your keys override per category). Use **Copy from tenant default** to start from tenant weights. Use `\"*\"` or `\"$OTHERS\"` as a key for the weight applied to category tags that do not match any other named key (both are supported; if both are set, `\"*\"` wins). Configure tenant defaults under Edit Tenant → wagering plugins → category-weighted."},{"key":"categoryTagFields","label":"Wager fields used for category tags (this bonus only)","type":"json","showWhen":{"wageringPluginId":"category-weighted"},"required":true,"help":"**Required** when category-weighted is selected. JSON array of payload field names for weight lookup, e.g. `[\"category\"]` or `[\"category\",\"categories\"]`. Eligibility for funding and bonuses is only via **Selection criteria** (Mongo/sift); this list only affects **how stake is weighted** for category-weighted wagering math."},{"key":"wageringBasis","label":"Which share of a bet counts toward wagering","type":"select","options":[{"value":"all","label":"all of it"},{"value":"cash_only","label":"only the part paid with real money"},{"value":"bonus_only","label":"only the part paid with bonus money"}],"help":"Which part of each bet advances the wagering requirement. Default: all of it. Resolved per bet from the wallet deduction plan. Requires a wagering multiplier to be set."},{"key":"payoutSliceMixingMode","label":"Winnings of mixed-funded bets","type":"select","options":[{"value":"proportional","label":"Winnings follow wagering state (default)"},{"value":"isolated","label":"Winnings split by funding source"}],"help":"What happens to the winnings of a bet funded partly by cash and partly by this bonus. Default: the whole win follows the wagering state (locked while the requirement is open). \"Split by funding source\": each share keeps its own line — the cash-funded share of every win goes straight to withdrawable cash; only the bonus-funded share stays locked."},{"key":"selectionCriteria","label":"Selection criteria","type":"selection_criteria_builder","required":false,"help":"MongoDB-style query (sift) on the match document: `$and`, `$or`, `$in`, `$nin`, `$gte`, … plus temporal shorthand (`$daysOfWeek`, `$utcTime`, …). Per-channel admission: `selectionCriteriaByEvent` (e.g. `wager.placed`)."},{"key":"expirationDays","label":"{Bonus:one} expiration (days)","type":"number","step":1,"min":1,"help":"How long a granted {bonus:one} stays active: N × 24h from the grant moment (UTC duration — not end-of-day local time). Past the deadline an unfinished {bonus:one} expires and the remaining locked balance is forfeited. Leave empty for no expiry."},{"key":"priority","label":"Wager processing priority","type":"number","step":1,"min":0,"help":"Lower = processed first. Default: 20. Cash {bonus:other} are typically processed after free {round:other}."},{"key":"maxBonusRate","label":"Max bonus rate","type":"number","step":0.01,"min":0,"help":"Block new grants when user.bonus.rate (totalBonus/totalLosses) exceeds this. E.g. 0.5 = 50% bonus-to-loss ratio. Requires user.bonus.rate fact plugin. Leave empty to disable."},{"key":"bonusRatePeriod","label":"Bonus rate period","type":"select","options":[{"value":"WEEKLY","label":"Weekly (default)"},{"value":"DAILY","label":"Daily"},{"value":"MONTHLY","label":"Monthly"},{"value":"LIFETIME","label":"Lifetime"}],"help":"Time window for bonus-to-loss ratio calculation. Only used when max bonus rate is set."}],"pluginId":"deposit_match_bonus"},"requires":[{"kind":"fact","id":"deposit.count.lifetime"},{"kind":"reward","id":"wallet_cash_bonus"},{"kind":"wagering","id":"simple_multiplier"}],"optionalFacts":null},{"bonusId":"deposit_match_bonus","version":"2.0.0","description":"Deposit match bonus with tiered deposit ordinal support","requiresFacts":["deposit.count.lifetime"],"ui":{"kind":"bonus","bonusId":"deposit_match_bonus","name":"Deposit Match Bonus","description":"Deposit match bonus with tiered deposit ordinal support","defaults":{"minDeposit":20,"maxBonus":200,"matchRate":0.5,"requirementsMultiplier":10,"onlyFirstDeposit":true,"rewardPlugin":"wallet_cash_bonus","wageringPluginId":"simple_multiplier","tiers":null,"categoryWeights":null,"categoryTagFields":["category"],"selectionCriteria":{"$daysOfWeek":[0,1,2,3,4,5,6]},"selectionCriteriaByEvent":null},"fields":[{"key":"rewardPlugin","label":"Reward plugin","type":"text"},{"key":"tiers","label":"Deposit Tiers","type":"array","help":"Each row matches a range of deposit ordinals (e.g. 1st deposit, 2nd–5th, etc.). Add one row per range you want to reward.","itemFields":[{"key":"minOrdinal","label":"From deposit #","type":"number","step":1,"min":1},{"key":"maxOrdinal","label":"To deposit #","type":"number","step":1,"min":1},{"key":"matchRate","label":"Match rate (1=100%)","type":"number","step":0.01,"min":0,"max":10},{"key":"maxBonus","label":"Max bonus","type":"currency_map","modeKey":"maxBonusCurrencyMode","usedWhen":"when it is paid"},{"key":"minDeposit","label":"Min deposit","type":"currency_map","modeKey":"minDepositCurrencyMode","usedWhen":"when they deposit"},{"key":"requirementsMultiplier","label":"Wagering x","type":"number","step":1,"min":0}],"defaultItem":{"minOrdinal":1,"maxOrdinal":999999,"matchRate":1,"maxBonus":{"USD":"500"},"maxBonusCurrencyMode":"convert","minDeposit":{"USD":"20"},"minDepositCurrencyMode":"convert","requirementsMultiplier":5}},{"key":"wageringPluginId","label":"Wagering contribution (how stake counts toward the requirement)","type":"select","help":"This is separate from “Wagering x” in tiers above (that is the turnover multiplier). Simple = each $1 staked counts $1 toward the requirement. Category-weighted = stake × category weight. Tenant Edit → category-weighted sets defaults; optional fields below override per bonus definition (stored on each granted instance). Ensure plugins.wagering includes category-weighted when using that option.","options":[{"value":"simple_multiplier","label":"Simple — full stake counts (1:1)"},{"value":"category-weighted","label":"Category-weighted — tenant defaults + optional per-bonus overrides"}]},{"key":"categoryWeights","label":"Category weights (this bonus only)","type":"json","showWhen":{"wageringPluginId":"category-weighted"},"required":true,"help":"**Required** when category-weighted is selected. JSON object with at least one category key, merged over tenant category-weighted defaults (your keys override per category). Use **Copy from tenant default** to start from tenant weights. Use `\"*\"` or `\"$OTHERS\"` as a key for the weight applied to category tags that do not match any other named key (both are supported; if both are set, `\"*\"` wins). Configure tenant defaults under Edit Tenant → wagering plugins → category-weighted."},{"key":"categoryTagFields","label":"Wager fields used for category tags (this bonus only)","type":"json","showWhen":{"wageringPluginId":"category-weighted"},"required":true,"help":"**Required** when category-weighted is selected. JSON array of payload field names for weight lookup, e.g. `[\"category\"]` or `[\"category\",\"categories\"]`. Eligibility for funding and bonuses is only via **Selection criteria** (Mongo/sift); this list only affects **how stake is weighted** for category-weighted wagering math."},{"key":"wageringBasis","label":"Which share of a bet counts toward wagering","type":"select","options":[{"value":"all","label":"all of it"},{"value":"cash_only","label":"only the part paid with real money"},{"value":"bonus_only","label":"only the part paid with bonus money"}],"help":"Which part of each bet advances the wagering requirement. Default: all of it. Resolved per bet from the wallet deduction plan. Requires a wagering multiplier to be set."},{"key":"payoutSliceMixingMode","label":"Winnings of mixed-funded bets","type":"select","options":[{"value":"proportional","label":"Winnings follow wagering state (default)"},{"value":"isolated","label":"Winnings split by funding source"}],"help":"What happens to the winnings of a bet funded partly by cash and partly by this bonus. Default: the whole win follows the wagering state (locked while the requirement is open). \"Split by funding source\": each share keeps its own line — the cash-funded share of every win goes straight to withdrawable cash; only the bonus-funded share stays locked."},{"key":"selectionCriteria","label":"Selection criteria","type":"selection_criteria_builder","required":false,"help":"MongoDB-style query (sift) on the match document: `$and`, `$or`, `$in`, `$nin`, `$gte`, … plus temporal shorthand (`$daysOfWeek`, `$utcTime`, …). Per-channel admission: `selectionCriteriaByEvent` (e.g. `wager.placed`)."},{"key":"expirationDays","label":"{Bonus:one} expiration (days)","type":"number","step":1,"min":1,"help":"How long a granted {bonus:one} stays active: N × 24h from the grant moment (UTC duration — not end-of-day local time). Past the deadline an unfinished {bonus:one} expires and the remaining locked balance is forfeited. Leave empty for no expiry."},{"key":"priority","label":"Wager processing priority","type":"number","step":1,"min":0,"help":"Lower = processed first. Default: 20. Cash {bonus:other} are typically processed after free {round:other}."},{"key":"maxBonusRate","label":"Max bonus rate","type":"number","step":0.01,"min":0,"help":"Block new grants when user.bonus.rate (totalBonus/totalLosses) exceeds this. E.g. 0.5 = 50% bonus-to-loss ratio. Requires user.bonus.rate fact plugin. Leave empty to disable."},{"key":"bonusRatePeriod","label":"Bonus rate period","type":"select","options":[{"value":"WEEKLY","label":"Weekly (default)"},{"value":"DAILY","label":"Daily"},{"value":"MONTHLY","label":"Monthly"},{"value":"LIFETIME","label":"Lifetime"}],"help":"Time window for bonus-to-loss ratio calculation. Only used when max bonus rate is set."}],"pluginId":"deposit_match_bonus"},"requires":[{"kind":"fact","id":"deposit.count.lifetime"},{"kind":"reward","id":"wallet_cash_bonus"},{"kind":"wagering","id":"simple_multiplier"}],"optionalFacts":null},{"bonusId":"manual_bonus_grant","version":"2.0.0","description":"Deposit-match style bonus evaluated on manual.bonus.granted (synthetic stake from event amount)","requiresFacts":["deposit.count.lifetime"],"ui":{"kind":"bonus","bonusId":"manual_bonus_grant","name":"Manual bonus grant (match-style)","description":"Deposit-match style bonus evaluated on manual.bonus.granted (synthetic stake from event amount)","defaults":{"minDeposit":20,"maxBonus":200,"matchRate":0.5,"requirementsMultiplier":10,"onlyFirstDeposit":true,"rewardPlugin":"wallet_cash_bonus","wageringPluginId":"simple_multiplier","tiers":null,"categoryWeights":null,"categoryTagFields":["category"],"selectionCriteria":{"$daysOfWeek":[0,1,2,3,4,5,6]},"selectionCriteriaByEvent":null},"fields":[{"key":"rewardPlugin","label":"Reward plugin","type":"text"},{"key":"tiers","label":"Deposit Tiers","type":"array","help":"Each row matches a range of deposit ordinals (e.g. 1st deposit, 2nd–5th, etc.). Add one row per range you want to reward.","itemFields":[{"key":"minOrdinal","label":"From deposit #","type":"number","step":1,"min":1},{"key":"maxOrdinal","label":"To deposit #","type":"number","step":1,"min":1},{"key":"matchRate","label":"Match rate (1=100%)","type":"number","step":0.01,"min":0,"max":10},{"key":"maxBonus","label":"Max bonus","type":"currency_map","modeKey":"maxBonusCurrencyMode","usedWhen":"when it is paid"},{"key":"minDeposit","label":"Min deposit","type":"currency_map","modeKey":"minDepositCurrencyMode","usedWhen":"when they deposit"},{"key":"requirementsMultiplier","label":"Wagering x","type":"number","step":1,"min":0}],"defaultItem":{"minOrdinal":1,"maxOrdinal":999999,"matchRate":1,"maxBonus":{"USD":"500"},"maxBonusCurrencyMode":"convert","minDeposit":{"USD":"20"},"minDepositCurrencyMode":"convert","requirementsMultiplier":5}},{"key":"wageringPluginId","label":"Wagering contribution (how stake counts toward the requirement)","type":"select","help":"This is separate from “Wagering x” in tiers above (that is the turnover multiplier). Simple = each $1 staked counts $1 toward the requirement. Category-weighted = stake × category weight. Tenant Edit → category-weighted sets defaults; optional fields below override per bonus definition (stored on each granted instance). Ensure plugins.wagering includes category-weighted when using that option.","options":[{"value":"simple_multiplier","label":"Simple — full stake counts (1:1)"},{"value":"category-weighted","label":"Category-weighted — tenant defaults + optional per-bonus overrides"}]},{"key":"categoryWeights","label":"Category weights (this bonus only)","type":"json","showWhen":{"wageringPluginId":"category-weighted"},"required":true,"help":"**Required** when category-weighted is selected. JSON object with at least one category key, merged over tenant category-weighted defaults (your keys override per category). Use **Copy from tenant default** to start from tenant weights. Use `\"*\"` or `\"$OTHERS\"` as a key for the weight applied to category tags that do not match any other named key (both are supported; if both are set, `\"*\"` wins). Configure tenant defaults under Edit Tenant → wagering plugins → category-weighted."},{"key":"categoryTagFields","label":"Wager fields used for category tags (this bonus only)","type":"json","showWhen":{"wageringPluginId":"category-weighted"},"required":true,"help":"**Required** when category-weighted is selected. JSON array of payload field names for weight lookup, e.g. `[\"category\"]` or `[\"category\",\"categories\"]`. Eligibility for funding and bonuses is only via **Selection criteria** (Mongo/sift); this list only affects **how stake is weighted** for category-weighted wagering math."},{"key":"wageringBasis","label":"Which share of a bet counts toward wagering","type":"select","options":[{"value":"all","label":"all of it"},{"value":"cash_only","label":"only the part paid with real money"},{"value":"bonus_only","label":"only the part paid with bonus money"}],"help":"Which part of each bet advances the wagering requirement. Default: all of it. Resolved per bet from the wallet deduction plan. Requires a wagering multiplier to be set."},{"key":"payoutSliceMixingMode","label":"Winnings of mixed-funded bets","type":"select","options":[{"value":"proportional","label":"Winnings follow wagering state (default)"},{"value":"isolated","label":"Winnings split by funding source"}],"help":"What happens to the winnings of a bet funded partly by cash and partly by this bonus. Default: the whole win follows the wagering state (locked while the requirement is open). \"Split by funding source\": each share keeps its own line — the cash-funded share of every win goes straight to withdrawable cash; only the bonus-funded share stays locked."},{"key":"selectionCriteria","label":"Selection criteria","type":"selection_criteria_builder","required":false,"help":"MongoDB-style query (sift) on the match document: `$and`, `$or`, `$in`, `$nin`, `$gte`, … plus temporal shorthand (`$daysOfWeek`, `$utcTime`, …). Per-channel admission: `selectionCriteriaByEvent` (e.g. `wager.placed`)."},{"key":"expirationDays","label":"{Bonus:one} expiration (days)","type":"number","step":1,"min":1,"help":"How long a granted {bonus:one} stays active: N × 24h from the grant moment (UTC duration — not end-of-day local time). Past the deadline an unfinished {bonus:one} expires and the remaining locked balance is forfeited. Leave empty for no expiry."},{"key":"priority","label":"Wager processing priority","type":"number","step":1,"min":0,"help":"Lower = processed first. Default: 20. Cash {bonus:other} are typically processed after free {round:other}."},{"key":"maxBonusRate","label":"Max bonus rate","type":"number","step":0.01,"min":0,"help":"Block new grants when user.bonus.rate (totalBonus/totalLosses) exceeds this. E.g. 0.5 = 50% bonus-to-loss ratio. Requires user.bonus.rate fact plugin. Leave empty to disable."},{"key":"bonusRatePeriod","label":"Bonus rate period","type":"select","options":[{"value":"WEEKLY","label":"Weekly (default)"},{"value":"DAILY","label":"Daily"},{"value":"MONTHLY","label":"Monthly"},{"value":"LIFETIME","label":"Lifetime"}],"help":"Time window for bonus-to-loss ratio calculation. Only used when max bonus rate is set."}],"pluginId":"manual_bonus_grant"},"requires":[{"kind":"fact","id":"deposit.count.lifetime"},{"kind":"reward","id":"wallet_cash_bonus"},{"kind":"wagering","id":"simple_multiplier"}],"optionalFacts":null},{"bonusId":"free_rounds_bonus","version":"3.0.0","description":"Wager-based free rounds: qualifying wagers are free, tracked by round count","requiresFacts":["deposit.count.lifetime"],"ui":{"kind":"bonus","pluginId":"free_rounds_bonus","bonusId":"free_rounds_bonus","name":"Free Rounds (wager-based)","description":"Wager-based free rounds: qualifying wagers are free, tracked by round count","defaults":{"roundsTotal":20,"roundValue":0.5,"roundValueCurrencyMode":null,"consumeMultipleRounds":false,"fundRemainderFromBalance":false,"wageringMultiplier":50,"overMaxBehavior":"reject","minDeposit":20,"onlyFirstDeposit":true,"requireExplicitFlag":false,"categoryTagFields":["category"],"selectionCriteria":{"$daysOfWeek":[0,1,2,3,4,5,6]},"selectionCriteriaByEvent":null,"winningsTarget":"BONUS_BUCKET","phase2WageringPluginId":null,"categoryWeights":null},"fields":[{"key":"roundsTotal","label":"Number of free {round:other}","type":"number","step":1,"min":1,"ui":{"tier":"basic"}},{"key":"roundValue","label":"Value of one free {round:one}","type":"currency_map","required":true,"modeKey":"roundValueCurrencyMode","usedWhen":"when they play","missingOutcome":"no free {round:other}","help":"The wager amount that equals one free {round:one}. Exact decimals — 0.00000001 is valid.","ui":{"tier":"basic"}},{"key":"_winningsSettlementHeading","type":"heading","label":"Winnings settlement (wallet free {round:other})"},{"key":"winningsTarget","label":"Free {round:one} winnings go to","type":"segmented","options":[{"value":"BONUS_BUCKET","label":"Bonus wallet"},{"value":"CASH","label":"Cash wallet"}],"help":"Click a button: Bonus = grant / bonus path (then wagering applies). Cash = wallet cash account. Saved on the tenant and on each new free-{round:one} package at grant time.","ui":{"tier":"basic"}},{"key":"wageringMultiplier","label":"Wagering multiplier on winnings","type":"number","step":1,"min":0,"showWhen":{"winningsTarget":"BONUS_BUCKET"},"help":"After all {round:other} are used, total winnings must be wagered this many times before withdrawal. Only applies when “Free {round:one} winnings go to” is Bonus; if you choose Cash, winnings are posted to cash immediately.","ui":{"tier":"basic","visibility":{"winningsTarget":"BONUS_BUCKET"}}},{"key":"phase2WageringPluginId","label":"Phase 2 wagering contribution (how stake counts toward winnings requirement)","type":"select","showWhen":{"winningsTarget":"BONUS_BUCKET"},"help":"After all free {round:other} are used, winnings must be wagered before withdrawal. Simple = each $1 staked counts $1. Category-weighted = stake × category weight. Ensure plugins.wagering includes the selected plugin. Hidden when winnings go to Cash (no wagering on winnings).","options":[{"value":"simple_multiplier","label":"Simple — full stake counts (1:1)"},{"value":"category-weighted","label":"Category-weighted — tenant defaults + optional per-bonus overrides"}]},{"key":"categoryWeights","label":"Category weights (this bonus only)","type":"json","showWhen":{"winningsTarget":"BONUS_BUCKET","phase2WageringPluginId":"category-weighted"},"required":true,"help":"**Required** when category-weighted is selected. JSON object with at least one category key, merged over tenant category-weighted defaults (your keys override per category). Use **Copy from tenant default** to start from tenant weights. Use `\"*\"` or `\"$OTHERS\"` as a key for the weight applied to category tags that do not match any other named key (both are supported; if both are set, `\"*\"` wins). Configure tenant defaults under Edit Tenant → wagering plugins → category-weighted."},{"key":"categoryTagFields","label":"Wager fields used for category tags (this bonus only)","type":"json","showWhen":{"winningsTarget":"BONUS_BUCKET","phase2WageringPluginId":"category-weighted"},"required":true,"help":"**Required** when category-weighted is selected. JSON array of payload field names for weight lookup, e.g. `[\"category\"]` or `[\"category\",\"categories\"]`. Eligibility for funding and bonuses is only via **Selection criteria** (Mongo/sift); this list only affects **how stake is weighted** for category-weighted wagering math."},{"key":"consumeMultipleRounds","label":"Allow a single wager to consume multiple {round:other}","type":"boolean","help":"When enabled, a $5 wager with roundValue $0.50 consumes 10 {round:other}"},{"key":"fundRemainderFromBalance","label":"Fund remainder from player balance","type":"boolean","showWhen":{"consumeMultipleRounds":true},"help":"When enabled with multi-{round:one}, non-exact multiples charge the remainder from the player balance. Otherwise, non-exact multiples are rejected."},{"key":"overMaxBehavior","label":"Over-roundValue wager behavior (single-{round:one} mode)","type":"select","showWhen":{"consumeMultipleRounds":false},"options":[{"value":"reject","label":"Reject (error)"},{"value":"ignore","label":"Ignore (treat as normal wager)"}],"help":"What to do when a wager without freeRound flag exceeds roundValue (only applies when consumeMultipleRounds is off)"},{"key":"minDeposit","label":"Minimum deposit","type":"currency_map","modeKey":"minDepositCurrencyMode","usedWhen":"when they deposit"},{"key":"onlyFirstDeposit","label":"First deposit only","type":"boolean"},{"key":"requireExplicitFlag","label":"Require freeRound flag on wager","type":"boolean","help":"If enabled, only wagers with freeRound:true or freeRoundsCount are counted"},{"key":"selectionCriteria","label":"Selection criteria","type":"selection_criteria_builder","required":false,"help":"MongoDB-style query (sift) on the match document: `$and`, `$or`, `$in`, `$nin`, `$gte`, … plus temporal shorthand (`$daysOfWeek`, `$utcTime`, …). Per-channel admission: `selectionCriteriaByEvent` (e.g. `wager.placed`)."},{"key":"priority","label":"Wager processing priority","type":"number","step":1,"min":0,"help":"Lower = processed first. Default: 10. Free {round:other} are typically processed before cash {bonus:other}."},{"key":"maxBonusRate","label":"Max bonus rate","type":"number","step":0.01,"min":0,"help":"Block new grants when user.bonus.rate (totalBonus/totalLosses) exceeds this. E.g. 0.5 = 50% bonus-to-loss ratio. Requires user.bonus.rate fact plugin. Leave empty to disable."},{"key":"bonusRatePeriod","label":"Bonus rate period","type":"select","options":[{"value":"WEEKLY","label":"Weekly (default)"},{"value":"DAILY","label":"Daily"},{"value":"MONTHLY","label":"Monthly"},{"value":"LIFETIME","label":"Lifetime"}],"help":"Time window for bonus-to-loss ratio calculation. Only used when max bonus rate is set."},{"key":"_rewardHeading","type":"heading","label":"Reward: Free {Round:other} Package"},{"key":"reward_expirationDays","label":"Package expiration (days)","type":"number","step":1,"min":1,"help":"How long the free-{round:other} package remains active before expiring. Default: 7."}],"rewardConfig":{"pluginId":"wallet_free_rounds","defaults":{"expirationDays":7},"fieldMapping":[{"configKey":"expirationDays","formKey":"reward_expirationDays","type":"number"}]}},"requires":[{"kind":"wagering","id":"round_counter"},{"kind":"wagering","id":"simple_multiplier"},{"kind":"completion","id":"free_rounds_phase_transition"},{"kind":"reward","id":"wallet_free_rounds"}],"optionalFacts":null},{"bonusId":"welcome_free_rounds_bonus","version":"1.0.0","description":"Welcome free rounds (first deposit) for a specific activity","requiresFacts":["deposit.count.lifetime"],"ui":{"kind":"bonus","bonusId":"welcome_free_rounds_bonus","name":"Welcome Free Rounds Bonus","description":"Welcome free rounds (first deposit) for a specific activity","defaults":{"rewardPlugin":"wallet_free_rounds","minDeposit":20,"spinCount":20,"spinValue":0.5,"conversionPolicy":"ALL_WINNINGS","conversionCost":0,"conversionPercent":100,"winningsWageringMultiplier":null,"winningsBonusBucket":"free_round_winnings","selectionCriteria":{"$daysOfWeek":[0,1,2,3,4,5,6]},"selectionCriteriaByEvent":null},"fields":[{"key":"minDeposit","label":"Minimum deposit","type":"currency_map","modeKey":"minDepositCurrencyMode","usedWhen":"when they deposit"},{"key":"spinCount","label":"Spin count","type":"number","step":1,"min":0},{"key":"spinValue","label":"Spin value","type":"currency_map","modeKey":"spinValueCurrencyMode","usedWhen":"when they play","missingOutcome":"no free {round:other}"},{"key":"conversionPolicy","label":"Conversion policy","type":"select","options":["ALL_WINNINGS","EXCESS_ABOVE_COST","PERCENTAGE"]},{"key":"conversionCost","label":"Conversion cost (for EXCESS_ABOVE_COST)","type":"currency_map","modeKey":"conversionCostCurrencyMode","usedWhen":"when winnings are settled"},{"key":"maxConversion","label":"Max conversion (optional cap)","type":"currency_map","modeKey":"maxConversionCurrencyMode","usedWhen":"when winnings are settled"},{"key":"conversionPercent","label":"Conversion percent (for PERCENTAGE)","type":"number","step":1,"min":0,"max":100},{"key":"winningsWageringMultiplier","label":"Wager winnings X times (optional)","type":"number","step":1,"min":0,"required":false,"description":"If set, converted winnings are held in a bonus bucket and require turnover of (winnings * multiplier) before converting to cash."},{"key":"winningsBonusBucket","label":"Winnings bonus bucket","type":"text","required":false,"description":"Where converted winnings are credited when wagering is enabled. Default: free_round_winnings."},{"key":"selectionCriteria","label":"Selection criteria","type":"selection_criteria_builder","required":false,"help":"MongoDB-style query (sift) on the match document: `$and`, `$or`, `$in`, `$nin`, `$gte`, … plus temporal shorthand (`$daysOfWeek`, `$utcTime`, …). Per-channel admission: `selectionCriteriaByEvent` (e.g. `wager.placed`)."},{"key":"maxBonusRate","label":"Max bonus rate","type":"number","step":0.01,"min":0,"help":"Block new grants when user.bonus.rate (totalBonus/totalLosses) exceeds this. E.g. 0.5 = 50% bonus-to-loss ratio. Requires user.bonus.rate fact plugin. Leave empty to disable."},{"key":"bonusRatePeriod","label":"Bonus rate period","type":"select","options":[{"value":"WEEKLY","label":"Weekly (default)"},{"value":"DAILY","label":"Daily"},{"value":"MONTHLY","label":"Monthly"},{"value":"LIFETIME","label":"Lifetime"}],"help":"Time window for bonus-to-loss ratio calculation. Only used when max bonus rate is set."}],"pluginId":"welcome_free_rounds_bonus"},"requires":[{"kind":"fact","id":"deposit.count.lifetime"},{"kind":"reward","id":"wallet_free_rounds"}],"optionalFacts":null},{"bonusId":"welcome_free_rounds_funds_bonus","version":"1.0.0","description":"Welcome free rounds with wager funding via casino promo account","requiresFacts":["deposit.count.lifetime"],"ui":{"kind":"bonus","bonusId":"welcome_free_rounds_funds_bonus","name":"Welcome Free Rounds (Funds)","description":"Welcome free rounds with wager funding via casino promo account","defaults":{"rewardPlugin":"wallet_free_rounds","minDeposit":20,"spinCount":20,"spinValue":0.5,"conversionPolicy":"ALL_WINNINGS","conversionCost":0,"conversionPercent":100,"winningsWageringMultiplier":null,"winningsBonusBucket":"free_round_winnings","minBetAmount":null,"maxBetAmount":null,"selectionCriteria":{"$daysOfWeek":[0,1,2,3,4,5,6]}},"fields":[{"key":"minDeposit","label":"Minimum deposit","type":"currency_map","modeKey":"minDepositCurrencyMode","usedWhen":"when they deposit"},{"key":"spinCount","label":"Spin count","type":"number","step":1,"min":0},{"key":"spinValue","label":"Spin value","type":"currency_map","modeKey":"spinValueCurrencyMode","usedWhen":"when they play","missingOutcome":"no free {round:other}"},{"key":"minBetAmount","label":"Minimum bet","type":"currency_map","modeKey":"minBetAmountCurrencyMode","usedWhen":"in the bonus terms"},{"key":"maxBetAmount","label":"Maximum bet","type":"currency_map","modeKey":"maxBetAmountCurrencyMode","usedWhen":"in the bonus terms"},{"key":"conversionPolicy","label":"Conversion policy","type":"select","options":["ALL_WINNINGS","EXCESS_ABOVE_COST","PERCENTAGE"]},{"key":"conversionCost","label":"Conversion cost (for EXCESS_ABOVE_COST)","type":"currency_map","modeKey":"conversionCostCurrencyMode","usedWhen":"when winnings are settled"},{"key":"maxConversion","label":"Max conversion (optional cap)","type":"currency_map","modeKey":"maxConversionCurrencyMode","usedWhen":"when winnings are settled"},{"key":"conversionPercent","label":"Conversion percent (for PERCENTAGE)","type":"number","step":1,"min":0,"max":100},{"key":"winningsWageringMultiplier","label":"Wager winnings X times (optional)","type":"number","step":1,"min":0,"required":false},{"key":"winningsBonusBucket","label":"Winnings bonus bucket","type":"text","required":false},{"key":"selectionCriteria","label":"Selection criteria","type":"selectionCriteria"},{"key":"maxBonusRate","label":"Max bonus rate","type":"number","step":0.01,"min":0,"help":"Block new grants when user.bonus.rate (totalBonus/totalLosses) exceeds this. E.g. 0.5 = 50% bonus-to-loss ratio. Requires user.bonus.rate fact plugin. Leave empty to disable."},{"key":"bonusRatePeriod","label":"Bonus rate period","type":"select","options":[{"value":"WEEKLY","label":"Weekly (default)"},{"value":"DAILY","label":"Daily"},{"value":"MONTHLY","label":"Monthly"},{"value":"LIFETIME","label":"Lifetime"}],"help":"Time window for bonus-to-loss ratio calculation. Only used when max bonus rate is set."}],"pluginId":"welcome_free_rounds_funds_bonus"},"requires":[{"kind":"fact","id":"deposit.count.lifetime"},{"kind":"reward","id":"wallet_free_rounds"}],"optionalFacts":null},{"bonusId":"periodic_cashback_bonus","version":"2.0.0","description":"Periodic cashback (weekly, percentage of net loss) paid via periodic_cashback reward plugin","requiresFacts":["user.wagering.weekly"],"ui":{"kind":"bonus","bonusId":"periodic_cashback_bonus","name":"Periodic Cashback","description":"Periodic cashback (weekly, percentage of net loss) paid via periodic_cashback reward plugin","defaults":{"schedule":"WEEKLY","dayOfWeek":null,"percentage":10,"minLoss":0,"maxCashback":null,"creditTo":"bonus","wageringMultiplier":1,"wageringPluginId":"simple_multiplier","categoryWeights":null,"categoryTagFields":["category"],"payPartialPeriod":true,"aggregationMethod":null,"stakeBasis":null,"eligibility":{"segments":null,"minTotalStakes":0,"minDeposit":null,"onlyFirstDeposit":null},"selectionCriteria":null,"selectionCriteriaByEvent":null,"tierFactId":null,"tierField":"name","tierPercentages":null},"fields":[{"key":"schedule","label":"Period","type":"cadence-picker","periodNarrowing":["calendar"],"options":[{"value":"DAILY","label":"daily"},{"value":"WEEKLY","label":"weekly"},{"value":"MONTHLY","label":"monthly"}]},{"key":"dayOfWeek","label":"Day of week","type":"select","options":[{"value":null,"label":"Any (not specified)"},{"value":0,"label":"Sunday"},{"value":1,"label":"Monday"},{"value":2,"label":"Tuesday"},{"value":3,"label":"Wednesday"},{"value":4,"label":"Thursday"},{"value":5,"label":"Friday"},{"value":6,"label":"Saturday"}],"help":"Only applies to WEEKLY schedule. Specifies which day the period resets.","showWhen":{"schedule":"WEEKLY"}},{"key":"percentage","label":"Cashback %","type":"number","step":1,"min":0,"max":100},{"key":"minLoss","label":"Minimum net loss","type":"number","step":1,"min":0},{"key":"maxCashback","label":"Max cashback (0 = unlimited)","type":"number","step":1,"min":0},{"key":"creditTo","label":"Credit to","type":"select","options":["bonus","cash"]},{"key":"wageringMultiplier","label":"Wagering multiplier","type":"number","step":1,"min":0,"help":"How many times the cashback amount must be wagered before withdrawal. 0 = no wagering required.","showWhen":{"creditTo":"bonus"}},{"key":"wageringPluginId","label":"Wagering contribution (how stake counts toward the requirement)","type":"select","showWhen":{"creditTo":"bonus"},"help":"Applies when cashback is credited to bonus balance with a wagering multiplier > 0. Simple = each $1 staked counts $1 toward the requirement. Category-weighted = stake × category weight. Ensure plugins.wagering includes category-weighted when using that option.","options":[{"value":"simple_multiplier","label":"Simple — full stake counts (1:1)"},{"value":"category-weighted","label":"Category-weighted — tenant defaults + optional per-bonus overrides"}]},{"key":"categoryWeights","label":"Category weights (this bonus only)","type":"json","showWhen":{"wageringPluginId":"category-weighted","creditTo":"bonus"},"required":true,"help":"**Required** when category-weighted is selected. JSON object with at least one category key, merged over tenant category-weighted defaults (your keys override per category). Use **Copy from tenant default** to start from tenant weights. Use `\"*\"` or `\"$OTHERS\"` as a key for the weight applied to category tags that do not match any other named key (both are supported; if both are set, `\"*\"` wins). Configure tenant defaults under Edit Tenant → wagering plugins → category-weighted."},{"key":"categoryTagFields","label":"Wager fields used for category tags (this bonus only)","type":"json","showWhen":{"wageringPluginId":"category-weighted","creditTo":"bonus"},"required":true,"help":"**Required** when category-weighted is selected. JSON array of payload field names for weight lookup, e.g. `[\"category\"]` or `[\"category\",\"categories\"]`. Eligibility for funding and bonuses is only via **Selection criteria** (Mongo/sift); this list only affects **how stake is weighted** for category-weighted wagering math."},{"key":"payPartialPeriod","label":"Pay partial first period","type":"boolean","help":"If disabled, users who receive this bonus mid-period will not get cashback until the first full period completes."},{"key":"aggregationMethod","label":"Aggregation method","type":"select","options":[{"value":null,"label":"Inherit from tenant (default)"},{"value":"realtime","label":"Real-time counters (fact plugins)"},{"value":"ledger-query","label":"Ledger query (at period end)"}],"help":"How wagering totals are computed. \"Inherit\" uses the tenant-level cashbackAggregation setting. Auto-detection uses ledger query if the ledger supports it."},{"key":"stakeBasis","label":"Stake basis (net loss)","type":"select","options":[{"value":null,"label":"Inherit from tenant (default)"},{"value":"all_wagered","label":"all of it"},{"value":"cash_balance_only","label":"only the part paid with real money"}],"help":"Which part of each wager counts toward total stakes for net loss. \"only the part paid with real money\" excludes wallet grant debits; use tenant walletConfig.cashbackStakeBasis to set a default."},{"key":"baseFactId","label":"Base metric fact id (optional)","type":"text","help":"Fact id supplying the wagering totals for net loss (value must expose totalCashStakes/totalCashWinnings, e.g. a period-scoped counter clone). Required for cadence/named-period definitions; overrides the built-in user.wagering.* fact for DAILY/WEEKLY/MONTHLY."},{"key":"eligibility.segments","label":"Required segments","type":"text","help":"Comma-separated list of user segments. User must be in at least one to qualify. Leave empty for all users."},{"key":"eligibility.minTotalStakes","label":"Min total stakes in period","type":"number","step":1,"min":0,"help":"Minimum total wager amount in the period to qualify for cashback. 0 = no minimum."},{"key":"tierFactId","label":"Tier fact id (optional)","type":"text","help":"Fact id whose value selects the cashback %. Leave blank for a flat rate. E.g. a vip.level.counter instance id."},{"key":"tierField","label":"Tier field","type":"text","help":"Property of the tier fact's value to use as the key (e.g. \"name\" for a VIP tier name, \"level\" for the number). Ignored if the fact value is a scalar."},{"key":"tierProjection","label":"Tier milestone value","type":"select","options":[{"value":null,"label":"Fact value (default)"},{"value":"currentMilestone.label","label":"Current milestone label"},{"value":"currentMilestone.name","label":"Current milestone technical name"},{"value":"currentMilestone.ordinal","label":"Current milestone number"},{"value":"currentMilestone.threshold","label":"Current milestone threshold"}],"help":"Select a safe projection from a scalar fact milestone. This replaces legacy VIP-specific object fields."},{"key":"tierPercentages","label":"Tier percentages (JSON)","type":"json","help":"JSON map of tier key → cashback %. Optional \"DEFAULT\" fallback. Example: {\"Diamond\":12,\"Elite\":15,\"DEFAULT\":10}. When set, overrides the flat Percentage for matching tiers."},{"key":"selectionCriteria","label":"Selection criteria","type":"selection_criteria_builder","required":false,"help":"MongoDB-style query (sift) on the match document: `$and`, `$or`, `$in`, `$nin`, `$gte`, … plus temporal shorthand (`$daysOfWeek`, `$utcTime`, …). Per-channel admission: `selectionCriteriaByEvent` (e.g. `wager.placed`)."},{"key":"maxBonusRate","label":"Max bonus rate","type":"number","step":0.01,"min":0,"help":"Block new grants when user.bonus.rate (totalBonus/totalLosses) exceeds this. E.g. 0.5 = 50% bonus-to-loss ratio. Requires user.bonus.rate fact plugin. Leave empty to disable."},{"key":"bonusRatePeriod","label":"Bonus rate period","type":"select","options":[{"value":"WEEKLY","label":"Weekly (default)"},{"value":"DAILY","label":"Daily"},{"value":"MONTHLY","label":"Monthly"},{"value":"LIFETIME","label":"Lifetime"}],"help":"Time window for bonus-to-loss ratio calculation. Only used when max bonus rate is set."}],"pluginId":"periodic_cashback_bonus"},"requires":[{"kind":"reward","id":"periodic_cashback"}],"optionalFacts":{"realtime":["user.wagering.daily","user.wagering.weekly","user.wagering.monthly"]}},{"bonusId":"periodic_claimable_bonus","version":"1.0.0","description":"Periodic claimable bonus (WEEKLY, claim-required) — grants computed amount via paired periodic_claimable reward plugin","requiresFacts":[],"ui":{"kind":"bonus","bonusId":"periodic_claimable_bonus","name":"Periodic Claimable Bonus","description":"Periodic claimable bonus (WEEKLY, claim-required) — grants computed amount via paired periodic_claimable reward plugin","defaults":{"bonusType":"periodic_claimable_bonus","schedule":"WEEKLY","dayOfWeek":null,"payPartialPeriod":true,"claimPolicy":"claim-required","amountExpression":null,"amountFactId":null,"amountByCurrency":null,"currencyMode":null,"rewardPlugin":"periodic_claimable","wageringPluginId":null,"wageringParams":null,"forfeitAfterPeriods":1,"selectionCriteria":null,"selectionCriteriaByEvent":null,"cadence":{"cron":"0 0 * * 1"},"anchorMs":null},"fields":[{"key":"schedule","label":"Cadence","type":"cadence-picker","options":[{"value":"DAILY","label":"daily"},{"value":"WEEKLY","label":"weekly"},{"value":"MONTHLY","label":"monthly"},{"value":"HOURLY","label":"hourly"},{"value":"FOUR_HOURLY","label":"every 4 hours"},{"value":"CUSTOM","label":"Custom (operator-defined period IDs)"}],"help":"How often the bonus is evaluated. Preset uses UTC hour/day/week/month buckets; Cron and Interval modes accept operator-defined cadences and emit `config.cadence` with `schedule = CUSTOM` as the sentinel."},{"key":"dayOfWeek","label":"Day of week","type":"select","options":[{"value":null,"label":"Any (not specified)"},{"value":0,"label":"Sunday"},{"value":1,"label":"Monday"},{"value":2,"label":"Tuesday"},{"value":3,"label":"Wednesday"},{"value":4,"label":"Thursday"},{"value":5,"label":"Friday"},{"value":6,"label":"Saturday"}],"help":"Only applies to WEEKLY cadence — which day the period resets.","showWhen":{"schedule":"WEEKLY"}},{"key":"claimPolicy","label":"Claim policy","type":"radio","options":[{"value":"claim-required","label":"Claim-required — player must claim the PENDING grant before the next period boundary or it is forfeited."},{"value":"auto-grant","label":"Auto-grant — the bonus is credited immediately at period end; no claim step."}],"help":"Determines whether the grant lands as PENDING (player must claim) or ACTIVE (auto-credited)."},{"key":"amountExpression","label":"Amount expression","type":"textarea","rows":3,"help":"Arithmetic expression evaluated against registered facts (e.g. `xp * 0.01 + vipLevel * 5`). Result must be a non-negative finite number or null. Validated at save against the tenant's registered fact IDs."},{"key":"amountFactId","label":"Amount fact ID (advanced)","type":"text","help":"Optional synthetic fact ID used to resolve the computed amount at runtime. Leave blank to auto-derive as `<definitionId>:amount`."},{"key":"wageringPluginId","label":"Wagering plugin (optional)","type":"select","options":[{"value":"","label":"None — no wagering requirement"},{"value":"simple_multiplier","label":"Simple — full stake counts (1:1)"},{"value":"category-weighted","label":"Category-weighted — tenant defaults + optional overrides"}],"help":"Applied when the claim transitions to ACTIVE (claim-required) or directly at grant (auto-grant). Leave as None for a no-wagering grant."},{"key":"categoryWeights","label":"Category weights","type":"json","showWhen":{"wageringPluginId":"category-weighted"},"help":"JSON object: category tag → multiplier (e.g. {\"SLOT\":1,\"POKER\":0.5,\"DEFAULT\":0.2}). Values must be finite positive numbers. Use \"DEFAULT\" as the fallback key for tags that do not match any other key (otherwise unmatched tags use a multiplier of 1). Leave empty to inherit the tenant-level default (see Edit Tenant → Wagering plugins)."},{"key":"categoryTagFields","label":"Category tag fields","type":"json","showWhen":{"wageringPluginId":"category-weighted"},"help":"JSON array of event-payload field names whose value names the wager's category (e.g. [\"category\", \"gameCategory\"]). The first non-empty field wins. Defaults to [\"category\"]."},{"key":"rewardPlugin","label":"Reward plugin","type":"select","options":[{"value":"periodic_claimable","label":"periodic_claimable (locked)"}],"help":"Locked — periodic claimable bonus always pairs with the `periodic_claimable` reward plugin."},{"key":"selectionCriteria","label":"Selection criteria","type":"selection_criteria_builder","required":false,"help":"MongoDB-style query (sift) on the match document: `$and`, `$or`, `$in`, `$nin`, `$gte`, … plus temporal shorthand (`$daysOfWeek`, `$utcTime`, …). Per-channel admission: `selectionCriteriaByEvent` (e.g. `wager.placed`)."}],"pluginId":"periodic_claimable_bonus"},"requires":[{"kind":"reward","id":"periodic_claimable"}],"optionalFacts":null},{"bonusId":"manual_bonus","version":"1.0.0","description":"Manual bonus - operator-granted bonus for specific users or segments","requiresFacts":["user.segments"],"ui":{"kind":"bonus","bonusId":"manual_bonus","name":"Manual Bonus","description":"Manual bonus - operator-granted bonus for specific users or segments","defaults":{"wageringMultiplier":1,"wageringPluginId":"simple_multiplier","categoryWeights":null,"categoryTagFields":["category"],"minAmount":0,"maxAmount":10000,"allowedSegments":[],"selectionCriteria":{"$daysOfWeek":[0,1,2,3,4,5,6]},"selectionCriteriaByEvent":null},"fields":[{"key":"wageringMultiplier","label":"Wagering multiplier","type":"number","step":1,"min":0,"help":"Wager requirement = grantedAmount * multiplier. Set to 0 for no wagering."},{"key":"wageringPluginId","label":"Wagering contribution (how stake counts toward the requirement)","type":"select","help":"Simple = each $1 staked counts $1 toward the requirement. Category-weighted = stake × category weight from tenant defaults, optionally overridden below. Requires plugins.wagering to include category-weighted when selected.","options":[{"value":"simple_multiplier","label":"Simple — full stake counts (1:1)"},{"value":"category-weighted","label":"Category-weighted — tenant defaults + optional per-bonus overrides"}]},{"key":"categoryWeights","label":"Category weights (this bonus only)","type":"json","showWhen":{"wageringPluginId":"category-weighted"},"required":true,"help":"**Required** when category-weighted is selected. JSON object with at least one category key, merged over tenant category-weighted defaults (your keys override per category). Use **Copy from tenant default** to start from tenant weights. Use `\"*\"` or `\"$OTHERS\"` as a key for the weight applied to category tags that do not match any other named key (both are supported; if both are set, `\"*\"` wins). Configure tenant defaults under Edit Tenant → wagering plugins → category-weighted."},{"key":"categoryTagFields","label":"Wager fields used for category tags (this bonus only)","type":"json","showWhen":{"wageringPluginId":"category-weighted"},"required":true,"help":"**Required** when category-weighted is selected. JSON array of payload field names for weight lookup, e.g. `[\"category\"]` or `[\"category\",\"categories\"]`. Eligibility for funding and bonuses is only via **Selection criteria** (Mongo/sift); this list only affects **how stake is weighted** for category-weighted wagering math."},{"key":"selectionCriteria","label":"Selection criteria","type":"selection_criteria_builder","required":false,"help":"MongoDB-style query (sift) on the match document: `$and`, `$or`, `$in`, `$nin`, `$gte`, … plus temporal shorthand (`$daysOfWeek`, `$utcTime`, …). Per-channel admission: `selectionCriteriaByEvent` (e.g. `wager.placed`)."},{"key":"minAmount","label":"Minimum amount","type":"number","step":1,"min":0,"help":"Minimum bonus amount that can be granted."},{"key":"maxAmount","label":"Maximum amount","type":"number","step":1,"min":0,"help":"Maximum bonus amount that can be granted."},{"key":"maxBonusRate","label":"Max bonus rate","type":"number","step":0.01,"min":0,"help":"Block new grants when user.bonus.rate (totalBonus/totalLosses) exceeds this. E.g. 0.5 = 50% bonus-to-loss ratio. Requires user.bonus.rate fact plugin. Leave empty to disable."},{"key":"bonusRatePeriod","label":"Bonus rate period","type":"select","options":[{"value":"WEEKLY","label":"Weekly (default)"},{"value":"DAILY","label":"Daily"},{"value":"MONTHLY","label":"Monthly"},{"value":"LIFETIME","label":"Lifetime"}],"help":"Time window for bonus-to-loss ratio calculation. Only used when max bonus rate is set."}],"pluginId":"manual_bonus"},"requires":[{"kind":"fact","id":"user.segments"},{"kind":"reward","id":"wallet_cash_bonus"},{"kind":"wagering","id":"simple_multiplier"}],"optionalFacts":null},{"bonusId":"manual_wagered_cash_bonus","version":"1.0.0","description":"Manual cash bonus with wagering — event amount is the full grant (not deposit-match stake)","requiresFacts":[],"ui":{"kind":"bonus","bonusId":"manual_wagered_cash_bonus","name":"Manual Wagered Cash","description":"Manual cash bonus with wagering — event amount is the full grant (not deposit-match stake)","defaults":{"minGrant":0.01,"maxGrant":10000000,"requirementsMultiplier":10,"rewardPlugin":"wallet_cash_bonus","wageringPluginId":"simple_multiplier","categoryWeights":null,"categoryTagFields":["category"],"selectionCriteria":{"$daysOfWeek":[0,1,2,3,4,5,6]},"selectionCriteriaByEvent":null},"fields":[{"key":"rewardPlugin","label":"Reward plugin","type":"text"},{"key":"requirementsMultiplier","label":"Wagering multiplier (turnover × granted bonus)","type":"number","step":1,"min":0,"help":"Total wagering required = granted amount × this multiplier (same role as tier “Wagering x” on deposit match)."},{"key":"wageringPluginId","label":"Wagering contribution (how stake counts toward the requirement)","type":"select","help":"Simple = each $1 staked counts $1 toward the requirement. Category-weighted = stake × category weight. Ensure plugins.wagering includes category-weighted when using that option.","options":[{"value":"simple_multiplier","label":"Simple — full stake counts (1:1)"},{"value":"category-weighted","label":"Category-weighted — tenant defaults + optional per-bonus overrides"}]},{"key":"categoryWeights","label":"Category weights (this bonus only)","type":"json","showWhen":{"wageringPluginId":"category-weighted"},"required":true,"help":"**Required** when category-weighted is selected. JSON object with at least one category key, merged over tenant category-weighted defaults (your keys override per category). Use **Copy from tenant default** to start from tenant weights. Use `\"*\"` or `\"$OTHERS\"` as a key for the weight applied to category tags that do not match any other named key (both are supported; if both are set, `\"*\"` wins). Configure tenant defaults under Edit Tenant → wagering plugins → category-weighted."},{"key":"categoryTagFields","label":"Wager fields used for category tags (this bonus only)","type":"json","showWhen":{"wageringPluginId":"category-weighted"},"required":true,"help":"**Required** when category-weighted is selected. JSON array of payload field names for weight lookup, e.g. `[\"category\"]` or `[\"category\",\"categories\"]`. Eligibility for funding and bonuses is only via **Selection criteria** (Mongo/sift); this list only affects **how stake is weighted** for category-weighted wagering math."},{"key":"selectionCriteria","label":"Selection criteria","type":"selection_criteria_builder","required":false,"help":"MongoDB-style query (sift) on the match document: `$and`, `$or`, `$in`, `$nin`, `$gte`, … plus temporal shorthand (`$daysOfWeek`, `$utcTime`, …). Per-channel admission: `selectionCriteriaByEvent` (e.g. `wager.placed`)."},{"key":"minGrant","label":"Minimum grant amount","type":"number","step":0.01,"min":0},{"key":"maxGrant","label":"Maximum grant amount","type":"number","step":1,"min":0},{"key":"priority","label":"Wager processing priority","type":"number","step":1,"min":0,"help":"Lower = processed first. Default: 20."},{"key":"maxBonusRate","label":"Max bonus rate","type":"number","step":0.01,"min":0,"help":"Block new grants when user.bonus.rate (totalBonus/totalLosses) exceeds this. E.g. 0.5 = 50% bonus-to-loss ratio. Requires user.bonus.rate fact plugin. Leave empty to disable."},{"key":"bonusRatePeriod","label":"Bonus rate period","type":"select","options":[{"value":"WEEKLY","label":"Weekly (default)"},{"value":"DAILY","label":"Daily"},{"value":"MONTHLY","label":"Monthly"},{"value":"LIFETIME","label":"Lifetime"}],"help":"Time window for bonus-to-loss ratio calculation. Only used when max bonus rate is set."}],"pluginId":"manual_wagered_cash_bonus"},"requires":[{"kind":"reward","id":"wallet_cash_bonus"},{"kind":"wagering","id":"simple_multiplier"}],"optionalFacts":null},{"bonusId":"formula_amount_bonus","version":"1.0.0","description":"Generic formula-amount bonus — grant amount = bounded expression over event payload + facts","requiresFacts":[],"ui":{"kind":"bonus","pluginId":"formula_amount_bonus","bonusId":"formula_amount_bonus","name":"Formula Amount Bonus","description":"Generic formula-amount bonus — grant amount = bounded expression over event payload + facts","defaults":{"bonusType":"formula_amount_bonus","amountExpression":"0","amountByCurrency":null,"currencyMode":null,"payloadFields":[],"declaredFacts":[],"rewardPlugin":"wallet_cash_bonus","requirementsMultiplier":0,"wageringPluginId":"simple_multiplier","instanceFieldCapture":[],"lifecycleEventActions":[],"selectionCriteria":null,"selectionCriteriaByEvent":null},"fields":[{"key":"amountByCurrency","label":"Flat amount","type":"currency_map","modeKey":"currencyMode","usedWhen":"when it is paid","missingOutcome":"nothing","help":"A fixed amount, kept exactly as typed. Leave empty and use an amount expression instead when the amount should scale with the deposit or wager.","ui":{"tier":"basic"}},{"key":"amountExpression","label":"Amount expression","type":"textarea","help":"Arithmetic expression evaluated against declared payload fields and facts. Example: `depositAmount * 0.5` (50% match) or `xp * 0.01 + vipLevel * 5`. Must resolve to a finite, non-negative number. Ignored when a flat amount per currency is set above.","ui":{"tier":"basic"}},{"key":"payloadFields","label":"Payload fields (referenced from event ctx)","type":"stringArray","help":"Identifiers your expression reads from the event payload (e.g. `depositAmount`, `manualBonusAmount`). Each name is resolved from `ctx[name]` and must be a finite number at evaluate time.","ui":{"tier":"basic"}},{"key":"requiresFacts","label":"Required facts (referenced from facts context)","type":"stringArray","help":"Fact IDs your expression reads from the facts context. The framework prefetches them before evaluation.","ui":{"tier":"basic"}},{"key":"rewardPlugin","label":"Reward plugin","type":"enum","options":[{"value":"tracking_only","label":"Tracking Only"},{"value":"wallet_cash_bonus","label":"Wallet Cash Bonus"},{"value":"periodic_cashback","label":"Periodic Cashback"},{"value":"periodic_claimable","label":"Periodic Claimable Reward"},{"value":"wallet_free_rounds","label":"Wallet Free Rounds"},{"value":"free_rounds","label":"Free Rounds (alias)"}],"help":"Which reward plugin receives the GRANT_REWARD intent.","ui":{"tier":"basic"}},{"key":"requirementsMultiplier","label":"Wagering multiplier (0 = no wagering)","type":"number","help":"When > 0, the plugin also emits a SET_REQUIREMENTS intent. Its `requirementsTotal` is `amount × requirementsMultiplier`, or `event amount × requirementsMultiplier` when an event requirement basis is authored.","ui":{"tier":"advanced"}},{"key":"wageringPluginId","label":"Wagering plugin (when wagering is set)","type":"enum","options":[{"value":"simple_multiplier","label":"Simple — full stake counts (1:1)"},{"value":"category-weighted","label":"Category-weighted"}],"ui":{"tier":"advanced","visibility":{}}},{"key":"selectionCriteria","label":"Selection criteria","type":"selection_criteria_builder","required":false,"help":"MongoDB-style query (sift) on the match document: `$and`, `$or`, `$in`, `$nin`, `$gte`, … plus temporal shorthand (`$daysOfWeek`, `$utcTime`, …). Per-channel admission: `selectionCriteriaByEvent` (e.g. `wager.placed`)."}]},"requires":[],"optionalFacts":null},{"bonusId":"referral_qualification_bonus","version":"1.0.0","description":"Tracks a referred child deposit through wagering qualification","requiresFacts":[],"ui":{"kind":"bonus","bonusId":"referral_qualification_bonus","name":"Referral Qualification","description":"Tracks a referred child deposit through wagering qualification","defaults":{"lifecycleEvent":"referral.qualified","referrerField":"referrerUserId","requirementsMultiplier":1},"fields":[{"key":"lifecycleEvent","label":"Lifecycle event","type":"text","required":true},{"key":"referrerField","label":"Referrer payload field","type":"text","required":true},{"key":"requirementsMultiplier","label":"Wagering multiplier","type":"number","min":0,"required":true}],"pluginId":"referral_qualification_bonus"},"requires":[{"kind":"reward","id":"tracking_only"},{"kind":"wagering","id":"simple_multiplier"},{"kind":"completion","id":"referral_lifecycle"}],"optionalFacts":null},{"bonusId":"event_tracking_bonus","version":"1.0.0","description":"Tracks exact event-derived wagering progress without funds","requiresFacts":[],"ui":{"kind":"bonus","bonusId":"event_tracking_bonus","name":"Event Tracking","description":"Tracks exact event-derived wagering progress without funds","defaults":{"requirementsMultiplier":1},"fields":[],"pluginId":"event_tracking_bonus"},"requires":[{"kind":"reward","id":"tracking_only"},{"kind":"wagering","id":"simple_multiplier"},{"kind":"completion","id":"lifecycle_event_actions"}],"optionalFacts":null}],"catalog":{"bonus":[{"id":"welcome_bonus","name":"Welcome Bonus","description":"100% deposit match up to a cap (example bonus plugin).","version":"1.0.0"},{"id":"welcome_bonus_cash_first","name":"Welcome Bonus (Cash First)","description":"Welcome bonus that funds bets with cash first, then bonus balance.","version":"1.0.0"},{"id":"deposit_match_bonus","name":"Deposit Match Bonus","description":"50% deposit match up to a cap (example second bonus plugin).","version":"1.0.0"},{"id":"welcome_free_rounds_bonus","name":"Welcome Free Rounds Bonus","description":"Grants a free spins package on first deposit for a specific game.","version":"1.0.0"},{"id":"welcome_free_rounds_funds_bonus","name":"Welcome Free Rounds (Funds)","description":"Welcome free spins with wager funding via casino promo account.","version":"1.0.0"},{"id":"free_rounds_bonus","name":"Free Rounds (Wager-Based)","description":"Qualifying wagers are free — tracked by round count, winnings subject to wagering requirements.","version":"1.0.0"},{"id":"periodic_cashback_bonus","name":"Periodic Cashback","description":"Periodic cashback (daily/weekly/monthly) returning a percentage of net losses as bonus or cash.","version":"2.0.0"},{"id":"periodic_claimable_bonus","name":"Periodic Claimable Bonus","description":"Periodic (daily/weekly/monthly) bonus with computed amount from a facts expression. Creates a PENDING instance the player can claim before the next period boundary; unclaimed grants are forfeited on the next period.end (Story 11.1).","version":"1.0.0"},{"id":"formula_amount_bonus","name":"Formula Amount Bonus","description":"Generic formula-driven bonus: grant amount = bounded `expr-eval` expression over declared event-payload fields and declared facts. Emits GRANT_REWARD routed to a configured reward plugin; emits SET_REQUIREMENTS when `requirementsMultiplier > 0` (Story 13.3 W3).","version":"1.0.0"},{"id":"manual_bonus","name":"Manual Bonus","description":"Operator-granted manual bonus for specific users or segments.","version":"1.0.0"},{"id":"manual_bonus_grant","name":"Manual Bonus Grant (match-style)","description":"Deposit-match tier / cap / wagering shape evaluated on `manual.bonus.granted`: the synthetic stake is the event's `manualBonusAmount` instead of a deposit. Tiers are still selected by the player's lifetime deposit ordinal (falling back to the first-deposit tier when no tier matches), so it declares the same fact dependency as `deposit_match_bonus`. Use `deposit_match_bonus` for deposit-triggered matches.","version":"1.0.0"},{"id":"manual_wagered_cash_bonus","name":"Manual Wagered Cash","description":"ManualBonusGranted: credits the event amount as bonus cash with turnover (same wagering model as deposit match, without match rate).","version":"1.0.0"},{"id":"referral_qualification_bonus","name":"Referral Qualification","description":"Creates a zero-money child tracking row and delivers a referrer lifecycle event after wagering.","version":"1.0.0"},{"id":"event_tracking_bonus","name":"Event Tracking","description":"Creates a zero-money tracking instance with an exact event-derived requirement and one lifecycle event action.","version":"1.0.0"}],"fact":[{"id":"deposit.count.lifetime","name":"Deposit Count (Lifetime)","description":"Tracks lifetime number of settled deposits per user.","version":"1.0.0"},{"id":"deposit.ordinal.bucketed","name":"Deposit Ordinal (Bucketed)","description":"Tracks ordinal deposit number using bucketed storage. Counts tenant-wide: all users share one counter, split across 10 records by default, which caps the tenant at roughly 5,000 settled deposits per second.","version":"1.0.0"},{"id":"user.profile","name":"User Profile","description":"Tracks user profile attributes (country, KYC level, VIP tier, etc.).","version":"1.0.0"},{"id":"user.wagering.weekly","name":"User Wagering (Weekly)","description":"Accumulates weekly wagering totals (stakes/winnings) from bet events.","version":"1.0.0"},{"id":"user.wagering.daily","name":"User Wagering (Daily)","description":"Accumulates daily wagering totals (stakes/winnings) from bet events.","version":"1.0.0"},{"id":"user.wagering.monthly","name":"User Wagering (Monthly)","description":"Accumulates monthly wagering totals (stakes/winnings) from bet events.","version":"1.0.0"},{"id":"user.segments","name":"User Segments","description":"Tracks user segment/tag assignments for targeted promotions.","version":"1.0.0"},{"id":"user.bonus.rate","name":"User Bonus Rate","description":"Bonus intensity: total bonus amount / total losses over a period. Used for maxBonusRate limit enforcement.","version":"1.0.0"},{"id":"computed.fact","name":"Computed Fact (Expression)","description":"Evaluates a tenant-configured arithmetic expression over other registered facts (e.g. `xp * 0.01 + vipLevel * 5`). Returns a non-negative finite number or null. Used by `periodic_claimable_bonus` for amount derivation (Story 11.1).","version":"1.0.0"},{"id":"counter","name":"Counter","description":"Generic event counter (Story 12.0). Subscribes to canonical events, computes a per-event delta from configured event fields, accumulates over an optional time window (lifetime / rolling / tumbling cron / tumbling interval), and optionally emits threshold-crossing events. Unifies and is intended to supersede (Phase 2 migration pending) `experience.counter`, `vip.level.counter`, `vip.level.rolling.counter`, `user.wagering.daily`, `user.wagering.weekly`, and `user.wagering.monthly`.","version":"1.0.0"},{"id":"experience.counter","name":"Experience Counter","description":"VIP experience: on each admitted wager, increments a named counter by stake × FX-to-quote × category weight (Story 10.3).","version":"1.0.0"},{"id":"wallet.attributed.counter","name":"Wallet-attributed Accumulator","description":"ADR-004 / Story 43-13. Sum meter that accumulates a picked numeric event field (`config.delta.field`, e.g. `rake` or `stake`) apportioned to each FUNDING WALLET by that wallet's share of the bet, keyed per wallet. It consumes the funding split the money path already stamped on the event (`_walletFundingSplit`) rather than re-deriving it, and never posts money. Optional per-wallet `thresholds` emit a crossing carrying the `walletId`/`grantKey`, which is what composes tranche release. In the studio this is the \"add up\" production with the per-wallet attribution toggle on.","version":"1.0.0"},{"id":"managed.fact","name":"Managed Fact (rules-driven)","description":"The READ side of an evaluator-managed fact — the studio's \"my own rules\" production. It subscribes to no events and never writes: the cadence-driven fact-transition pass is the sole writer, and this plugin reads back what the pass persisted (value plus `provenance` = period / prev). The fact's rules, starting value and cadence live in the tenant `factTransitionRules` section keyed by the same instance name, NOT in this plugin's config; the only config key is a `thresholds` MIRROR of the milestones the pass emits, kept so the milestone-binding picker can discover them.","version":"1.0.0"},{"id":"vip.level.counter","name":"VIP Level Counter","description":"Reads a named experience counter and emits `ThresholdCrossed` domain events when a tenant-configured level boundary is crossed (Story 10.4).","version":"1.0.0"},{"id":"vip.level.rolling.counter","name":"VIP Level Counter (Rolling Window)","description":"Rolling-window variant of the VIP level counter: evaluates thresholds against XP earned within a tenant-configured rolling window and emits `ThresholdCrossed` in both directions (Story 10.5 / FR38).","version":"1.0.0"}],"reward":[{"id":"tracking_only","name":"Tracking Only","description":"Persists an entitlement row without posting money to the ledger.","version":"1.0.0"},{"id":"wallet_cash_bonus","name":"Wallet Cash Bonus","description":"Credits bonus balance to a per-deposit wallet grant account via FUNDS ledger.","version":"1.0.0"},{"id":"periodic_cashback","name":"Periodic Cashback","description":"Periodic cashback posted to per-deposit wallet accounts.","version":"2.0.0"},{"id":"periodic_claimable","name":"Periodic Claimable Reward","description":"Reward plugin paired with `periodic_claimable_bonus`. Single-shot grant via WalletManager when the player claims a PENDING instance (`pending → active`); no posting on the grant intent itself (Story 11.1, ADR-5).","version":"1.0.0"},{"id":"wallet_free_rounds","name":"Wallet Free Rounds","description":"Free rounds with winnings posted to per-deposit wallet accounts.","version":"1.0.0"},{"id":"free_rounds","name":"Free Rounds (alias)","description":"Same as wallet_free_rounds. Use wallet_free_rounds in new configs.","version":"1.0.0"}],"wagering":[{"id":"simple_multiplier","name":"Simple Multiplier","description":"1x stake contribution toward wagering requirements.","version":"1.0.0"},{"id":"round_counter","name":"Round Counter","description":"Counts qualifying wager events as rounds (for free rounds bonuses).","version":"1.0.0"},{"id":"category-weighted","name":"Category Weighted","description":"Applies category-specific weights and optional eligibility filtering.","version":"1.0.0"},{"id":"max-wager-enforced","name":"Max Wager Enforced (wrapper)","description":"Wraps another wagering plugin (config.basePluginId) and enforces a maximum bet on the total stake; violationPolicy picks flag / exclude contribution / reject.","version":"1.0.0"}],"selector":[{"id":"configurable","name":"Configurable Bonus Selector","description":"Selects which bonus instances receive wager events based on config-driven tracks.","version":"1.0.0"}],"completion":[{"id":"wallet_configurable","name":"Wallet Configurable Bonus Completion","description":"Handles bonus completion actions for wallet-mode bonuses (convert to cash, etc.).","version":"1.0.0"},{"id":"free_rounds_phase_transition","name":"Free Rounds Phase Transition","description":"Transitions free rounds bonus from round-counting phase to wagering-on-winnings phase.","version":"1.0.0"},{"id":"referral_lifecycle","name":"Referral Lifecycle Delivery","description":"Durably delivers a deterministic tenant event to the captured referrer.","version":"1.0.0"},{"id":"lifecycle_event_actions","name":"Lifecycle Event Actions","description":"Durably emits one deterministic, schema-validated tenant event after wagering completion.","version":"1.0.0"}]}}