Docs·Rewards and mechanics
Rewards and mechanics
The battlepass and the item mall
The platform's two THD sinks: a per-season pass whose payment is burned, a shop whose platform receipts are burned, and one discount that never stacks.
Status#
These are the platform’s two THD sinks: the only contracts where a user spends THD and the supply falls. Everything else in the reward layer pays THD out. The battlepass is also the largest destination of the Spoils user pool — 5,000 bps of it at construction. See Spoils.
A season is a calendar quarter#
A season is a fixed calendar quarter beginning 1 January, 1 April, 1 July and 1 October, UTC. There is no season-duration constant and nothing an owner can move: the index is year × 4 + (quarter − 1), derived from block.timestamp by the same civil-date routine ProtocolFeeSplitter uses.
| Constant or view | Value |
|---|---|
MONTHS_PER_SEASON · SEASONS_PER_YEAR | 3 · 4 |
seasonDays(index) | 90, 91 or 92 — measured, not assumed. A fixed 90 would over-charge every buyer in a 92-day quarter by two days’ worth. |
BACK_CLAIM_WINDOW | 30 days — 2,592,000 seconds, after seasonEnd |
claimDeadline(index) | seasonEnd(index) + 30 days |
The two points boards: Spending and Earnings#
| Board | Ranks on | Leaderboard |
|---|---|---|
| Spending | Net bonding-curve buying per launch plus prediction-market stakes, in any curve or market currency | /leaderboard?board=spending |
| Earnings | The fee-split income paid to creators and founding-crew members, KOLs and referrers, in any curve currency | /leaderboard?board=earnings |
// lib/points/formula.ts — dayPoints, once per board
a board's day points = the board's measure // Spending, or Earnings
× (checked in that day ? 1 : 0)
× streak multiplier // 1 + 0.5 × min(1, (days ÷ 20)^0.7)
× premium multiplier // 1.25 while a pass is held, else 1| Input | What counts |
|---|---|
| Spending | Net bonding-curve buying (below) and prediction-market stakes in full |
| Earnings | The creator’s trade-fee share, the creator’s half of a KOL window that closed unallocated, founding-crew credits (and the creator’s remainder of a crew sync), KOL allocations and referrer credits — in ETH or in the launch’s ERC-20 |
| Not earnings | Trading profit, LP income, prediction winnings, settler THD and wheel prizes. Markets pay no fee split to a user |
| Unpriced | An amount with no posted price in force at its event is left out and counted, never taken at face value |
| Check-in | A day with no check-in scores zero on both boards, whatever moved |
| Streak | × 1 to × 1.5, flat from day 20, covered days included |
| Premium | × 1.25 while a monthly pass is held |
| Rule | As built |
|---|---|
| Daily rank | Points, descending. Equal points share a rank — 1, 2, 2, 4 — and the season points and pot share that go with it. Time decides nothing; tied rows are listed by address |
| Season score | Σ max(0, 101 − rank) per board. Every shared rank of 100 or better scores, so ties at the cut can put more than 100 wallets in the money |
| Pot between the boards | Split daily by fee value: the fees the day’s counted spending paid, against the fee income paid to the day’s earners. A board with nobody on it that day passes its share to the other |
| Pot inside a board | Pro rata by points |
| Frozen days | Each closed UTC day is frozen once, with every event it read, and served at /api/v1/points/{spending|earnings} |
| Figure | Value |
|---|---|
| Counted net buying | 10 ETH |
| Trade fee, 200 bps — Spending’s fee value | 0.2 ETH |
| Creator 3,000 bps + KOLs 4,000 bps + referrer 1,000 bps of the 3,000 bps ledger leg — Earnings’ fee value | 0.06 + 0.08 + 0.006 = 0.146 ETH |
| Split | 57.8% Spending · 42.2% Earnings |
| Day | Trade | Close | High | Counts as spending |
|---|---|---|---|---|
| 1 | Buy 1 ETH | 1 | 1 | +1 |
| 2 | Sell for 1 ETH | 0 | 1 | 0 |
| 3 | Buy 1 ETH | 1 | 1 | 0 |
| 4 | Buy 1 ETH | 2 | 2 | +1 |
| 5 | Buy 1 ETH, sell for 1 ETH | 2 | 2 | 0 |
// lib/points/net-buying.ts
net = Σ ETH paid on buys − Σ ETH received on sells and redeems // per wallet, per launch
close = net at the end of the UTC day
spending = max(0, close − highest earlier close) // the high never fallsPremium, and why the payment is burned#
| Property | Value |
|---|---|
| Currency | THD, and nothing else. The price and the reward are then in the same unit, so it needs no oracle. |
| Price | Per calendar month (owner ruling 28-09-2026). The price is PricingRules’ PREMIUM_PASS price in THD, read by premiumMonthlyPriceThd and settable by the owner Safe; a change reaches future purchases only. The opening figure is $4.99 converted once at deploy through the TGE price (targetEthWei ÷ targetThd on the liquidity manager) and the posted ETH/USD — about 1,486 THD at ETH ≈ $2,686. |
| Calibration key | money.premiumMonthlyPriceUsd — $4.99 on production, scaled on dev and staging together with the TGE ETH target, so the THD figure is the same everywhere. |
| Scope | One pass per account per calendar month; it ends at the next 1st, 00:00 UTC. buyPremiumAtMost buys for msg.sender and nobody else — gifting is refused by design. |
| What it grants | A 1.25× points multiplier, applied off chain, and a mall discount. Nothing else. |
The price is pro-rated by the days left in the month and rounds down, in the buyer’s favour:
// Battlepass.quotePremium
price = premiumMonthlyPriceThd * daysRemaining * (10_000 - discountBps)
/ (daysInMonth * 10_000);Waiting therefore does not save money, it buys less. The one approved milestone discount is 5,000 bps — 50% off one month’s pass, at day 30, proved against a second Merkle root published per season, spent once a season, and reserving nothing.
Because the payment is destroyed, premium is deflation, not revenue. It never reaches totalProtocolFeesReceived and it unlocks no THD. Trading contributes fees, which the meter counts; buying premium contributes burn, which the meter never sees. The two are not interchangeable and must never be added together. See THD and the fee meter.
Claiming, and the 25% late penalty#
Rewards are committed as one cumulative Merkle root per season, republishable as often as daily. Per-account progress is a running total in claimedOf[seasonIndex][account], and a claim pays the delta — which is what makes the root republishable without a per-claim bitmap. The leaf is keccak256(abi.encode(seasonIndex, account, cumulative)).
| When | Paid | Burned |
|---|---|---|
| Before the season ends | The whole delta | Nothing |
| Within the 30-day back-claim window | 75% | 25% — BACK_CLAIM_BURN_BPS = 2500 |
| After the deadline | Nothing — claim reverts | The whole residue — burnExpired(season), callable by anyone |
| Rule | Value | Refusal |
|---|---|---|
| THD claim gate | Passed once: the minimum earned or spent on the platform, and X connected | ClaimGateNotPassed(account, claimGate) |
| Vest | None | — |
| Unclaimed after the back-claim | Burned — totalSupply() falls | SeasonWindowClosed(season, closesAt, now) |
totalThdBurned() is the three burn counters combined — totalPremiumBurned, totalLateClaimBurned and totalExpiredBurned. A root may never allocate more than the contract holds: publishRewards checks the new outstanding against available() plus the season’s existing outstanding, so an over-allocation reverts at publication rather than at the last claimant.
The owner Safe publishes both roots itself. There is no keeper role on the battlepass.
The item mall#
The mall sells typed items whose effect must be one of six — Cosmetic, Access, Qualifier, Credit, Consumable, Boost. There is no seventh, and adding one is the change the contract exists to refuse. Delivery is a row in a balance mapping plus an event: no hook, no callback, no delegatecall.
| Receipt | Currency | Destination | A protocol fee? |
|---|---|---|---|
| Platform shop sale | THD only | Burned, with the supply re-read | No — supply destroyed, nothing received |
| Third-party shop opening fee | 1 ether on production — the SHOP_OPENING price in PricingRules, owner-settable since 26-09-2026 | marketingTreasury, direct | No — never the splitter |
| Third-party shop sale | The shop’s own launched token. THD can never be one — no launch produced THD | The shop’s stored payout, 100% | No |
| Creator milestone sale | The launch’s fundraise currency | Split three ways — MILESTONE_CREATOR_BPS 2500 to the creator, MILESTONE_PLATFORM_BPS 2500 to the protocol treasury direct (ProtocolFeeSplitter.treasury() read live), MILESTONE_SEASON_BPS 5000 to SeasonRewardsPool | No — never the splitter, in any currency (ruled 28-09-2026) |
The three milestone legs each round down and the remainder is carried into the next sale in the same currency rather than swept or re-attributed; sweepStrandedEth subtracts the carried dust so a sweep can never take it.
How a mall price is computed#
ItemPricing is not a bonding curve, not a decay and not an oracle. It is a fixed unit price with exactly one discount, and it is stateless — the mall calls it as a staticcall, and a module is swapped rather than edited.
// ItemPricing.quote — multiply first, divide once
total = unitPrice * quantity * (10_000 - discount) / (scale * 10_000);One truncation, at the end, in the buyer’s favour; the remainder is never charged. scale is 1 for a countable item and 10 ** decimals for a token package.
| # | Condition | Discount applied | rule |
|---|---|---|---|
| 1 | milestoneDiscountBps > 0 | The milestone’s, alone. Premium is not consulted. | MILESTONE |
| 2 | Buyer is premium and the listing sets a premium figure | The listing’s premium figure | PREMIUM |
| 3 | Otherwise | None | LIST |
Taking the larger of two discounts was considered and rejected: adding a discount that stacks is not a pricing change, it is a policy change.
The mall’s discount bounds#
| Constant | Value |
|---|---|
PLATFORM_PREMIUM_DISCOUNT_BPS | 2500 — 25% off in the platform shop |
MIN_SHOP_PREMIUM_DISCOUNT_BPS · MAX_SHOP_PREMIUM_DISCOUNT_BPS | 1000 · 3000 — a creator’s own figure, which replaces the platform’s 25% rather than stacking with it |
PREMIUM_PREVIEW_WINDOW | 7 days — sight only. Premium never buys earlier, only sooner-seen. |
MILESTONE_WALLET_CAP_BPS | 200 — 2% of supply per wallet, matching BondingCurve’s early wallet cap but permanent rather than time-limited |
MIN_FLASHBACK_VEST | 90 days |
If premiumRegistry is unset, nobody is premium and everybody pays list price — an unset pointer degrades to the undiscounted price rather than to a free one.
Three things that must never be added#
Both contracts carry the same three prohibitions in their own source, and they are design rules rather than implementation notes:
| Never | Why |
|---|---|
| A reward that grants points directly | Points already drive the daily share, the season standing and the band contribution. A reward denominated in points pays for the same behaviour twice. A tier jump is the same thing under another name, and was removed once already. |
| A bigger season-standing share for premium | Zero-sum — it moves shares without growing the pot — and it stacks with the 1.25× multiplier into a compounding advantage. |
| Premium-only milestones | The 3/7/14/30 ladder is the retention funnel for new users. Gating it behind payment removes the mechanic from the only cohort it exists for. |
The 1.25× multiplier is the single permitted exception to activity-weighted rewards, and it is permitted because the payment is burned and it is capped: its value to the buyer decays to zero as adoption rises, at which point the whole cohort has burned THD for nothing but the burn. It is not a precedent for a second lever.