feat(indexer): actor-joined priced fees with MM-registry cohort split #1211

Open
opened 2026-09-06 12:05:13 +00:00 by ops-worker · 9 comments
Member

Summary

Join each priced protocol_fee_events row 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) on GET /api/v1/protocol/fees.

Today the fee API is source × token only. Rows have tx_hash and stamped fee_usd, but no actor. traders.tier_id defaults 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_id or wallets on fee rows. Prefer landing after #1209 so pair_creation participates. SKU invoices (#1210) may stay unknown until 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/fees adds aggregate by_cohort (retail / mm / unknown) with priced USD share
And unregistered default tier_id=0 is retail, not MM
And public JSON contains no wallet addresses
And later DeregisterWallet does not rewrite historical cohorts

Not a duplicate / already implemented

Issue Why it is not this ticket
#1213 (closed) Design/home. No ingest.
#1209 / #1210 New FeeSource parsers. This ticket adds actor + cohort, not those sources.
#1202 SPA S1 events (privacy). Not a gate. Do not expand it.
#364 Registry sync into live traders. Live state is the trap, not the as-of classifier.
#514 / #597 Tier ladder / MM bot. Out of scope to implement here.
#586 / #683 Existing census / economic USD. Must stay green.

Not implemented: protocol_fee_events has no actor/cohort column. ProtocolFeesResponse has by_source + by_token only. ingest_protocol_fee never 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.

FeeEventDraft and insert_fee_event persist those fields only. ingest_protocol_fee never receives the wallet.

GET /api/v1/protocol/fees (protocol_fees.rs) is O(1) rollup / 60s cache: by_source + by_token. No by_cohort. Headline fees must not use traders.total_fees_paid (PFee-1).

Actors exist on other tables

