feat(indexer): anonymous UTC-day activity digest (counts + fee-path mix) #1206

Open
opened 2026-09-04 08:13:48 +00:00 by PlasticDigits · 7 comments

Summary

Add one read-only indexer HTTP digest that returns a UTC calendar-day JSON of protocol activity counts plus treasury fee-path mix, with no wallets, no tx hashes, and no joins from events to people. Research agents today must stitch trailing /protocol/fees, Llama UTC-day USD, overview trader counts, and unredacted pair/trader tapes. That is not an anonymous digest.

Bundle (do not split):

  1. GET /api/v1/evidence/digest — required day=YYYY-MM-DD (UTC [00:00Z, +1d)). One document. No pagination (aggregates only).
  2. Activity counts — protocol-wide COUNT(*) by surface ∈ {swap, wrap, limit, lp} and kind (swap; wrap/unwrap; place/cancel/fill; add/remove).
  3. Fee-path mix — same UTC day, all seven FeeSource values (swap_amm, book_take, limit_place, wrap, unwrap, ust1_mint, ust1_redeem): event_count, priced amount_usd, share_pct. Protocol-grain token mix (no wallets).
  4. Anonymity — never emit sender, receiver, maker, owner, provider, actor_hash, tx_hash, swap_event_id, trader addresses, or unique-trader counts. No SQL joins from fee/activity rows to people tables.
  5. OpenAPI + tests — utoipa path on ApiDoc, handler tests for every count/mix path and the abuse matrix below.

This is a new business endpoint on existing ingest. No frontend. No new tables. No CSV. No API keys. No event tape.

Related (do not merge into this ticket):

  • #1205 — per-event redacted UTC-day JSON. Keeps tx_hash (LCD can deanonymize a row) and actor_hash. Different product: evidence tape, not a digest. Do not implement this digest inside #1205.
  • #1204 — docs/OpenAPI pack for existing swaps/pools/fees/burns/windows routes. Explicitly “no new business endpoints.”
  • #1202 — dApp product events (campaign → tx_succeeded). Off-chain, unredacted by design, joins txhash to fees. Forbidden here.
  • #586 — trailing 24h/7d/30d GET /api/v1/protocol/fees source+token mix (shipped). Not a UTC calendar day.
  • #689 — GET /api/v1/protocol/fees/daily grain series (hourly/daily/monthly) of fees_usd + total event_count. No by_source. Not one-day evidence.
  • #631 / #687 — DeFiLlama GET /api/v1/defillama/daily?timestamp= listing adapter: gem-exclude, 404 if day not rolled, unix timestamp, USD + Llama fees.* only. No LP/limit activity counts. Not shareable as an evidence digest.
  • #216 L10 — swap count is parent swap_events only; do not add limit_order_fills into surface=swap.
  • #614 — UST1 mint/redeem is a fee path, not wrap, not a swap count.
  • #557 — USD and raw amounts as plain digit strings.
  • #576 — trailing windows ≠ midnight UTC buckets.
  • Operator SQL in docs/runbooks/suspicious-activity-queries.md stays the unredacted incident path.

Current codebase

There is no anonymous one-day document that combines activity counts with fee-path mix. Nearby HTTP is either trailing, listing-adapter, chart series, people-scoped, or (proposed) event-level.

Fee mix exists, but not as a UTC-day evidence digest

