Docs·Rewards and mechanics

Rewards and mechanics

Bands and seasons

Rosters, ranks and coups on chain with every computation off it; a band supply gated on lifetime revenue; and a quarterly pot that releases half, scaled by turnout.

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

Status#

Both surfaces are funded by the Spoils user pool, through SpoilsSplitter — bands and seasons each take a share of it, alongside the battlepass. See Spoils.

What a band is#

A band is a numbered on-chain roster: who is in it, what rank they hold, and who backed which coup. Three facts and nothing else. A band has no name on chain — the display name is metadata, and a band is a number here. Ids start at 1.

The rule that decides what is in the contract is stated in its own source: an assertion goes on chain, a computation does not. Volume, the four band statuses, the succession walk and the monthly winner are all computed off chain and are deliberately absent from storage.

Who may write, and what each writer can reach
WriterCan
The account itselfcreateBand, join, leave, optIn
The band’s leadersetRole, kick, handOverLeadership — for their own band only
The resolverresolveCoup, resolveHostileWin and concludeLostRace, and nothing else. It cannot open a coup, cannot opt anybody in, cannot touch a band with no open coup, cannot appoint a leader out of nowhere and cannot move a single wei.

Gas may be sponsored: BandRelay.executeSigned accepts an EIP-712 signature under the “The Hood Bands” domain (verifying contract: the relay, which BandRegistry deploys), with no relayer allowlist — an opt-in is ActionKind.OptIn, with the coup in bandId and the side in value. Opening a coup is the one action with no sponsored form — a sponsor able to pay would decide who may open one.

Founding, joining and every hostile coup action carry a ForceAttestor attestation for the caller and the band, issued only when the caller holds a force — for a band action, its leader’s force. An unaligned account cannot found; nobody joins or raids a band of another force.

How many bands exist#

How many bands may exist at all is gated on the platform’s cumulative revenue, derived live and never stored:

// the cap createBand enforces (derived off chain since 27-09-2026)
if (lifetimeFeesWei < BAND_UNLOCK_FEES_WEI) return 0;             // 25 ether
return OPENING_BAND_CAP                                           // 10
     + (lifetimeFeesWei - BAND_UNLOCK_FEES_WEI)
       / FEES_PER_ADDITIONAL_BAND_WEI;                            // 5 ether
The source's own worked schedule
Lifetime protocol feesBands
Below 25 ETH0
25 ETH10
50 ETH15
150 ETH35
1,000 ETH205
2,000 ETH405

Ranks, tithes and caps#

Nine officers, and 20% of a band's share off the top
RankSlotsTithe each
Leader1, by construction6%
Vice-Leader1 — MAX_VICE_LEADERS4%
Director3 — MAX_DIRECTORS2%
Executive4 — MAX_EXECUTIVES1%
MemberThe rest, to MAX_MEMBERS = 50 including the leaderNone

The slot caps are what make a per-officer rate safe: nine fixed seats mean the tithe total cannot grow with the band. The tithes themselves are applied off chain, in the root — this contract holds the ranks, not the money. Each tithe is prorated by the UTC days the rank was held that month: a rank change counts from the UTC day of its block, and the rank held at a day’s close is that day’s rank.

Five time constants, and the asymmetry between two of them
ConstantValueWhat it delays
KICK_GRACE24 hoursA leader may not remove a member until this has elapsed since they joined.
REJOIN_COOLDOWN24 hoursAfter voluntarily leaving, before joining or founding any band. A kicked member has no cooldown — the asymmetry is stored rather than branched. It applies to founding as well as joining, because otherwise leave-then-found is the free route around it.
OPT_IN_WINDOW24 hoursHow long a coup accepts opt-ins.
SCORING_WINDOW7 daysHow long volume counts after opt-in closes. A coup is therefore resolvable 8 days after it opens.
COUP_COOLDOWN30 daysA rolling window from the previous coup’s resolution, before the same band may be couped again.

A leader cannot leave — they must hand over first. Joining, kicking and handing over are all refused while a coup is open on the band or a hostile race runs on it, and a leaver who opened the band’s open coup voids it: nobody is removed and the fee is not refunded.

Coups#

The coup fee is coupFeeWeiPerMember × memberCount, and msg.value must equal it exactly — overpayment is refused, because ETH in this contract has no way back out. The per-member figure is settable storage rather than a constant, because it is ETH-denominated and drifts; the design figure is 0.0005 ETH per member, which is 0.025 ETH against a full 50-member band.

