feat(indexer): actor-joined priced fees with MM-registry cohort split #1211
Labels
No labels
agent:fix_bugfix
agent:fix_conflicts
agent:fix_security
agent:gap_analysis
agent:implement
agent:implement
agent:implement
agent:open_issues
agent:ready
agent:research
agent:security_audit
agent:verify
architecture
backend
blocker:hybrid
blocker:launch
blocker:limit-orders
blocker:v2
block:log_only
block:security
bug
ci
contracts
correctness
deploy
dev
devops
docs
documentation
duplicate
e2e
enhancement
epic
feature
frontend
functional-completion
gas
good first issue
governance
help wanted
high-risk
hooks
hybrid
indexer
infra
infrastructure
integrators
invalid
launch-blocker
limit-orders
localnet
localterra
low priority
missing-implementation
needs-design
ops
performance
priority
high
priority
medium
product
qa
QA
question
ready
ready
research
scripts
security
security-hardening
smartcontracts
tech-debt
testing
ux
UX
v2
verification
wontfix
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
code/cl8y-dex-terraclassic#1211
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Join each priced
protocol_fee_eventsrow to the fee-paying actor, classify that actor against the fee-discount registry as-of the fee’s block, and expose a privacy-safe cohort split (retail/mm/unknown) onGET /api/v1/protocol/fees.Today the fee API is source × token only. Rows have
tx_hashand stampedfee_usd, but no actor.traders.tier_iddefaults to 0 for never-registered wallets — the same integer as governance Tier 0 (MM). A naive live join would mis-label retail as MM and rewrite history on deregister.#1202 does not block this issue. Source-to-fee client events stay on privacy hold; do not expand #1202 and do not put
campaign_idor wallets on fee rows. Prefer landing after #1209 sopair_creationparticipates. SKU invoices (#1210) may stayunknownuntil that stream exists.Design/home: #1213. Closed marketing tracker: cl8y-marketing#5.
Given / When / Then
Given priced protocol fee rows with an attributable fee payer
When ingest tags cohort from the fee-discount registry as-of that block
Then
GET /api/v1/protocol/feesadds aggregateby_cohort(retail/mm/unknown) with priced USD shareAnd unregistered default
tier_id=0is retail, not MMAnd public JSON contains no wallet addresses
And later
DeregisterWalletdoes not rewrite historical cohortsNot a duplicate / already implemented
FeeSourceparsers. This ticket adds actor + cohort, not those sources.traders. Live state is the trap, not the as-of classifier.Not implemented:
protocol_fee_eventshas no actor/cohort column.ProtocolFeesResponsehasby_source+by_tokenonly.ingest_protocol_feenever receives a wallet.Current codebase
Protocol fees have no actor
protocol_fee_events(20260821120000_protocol_fees.sql):tx_hash,source,ordinal,asset_id,amount_raw,fee_usd. Unique(tx_hash, source, ordinal). No sender / trader / cohort column.FeeEventDraftandinsert_fee_eventpersist those fields only.ingest_protocol_feenever receives the wallet.GET /api/v1/protocol/fees(protocol_fees.rs) is O(1) rollup / 60s cache:by_source+by_token. Noby_cohort. Headline fees must not usetraders.total_fees_paid(PFee-1).Actors exist on other tables
swap_ammtraderwhen present), else wasmsenderswap_events.senderbook_takelimit_order_fills.makerlimit_placelimit_order_placements.ownerwrap/unwrapsenderust1_mint/ust1_redeemsenderpair_creation(after #1209)create_pairsenderRouter hops: pair wasm
senderis often the router. Collapsing all router flow onto the router address would hide retail vs MM.Fee-discount registry is not a safe live join
On-chain (
fee-discount): self-register → tiers 1–9; governanceRegisterWallet→ Tier 0 (MM) and Tier 255 (blacklist). Indexertrader_tracker.rsupdates livetraders.tier_id/registered. Deregister writestier_id = 0, registered = false.Traps:
traders.tier_idDEFAULT 0. Unregistered retail ≠ Tier 0 MM.tradersis current state. Deregister must not re-tag historical fees.Why the new implementation is needed
Retail vs MM fee share cannot be read from
/protocol/feesor fromtraders. Without an actor cohort, MM self-flow and team Tier 0 wallets can be counted as acquired retail fees.Tier 0 is 100% off swap/place, so MM share of those sources may be ~0. The split is still required: exclude MM from retail growth, attribute wrap/window/create fees those wallets still pay, catch fail-closed registry outages, and prevent taker
book_takefrom being credited to the MM maker.Constraints / guardrails
campaign_idon fee rows. No raw wallet in client beacons. Indexer stays fee truth.fee_usdwhere NOT NULL. Unpriced incrementsevent_countbut does not enter USD share. Do not usetraders.total_fees_paidorspread_amount.book_take= taker, not maker. Trusted router: prefer wasmtraderwhen attributable; never treat the router/factory pin as the cohort identity.mmiff as-of height the wallet is registered and tier_id = 0. Self-register 1–9 = retail. Tier 255 = retail for this split (they pay full fee). Unregistered defaulttier_id = 0= retail.RegisterWallet/DeregisterWallethappens later.unknown.protocol_fee_stats_by_cohort(or equivalent). Additive JSON.window=remains24h|7d|30d./route/solve.Relevant files
indexer/src/indexer/protocol_fees.rs,indexer/src/indexer/parser.rsindexer/src/db/queries/protocol_fees.rsindexer/migrations/20260821120000_protocol_fees.sqlindexer/src/db/queries/swap_events.rs,limit_order_fills.rs,limit_order_lifecycle.rsindexer/src/indexer/trader_tracker.rs,indexer/src/db/queries/traders.rsindexer/src/api/protocol_fees.rsindexer/tests/indexer_protocol_fees.rs,indexer_tier_sync.rsfrontend-dapp/src/types/index.ts,components/protocol/ProtocolFeeStats.tsxsmartcontracts/contracts/fee-discount/src/docs/indexer-invariants.md,docs/security-model.mdskills/AGENTS_FEE_DISCOUNT_TIERS.md,AGENTS_INDEXER_FEE_LEDGER_HOME.mdRecommended direction
actor(table above). Storeactor(internal) +cohortenumretail|mm|unknownonprotocol_fee_events(or a 1:1 join table). Public API never selectsactor.mm= registered ∧ tier 0. Persist the tag; do not recompute from livetraderson GET.fee_usd IS NOT NULLper cohort perwindow.share_pctis cohort USD / priced USD (NULL if no priced USD).by_cohort: [{ cohort, amount_usd, share_pct, event_count }]onGET /api/v1/protocol/fees. Invalidwindowstill 400.by_cohortis present; hide idle. Static labels. No wallet chrome. No new nestedcard-glass(C653).(tx_hash, source, ordinal). Do not rewrite non-nullfee_usd.Reject: default
tier_id=0⇒ MM; maker asbook_takeactor; public wallet lists.Acceptance criteria
retail|mm|unknowncohort as-ofblock_height.GET /api/v1/protocol/feesadditiveby_cohortfor24h|7d|30dwith priced USD semantics matchingby_source(idle"0", unpriced activitynull).terra1/ wallet fields for this feature.traders.tier_id = 0classify retail, notmm.RegisterWallettier 0 → subsequent fees from that actor aremmuntilDeregisterWallet; fees before register stay retail/unknown. Historical rows stay put on deregister.retailfor this split.book_takefollows the taker, not the maker.unknown.FeeSourceenum / clienttrack()unchanged. Frontend labels (if shipped) are static; hide-idle.Test plan (functional paths)
swap_amm, priced)retailtraders.tier_iddefault 0,registered=falseretailRegisterWallettier 0, then wrap/create with fee > 0mm; amount 0 → no row (existing fail-closed)book_take)retail(taker)mmif taker is tier 0 registeredlimit_placeby tier 0 MMmmretailretailmm; new fees retail/unknowntrader= retail EOAretail, not routerunknown(do not guess)unknownfee_usdNULL)event_count; omitted from USDshare_pctby_cohortpresent; additive"0"by_source/ Defillama / overviewmake verify-issue-586/#614/#683stay greenTest plan (attack, hack, and abuse)
tier_id=0as MMbook_taketo fillmakersender/traderfrom unreserved contracttraderterra1in/protocolJSONspread_amount, burn tax, gas,hook_fee_amount,traders.total_fees_paid$0SUMof fee eventsVerification criteria
by_cohortkeys, priced vs unpriced, window allowlist, uniqueness.retailpriced row;RegisterWallettier 0 → later wrap or create (if fee > 0) →mm; public GET body has no actor address.docs/indexer-invariants.mdnotes actor-at-ingest + public cohort aggregates.make verify-issue-1211plusmake verify-issue-586/make verify-issue-614.Clarified with Market: #1202 HOLD does not block #1211 CMM fee ledger / MM cohort (P0). Proceed with ready+agent:implement.
Related ingest siblings: #1209 #1210 #1211. Pair-creation implement home is #1209 (not marketing #1). SKU/settings invoices are #1210 (not marketing #4). Design/home epic: #1213.
Clarification (Market sensor, 2026-09-11)
Do not block this issue on dex#1202 privacy DoR.
#1202= SPA S1 client events (Privacy Notice / opt-out / no raw wallets to 3P).#1211= indexer fee ledger / actor-joined priced fees + MM cohort — CMM-collectible fee truth.Prior Ops clarification (2026-09-06): treat
#1211as P0 fee ledger,ready+agent:implement, not gated on#1202. Please keep labels; remove or amend any HARD HOLD wording that ties this to#1202.feat: actor-joined priced fees with MM-registry cohort splitto feat(indexer): actor-joined priced fees with MM-registry cohort splitRepair: removed #1202 HARD HOLD per Market/Ops (2026-09-11). Cohort split stays separate from #1209/#1210 ingest. Default traders.tier_id=0 is not MM. Labels ready+agent:implement kept.
Occupying-runner check: implement labels already present; no extra labels added.
/agent implement
/agent implement
/agent implement
Verification (2026-09-24): keep #1211 OPEN. The current fee migration has no actor/cohort, FeeEventDraft and ingest_protocol_fee do not carry a payer, GET exposes by_source/by_token but no by_cohort, and there is no verify-issue-1211 target. No newer issue replaces this cohort work: #1269 is closed uniqueness work; #1213 is the closed design/home.
Remaining:
Related: #1209 / #1210 source work; #1213 home; #1269 unique key; #364 live tier projection; #285 emitter trust; #1202 privacy boundary (not a blocker); #586 / #613 / #614 / #683 existing fee regression checks.