Surface What it returns Why it is not this ticket
GET /api/v1/protocol/fees?window= O(1) by_source + by_token with event_count / amount_usd / share_pct (indexer/src/api/protocol_fees.rs) Trailing 24h | 7d | 30d only (parse_fee_window). Reads rollup tables, not a calendar day.
GET /api/v1/protocol/fees/daily Grain series: fees_usd + total event_count (indexer/src/api/protocol_fee_series.rs) Chart flow (#689). No by_source. from/to/window → 400. Does not COUNT swaps/LP/limits.
GET /api/v1/defillama/daily?timestamp= UTC-day volume_usd, trade_count, fees.{swap_amm,…} (indexer/src/api/defillama.rs) Listing adapter: gem-exclude, 404 if not rolled, unix 00:00 required. Embeds UST1/USTR contract addresses. No lp / limit place+cancel activity counts.
GET /api/v1/overview Trailing volume/fees + unique_traders_24h (indexer/src/api/overview.rs) People-adjacent census. Trailing windows. Not a digest.

FeeSource is already canonical (indexer/src/indexer/protocol_fees.rs): swap_amm, book_take, limit_place, wrap, unwrap, ust1_mint, ust1_redeem. Wrap/unwrap rows are treasury fees (pinned mapper notify_deposit / unwrap, amount key fee). UST1 window is a different family. Do not invent new source strings.

Activity counts are scattered and often people-scoped

Count wanted Storage Live HTTP today
swap swap_events Pair /trades, trader /trades (unredacted sender). Overview total_trades_* is trailing, not UTC day. Llama trade_count is gem-filtered and 404-gated.
lp liquidity_events (add | remove) Pair /liquidity-events (provider). No protocol-wide daily count.
limit limit_order_placements / _cancellations / _fills Pair + trader limit routes (owner / maker). Fill count ≠ swap count (L10).
wrap activity protocol_fee_events.source ∈ {wrap,unwrap} Fee rollups only. No wrap-principal tape (and this digest must not pretend wrap count is principal volume).

Router: indexer/src/api/mod.rs build_router. Pair/trader handlers stay unredacted. There is no evidence module on main.

Joins to people are the default on existing tapes

Trader routes take a bech32 path param. Pair trades emit sender. GT /gt/events emits full maker. Overview materializes unique_traders_24h (distinct swap senders). #1205 (open) still keeps tx_hash so LCD lookup deanonymizes a row, plus actor_hash that is stable across surfaces.

A digest that is safe to share must not SELECT those columns, must not COUNT(DISTINCT sender), and must not JOIN protocol_fee_events to swap_events / traders on tx_hash.

Auth and governors

Public GET. Global tower_governor (default 60 RPS; prod refuses 0). Keys are socket peer IP (PeerIpKeyExtractor) — no trusted X-Forwarded-For. LCD-heavy list is a second 10 RPS router (limit-book, route/solve, blacklist-check, CG/CMC orderbook). This digest is Postgres COUNT / GROUP BY only — not LCD-heavy. Internal errors: internal_err() → "Internal server error".


Why the new implementation is needed

Evidence consumers need one day’s protocol picture they can store and share without a wallet list and without a tx list that joins to people.

Today that requires:

  1. Using trailing /protocol/fees (wrong clock vs UTC day, #576).
  2. Or Llama daily (wrong product: gems omitted, 404 until rollup, unix timestamp, no LP/limit activity).
  3. Or paging pair/trader tapes and stripping sender / maker / owner / provider after the fact — which still leaves tx_hash joinable via LCD.
  4. Or waiting on #1205 event pages, which are designed to stay chain-linkable.

Without a dedicated digest, agents either keep PII, mix trailing windows with calendar days, or treat listing adapters as protocol truth. Counts + fee-path mix in one anonymous JSON is the missing cover sheet.


Constraints / guardrails

  1. Read-only. GET only. No indexer DB writes. Not on lcd_heavy_router. No LCD on the request path.
  2. No new ingest. Do not add wrap_events. Do not persist wrap principal. Export COUNT / GROUP BY over what is already indexed.
  3. No API keys / bearer / ?redact=0 / ?join=1. Public API stays unauthenticated. Anonymity is the access control. No unredacted twin. No secret salt.
  4. One UTC calendar day. Same clock as #631 / #1205 ([day 00:00:00Z, next 00:00:00Z)). No multi-day from/to. No trailing 24h. day is YYYY-MM-DD parsed in Rust; bind timestamptz parameters — never concatenate SQL. Reuse utc_day_start / NaiveDate parsing already used by DeFiLlama / volume daily — do not invent a third clock.
  5. No wallets / no tx joins to people. Response JSON (including nested objects and string values) must not contain:
    • user bech32 (terra1… actors);
    • keys sender, receiver, maker, owner, provider, trader, actor_hash;
    • tx_hash, swap_tx_hash, swap_event_id, txhash;
    • unique_traders / distinct_senders / any COUNT(DISTINCT sender|maker|owner|provider).
      Pair/token contracts may appear only on fee by_token rows (protocol assets), never as “who did this.” Native denoms uusd / uluna stay. Factory address must not be required in the digest (Llama embeds it in methodology; this route should omit a factory pin unless it is a static methodology string with no user addresses).
  6. No per-pair / per-trader breakdown. Protocol grain only. A pair with one swap plus a public /trades tape would re-identify. Do not emit pairs[], leaderboards, or top-N wallets.
  7. Do not JOIN protocol_fee_events to swap_events, traders, or LCD. Fee-path mix is GROUP BY source (and token mix GROUP BY asset_id) on protocol_fee_events in the UTC window. Activity counts are independent COUNT(*) on each event table. Correlation across people is out of scope.
  8. L10. counts.swap = COUNT(*) from swap_events in the window. Limit fills increment counts.limit.kind=fill only. Do not add fill count into swap count. Hybrid pool+book is still one swap row.
  9. Wrap honesty. Wrap/unwrap activity counts are fee-event counts (protocol_fee_events.source wrap/unwrap), labeled as treasury-fee events, not wrap principal. Do not fold ust1_mint / ust1_redeem into wrap. Those are fees.by_source paths and optional counts keys under a window family, not surface=wrap.
  10. Fee-path enum. Emit all seven FeeSource::ALL rows every day (idle source → event_count=0, amount_usd="0"). Unknown DB source strings: omit from JSON (do not interpolate into JS). Same PFee/L7 rules: not spread_amount, burn tax, gas, hook fees, community-tax extra-debit.
  11. USD idle vs unpriced. Match #586 / #548 C3: idle source → "0"; activity + unpriced → JSON null (not "0", not omit in a way that a client treats as $0). share_pct null when the day’s priced fee SUM is 0. Headline fees.total_usd uses the same idle/unpriced rule as overview fees (priced SUM; one unpriced source must not silently zero a priced wrap — follow EFee-6 / #687 for the headline, fail-closed per source).
  12. Amounts. Plain integer/decimal digit strings (bd_plain_string / #557). No scientific notation.
  13. Gems. Include indexed gem/test-pair activity in counts (this is evidence, not Llama). Document that the digest is not L639-safe. Fee USD still uses the protocol catalog (gems unpriced → null per source, not fake $1).
  14. Errors. Bad day / future day (strictly after today’s UTC date) / format=csv / unknown query that would change grain (window, timestamp, from, to, trader, sender) → 400 with a short message. DB failure → 500 "Internal server error". Empty indexed day → 200 with zero counts and zero-idle fee rows (not 404). Today’s incomplete UTC day → 200 with "complete": false. Do not copy Llama’s 404-if-not-rolled.
  15. Do not mutate /protocol/fees, /protocol/fees/daily, /defillama/daily, /overview, /gt/events, pair/trader tapes, or #1205’s proposed /evidence/daily.
  16. No CSV (format=csv → 400).
  17. IPv4 peer governor still applies. Do not add SmartIp/XFF.
  18. Timeouts. Stay under the 30s TimeoutLayer. Bounded SQL: a handful of COUNT(*) / GROUP BY source / GROUP BY asset_id with time bounds. No SELECT * of the day’s events. No unbounded DISTINCT over senders.
  19. Token mix cap. If by_token would be large, reuse the existing /protocol/fees “other” bucket (is_other) — do not return thousands of dust tokens. Still no wallets.

Relevant files

Path Role
indexer/src/api/mod.rs build_router, ApiDoc paths/tags/schemas, governors
indexer/src/api/protocol_fees.rs Trailing mix shape (by_source / by_token) — clock is wrong; copy field semantics, not the window
indexer/src/api/protocol_fee_series.rs UTC series of totals — do not overload with day= mix
indexer/src/api/defillama.rs UTC-day fee USD by source (listing) — do not 404, do not gem-exclude counts
indexer/src/api/overview.rs unique_traders_24h — do not copy this field
indexer/src/indexer/protocol_fees.rs FeeSource, wrap pin, UST1 window, PFee/L7
indexer/src/db/queries/protocol_fees.rs Rollup GROUP BY source idiom
indexer/src/db/queries/swap_events.rs Swap count source
indexer/src/db/queries/liquidity.rs LP count source
indexer/src/db/queries/limit_order_fills.rs / limit_order_lifecycle.rs Limit fill/place/cancel counts
indexer/src/api/errors.rs internal_err
indexer/src/indexer/defillama.rs utc_day_start / timestamp parse — reuse day bounds, not Llama response
indexer/tests/security.rs Rate limit, 400/500, OpenAPI smoke
docs/indexer-invariants.md New invariant row (anonymous digest vs #1205 tape vs Llama vs trailing fees)
docs/runbooks/suspicious-activity-queries.md Pointer: anonymous digest vs unredacted incident SQL

New (expected): indexer/src/api/evidence_digest.rs (handler + queries), indexer/tests/api_evidence_digest.rs.

If #1205 lands first and adds indexer/src/api/evidence.rs, put the digest handler in that module as a second path — do not fold digest fields onto paginated events.


One handler, bounded aggregate SQL, assemble in Rust.

GET /api/v1/evidence/digest?day=2026-09-03

Response shape (illustrative — additive JSON, #[serde(skip_serializing_if)] for absent optionals):

{
  "day": "2026-09-03",
  "timezone": "UTC",
  "complete": true,
  "counts": {
    "swap": { "swap": 10 },
    "lp": { "add": 2, "remove": 1 },
    "limit": { "place": 4, "cancel": 1, "fill": 3 },
    "wrap": { "wrap": 1, "unwrap": 2 }
  },
  "fees": {
    "total_usd": "12.34",
    "by_source": [
      { "source": "swap_amm", "event_count": 8, "amount_usd": "10.00", "share_pct": "81.03" },
      { "source": "book_take", "event_count": 3, "amount_usd": "1.00", "share_pct": "8.10" },
      { "source": "limit_place", "event_count": 4, "amount_usd": "0.50", "share_pct": "4.05" },
      { "source": "wrap", "event_count": 1, "amount_usd": "0.40", "share_pct": "3.24" },
      { "source": "unwrap", "event_count": 2, "amount_usd": "0.44", "share_pct": "3.57" },
      { "source": "ust1_mint", "event_count": 0, "amount_usd": "0", "share_pct": "0" },
      { "source": "ust1_redeem", "event_count": 0, "amount_usd": "0", "share_pct": "0" }
    ],
    "by_token": [
      { "symbol": "cUSTC", "contract_or_denom": "terra1…", "amount_usd": "11.00", "share_pct": "89.14", "is_other": false }
    ]
  }
}

Implementation notes:

  • Parse day with chrono::NaiveDate + and_hms_opt(0,0,0) + Utc. Reject 2026-13-40, 2026-09-03T00:00:00Z, unix timestamps, and day=today.
  • complete = day < Utc::now().date_naive().
  • SQL: five-ish statements (COUNT swap; COUNT LP GROUP BY event_type; COUNT place/cancel/fill; COUNT wrap/unwrap fees; fee GROUP BY source; optional token mix). Bind $1/$2 as window start/end.
  • Register on the global api_router (with /api/v1/protocol/fees), not lcd_heavy_router.
  • Add utoipa::path + ApiDoc path/schema + tag Evidence.
  • Short curl + invariant row in docs/indexer-invariants.md. Do not expand #1204’s five-surface pack here.
  • counts.wrap is fee-event counts. Document that next to fees.by_source wrap/unwrap so agents do not double-count wrap as both volume and fee path.
  • Optional counts.window { "ust1_mint", "ust1_redeem" } as fee-event counts if it keeps wrap vs window honest; if omitted, those exist only under fees.by_source. Pick one and test it — prefer explicit counts.window so mint/redeem are not mistaken for wrap.

Acceptance criteria

  • AC1. GET /api/v1/evidence/digest?day=YYYY-MM-DD returns 200 JSON with day, timezone=UTC, complete, counts, fees.by_source (length 7, FeeSource::ALL order stable).
  • AC2. counts.swap.swap equals COUNT(*) of swap_events in the UTC window. Limit fills do not increase swap count (L10). Hybrid pool+book is one swap.
  • AC3. counts.lp add/remove match liquidity_events.event_type. GT names join/exit never appear.
  • AC4. counts.limit covers place + cancel + fill tables. Fill count may differ from swap count.
  • AC5. counts.wrap includes only protocol_fee_events wrap/unwrap. Seeded ust1_mint does not increment wrap.
  • AC6. fees.by_source for that day matches GROUP BY source on protocol_fee_events in the same window (event_count). Idle sources present with event_count=0 and amount_usd="0".
  • AC7. Trailing /protocol/fees?window=24h is not required to equal this document (different clock). A test that seeds all events at 00:30Z vs previous day 23:30Z proves calendar bounds.
  • AC8. Response body contains no seeded trader/maker/owner/provider bech32. Keys listed in guardrail 5 are absent. No tx_hash / actor_hash.
  • AC9. Missing day, malformed day, future day, format=csv, window=24h, timestamp=, from=, sender= → 400 (or sender ignored and still not used — prefer 400 for identity-shaped params so a client cannot think it filtered by wallet).
  • AC10. Empty indexed day → 200 zero counts + idle fee rows (not 404). Today UTC → "complete": false. A past day → "complete": true.
  • AC11. ApiDoc includes the path; GET /api-docs/openapi.json lists /api/v1/evidence/digest. /protocol/fees, /protocol/fees/daily, /defillama/daily contracts unchanged.
  • AC12. Handler uses sqlx bound parameters only. internal_err on DB failure (no sqlx text).
  • AC13. Route is not on the LCD-heavy governor list.
  • AC14. Handler SQL (string assert or query spy) does not mention traders, sender, maker, owner, provider, or COUNT(DISTINCT. Mentions only event/fee aggregate tables (+ assets for token symbols).
  • AC15. Amounts/USD are plain digit strings. Unpriced+activity source → amount_usd JSON null.

Test plan (functional paths)

Use the existing indexer test Postgres harness (same pattern as indexer/tests/api_pairs.rs / protocol fee tests). New file indexer/tests/api_evidence_digest.rs.

  1. Seed all surfaces + all seven fee sources on one UTC day, plus spillover events the previous and next UTC day. day= counts/mix include only the middle day.
  2. L10 — one hybrid swap + one limit fill sharing a taker swap: counts.swap.swap=1, counts.limit.fill=1.
  3. LP — add and remove; no join/exit.
  4. Wrap vs window — wrap + unwrap + ust1_mint + ust1_redeem: wrap counts exclude mint/redeem; fee-path rows keep all four distinct.
  5. Idle sources — no book_take that day → row still present, zeros.
  6. Unpriced vs idle — fee activity with fee_usd null → source amount_usd null, idle sibling "0". Headline follows EFee-6 (priced SUM; do not zero wrap because AMM is unpriced).
  7. Empty day — 200 zeros, not 404.
  8. Incomplete today — complete=false.
  9. Calendar vs trailing — events at previous day 23:30Z and day 00:30Z: digest day= matches calendar; does not need to match /protocol/fees?window=24h.
  10. Token mix — two fee tokens + dust → other bucket if that is the implemented cap; no trader addresses in by_token.
  11. Gems — gem-pair swap increments counts.swap; Llama daily (if asserted in the same harness) still omits gems. Confirms this is not a Llama clone.
  12. OpenAPI — spec contains the path.
  13. Regression — existing /protocol/fees, /protocol/fees/daily, /defillama/daily tests still pass without response-shape changes.
  14. Anonymity stringify — serde_json::to_string of the digest does not contain seeded actor bech32, tx_hash hex from fixtures, or keys sender/maker/actor_hash.

Test plan (attack, hack, and abuse)

  1. SQL injection in day — day=2026-09-03'%20OR%201=1--, day=2026-09-03;DROP TABLE swap_events. 400, tables intact.
  2. Identity query params — sender=terra1…, trader=, maker=, address=. 400 (preferred) or ignored; response must not filter or echo the address; counts remain protocol-wide.
  3. Join gadgets — tx=, tx_hash=, include_txs=1, events=1, unredacted=1, redact=0, join=traders. Ignored or 400; body still has no txs/wallets.
  4. COUNT(DISTINCT sender) smuggling — if a future patch adds it, AC14 SQL assert fails. Test that unique_traders is absent even when overview has a non-zero unique_traders_24h in the same DB.
  5. Future day — day=2099-01-01 → 400.
  6. Llama-shaped timestamp= — unix 00:00 on this route → 400 (wrong API), not a silent alias.
  7. window=24h on this route → 400 (wrong clock).
  8. Method abuse — POST/PUT/DELETE → 405.
  9. CSV / formula — format=csv → 400.
  10. Error sanitization — closed pool → "Internal server error", no sqlx.
  11. Rate limit — burst under test governor like security.rs; 429 with Retry-After. Not LCD-heavy.
  12. Response size — digest JSON stays small (aggregates). A 10k-event day still returns one object, not 10k rows (assert no events array).
  13. Bech32 in token mix — CW20 token contracts may appear on by_token. Seeded user actors still absent. Do not put wrap-mapper or user wallets in by_token.symbol.
  14. Do not scan pair_reserves or traders. Query spy / SQL string assert.
  15. Cross-tenant mix-up — seed two senders; digest totals match sums; no per-sender split field exists to enumerate.

Verification criteria

  • cargo test --manifest-path indexer/Cargo.toml --test api_evidence_digest -- --nocapture (and existing security.rs / protocol fee / defillama tests) green.
  • Manual: curl GET /api/v1/evidence/digest?day=<seeded> against the test server; jq shows counts + seven fees.by_source; rg -o 'terra1[a-z0-9]+' on the body matches only token contracts from fee mix fixtures (or matches nothing if only native denoms), never seeded traders.
  • OpenAPI: curl /api-docs/openapi.json | jq '.paths["/api/v1/evidence/digest"]' non-null.
  • Invariants doc updated: anonymous digest vs #1205 event tape (tx_hash allowed there, forbidden here) vs Llama vs trailing /protocol/fees.
  • No change to /gt/events, /protocol/fees, /defillama/daily, or trader route contracts.

Out of scope

  • Frontend / Protocol page charts (already consume trailing mix + daily series).
  • #1205 paginated event export (actor_hash / tx_hash).
  • #1204 OpenAPI pack for existing five surfaces.
  • #1202 product analytics (tx_succeeded join).
  • Wrap principal tape, new mapper ingest, CSV, NDJSON, object storage, scheduled dumps.
  • Unredacted export, API tokens, HMAC salts, operator-only routes.
  • Unique trader / active pair census (overview already has trailing versions).
  • Per-pair or per-token activity leaderboards.
  • Multi-day ranges, block-height windows.
  • Changing suspicious-activity SQL runbook recipes (a one-line pointer is enough).
  • Deploy, image, or host selection.
## Summary Add one **read-only indexer HTTP digest** that returns a **UTC calendar-day JSON** of protocol activity **counts** plus **treasury fee-path mix**, with **no wallets, no tx hashes, and no joins from events to people**. Research agents today must stitch trailing `/protocol/fees`, Llama UTC-day USD, overview trader counts, and unredacted pair/trader tapes. That is not an anonymous digest. Bundle (do not split): 1. **`GET /api/v1/evidence/digest`** — required `day=YYYY-MM-DD` (UTC `[00:00Z, +1d)`). One document. No pagination (aggregates only). 2. **Activity counts** — protocol-wide `COUNT(*)` by surface ∈ {`swap`, `wrap`, `limit`, `lp`} and kind (`swap`; `wrap`/`unwrap`; `place`/`cancel`/`fill`; `add`/`remove`). 3. **Fee-path mix** — same UTC day, all seven `FeeSource` values (`swap_amm`, `book_take`, `limit_place`, `wrap`, `unwrap`, `ust1_mint`, `ust1_redeem`): `event_count`, priced `amount_usd`, `share_pct`. Protocol-grain token mix (no wallets). 4. **Anonymity** — never emit `sender`, `receiver`, `maker`, `owner`, `provider`, `actor_hash`, `tx_hash`, `swap_event_id`, trader addresses, or unique-trader counts. No SQL joins from fee/activity rows to people tables. 5. **OpenAPI + tests** — `utoipa` path on `ApiDoc`, handler tests for every count/mix path and the abuse matrix below. This is a **new business endpoint** on existing ingest. No frontend. No new tables. No CSV. No API keys. No event tape. Related (do **not** merge into this ticket): - [#1205](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/1205) — **per-event** redacted UTC-day JSON. Keeps `tx_hash` (LCD can deanonymize a row) and `actor_hash`. Different product: evidence **tape**, not a digest. Do not implement this digest inside `#1205`. - [#1204](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/1204) — docs/OpenAPI pack for **existing** swaps/pools/fees/burns/windows routes. Explicitly “no new business endpoints.” - [#1202](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/1202) — dApp **product** events (campaign → `tx_succeeded`). Off-chain, unredacted by design, joins `txhash` to fees. Forbidden here. - [#586](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/586) — trailing `24h`/`7d`/`30d` `GET /api/v1/protocol/fees` source+token mix (shipped). Not a UTC calendar day. - [#689](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/689) — `GET /api/v1/protocol/fees/daily` grain **series** (`hourly`/`daily`/`monthly`) of `fees_usd` + total `event_count`. No `by_source`. Not one-day evidence. - [#631](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/631) / [#687](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/687) — DeFiLlama `GET /api/v1/defillama/daily?timestamp=` listing adapter: gem-exclude, **404** if day not rolled, unix `timestamp`, USD + Llama `fees.*` only. No LP/limit **activity** counts. Not shareable as an evidence digest. - [#216](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/216) **L10** — swap **count** is parent `swap_events` only; do not add `limit_order_fills` into `surface=swap`. - [#614](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/614) — UST1 mint/redeem is a **fee path**, not wrap, not a swap count. - [#557](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/557) — USD and raw amounts as plain digit strings. - [#576](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/576) — trailing windows ≠ midnight UTC buckets. - Operator SQL in [`docs/runbooks/suspicious-activity-queries.md`](docs/runbooks/suspicious-activity-queries.md) stays the **unredacted** incident path. --- ## Current codebase There is **no** anonymous one-day document that combines activity **counts** with fee-**path** mix. Nearby HTTP is either trailing, listing-adapter, chart series, people-scoped, or (proposed) event-level. ### Fee mix exists, but not as a UTC-day evidence digest | Surface | What it returns | Why it is not this ticket | |---------|-----------------|---------------------------| | `GET /api/v1/protocol/fees?window=` | O(1) `by_source` + `by_token` with `event_count` / `amount_usd` / `share_pct` ([`indexer/src/api/protocol_fees.rs`](indexer/src/api/protocol_fees.rs)) | Trailing `24h` \| `7d` \| `30d` only (`parse_fee_window`). Reads **rollup** tables, not a calendar day. | | `GET /api/v1/protocol/fees/daily` | Grain series: `fees_usd` + total `event_count` ([`indexer/src/api/protocol_fee_series.rs`](indexer/src/api/protocol_fee_series.rs)) | Chart flow (#689). **No** `by_source`. `from`/`to`/`window` → **400**. Does not COUNT swaps/LP/limits. | | `GET /api/v1/defillama/daily?timestamp=` | UTC-day `volume_usd`, `trade_count`, `fees.{swap_amm,…}` ([`indexer/src/api/defillama.rs`](indexer/src/api/defillama.rs)) | Listing adapter: gem-exclude, **404** if not rolled, unix 00:00 required. Embeds UST1/USTR **contract addresses**. No `lp` / limit place+cancel **activity** counts. | | `GET /api/v1/overview` | Trailing volume/fees + **`unique_traders_24h`** ([`indexer/src/api/overview.rs`](indexer/src/api/overview.rs)) | People-adjacent census. Trailing windows. Not a digest. | `FeeSource` is already canonical ([`indexer/src/indexer/protocol_fees.rs`](indexer/src/indexer/protocol_fees.rs)): `swap_amm`, `book_take`, `limit_place`, `wrap`, `unwrap`, `ust1_mint`, `ust1_redeem`. Wrap/unwrap rows are **treasury fees** (pinned mapper `notify_deposit` / `unwrap`, amount key `fee`). UST1 window is a **different** family. Do not invent new source strings. ### Activity counts are scattered and often people-scoped | Count wanted | Storage | Live HTTP today | |--------------|---------|-----------------| | **swap** | `swap_events` | Pair `/trades`, trader `/trades` (unredacted `sender`). Overview `total_trades_*` is **trailing**, not UTC day. Llama `trade_count` is gem-filtered and 404-gated. | | **lp** | `liquidity_events` (`add` \| `remove`) | Pair `/liquidity-events` (`provider`). **No** protocol-wide daily count. | | **limit** | `limit_order_placements` / `_cancellations` / `_fills` | Pair + trader limit routes (`owner` / `maker`). Fill count ≠ swap count (**L10**). | | **wrap activity** | `protocol_fee_events.source` ∈ {`wrap`,`unwrap`} | Fee rollups only. **No** wrap-principal tape (and this digest must not pretend wrap count is principal volume). | Router: [`indexer/src/api/mod.rs`](indexer/src/api/mod.rs) `build_router`. Pair/trader handlers stay unredacted. There is **no** `evidence` module on `main`. ### Joins to people are the default on existing tapes Trader routes take a bech32 path param. Pair trades emit `sender`. GT `/gt/events` emits full `maker`. Overview materializes `unique_traders_24h` (distinct swap senders). `#1205` (open) still keeps `tx_hash` so LCD lookup deanonymizes a row, plus `actor_hash` that is stable across surfaces. A digest that is safe to share **must not** SELECT those columns, must not `COUNT(DISTINCT sender)`, and must not JOIN `protocol_fee_events` to `swap_events` / `traders` on `tx_hash`. ### Auth and governors Public `GET`. Global `tower_governor` (default **60 RPS**; prod refuses `0`). Keys are **socket peer IP** (`PeerIpKeyExtractor`) — no trusted `X-Forwarded-For`. LCD-heavy list is a **second** 10 RPS router (limit-book, route/solve, blacklist-check, CG/CMC orderbook). This digest is Postgres `COUNT` / `GROUP BY` only — **not** LCD-heavy. Internal errors: `internal_err()` → `"Internal server error"`. --- ## Why the new implementation is needed Evidence consumers need **one day’s** protocol picture they can store and share **without a wallet list and without a tx list that joins to people**. Today that requires: 1. Using trailing `/protocol/fees` (wrong clock vs UTC day, [#576](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/576)). 2. Or Llama daily (wrong product: gems omitted, **404** until rollup, unix timestamp, no LP/limit activity). 3. Or paging pair/trader tapes and stripping `sender` / `maker` / `owner` / `provider` after the fact — which still leaves `tx_hash` joinable via LCD. 4. Or waiting on [#1205](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/1205) event pages, which are **designed** to stay chain-linkable. Without a dedicated digest, agents either keep PII, mix trailing windows with calendar days, or treat listing adapters as protocol truth. Counts + fee-path mix in one anonymous JSON is the missing cover sheet. --- ## Constraints / guardrails 1. **Read-only.** `GET` only. No indexer DB writes. Not on `lcd_heavy_router`. No LCD on the request path. 2. **No new ingest.** Do not add `wrap_events`. Do not persist wrap **principal**. Export `COUNT` / `GROUP BY` over what is already indexed. 3. **No API keys / bearer / `?redact=0` / `?join=1`.** Public API stays unauthenticated. Anonymity **is** the access control. No unredacted twin. No secret salt. 4. **One UTC calendar day.** Same clock as #631 / #1205 (`[day 00:00:00Z, next 00:00:00Z)`). No multi-day `from`/`to`. No trailing `24h`. `day` is `YYYY-MM-DD` parsed in Rust; bind `timestamptz` parameters — never concatenate SQL. Reuse `utc_day_start` / NaiveDate parsing already used by DeFiLlama / volume daily — do not invent a third clock. 5. **No wallets / no tx joins to people.** Response JSON (including nested objects and string values) must not contain: - user bech32 (`terra1…` **actors**); - keys `sender`, `receiver`, `maker`, `owner`, `provider`, `trader`, `actor_hash`; - `tx_hash`, `swap_tx_hash`, `swap_event_id`, `txhash`; - `unique_traders` / `distinct_senders` / any `COUNT(DISTINCT sender|maker|owner|provider)`. Pair/token **contracts** may appear **only** on fee `by_token` rows (protocol assets), never as “who did this.” Native denoms `uusd` / `uluna` stay. Factory address must not be required in the digest (Llama embeds it in methodology; this route should omit a factory pin unless it is a static methodology string with **no** user addresses). 6. **No per-pair / per-trader breakdown.** Protocol grain only. A pair with one swap plus a public `/trades` tape would re-identify. Do not emit `pairs[]`, leaderboards, or top-N wallets. 7. **Do not JOIN** `protocol_fee_events` to `swap_events`, `traders`, or LCD. Fee-path mix is `GROUP BY source` (and token mix `GROUP BY asset_id`) on `protocol_fee_events` in the UTC window. Activity counts are independent `COUNT(*)` on each event table. Correlation across people is out of scope. 8. **L10.** `counts.swap` = `COUNT(*)` from `swap_events` in the window. Limit fills increment `counts.limit.kind=fill` only. Do not add fill count into swap count. Hybrid pool+book is still **one** swap row. 9. **Wrap honesty.** Wrap/unwrap **activity** counts are fee-event counts (`protocol_fee_events.source` `wrap`/`unwrap`), labeled as treasury-fee events, not wrap principal. Do **not** fold `ust1_mint` / `ust1_redeem` into wrap. Those are `fees.by_source` paths and optional `counts` keys under a **window** family, not `surface=wrap`. 10. **Fee-path enum.** Emit all seven `FeeSource::ALL` rows every day (idle source → `event_count=0`, `amount_usd="0"`). Unknown DB source strings: omit from JSON (do not interpolate into JS). Same PFee/L7 rules: not `spread_amount`, burn tax, gas, hook fees, community-tax extra-debit. 11. **USD idle vs unpriced.** Match #586 / #548 **C3**: idle source → `"0"`; activity + unpriced → JSON `null` (not `"0"`, not omit in a way that a client treats as $0). `share_pct` null when the day’s priced fee SUM is 0. Headline `fees.total_usd` uses the same idle/unpriced rule as overview fees (priced SUM; one unpriced source must not silently zero a priced wrap — follow EFee-6 / #687 for the **headline**, fail-closed per source). 12. **Amounts.** Plain integer/decimal digit strings (`bd_plain_string` / #557). No scientific notation. 13. **Gems.** Include indexed gem/test-pair activity in **counts** (this is evidence, not Llama). Document that the digest is **not** L639-safe. Fee USD still uses the protocol catalog (gems unpriced → `null` per source, not fake $1). 14. **Errors.** Bad `day` / future `day` (strictly after today’s UTC date) / `format=csv` / unknown query that would change grain (`window`, `timestamp`, `from`, `to`, `trader`, `sender`) → **400** with a short message. DB failure → **500** `"Internal server error"`. Empty indexed day → **200** with zero counts and zero-idle fee rows (not 404). Today’s incomplete UTC day → **200** with `"complete": false`. Do **not** copy Llama’s 404-if-not-rolled. 15. **Do not mutate** `/protocol/fees`, `/protocol/fees/daily`, `/defillama/daily`, `/overview`, `/gt/events`, pair/trader tapes, or #1205’s proposed `/evidence/daily`. 16. **No CSV** (`format=csv` → **400**). 17. **IPv4 peer governor** still applies. Do not add SmartIp/XFF. 18. **Timeouts.** Stay under the 30s `TimeoutLayer`. Bounded SQL: a handful of `COUNT(*)` / `GROUP BY source` / `GROUP BY asset_id` with time bounds. No `SELECT *` of the day’s events. No unbounded `DISTINCT` over senders. 19. **Token mix cap.** If `by_token` would be large, reuse the existing `/protocol/fees` “other” bucket (`is_other`) — do not return thousands of dust tokens. Still no wallets. --- ## Relevant files | Path | Role | |------|------| | [`indexer/src/api/mod.rs`](indexer/src/api/mod.rs) | `build_router`, `ApiDoc` paths/tags/schemas, governors | | [`indexer/src/api/protocol_fees.rs`](indexer/src/api/protocol_fees.rs) | Trailing mix shape (`by_source` / `by_token`) — **clock is wrong**; copy field semantics, not the window | | [`indexer/src/api/protocol_fee_series.rs`](indexer/src/api/protocol_fee_series.rs) | UTC series of **totals** — do not overload with `day=` mix | | [`indexer/src/api/defillama.rs`](indexer/src/api/defillama.rs) | UTC-day fee USD by source (listing) — do not 404, do not gem-exclude counts | | [`indexer/src/api/overview.rs`](indexer/src/api/overview.rs) | `unique_traders_24h` — **do not** copy this field | | [`indexer/src/indexer/protocol_fees.rs`](indexer/src/indexer/protocol_fees.rs) | `FeeSource`, wrap pin, UST1 window, PFee/L7 | | [`indexer/src/db/queries/protocol_fees.rs`](indexer/src/db/queries/protocol_fees.rs) | Rollup `GROUP BY source` idiom | | [`indexer/src/db/queries/swap_events.rs`](indexer/src/db/queries/swap_events.rs) | Swap count source | | [`indexer/src/db/queries/liquidity.rs`](indexer/src/db/queries/liquidity.rs) | LP count source | | [`indexer/src/db/queries/limit_order_fills.rs`](indexer/src/db/queries/limit_order_fills.rs) / `limit_order_lifecycle.rs` | Limit fill/place/cancel counts | | [`indexer/src/api/errors.rs`](indexer/src/api/errors.rs) | `internal_err` | | [`indexer/src/indexer/defillama.rs`](indexer/src/indexer/defillama.rs) | `utc_day_start` / timestamp parse — reuse day bounds, not Llama response | | [`indexer/tests/security.rs`](indexer/tests/security.rs) | Rate limit, 400/500, OpenAPI smoke | | [`docs/indexer-invariants.md`](docs/indexer-invariants.md) | New invariant row (anonymous digest vs #1205 tape vs Llama vs trailing fees) | | [`docs/runbooks/suspicious-activity-queries.md`](docs/runbooks/suspicious-activity-queries.md) | Pointer: anonymous digest vs unredacted incident SQL | New (expected): `indexer/src/api/evidence_digest.rs` (handler + queries), `indexer/tests/api_evidence_digest.rs`. If #1205 lands first and adds `indexer/src/api/evidence.rs`, put the digest handler in that module **as a second path** — do not fold digest fields onto paginated events. --- ## Recommended direction **One handler, bounded aggregate SQL, assemble in Rust.** ``` GET /api/v1/evidence/digest?day=2026-09-03 ``` Response shape (illustrative — additive JSON, `#[serde(skip_serializing_if)]` for absent optionals): ```json { "day": "2026-09-03", "timezone": "UTC", "complete": true, "counts": { "swap": { "swap": 10 }, "lp": { "add": 2, "remove": 1 }, "limit": { "place": 4, "cancel": 1, "fill": 3 }, "wrap": { "wrap": 1, "unwrap": 2 } }, "fees": { "total_usd": "12.34", "by_source": [ { "source": "swap_amm", "event_count": 8, "amount_usd": "10.00", "share_pct": "81.03" }, { "source": "book_take", "event_count": 3, "amount_usd": "1.00", "share_pct": "8.10" }, { "source": "limit_place", "event_count": 4, "amount_usd": "0.50", "share_pct": "4.05" }, { "source": "wrap", "event_count": 1, "amount_usd": "0.40", "share_pct": "3.24" }, { "source": "unwrap", "event_count": 2, "amount_usd": "0.44", "share_pct": "3.57" }, { "source": "ust1_mint", "event_count": 0, "amount_usd": "0", "share_pct": "0" }, { "source": "ust1_redeem", "event_count": 0, "amount_usd": "0", "share_pct": "0" } ], "by_token": [ { "symbol": "cUSTC", "contract_or_denom": "terra1…", "amount_usd": "11.00", "share_pct": "89.14", "is_other": false } ] } } ``` Implementation notes: - Parse `day` with `chrono::NaiveDate` + `and_hms_opt(0,0,0)` + `Utc`. Reject `2026-13-40`, `2026-09-03T00:00:00Z`, unix timestamps, and `day=today`. - `complete` = `day < Utc::now().date_naive()`. - SQL: five-ish statements (`COUNT` swap; `COUNT` LP `GROUP BY event_type`; `COUNT` place/cancel/fill; `COUNT` wrap/unwrap fees; fee `GROUP BY source`; optional token mix). Bind `$1`/`$2` as window start/end. - Register on the **global** `api_router` (with `/api/v1/protocol/fees`), not `lcd_heavy_router`. - Add `utoipa::path` + `ApiDoc` path/schema + tag **Evidence**. - Short curl + invariant row in [`docs/indexer-invariants.md`](docs/indexer-invariants.md). Do **not** expand #1204’s five-surface pack here. - `counts.wrap` is fee-event counts. Document that next to `fees.by_source` wrap/unwrap so agents do not double-count wrap as both volume and fee path. - Optional `counts.window` `{ "ust1_mint", "ust1_redeem" }` as fee-event counts **if** it keeps wrap vs window honest; if omitted, those exist only under `fees.by_source`. Pick one and test it — prefer **explicit `counts.window`** so mint/redeem are not mistaken for wrap. --- ## Acceptance criteria - **AC1.** `GET /api/v1/evidence/digest?day=YYYY-MM-DD` returns **200** JSON with `day`, `timezone=UTC`, `complete`, `counts`, `fees.by_source` (length 7, `FeeSource::ALL` order stable). - **AC2.** `counts.swap.swap` equals `COUNT(*)` of `swap_events` in the UTC window. Limit fills do **not** increase swap count (**L10**). Hybrid pool+book is one swap. - **AC3.** `counts.lp` `add`/`remove` match `liquidity_events.event_type`. GT names `join`/`exit` never appear. - **AC4.** `counts.limit` covers place + cancel + fill tables. Fill count may differ from swap count. - **AC5.** `counts.wrap` includes only `protocol_fee_events` `wrap`/`unwrap`. Seeded `ust1_mint` does not increment wrap. - **AC6.** `fees.by_source` for that day matches `GROUP BY source` on `protocol_fee_events` in the same window (event_count). Idle sources present with `event_count=0` and `amount_usd="0"`. - **AC7.** Trailing `/protocol/fees?window=24h` is **not** required to equal this document (different clock). A test that seeds all events at `00:30Z` vs previous day `23:30Z` proves calendar bounds. - **AC8.** Response body contains **no** seeded trader/maker/owner/provider bech32. Keys listed in guardrail 5 are absent. No `tx_hash` / `actor_hash`. - **AC9.** Missing `day`, malformed `day`, future `day`, `format=csv`, `window=24h`, `timestamp=`, `from=`, `sender=` → **400** (or `sender` ignored **and** still not used — prefer **400** for identity-shaped params so a client cannot think it filtered by wallet). - **AC10.** Empty indexed day → **200** zero counts + idle fee rows (not 404). Today UTC → `"complete": false`. A past day → `"complete": true`. - **AC11.** `ApiDoc` includes the path; `GET /api-docs/openapi.json` lists `/api/v1/evidence/digest`. `/protocol/fees`, `/protocol/fees/daily`, `/defillama/daily` contracts unchanged. - **AC12.** Handler uses sqlx bound parameters only. `internal_err` on DB failure (no sqlx text). - **AC13.** Route is not on the LCD-heavy governor list. - **AC14.** Handler SQL (string assert or query spy) does not mention `traders`, `sender`, `maker`, `owner`, `provider`, or `COUNT(DISTINCT`. Mentions only event/fee aggregate tables (+ `assets` for token symbols). - **AC15.** Amounts/USD are plain digit strings. Unpriced+activity source → `amount_usd` JSON `null`. --- ## Test plan (functional paths) Use the existing indexer test Postgres harness (same pattern as [`indexer/tests/api_pairs.rs`](indexer/tests/api_pairs.rs) / protocol fee tests). New file `indexer/tests/api_evidence_digest.rs`. 1. **Seed all surfaces + all seven fee sources** on one UTC day, plus spillover events the previous and next UTC day. `day=` counts/mix include only the middle day. 2. **L10** — one hybrid swap + one limit fill sharing a taker swap: `counts.swap.swap=1`, `counts.limit.fill=1`. 3. **LP** — add and remove; no `join`/`exit`. 4. **Wrap vs window** — wrap + unwrap + `ust1_mint` + `ust1_redeem`: wrap counts exclude mint/redeem; fee-path rows keep all four distinct. 5. **Idle sources** — no `book_take` that day → row still present, zeros. 6. **Unpriced vs idle** — fee activity with `fee_usd` null → source `amount_usd` null, idle sibling `"0"`. Headline follows EFee-6 (priced SUM; do not zero wrap because AMM is unpriced). 7. **Empty day** — 200 zeros, not 404. 8. **Incomplete today** — `complete=false`. 9. **Calendar vs trailing** — events at previous day 23:30Z and day 00:30Z: digest `day=` matches calendar; does not need to match `/protocol/fees?window=24h`. 10. **Token mix** — two fee tokens + dust → `other` bucket if that is the implemented cap; no trader addresses in `by_token`. 11. **Gems** — gem-pair swap increments `counts.swap`; Llama daily (if asserted in the same harness) still omits gems. Confirms this is not a Llama clone. 12. **OpenAPI** — spec contains the path. 13. **Regression** — existing `/protocol/fees`, `/protocol/fees/daily`, `/defillama/daily` tests still pass without response-shape changes. 14. **Anonymity stringify** — `serde_json::to_string` of the digest does not contain seeded actor bech32, `tx_hash` hex from fixtures, or keys `sender`/`maker`/`actor_hash`. --- ## Test plan (attack, hack, and abuse) 1. **SQL injection in `day`** — `day=2026-09-03'%20OR%201=1--`, `day=2026-09-03;DROP TABLE swap_events`. **400**, tables intact. 2. **Identity query params** — `sender=terra1…`, `trader=`, `maker=`, `address=`. **400** (preferred) or ignored; response **must not** filter or echo the address; counts remain protocol-wide. 3. **Join gadgets** — `tx=`, `tx_hash=`, `include_txs=1`, `events=1`, `unredacted=1`, `redact=0`, `join=traders`. Ignored or 400; body still has no txs/wallets. 4. **`COUNT(DISTINCT sender)` smuggling** — if a future patch adds it, AC14 SQL assert fails. Test that `unique_traders` is absent even when overview has a non-zero `unique_traders_24h` in the same DB. 5. **Future day** — `day=2099-01-01` → **400**. 6. **Llama-shaped `timestamp=`** — unix 00:00 on this route → **400** (wrong API), not a silent alias. 7. **`window=24h`** on this route → **400** (wrong clock). 8. **Method abuse** — `POST`/`PUT`/`DELETE` → **405**. 9. **CSV / formula** — `format=csv` → **400**. 10. **Error sanitization** — closed pool → `"Internal server error"`, no sqlx. 11. **Rate limit** — burst under test governor like `security.rs`; **429** with `Retry-After`. Not LCD-heavy. 12. **Response size** — digest JSON stays small (aggregates). A 10k-event day still returns one object, not 10k rows (assert no `events` array). 13. **Bech32 in token mix** — CW20 **token** contracts may appear on `by_token`. Seeded **user** actors still absent. Do not put wrap-mapper or user wallets in `by_token.symbol`. 14. **Do not scan `pair_reserves` or `traders`.** Query spy / SQL string assert. 15. **Cross-tenant mix-up** — seed two senders; digest totals match sums; no per-sender split field exists to enumerate. --- ## Verification criteria - `cargo test --manifest-path indexer/Cargo.toml --test api_evidence_digest -- --nocapture` (and existing `security.rs` / protocol fee / defillama tests) green. - Manual: curl `GET /api/v1/evidence/digest?day=<seeded>` against the test server; `jq` shows `counts` + seven `fees.by_source`; `rg -o 'terra1[a-z0-9]+'` on the body matches only token contracts from fee mix fixtures (or matches nothing if only native denoms), never seeded traders. - OpenAPI: `curl /api-docs/openapi.json | jq '.paths["/api/v1/evidence/digest"]'` non-null. - Invariants doc updated: anonymous digest vs #1205 event tape (`tx_hash` allowed there, forbidden here) vs Llama vs trailing `/protocol/fees`. - No change to `/gt/events`, `/protocol/fees`, `/defillama/daily`, or trader route contracts. --- ## Out of scope - Frontend / Protocol page charts (already consume trailing mix + daily series). - #1205 paginated event export (`actor_hash` / `tx_hash`). - #1204 OpenAPI pack for existing five surfaces. - #1202 product analytics (`tx_succeeded` join). - Wrap **principal** tape, new mapper ingest, CSV, NDJSON, object storage, scheduled dumps. - Unredacted export, API tokens, HMAC salts, operator-only routes. - Unique trader / active pair census (overview already has trailing versions). - Per-pair or per-token **activity** leaderboards. - Multi-day ranges, block-height windows. - Changing suspicious-activity SQL runbook recipes (a one-line pointer is enough). - Deploy, image, or host selection.
Member

/agent implement

/agent implement
Author
Owner

cl8y-agent-control: queued implement job c7b93778-8a41-405a-8e52-5b53f767308a (not executed; no Hetzner VM).

cl8y-agent-control: queued `implement` job `c7b93778-8a41-405a-8e52-5b53f767308a` (not executed; no Hetzner VM).
Author
Owner

/agent implement

/agent implement
Author
Owner

cl8y-agent-control: queued design_author job d90b2425-e5b2-4675-9877-aa95148693f4 (not executed; no Hetzner VM).

cl8y-agent-control: queued `design_author` job `d90b2425-e5b2-4675-9877-aa95148693f4` (not executed; no Hetzner VM).
Author
Owner

cl8y-agent-control: queued design_author job 61f15a45-8b48-4f7d-bab1-bcea2967ceb8 (not executed; no Hetzner VM).

cl8y-agent-control: queued `design_author` job `61f15a45-8b48-4f7d-bab1-bcea2967ceb8` (not executed; no Hetzner VM).
Author
Owner

cl8y-agent-control: needs_human inbox card POST failed. Job stays parked.

cl8y-agent-control: needs_human inbox card POST failed. Job stays parked.
Author
Owner

Verified 2026-09-24 against main 54c4868e: #1206 is not closable. Current router exposes only GET /api/v1/evidence/daily (#1205); the digest handler/route, API integration tests, OpenAPI path, and make verify-issue-1206 are absent. No newer issue through #1335 tracks or replaces this scope, and #1205 is a separate tx-linkable event tape.

Remaining:

  1. Implement the read-only UTC-day aggregate, its seven baseline fee paths/token mix, and anonymity/query/UTC/cache requirements.
  2. Add Postgres count/privacy/abuse tests, OpenAPI coverage, and make verify-issue-1206.
  3. Reconcile the exact-seven contract if #1209/#1210 land first; keep #1211 cohort and #1202 client events separate.

Related: #1204, #1205, #586, #689, #631, #687, #216, #614, #557, #576, #1209, #1210, #1211, #1202.

The worktree adds proposed ADR 0014, the invariant row, architecture/runbook links, and the third-party agent playbook. Those docs do not claim the route is shipped.

Verified 2026-09-24 against main 54c4868e: #1206 is not closable. Current router exposes only GET /api/v1/evidence/daily (#1205); the digest handler/route, API integration tests, OpenAPI path, and make verify-issue-1206 are absent. No newer issue through #1335 tracks or replaces this scope, and #1205 is a separate tx-linkable event tape. Remaining: 1. Implement the read-only UTC-day aggregate, its seven baseline fee paths/token mix, and anonymity/query/UTC/cache requirements. 2. Add Postgres count/privacy/abuse tests, OpenAPI coverage, and make verify-issue-1206. 3. Reconcile the exact-seven contract if #1209/#1210 land first; keep #1211 cohort and #1202 client events separate. Related: #1204, #1205, #586, #689, #631, #687, #216, #614, #557, #576, #1209, #1210, #1211, #1202. The worktree adds proposed ADR 0014, the invariant row, architecture/runbook links, and the third-party agent playbook. Those docs do not claim the route is shipped.
Sign in to join this conversation.
No milestone
No project
No assignees
2 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#1206
No description provided.