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.
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.
| Writer | Can |
|---|---|
| The account itself | createBand, join, leave, optIn |
| The band’s leader | setRole, kick, handOverLeadership — for their own band only |
| The resolver | resolveCoup, 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| Lifetime protocol fees | Bands |
|---|---|
| Below 25 ETH | 0 |
| 25 ETH | 10 |
| 50 ETH | 15 |
| 150 ETH | 35 |
| 1,000 ETH | 205 |
| 2,000 ETH | 405 |
Ranks, tithes and caps#
| Rank | Slots | Tithe each |
|---|---|---|
| Leader | 1, by construction | 6% |
| Vice-Leader | 1 — MAX_VICE_LEADERS | 4% |
| Director | 3 — MAX_DIRECTORS | 2% |
| Executive | 4 — MAX_EXECUTIVES | 1% |
| Member | The rest, to MAX_MEMBERS = 50 including the leader | None |
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.
| Constant | Value | What it delays |
|---|---|---|
KICK_GRACE | 24 hours | A leader may not remove a member until this has elapsed since they joined. |
REJOIN_COOLDOWN | 24 hours | After 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_WINDOW | 24 hours | How long a coup accepts opt-ins. |
SCORING_WINDOW | 7 days | How long volume counts after opt-in closes. A coup is therefore resolvable 8 days after it opens. |
COUP_COOLDOWN | 30 days | A 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
| Internal | Hostile | |
|---|---|---|
| Who takes part | Members of the band | Addresses in no band, outside the 24-hour rejoin cooldown |
| At once on one band | One — may open over a running hostile race, and suspends it | Any number — a race |
| Carries when | Backers’ volume > leader’s + opposition’s | First 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 coup | The roster | Up 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 success | The initiator leads | The initiator leads, the party joins. Nobody is removed: the deposed leader and every officer become Members |
| On failure | Every backer is removed, as a kick — no rejoin cooldown | Nobody is removed; the fee is kept |
Hostile coups — the three rulings of 27-09-2026
| Rule | What happens | Enforced by |
|---|---|---|
| Force | Checked 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 cooldown | Never dropped. Inside it you cannot open a hostile coup, register for one, join or found a band. | BandRegistry — RejoinCooldownActive from membershipOf(account).rejoinAllowedAt |
| Race | The 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
| Rule | What happens | Enforced by |
|---|---|---|
| Party of ten | A 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 race | An 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 band | An 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.
| Property | Value |
|---|---|
| Root shape | One 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). |
| Vest | Linear over vestDuration() — 90 days — from the month’s first publication. A re-publication keeps that moment. |
| Claim window | claimWindow() — 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 gate | Passed once — the minimum earned or spent on the platform, and X connected — or a claim reverts ClaimGateNotPassed(account, claimGate). |
| Refusals on publication | MonthNotEnded, MonthClaimWindowClosed, AllocationBelowClaimed and AllocationExceedsFunding — a month may promise no more than the unreserved balance plus its own outstanding. |
| Claiming | Permissionless, and always pays account rather than the caller: claim(monthIndex, account, amount, proof) pays what has vested minus claimedOf[monthIndex][account]. |
| The remainder | Between 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.
| Pot | Source | Arrives as |
|---|---|---|
| THD | The SpoilsSplitter’s seasons share | An ordinary ERC-20 transfer |
| Real currency | ProtocolFeeSplitter’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.
| Authority | Reach |
|---|---|
| Can | Publish a season root, repoint the treasury within TreasuryRegistry, and withdraw the unreserved pool. |
| Cannot | Pay 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 surface | A 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. |
| Pot | Vest | Claim window | Unclaimed after it | Claim gate |
|---|---|---|---|---|
| Band month THD | 90 days from the month’s first publication | 360 days from it | Burned — burnExpired(monthIndex) | Required |
| Season THD | 90 days from the season’s first publication | 360 days from it | Burned — burnExpiredThd(seasonIndex) | Required |
| Season ETH and other currencies | None | None | Stays claimable, stays reserved | Not checked |
| Unallocated THD in either | — | — | Never burned — the next period’s pot | — |