The whole fee is forwarded to ProtocolFeeSplitter in the same transaction, using the measured msg.value and never address(this).balance — forwarding a balance would let anyone donate ETH and buy THD emission one-for-one, which is the hole closed on 23-08-2026.

Internal and hostile

InternalHostile
Who takes partMembers of the bandAddresses in no band, outside the 24-hour rejoin cooldown
At once on one bandOne — may open over a running hostile race, and suspends itAny number — a race
Carries whenBackers’ volume > leader’s + opposition’sFirst coup whose counted volume reaches hostileVolumeBarWei() (10 ETH on production), with a full counted party of HOSTILE_MIN_TAKERS() = MAX_HOSTILE_TAKERS() = 10
Registrations per coupThe rosterUp to MAX_HOSTILE_REGISTRATIONS() (20): the party of 10 and 10 in reserve. An excluded registration’s slot passes to the next in line. One live coup per account per race
On successThe initiator leadsThe initiator leads, the party joins. Nobody is removed: the deposed leader and every officer become Members
On failureEvery backer is removed, as a kick — no rejoin cooldownNobody is removed; the fee is kept

Hostile coups — the three rulings of 27-09-2026

RuleWhat happensEnforced by
ForceChecked when you view the coup. Another force — or none — sees it but cannot register or open one. Switching after registering does not cancel it; the ordinary wrong-force rules then apply in the band.The page, and the resolver: a wrong-force registration counts toward neither takers nor volume and is not seated
Rejoin cooldownNever dropped. Inside it you cannot open a hostile coup, register for one, join or found a band.BandRegistry — RejoinCooldownActive from membershipOf(account).rejoinAllowedAt
RaceThe first coup to reach the bar, measured by block and log index, takes the band. Every rival of that round concludes as lost. Fees are not refunded.resolveHostileWin with the crossing point, published in HostileRaceWon; concludeLostRace for the rivals

Party, suspension and force — the rulings of 27-09-2026

RuleWhat happensEnforced by
Party of tenA win seats exactly 10. Nobody is removed: the deposed leader and every officer become Members. The band may reach 60 (50 + 10); joins are refused until it is back under 50.BandRegistry — HOSTILE_MIN_TAKERS = MAX_HOSTILE_TAKERS = 10; resolveHostileWin takes the officer list and refuses NotAnOfficer / OfficersNotAllDemoted
Internal coup suspends the raceAn internal coup may open over a running hostile race and holds it. Failed or voided: the race resumes with each coup’s remaining time. Carried: the race ends and every rival concludes as lost.BandRegistry — HostileRaceSuspended; deadlines = stored + pausedTotal − pauseBase
No force, no bandAn account on no force cannot found a band. A band whose leader holds no force accepts no hostile registration. If the leader’s force changes, the band follows and members on the other force get the 7-day wrong-force window.The app — force is not readable on chain — and the resolver, which refuses every registration against a band with no force

An ERC-20 trade counts at the posted price in force at its own block; with no price at that block it is excluded and the page shows how many trades were not counted — never face value.

What a band is paid#

BandRewardsDistributor performs no comparison, holds no volume, knows nothing about forces and cannot name a band. It holds the money and pays what a proof proves. The formula it implements off chain:

period  = a UTC calendar month; the first starts with Spoils (seededAt + 24h) — pre-TGE volume never counts
volume  = curve buys and sells + ETH stakes on settled markets, at event-time value, platform addresses excluded
banded  = the leg's account was in a band at that leg, and the leg's force was the band leader's force then
contestants = every force whose bands generated banded volume that period (> 0)
winner  = the contestant with the most band volume over the period
payout  = the pot × the WEAKEST CONTESTANT's share of the period's banded volume
fewer than two contestants, or a tie at the top = nothing paid, the whole pot rolls
loser   = nothing
tithes  = Leader 6% · Vice 4% · 3 Directors 2% · 4 Executives 1%   (20%, volume-gated)
          each prorated: rate × UTC days held in that rank ÷ days in the month; the rest of the slice goes to the members
