Relationships
#3931 Award grants (showcase approval, admin award points) skip campaign/streak multipliers
Opened by keeb · 10/25/2026· Shipped 10/25/2026
Summary
Point awards written directly to the score ledger don't get the score multipliers that every other grant gets. That includes active double-XP campaigns (swamp.double_xp_window) and the streak/engagement multiplier. Approving a profile showcase pays a flat 5,000,000 points even during a 2x/3x campaign, and so does an admin manual award. Multipliers should apply to every grant source, awards included.
Where
Both award writers hard-code the multiplier and then write through ScoreAwardLedger.writeAward, which bypasses the ClickHouse grant-projection MVs:
lib/app/showcase/showcase-reward.ts:createScoreGrant({ ..., source: "award", multiplier: 1, baseAmount: SHOWCASE_REWARD_POINTS })lib/app/admin/award-points.ts:createScoreGrant({ ..., source: "award", multiplier: 1 })
The event-driven projections in infrastructure/clickhouse/init/03-grant-projections.sql resolve _dx_mult per row from swamp.double_xp_window and write multiplier and amount from it. The streak/rolling-active-days multiplier is applied on the event-grant path too (02-score-grants.sql). The award path does neither.
Steps to reproduce
- Open a double-XP campaign in /admin/campaigns covering now.
- Approve a pending profile showcase (or grant points from the admin award panel).
- Read the resulting grant in
swamp.score_grants:multiplier = 1,amount = baseAmount.
Expected
An award granted inside a campaign window gets the same stacked campaign multiplier as event grants, and the operative's streak/engagement multiplier, with baseAmount holding the unmultiplied value. Fix it for every award source at once (one shared resolution in the award path or the ledger writer), not per caller, so any future award source picks it up automatically.
Notes
- The multiplier has to be resolved at grant time and frozen on the grant, just like the event path (monotonic ledger: grant once, never recompute).
- Idempotency stays keyed on
grantId(award:showcase:<id>), so retrying an award must not re-price it.
Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.