{"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":"number","step":1,"min":0},{"key":"minDeposit","label":"Min deposit","type":"number","step":1,"min":0},{"key":"requirementsMultiplier","label":"Wagering x","type":"number","step":1,"min":0}],"defaultItem":{"minOrdinal":1,"maxOrdinal":999999,"matchRate":1,"maxBonus":500,"minDeposit":20,"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 stakes (default)"},{"value":"cash_only","label":"Cash-funded share only"},{"value":"bonus_only","label":"Bonus-funded share only"}],"help":"Which funding share of each bet advances the wagering requirement. Default: every stake counts in full. \"Cash-funded share only\" counts just the part of the bet paid from cash; \"Bonus-funded share only\" counts just the part funded by this bonus. 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":"number","step":1,"min":0},{"key":"minDeposit","label":"Min deposit","type":"number","step":1,"min":0},{"key":"requirementsMultiplier","label":"Wagering x","type":"number","step":1,"min":0}],"defaultItem":{"minOrdinal":1,"maxOrdinal":999999,"matchRate":1,"maxBonus":500,"minDeposit":20,"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 stakes (default)"},{"value":"cash_only","label":"Cash-funded share only"},{"value":"bonus_only","label":"Bonus-funded share only"}],"help":"Which funding share of each bet advances the wagering requirement. Default: every stake counts in full. \"Cash-funded share only\" counts just the part of the bet paid from cash; \"Bonus-funded share only\" counts just the part funded by this bonus. 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":"number","step":1,"min":0},{"key":"minDeposit","label":"Min deposit","type":"number","step":1,"min":0},{"key":"requirementsMultiplier","label":"Wagering x","type":"number","step":1,"min":0}],"defaultItem":{"minOrdinal":1,"maxOrdinal":999999,"matchRate":1,"maxBonus":500,"minDeposit":20,"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 stakes (default)"},{"value":"cash_only","label":"Cash-funded share only"},{"value":"bonus_only","label":"Bonus-funded share only"}],"help":"Which funding share of each bet advances the wagering requirement. Default: every stake counts in full. \"Cash-funded share only\" counts just the part of the bet paid from cash; \"Bonus-funded share only\" counts just the part funded by this bonus. 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} (per currency)","type":"currency_map","help":"The wager amount that equals one free {round:one}, per currency. Enter the exact amount for each currency you support — 0.00000001 is valid for an 8-decimal denomination. Currencies you do not list get no free {round:other} unless automatic conversion is on below.","ui":{"tier":"basic"}},{"key":"roundValueCurrencyMode","label":"Currencies you have not listed","type":"segmented","options":[{"value":"match","label":"Get no free {round:other} (default)"},{"value":"convert","label":"Convert at the current rate"}],"help":"A free {round:one} value is COMPARED against the stake, so it has to be in the stake's own currency — there is no option to pay a fixed denomination here (ADR-010 §D17). Converting derives the value from the live rate at play time, so it is always current and you author the amount only once.","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":"number","step":1,"min":0},{"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":"number","step":1,"min":0},{"key":"spinCount","label":"Spin count","type":"number","step":1,"min":0},{"key":"spinValue","label":"Spin value","type":"number","step":0.01,"min":0},{"key":"conversionPolicy","label":"Conversion policy","type":"select","options":["ALL_WINNINGS","EXCESS_ABOVE_COST","PERCENTAGE"]},{"key":"conversionCost","label":"Conversion cost (for EXCESS_ABOVE_COST)","type":"number","step":0.01,"min":0},{"key":"maxConversion","label":"Max conversion (optional cap)","type":"number","step":0.01,"min":0},{"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":"number","step":1,"min":0},{"key":"spinCount","label":"Spin count","type":"number","step":1,"min":0},{"key":"spinValue","label":"Spin value","type":"number","step":0.01,"min":0},{"key":"minBetAmount","label":"Minimum bet","type":"number","step":0.01,"min":0},{"key":"maxBetAmount","label":"Maximum bet","type":"number","step":0.01,"min":0},{"key":"conversionPolicy","label":"Conversion policy","type":"select","options":["ALL_WINNINGS","EXCESS_ABOVE_COST","PERCENTAGE"]},{"key":"conversionCost","label":"Conversion cost (for EXCESS_ABOVE_COST)","type":"number","step":0.01,"min":0},{"key":"maxConversion","label":"Max conversion (optional cap)","type":"number","step":0.01,"min":0},{"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","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 wagered (cash + bonus/grant stakes)"},{"value":"cash_balance_only","label":"Cash stakes only (exclude bonus grant debits)"}],"help":"Which amounts count toward total stakes for net loss. Cash-only 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,"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":"HOURLY","label":"Hourly"},{"value":"FOUR_HOURLY","label":"4-hourly"},{"value":"DAILY","label":"Daily"},{"value":"WEEKLY","label":"Weekly"},{"value":"MONTHLY","label":"Monthly"},{"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,"payloadFields":[],"declaredFacts":[],"rewardPlugin":"wallet_cash_bonus","requirementsMultiplier":0,"wageringPluginId":"simple_multiplier","instanceFieldCapture":[],"lifecycleEventActions":[],"selectionCriteria":null,"selectionCriteriaByEvent":null},"fields":[{"key":"amountByCurrency","label":"Flat amount (per currency)","type":"currency_map","step":0.01,"min":0,"help":"A fixed amount, with the currency it is denominated in — e.g. USD 100 and ETH 0.05. A flat amount MUST name its currency: the same \"100\" is worth about five orders of magnitude more in ETH than in USDT, so an unqualified number cannot be granted. A player whose currency is not listed here does not receive this bonus. Leave empty and use an amount expression instead when the amount should scale with the deposit or wager (a percentage is currency-agnostic and needs no code here).","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"}]}}