the rest = split by member volume across the winning force's banded members
leaves  = one per member per month, keccak256(abi.encode(uint32 monthIndex, account, that month's THD)) — never a leader's to pass on

“The reward is the strength of the opposition”: winning against nobody pays nothing, because the pot released is sized by the weakest contestant. A force with no banded volume in the period is not a contestant, so a dormant force cannot hold the pot at zero for everyone else.

The distribution mechanism
PropertyValue
Root shapeOne root per calendar month, published by the owner Safe once the month has ended: publishMonth(monthIndex, root, allocated). Leaf is keccak256(abi.encode(monthIndex, account, amount)), where the amount is the account’s figure for that month. monthIndex = year × 12 + (month − 1).
VestLinear over vestDuration() — 90 days — from the month’s first publication. A re-publication keeps that moment.
Claim windowclaimWindow() — 360 days — from the month’s first publication. After it, the month’s allocated, unclaimed THD is burned by burnExpired(monthIndex), which anyone may call.
Claim gatePassed once — the minimum earned or spent on the platform, and X connected — or a claim reverts ClaimGateNotPassed(account, claimGate).
Refusals on publicationMonthNotEnded, MonthClaimWindowClosed, AllocationBelowClaimed and AllocationExceedsFunding — a month may promise no more than the unreserved balance plus its own outstanding.
ClaimingPermissionless, and always pays account rather than the caller: claim(monthIndex, account, amount, proof) pays what has vested minus claimedOf[monthIndex][account].
The remainderBetween 50% and 95% of the pot every month, by construction — and the unallocated part is never burned. It is next month’s pot. Only allocated-and-unclaimed THD burns at a deadline.
reserved()            =   Σ over published months (allocated − claimed)
reserved()           <=   thd.balanceOf(this)
unallocatedBalance()  =   balance − reserved()

Seasons#

A season is a real calendar quarter — 1 January, 1 April, 1 July, 1 October, 00:00:00 UTC. There is no duration constant in seconds; the index is computed on chain as year × 4 + (quarter − 1) by the same civil-date routine ProtocolFeeSplitter uses, copied verbatim so the two calendars cannot disagree.

Two pots, one leaderboard
PotSourceArrives as
THDThe SpoilsSplitter’s seasons shareAn ordinary ERC-20 transfer
Real currencyProtocolFeeSplitter’s opex gate — a share of treasury revenue above a monthly threshold. Production supplies seasonShareBps 2500 and opexThresholdWei 25 ETH.ETH through receive()

The release formula#

// SeasonRewardsPool.releaseFrom
scaled   = min(qualifiers, QUALIFIER_TARGET);                   // 100
released = pool * RELEASE_SHARE_BPS * scaled                    // 5000
         / (BPS_DENOMINATOR * QUALIFIER_TARGET);

// i.e.  pool x 50% x min(1, qualifiers / 100)

Neither constant is settable and neither has a setter. The 50% is smoothing — it is what makes a season a spend-down rather than an emptying. The 100 is the same number as the scoring table’s top 100, not a second constant.

qualifiers is supplied by the owner with the root, and is the off-chain count of accounts that scored on at least 10 days of the quarter. Zero is refused. Both pots use the same count and the same formula, so a thin quarter releases proportionally less of both and the rest rolls forward.

A per-participant share is off chain entirely: the season score is Σ max(0, 101 − that day’s rank) over the quarter’s daily standings, committed in a root whose leaf is keccak256(abi.encode(seasonIndex, account, thdCumulative, ethCumulative)). Claiming is permissionless and always pays account, settling both pots at once.

What the owner Safe can and cannot do
AuthorityReach
CanPublish a season root, repoint the treasury within TreasuryRegistry, and withdraw the unreserved pool.
CannotPay an account the published root does not name, publish a season that has not ended, re-publish a season after its THD claim window (SeasonThdClaimWindowClosed), or withdraw a reserved entitlement — withdrawals are bounded by availableThd() and availableEth().
The trust surfaceA root can be re-published after claims, which means an entitlement nobody has claimed yet can be lowered. That is stated in the source as a trust surface, not hidden as a bug.
Vest, deadline and gate — band months and season THD, since 28-09-2026
PotVestClaim windowUnclaimed after itClaim gate
Band month THD90 days from the month’s first publication360 days from itBurned — burnExpired(monthIndex)Required
Season THD90 days from the season’s first publication360 days from itBurned — burnExpiredThd(seasonIndex)Required
Season ETH and other currenciesNoneNoneStays claimable, stays reservedNot checked
Unallocated THD in either——Never burned — the next period’s pot—