Docs·Metrics reference
Metrics reference
THD and the fee meter
The protocol fee meter, the unlock it drives, the eleven buckets, and why an unlock percentage is meaningless without its denominator.
protocolFeeMeterWei — the meter#
| unit | wei |
|---|---|
| derivation | ProtocolFeeSplitter.totalProtocolFeesReceived() |
| source | contracts/src/launchpad/ProtocolFeeSplitter.sol |
| freshness | Live read. Monotonic — two write sites, both +=, no setter, no delete. |
| coverage | FULL, but incomplete as a revenue figure. See below. |
| id | Read | Why it must be visible |
|---|---|---|
protocolFeeLifetimeWei | lifetimeFeesReceived() | THE revenue figure. Every wei from every armed payer, ever. No ceiling throttles it and finalise() does not freeze it. |
protocolFeeRefusedByCeilingWei | totalRefusedByCeiling() | Arrived, paid out in full, refused by the count for good. Monotonic. Replaced protocolFeePendingWei on 21-09-2026. |
protocolFeeDailyWei | dailyFeesWei(day), keyed timestamp / EPOCH_SECONDS | Armed-payer receipts per UTC day. Drives the Spoils daily release, so it keeps recording after finalise(). |
protocolFeeUncountedWei | totalUncounted() | Wei from unregistered senders, or received after finalise(). Never counted, by design. |
protocolFeeCountedRemainingToday | countedRemainingThisEpoch() | How much of today’s ceiling is left. |
protocolFeeFinalised | finalised() | One-way; every later receipt routes uncounted. |
Both figures are monotonic and neither has a downward path — no setter, no delete, and no write-down function since discardPending(uint256,string) was removed on 21-09-2026. A published revenue figure that fell would be a bug in the publisher, never a fact about the chain.
Each receipt is one FeeRouted(from, counted, amount, toLiquidity, toTreasury, meterAfter, label), and meterAfter is the meter at that log — which makes the meter’s whole history re-derivable without a single archive state read. That matters on a chain whose public RPC keeps only 6,555 blocks of state — about thirty minutes on testnet 46630 and about eleven on mainnet 4663.
The unlock the meter drives#
// THDEmissionControl.totalUnlocked()
effectiveFees = min(feeSplitter.totalProtocolFeesReceived(), FEES_TO_FULL_EMISSION);
unlocked = effectiveFees × 1_000_000_000e18 / FEES_TO_FULL_EMISSION;
thdUnlockBps = unlocked × 10_000 / 1_000_000_000e18;| id | Read |
|---|---|
thdUnlockedBaseUnits | totalUnlocked() |
thdRemainingToUnlock | remainingToUnlock() |
thdFeesToCompletion | feesToCompletion() — ceiling division |
thdEmissionComplete | isComplete() — totalUnlocked() >= TOTAL_SUPPLY |
thdEmissionCompletedAt | emissionCompletedAt() — 0 until stampCompletion() |
thdOutstandingBaseUnits | totalOutstanding() — accrued and unclaimed across the eleven buckets |
There is no per-unlock event. The unlock is a pure function of the meter, so the event that records it is ProtocolFeeSplitter.FeeRouted.meterAfter.
thdSupply — total and circulating#
THD mints its entire supply to the emission contract at genesis and has no mint function, so:
totalSupply = THD.totalSupply() // 1e27 minus every burn
circulating = totalSupply
− THD.balanceOf(emission) // never claimed by a bucket
− Σ THDMerkleDistributor.unclaimedBalance()
− Σ (THDVestedClaim.totalAllocation() − released)
− Σ THDImmediateClaim.claimable()The bucket destinations come from the addresses file’s thdBuckets[]. There is no totalEmitted on the emission contract: total claimed is Σ claimedOf(bucket) over the eleven, or Σ Claimed.amount from logs.
The eleven buckets#
BUCKET_COUNT is 11 on every deployed THDEmissionControl, mainnet included. See the eleven allocations for what each is for and how it pays.
| Bucket | bps | THD |
|---|---|---|
| Traders | 2200 | 220,000,000 |
| Creators | 1500 | 150,000,000 |
| KOLs / Referrers | 1700 | 170,000,000 |
| Liquidity | 1200 | 120,000,000 |
| LP Providers | 1000 | 100,000,000 |
| Spoils | 1100 | 110,000,000 |
| BD / Marketing | 400 | 40,000,000 |
| Ecosystem Grants | 300 | 30,000,000 |
| Insurance Backstop | 300 | 30,000,000 |
| Team | 200 | 20,000,000 |
| Settlers | 100 | 10,000,000 |
A shareBps read on chain that disagrees with the addresses file means the file describes a different deployment, not drift. A bucket whose destination reads as the zero address is a recorded fact — some destinations are deliberately left unset until the Safe that will hold them exists, and those buckets refuse to pay until then.
The THD/ETH pool#
| id | Read |
|---|---|
thdPoolFeesToTreasuryWei | ThdLiquidityManager.totalEthToRouter |
thdPoolFeesFromPoolWei | ThdLiquidityManager.totalEthFromPool |
thdRoutedByPoolBaseUnits | ThdLiquidityManager.totalThdToRouter — sent, not yet destroyed. The burn count is ThdFeeRouter.totalThdBurned, beside the burn. |
Every collection is one FeesRouted(caller, ethToRouter, thdToRouter, router). Both legs go in full to the immutable ThdFeeRouter, which holds them until anybody flushes: the ETH on to thdPoolTreasury, the THD burned or recycled to ThdLpRewards according to the monthly holder vote. The ETH is not a protocol fee, it never passes the splitter, and it must never appear in feesProtocolWei.
The contract publishes an identity every balance figure must satisfy:
address(this).balance == totalEthReceived + totalEthFromPool − totalEthToPool − totalEthToRouterMetering the pool’s fees would let THD trading unlock THD, which is why they are excluded structurally rather than by policy. See fees, by leg.