Runbook: Platform Fee Operations¶
Spec 060. Operating the unified platform-fee system: the FeeRouter contract (per network),
the AdminPanel Fees tab, and the relay-gateway's on-chain rate read for Predict.
Roles: rate changes need FEE_ADMIN_ROLE; treasury changes and service registration need
DEFAULT_ADMIN_ROLE (floppy-keystore flow for cold admin keys). The Fees tab uses the connected
wallet — on-chain roles are the enforcement, the UI only pre-validates.
View current fees¶
- Operations control plane → Fees (visible with ADMIN or FEE_ADMIN).
- The tab shows: every registered service (live bps, hard cap, enforcement point), the
network's fee treasury, the Polymarket bps the gateway is actually serving (with its
source), and the OpenSea referral status (display-only, never a member cost). - Cross-check the gateway directly:
curl -s $GATEWAY/status | jq .fees— expectpolymarket.source == "chain"on a healthy Polygon setup."env-fallback"means the router is unset or unreadable (see Diagnostics).
Change a fee rate¶
- Fees tab → Change a fee rate → pick the service, enter the new bps (0..cap), confirm the
wallet transaction. The contract refuses above-cap rates (
CapExceeded). - Verify (all three within ~1 minute):
- the tab's table shows the new rate and a new Change history row (you, old → new);
- the member surface quotes it (Earn: open a vault deposit review — the fee line shows the
new rate; Predict:
curl -s "$GATEWAY/v1/polymarket/137/fee-rate?token_id=<id>"shows the newbuilderTakerFeeBpsonce the ~30 s gateway cache turns over); FeeBpsChangedis on the explorer (Fees tab → "Full history on the block explorer").- In-flight member actions are protected: anyone quoted the old rate either pays at most the
quoted rate or their transaction reverts (
FeeAboveQuoted) and they re-review. No follow-up needed.
Emergency-zero a fee¶
Use when a fee is mischarging, a disclosure mismatch is reported, or comms require it.
- Fees tab → set the affected service's rate to 0 → confirm. Effect is immediate for all subsequent actions: no fee transfer, no fee line.
- If the UI is unavailable, from any FEE_ADMIN key: (Cold FEE_ADMIN key: follow the floppy-keystore signing flow, as for any admin action.)
- Verify as in "Change a fee rate". The change is recorded on-chain like any other.
Change the treasury destination¶
- Fees tab → Change the fee treasury (ADMIN only) → enter the new FairWins-controlled
address → confirm. The contract refuses the zero address;
TreasuryChangedis emitted. - Verify the next
FeeChargedevent's transfer lands at the new address. - If a network's treasury was never set (
unset — fees are skippedin the tab): fees on that network are silently zero by design (never lost). Set it, then re-check member quotes still match expectations.
Reconcile treasury receipts¶
Monthly (or on demand):
- Export
FeeChargedevents for the period (explorer CSV, orfeeRouter.queryFilter(feeRouter.filters.FeeCharged(), fromBlock, toBlock)). - Sum
feeAmountper asset; compare against the treasury address's incoming ERC-20 transfers from the FeeRouter for the same period. They must match exactly — everyFeeChargedequals one same-tx transfer. - Any mismatch is an incident: freeze rate changes, capture the diverging txs, escalate per the
security process. (The contract's invariant makes divergence impossible without a bug or an
unexpected upgrade — check
feeRouterImplagainstdeployments/.) - Polymarket builder-fee revenue arrives via Polymarket's builder program, not the treasury — reconcile it from Polymarket's builder dashboard as in the Predict runbook.
Diagnostics: disclosure/charge mismatch or wrong source¶
Symptoms: member reports a fee different from the confirm screen; Predict shows
source: "env-fallback" unexpectedly; Earn deposits blocked with "fee rate could not be
confirmed".
- Which layer? Earn quotes read the chain directly from the browser; Predict reads the
gateway.
curl -s $GATEWAY/status | jq .feesand compare with the Fees tab (direct chain read). source: "env-fallback":FEE_ROUTER_ADDRESSunset on the gateway → set it (or redeploy with a deployments record containingfeeRouter); the boot log fails loudly if it contradicts the record;- Polygon RPC trouble → check the gateway's chain health in
/status.chains; the reader serves the last good value up to 10× the TTL, then falls back to env bps (which are the spec-057-capped defaults — never higher than disclosed caps). - Earn "deposits paused": the browser could not read the router on a network that has one — almost always RPC. Confirm with the Fees tab on the same network; deposits self-restore when the read succeeds. This is fail-safe behavior (never show an understated rate), not an outage of member funds; withdrawals are unaffected.
- A member charged above the shown rate should be impossible (
maxFeeBps+ caps). Treat any credible report as a security incident: emergency-zero the service, capture the tx hash, compare the tx'sFeeChargedargs against the quoted rate, escalate.
Registering a new service¶
Engineering-led — see the developer guide (platform-fees.md). Operations involvement: confirm the cap, set the initial rate (it registers at 0), and add the service to fee reconciliation.