Source (today) Fee payer to join Existing row
swap_amm Swap discount subject (trusted-router trader when present), else wasm sender swap_events.sender
book_take Taker (parent swap actor), not limit_order_fills.maker Fill → swap join
limit_place Placement owner limit_order_placements.owner
wrap / unwrap Pinned mapper segment sender Not on the fee row
ust1_mint / ust1_redeem Pinned window segment sender Not on the fee row
pair_creation (after #1209) Factory create_pair sender Not on the fee row

Router hops: pair wasm sender is 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; governance RegisterWallet → Tier 0 (MM) and Tier 255 (blacklist). Indexer trader_tracker.rs updates live traders.tier_id / registered. Deregister writes tier_id = 0, registered = false.

Traps:

  1. traders.tier_id DEFAULT 0. Unregistered retail ≠ Tier 0 MM.
  2. Route-solve cache treats unknown sender as tier 0 (quote cache, not registry membership).
  3. Live traders is current state. Deregister must not re-tag historical fees.
  4. Behavioral LP/markout studies are not the live classifier.

Why the new implementation is needed

Retail vs MM fee share cannot be read from /protocol/fees or from traders. 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_take from being credited to the MM maker.


Constraints / guardrails

  • Do not wait on or expand #1202. No campaign_id on fee rows. No raw wallet in client beacons. Indexer stays fee truth.
  • Priced USD only for share %. Sum stamped fee_usd where NOT NULL. Unpriced increments event_count but does not enter USD share. Do not use traders.total_fees_paid or spread_amount.
  • Actor = fee payer, as-of the fee block. book_take = taker, not maker. Trusted router: prefer wasm trader when attributable; never treat the router/factory pin as the cohort identity.
  • MM = registry, not heuristics. mm iff 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 default tier_id = 0 = retail.
  • As-of, not live. Persist actor + cohort at ingest. Replay/reorg must not flip history when RegisterWallet / DeregisterWallet happens later.
  • Unjoined ≠ retail. Missing sender, ambiguous ordinal, or failed pin → unknown.
  • Privacy-safe public surface. Aggregates only. No addresses, no tier dump, no MM roster. Do not add a public “list MM wallets” API.
  • GET stays O(1) rollup / 60s cache. Materialize protocol_fee_stats_by_cohort (or equivalent). Additive JSON. window= remains 24h|7d|30d.
  • Fail closed on pins and emitters (#285).
  • Out of scope: implementing the MM bot (#597); changing on-chain tiers; third-party pixels; Telegram/X ↔ wallet joins; marketing Postgres as fee truth; exact-out /route/solve.

Relevant files

Path Why
indexer/src/indexer/protocol_fees.rs, indexer/src/indexer/parser.rs Fee ingest; no actor today
indexer/src/db/queries/protocol_fees.rs Insert / rollup
indexer/migrations/20260821120000_protocol_fees.sql Schema pattern
indexer/src/db/queries/swap_events.rs, limit_order_fills.rs, limit_order_lifecycle.rs Join keys
indexer/src/indexer/trader_tracker.rs, indexer/src/db/queries/traders.rs Registry as-of
indexer/src/api/protocol_fees.rs Public ledger
indexer/tests/indexer_protocol_fees.rs, indexer_tier_sync.rs Fee + tier tests
frontend-dapp/src/types/index.ts, components/protocol/ProtocolFeeStats.tsx Additive UI only
smartcontracts/contracts/fee-discount/src/ Registry (read-only)
docs/indexer-invariants.md, docs/security-model.md Privacy / fee-discount
skills/AGENTS_FEE_DISCOUNT_TIERS.md, AGENTS_INDEXER_FEE_LEDGER_HOME.md Tiers / home map

  1. At ingest, resolve actor (table above). Store actor (internal) + cohort enum retail|mm|unknown on protocol_fee_events (or a 1:1 join table). Public API never selects actor.
  2. Cohort as-of: apply indexed registry events in height order before tagging that block’s fees. mm = registered ∧ tier 0. Persist the tag; do not recompute from live traders on GET.
  3. Priced share: rollup sums fee_usd IS NOT NULL per cohort per window. share_pct is cohort USD / priced USD (NULL if no priced USD).
  4. API: additive by_cohort: [{ cohort, amount_usd, share_pct, event_count }] on GET /api/v1/protocol/fees. Invalid window still 400.
  5. Frontend (optional, same issue): Protocol fee panel may show Retail / Market maker / Unjoined when by_cohort is present; hide idle. Static labels. No wallet chrome. No new nested card-glass (C653).
  6. Backfill: optional, idempotent on (tx_hash, source, ordinal). Do not rewrite non-null fee_usd.

Reject: default tier_id=0 ⇒ MM; maker as book_take actor; public wallet lists.


Acceptance criteria

  • AC1. Each ingested fee row has an internal actor (or explicit unjoined) and a persisted retail|mm|unknown cohort as-of block_height.
  • AC2. GET /api/v1/protocol/fees additive by_cohort for 24h|7d|30d with priced USD semantics matching by_source (idle "0", unpriced activity null).
  • AC3. Public response contains no terra1 / wallet fields for this feature.
  • AC4. Unregistered wallets with traders.tier_id = 0 classify retail, not mm.
  • AC5. RegisterWallet tier 0 → subsequent fees from that actor are mm until DeregisterWallet; fees before register stay retail/unknown. Historical rows stay put on deregister.
  • AC6. Self-register 1–9 and tier 255 stay retail for this split. book_take follows the taker, not the maker.
  • AC7. Trusted-router swaps classify the trader, not the router (when attributable). Unjoinable wrap/window/create fees are unknown.
  • AC8. GET path stays O(1) rollup. S1 / FeeSource enum / client track() unchanged. Frontend labels (if shipped) are static; hide-idle.

Test plan (functional paths)

# Path Expect
T1 Unregistered EOA swap (swap_amm, priced) Cohort retail
T2 Same wallet, traders.tier_id default 0, registered=false Still retail
T3 RegisterWallet tier 0, then wrap/create with fee > 0 Cohort mm; amount 0 → no row (existing fail-closed)
T4 Retail taker fills MM maker (book_take) retail (taker)
T5 MM taker fills retail maker mm if taker is tier 0 registered
T6 limit_place by tier 0 MM No row if amount 0; residual fee → mm
T7 Self-register tier 5 swap retail
T8 Tier 255 swap (full fee) retail
T9 DeregisterWallet after a historical MM wrap fee Historical row stays mm; new fees retail/unknown
T10 Trusted router hop with trader = retail EOA retail, not router
T11 Router hop with no attributable trader unknown (do not guess)
T12 Wrap/unwrap pinned mapper with sender Join sender; missing sender → unknown
T13 Pair-creation (after #1209) Split follows factory sender
T14 Unpriced asset (fee_usd NULL) In event_count; omitted from USD share_pct
T15 API windows; bad window 400 by_cohort present; additive
T16 Idle configured, zero events Cohort amounts "0"
T17 Existing by_source / Defillama / overview make verify-issue-586 / #614 / #683 stay green

Test plan (attack, hack, and abuse)

# Vector Expect
A1 Treat default tier_id=0 as MM Forbidden
A2 Attribute book_take to fill maker Forbidden
A3 Spoof wasm sender / trader from unreserved contract Ignored (#285); no cohort theft
A4 Untrusted router trader Must not join spoofed trader
A5 Public API enumerates MM or retail wallets Forbidden
A6 Rewrite historical cohort on deregister / live LCD hydrate Forbidden
A7 Put raw terra1 in /protocol JSON Tests fail
A8 Behavioral markout / hardcoded ops wallets as classifier Forbidden; registry only
A9 Count spread_amount, burn tax, gas, hook_fee_amount, traders.total_fees_paid Forbidden
A10 Infer USD share from unpriced rows as $0 Unpriced omitted from USD
A11 GET-path SUM of fee events Forbidden
A12 Over-long / non-bech32 actor Unjoined; no panic

Verification criteria

  • Indexer fixtures: T1–T14 and A1–A8 (default tier 0, taker vs maker, as-of deregister, spoof emitter, no wallet in JSON).
  • API tests: by_cohort keys, priced vs unpriced, window allowlist, uniqueness.
  • Existing protocol-fee, tier-sync, economic USD, and Defillama tests pass.
  • One LocalTerra path: unregistered swap → retail priced row; RegisterWallet tier 0 → later wrap or create (if fee > 0) → mm; public GET body has no actor address.
  • docs/indexer-invariants.md notes actor-at-ingest + public cohort aggregates.
  • make verify-issue-1211 plus make verify-issue-586 / make verify-issue-614.
## Summary Join each **priced** `protocol_fee_events` row 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`) on `GET /api/v1/protocol/fees`. Today the fee API is source × token only. Rows have `tx_hash` and stamped `fee_usd`, but **no actor**. `traders.tier_id` defaults 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_id` or wallets on fee rows. Prefer landing after [#1209](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/1209) so `pair_creation` participates. SKU invoices ([#1210](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/1210)) may stay `unknown` until that stream exists. Design/home: [#1213](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/1213). Closed marketing tracker: [cl8y-marketing#5](https://git.cl8y.com/PlasticDigits/cl8y-marketing/issues/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/fees` adds aggregate `by_cohort` (`retail` / `mm` / `unknown`) with priced USD share And unregistered default `tier_id=0` is **retail**, not MM And public JSON contains **no** wallet addresses And later `DeregisterWallet` does not rewrite historical cohorts ### Not a duplicate / already implemented | Issue | Why it is not this ticket | | --- | --- | | [#1213](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/1213) (closed) | Design/home. No ingest. | | [#1209](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/1209) / [#1210](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/1210) | New `FeeSource` parsers. This ticket adds actor + cohort, not those sources. | | [#1202](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/1202) | SPA S1 events (privacy). **Not a gate.** Do not expand it. | | [#364](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/364) | Registry sync into live `traders`. Live state is the trap, not the as-of classifier. | | [#514](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/514) / [#597](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/597) | Tier ladder / MM bot. Out of scope to implement here. | | [#586](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/586) / [#683](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/683) | Existing census / economic USD. Must stay green. | **Not implemented:** `protocol_fee_events` has no actor/cohort column. `ProtocolFeesResponse` has `by_source` + `by_token` only. `ingest_protocol_fee` never receives a wallet. --- ## Current codebase ### Protocol fees have no actor `protocol_fee_events` ([`20260821120000_protocol_fees.sql`](indexer/migrations/20260821120000_protocol_fees.sql)): `tx_hash`, `source`, `ordinal`, `asset_id`, `amount_raw`, `fee_usd`. Unique `(tx_hash, source, ordinal)`. **No sender / trader / cohort column.** [`FeeEventDraft`](indexer/src/indexer/protocol_fees.rs) and `insert_fee_event` persist those fields only. [`ingest_protocol_fee`](indexer/src/indexer/parser.rs) never receives the wallet. `GET /api/v1/protocol/fees` ([`protocol_fees.rs`](indexer/src/api/protocol_fees.rs)) is O(1) rollup / 60s cache: `by_source` + `by_token`. No `by_cohort`. Headline fees must not use `traders.total_fees_paid` (PFee-1). ### Actors exist on other tables | Source (today) | Fee payer to join | Existing row | | --- | --- | --- | | `swap_amm` | Swap **discount subject** (trusted-router `trader` when present), else wasm `sender` | `swap_events.sender` | | `book_take` | **Taker** (parent swap actor), **not** `limit_order_fills.maker` | Fill → swap join | | `limit_place` | Placement **owner** | `limit_order_placements.owner` | | `wrap` / `unwrap` | Pinned mapper segment `sender` | Not on the fee row | | `ust1_mint` / `ust1_redeem` | Pinned window segment `sender` | Not on the fee row | | `pair_creation` (after #1209) | Factory `create_pair` sender | Not on the fee row | Router hops: pair wasm `sender` is 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**; governance `RegisterWallet` → **Tier 0** (MM) and **Tier 255** (blacklist). Indexer [`trader_tracker.rs`](indexer/src/indexer/trader_tracker.rs) updates live `traders.tier_id` / `registered`. Deregister writes `tier_id = 0, registered = false`. Traps: 1. `traders.tier_id` **DEFAULT 0**. Unregistered retail ≠ Tier 0 MM. 2. Route-solve cache treats **unknown sender as tier 0** (quote cache, not registry membership). 3. Live `traders` is **current** state. Deregister must not re-tag historical fees. 4. Behavioral LP/markout studies are **not** the live classifier. --- ## Why the new implementation is needed Retail vs MM **fee share** cannot be read from `/protocol/fees` or from `traders`. 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_take` from being credited to the MM **maker**. --- ## Constraints / guardrails - **Do not wait on or expand #1202.** No `campaign_id` on fee rows. No raw wallet in client beacons. Indexer stays fee truth. - **Priced USD only for share %.** Sum stamped `fee_usd` where NOT NULL. Unpriced increments `event_count` but does **not** enter USD share. Do not use `traders.total_fees_paid` or `spread_amount`. - **Actor = fee payer, as-of the fee block.** `book_take` = taker, not maker. Trusted router: prefer wasm `trader` when attributable; never treat the router/factory pin as the cohort identity. - **MM = registry, not heuristics.** `mm` iff 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 default `tier_id = 0` = **retail**. - **As-of, not live.** Persist actor + cohort **at ingest**. Replay/reorg must not flip history when `RegisterWallet` / `DeregisterWallet` happens later. - **Unjoined ≠ retail.** Missing sender, ambiguous ordinal, or failed pin → `unknown`. - **Privacy-safe public surface.** Aggregates only. **No addresses, no tier dump, no MM roster.** Do not add a public “list MM wallets” API. - **GET stays O(1) rollup / 60s cache.** Materialize `protocol_fee_stats_by_cohort` (or equivalent). Additive JSON. `window=` remains `24h|7d|30d`. - **Fail closed on pins and emitters** (#285). - **Out of scope:** implementing the MM bot (#597); changing on-chain tiers; third-party pixels; Telegram/X ↔ wallet joins; marketing Postgres as fee truth; exact-out `/route/solve`. --- ## Relevant files | Path | Why | | --- | --- | | `indexer/src/indexer/protocol_fees.rs`, `indexer/src/indexer/parser.rs` | Fee ingest; no actor today | | `indexer/src/db/queries/protocol_fees.rs` | Insert / rollup | | `indexer/migrations/20260821120000_protocol_fees.sql` | Schema pattern | | `indexer/src/db/queries/swap_events.rs`, `limit_order_fills.rs`, `limit_order_lifecycle.rs` | Join keys | | `indexer/src/indexer/trader_tracker.rs`, `indexer/src/db/queries/traders.rs` | Registry as-of | | `indexer/src/api/protocol_fees.rs` | Public ledger | | `indexer/tests/indexer_protocol_fees.rs`, `indexer_tier_sync.rs` | Fee + tier tests | | `frontend-dapp/src/types/index.ts`, `components/protocol/ProtocolFeeStats.tsx` | Additive UI only | | `smartcontracts/contracts/fee-discount/src/` | Registry (read-only) | | `docs/indexer-invariants.md`, `docs/security-model.md` | Privacy / fee-discount | | `skills/AGENTS_FEE_DISCOUNT_TIERS.md`, `AGENTS_INDEXER_FEE_LEDGER_HOME.md` | Tiers / home map | --- ## Recommended direction 1. **At ingest**, resolve `actor` (table above). Store `actor` (internal) + `cohort` enum `retail|mm|unknown` on `protocol_fee_events` (or a 1:1 join table). Public API never selects `actor`. 2. **Cohort as-of:** apply indexed registry events in height order **before** tagging that block’s fees. `mm` = registered ∧ tier 0. Persist the tag; do not recompute from live `traders` on GET. 3. **Priced share:** rollup sums `fee_usd IS NOT NULL` per cohort per `window`. `share_pct` is cohort USD / priced USD (NULL if no priced USD). 4. **API:** additive `by_cohort: [{ cohort, amount_usd, share_pct, event_count }]` on `GET /api/v1/protocol/fees`. Invalid `window` still 400. 5. **Frontend (optional, same issue):** Protocol fee panel may show **Retail** / **Market maker** / **Unjoined** when `by_cohort` is present; hide idle. Static labels. No wallet chrome. No new nested `card-glass` (**C653**). 6. **Backfill:** optional, idempotent on `(tx_hash, source, ordinal)`. Do not rewrite non-null `fee_usd`. Reject: default `tier_id=0` ⇒ MM; maker as `book_take` actor; public wallet lists. --- ## Acceptance criteria - [ ] **AC1.** Each ingested fee row has an internal actor (or explicit unjoined) and a persisted `retail|mm|unknown` cohort as-of `block_height`. - [ ] **AC2.** `GET /api/v1/protocol/fees` additive `by_cohort` for `24h|7d|30d` with priced USD semantics matching `by_source` (idle `"0"`, unpriced activity `null`). - [ ] **AC3.** Public response contains **no** `terra1` / wallet fields for this feature. - [ ] **AC4.** Unregistered wallets with `traders.tier_id = 0` classify **retail**, not `mm`. - [ ] **AC5.** `RegisterWallet` tier 0 → subsequent fees from that actor are `mm` until `DeregisterWallet`; fees **before** register stay retail/unknown. Historical rows stay put on deregister. - [ ] **AC6.** Self-register 1–9 and tier 255 stay `retail` for this split. `book_take` follows the **taker**, not the maker. - [ ] **AC7.** Trusted-router swaps classify the **trader**, not the router (when attributable). Unjoinable wrap/window/create fees are `unknown`. - [ ] **AC8.** GET path stays O(1) rollup. S1 / `FeeSource` enum / client `track()` unchanged. Frontend labels (if shipped) are static; hide-idle. --- ## Test plan (functional paths) | # | Path | Expect | | --- | --- | --- | | T1 | Unregistered EOA swap (`swap_amm`, priced) | Cohort `retail` | | T2 | Same wallet, `traders.tier_id` default 0, `registered=false` | Still `retail` | | T3 | `RegisterWallet` tier 0, then wrap/create with fee > 0 | Cohort `mm`; amount 0 → no row (existing fail-closed) | | T4 | Retail taker fills MM maker (`book_take`) | `retail` (taker) | | T5 | MM taker fills retail maker | `mm` if taker is tier 0 registered | | T6 | `limit_place` by tier 0 MM | No row if amount 0; residual fee → `mm` | | T7 | Self-register tier 5 swap | `retail` | | T8 | Tier 255 swap (full fee) | `retail` | | T9 | DeregisterWallet after a historical MM wrap fee | Historical row stays `mm`; new fees retail/unknown | | T10 | Trusted router hop with `trader` = retail EOA | `retail`, not router | | T11 | Router hop with no attributable trader | `unknown` (do not guess) | | T12 | Wrap/unwrap pinned mapper with sender | Join sender; missing sender → `unknown` | | T13 | Pair-creation (after #1209) | Split follows factory sender | | T14 | Unpriced asset (`fee_usd` NULL) | In `event_count`; omitted from USD `share_pct` | | T15 | API windows; bad window 400 | `by_cohort` present; additive | | T16 | Idle configured, zero events | Cohort amounts `"0"` | | T17 | Existing `by_source` / Defillama / overview | `make verify-issue-586` / `#614` / `#683` stay green | --- ## Test plan (attack, hack, and abuse) | # | Vector | Expect | | --- | --- | --- | | A1 | Treat default `tier_id=0` as MM | Forbidden | | A2 | Attribute `book_take` to fill `maker` | Forbidden | | A3 | Spoof wasm `sender` / `trader` from unreserved contract | Ignored (#285); no cohort theft | | A4 | Untrusted router `trader` | Must not join spoofed trader | | A5 | Public API enumerates MM or retail wallets | Forbidden | | A6 | Rewrite historical cohort on deregister / live LCD hydrate | Forbidden | | A7 | Put raw `terra1` in `/protocol` JSON | Tests fail | | A8 | Behavioral markout / hardcoded ops wallets as classifier | Forbidden; registry only | | A9 | Count `spread_amount`, burn tax, gas, `hook_fee_amount`, `traders.total_fees_paid` | Forbidden | | A10 | Infer USD share from unpriced rows as `$0` | Unpriced omitted from USD | | A11 | GET-path `SUM` of fee events | Forbidden | | A12 | Over-long / non-bech32 actor | Unjoined; no panic | --- ## Verification criteria - Indexer fixtures: T1–T14 and A1–A8 (default tier 0, taker vs maker, as-of deregister, spoof emitter, no wallet in JSON). - API tests: `by_cohort` keys, priced vs unpriced, window allowlist, uniqueness. - Existing protocol-fee, tier-sync, economic USD, and Defillama tests pass. - One LocalTerra path: unregistered swap → `retail` priced row; `RegisterWallet` tier 0 → later wrap or create (if fee > 0) → `mm`; public GET body has no actor address. - `docs/indexer-invariants.md` notes actor-at-ingest + public cohort aggregates. - `make verify-issue-1211` plus `make verify-issue-586` / `make verify-issue-614`.
Author
Member

Clarified with Market: #1202 HOLD does not block #1211 CMM fee ledger / MM cohort (P0). Proceed with ready+agent:implement.

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.

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 #1211 as 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.

## 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 `#1211` as **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`.
PlasticDigits changed title from feat: actor-joined priced fees with MM-registry cohort split to feat(indexer): actor-joined priced fees with MM-registry cohort split 2026-09-13 05:37:48 +00:00

Repair: 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.

Repair: 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.

Occupying-runner check: implement labels already present; no extra labels added.

/agent implement

/agent implement

/agent implement

/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:

  1. Resolve the payer per source and persist retail / MM / unknown at the fee’s chain position. MM means registered Tier 0; unregistered default tier 0 is retail, and later deregistration must not rewrite history.
  2. Add privacy-safe materialized by_cohort rollups using stamped priced USD for shares, unpriced rows for activity counts, and O(1) GET behavior.
  3. Add source/tier/privacy/API coverage, a verify-issue-1211 target, and the required LocalTerra smoke; keep the existing fee checks green.

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.

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: 1. Resolve the payer per source and persist retail / MM / unknown at the fee’s chain position. MM means registered Tier 0; unregistered default tier 0 is retail, and later deregistration must not rewrite history. 2. Add privacy-safe materialized by_cohort rollups using stamped priced USD for shares, unpriced rows for activity counts, and O(1) GET behavior. 3. Add source/tier/privacy/API coverage, a verify-issue-1211 target, and the required LocalTerra smoke; keep the existing fee checks green. Related: [#1209](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/1209) / [#1210](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/1210) source work; [#1213](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/1213) home; [#1269](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/1269) unique key; [#364](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/work_items/364) live tier projection; [#285](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/285) emitter trust; [#1202](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/1202) privacy boundary (not a blocker); [#586](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/586) / [#613](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/613) / [#614](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/614) / [#683](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/683) existing fee regression checks.
Sign in to join this conversation.
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
code/cl8y-dex-terraclassic#1211
No description provided.