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.

Version
1.0
Updated
2026-09-21
Source
THD contract sources · THD — Full Plan v1.43

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 viewValue
MONTHS_PER_SEASON · SEASONS_PER_YEAR3 · 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_WINDOW30 days — 2,592,000 seconds, after seasonEnd
claimDeadline(index)seasonEnd(index) + 30 days

The two points boards: Spending and Earnings#

Two daily boards — both live before TGE, both paying from TGE
BoardRanks onLeaderboard
SpendingNet bonding-curve buying per launch plus prediction-market stakes, in any curve or market currency/leaderboard?board=spending
EarningsThe 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
The inputs, each at event-time ETH value
InputWhat counts
SpendingNet bonding-curve buying (below) and prediction-market stakes in full
EarningsThe 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 earningsTrading profit, LP income, prediction winnings, settler THD and wheel prizes. Markets pay no fee split to a user
UnpricedAn amount with no posted price in force at its event is left out and counted, never taken at face value
Check-inA 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
Rank, season score and pot
RuleAs built
Daily rankPoints, 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 boardsSplit 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 boardPro rata by points
Frozen daysEach closed UTC day is frozen once, with every event it read, and served at /api/v1/points/{spending|earnings}
The pot split, worked from the shared fee parameters
FigureValue
Counted net buying10 ETH
Trade fee, 200 bps — Spending’s fee value0.2 ETH
Creator 3,000 bps + KOLs 4,000 bps + referrer 1,000 bps of the 3,000 bps ledger leg — Earnings’ fee value0.06 + 0.08 + 0.006 = 0.146 ETH
Split57.8% Spending · 42.2% Earnings
Net curve buying — per wallet, per launch
DayTradeCloseHighCounts as spending
1Buy 1 ETH11+1
2Sell for 1 ETH010
3Buy 1 ETH110
4Buy 1 ETH22+1
5Buy 1 ETH, sell for 1 ETH220
// 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 falls

Premium, and why the payment is burned#

Premium — the per-season pass
PropertyValue
CurrencyTHD, and nothing else. The price and the reward are then in the same unit, so it needs no oracle.
PricePer 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 keymoney.premiumMonthlyPriceUsd — $4.99 on production, scaled on dev and staging together with the TGE ETH target, so the THD figure is the same everywhere.
ScopeOne 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 grantsA 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)).

What a claim pays, by when it is made
WhenPaidBurned
Before the season endsThe whole deltaNothing
Within the 30-day back-claim window75%25% — BACK_CLAIM_BURN_BPS = 2500
After the deadlineNothing — claim revertsThe whole residue — burnExpired(season), callable by anyone
Every battlepass claim, since 28-09-2026
RuleValueRefusal
THD claim gatePassed once: the minimum earned or spent on the platform, and X connectedClaimGateNotPassed(account, claimGate)
VestNone—
Unclaimed after the back-claimBurned — totalSupply() fallsSeasonWindowClosed(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.

Three shop kinds, three currencies, three destinations
ReceiptCurrencyDestinationA protocol fee?
Platform shop saleTHD onlyBurned, with the supply re-readNo — supply destroyed, nothing received
Third-party shop opening fee1 ether on production — the SHOP_OPENING price in PricingRules, owner-settable since 26-09-2026marketingTreasury, directNo — never the splitter
Third-party shop saleThe shop’s own launched token. THD can never be one — no launch produced THDThe shop’s stored payout, 100%No
Creator milestone saleThe launch’s fundraise currencySplit 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 SeasonRewardsPoolNo — 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.

Precedence, not max() — exactly one discount ever applies
#ConditionDiscount appliedrule
1milestoneDiscountBps > 0The milestone’s, alone. Premium is not consulted.MILESTONE
2Buyer is premium and the listing sets a premium figureThe listing’s premium figurePREMIUM
3OtherwiseNoneLIST

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#

ConstantValue
PLATFORM_PREMIUM_DISCOUNT_BPS2500 — 25% off in the platform shop
MIN_SHOP_PREMIUM_DISCOUNT_BPS · MAX_SHOP_PREMIUM_DISCOUNT_BPS1000 · 3000 — a creator’s own figure, which replaces the platform’s 25% rather than stacking with it
PREMIUM_PREVIEW_WINDOW7 days — sight only. Premium never buys earlier, only sooner-seen.
MILESTONE_WALLET_CAP_BPS200 — 2% of supply per wallet, matching BondingCurve’s early wallet cap but permanent rather than time-limited
MIN_FLASHBACK_VEST90 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:

NeverWhy
A reward that grants points directlyPoints 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 premiumZero-sum — it moves shares without growing the pot — and it stacks with the 1.25× multiplier into a compounding advantage.
Premium-only milestonesThe 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.