feat(frontend): privacy-reviewed source-to-fee product events (Phase 0 S1) #1202

Open
opened 2026-09-04 03:50:07 +00:00 by PlasticDigits · 10 comments

Phase 0 S1: add privacy-reviewed DEX client product events so a campaign_id can survive from landing through tx_succeeded and join indexer fee rows via txhash. Descriptive attribution only. Indexer remains fee truth. Pair-creation fee indexing stays a separate ticket.

Formatted engineering report (current codebase, constraints, direction, acceptance, and test plans) is in the comment below.

Phase 0 S1: add privacy-reviewed DEX client product events so a campaign_id can survive from landing through tx_succeeded and join indexer fee rows via txhash. Descriptive attribution only. Indexer remains fee truth. Pair-creation fee indexing stays a separate ticket. Formatted engineering report (current codebase, constraints, direction, acceptance, and test plans) is in the comment below.
Author
Owner

Summary

Marketing cannot measure source → fee. The DEX SPA (frontend-dapp on dex.cl8y.com) has no PostHog, gtag, Plausible, or other product-analytics SDK. There is no UTM / campaign_id / cl8y_intent persistence. Swap and Create Pair replace the query string with a canonical set after land, so inbound campaign params are wiped unless captured first.

Add privacy-reviewed first-party product events so a campaign_id can survive from landing through tx_succeeded, joinable to indexer fee rows via txhash. Attribution is descriptive only — not causal ROI. The indexer remains the sole economic authority for fees. Do not change FeeSource. Pair-creation fee indexing stays a separate ticket.

Related spec lives in PlasticDigits/cl8y-marketing (strategy/operating-system.md product event contract; Phase 0 item 6 in strategy/90-day-plan.md). There is no strategy/source-to-fee-instrumentation.md or wallet-exclusions.md in-tree yet.


Current codebase

Architecture

Browser SPA (Vite/React) ──LCD/RPC──▶ Terra Classic
        │                    │
        │ quotes/charts      │ txs (sign → broadcast → poll)
        ▼                    ▼
   Indexer API          On-chain events
   (Axum/Postgres) ──▶ protocol_fee_events (fee truth)
  • Client: frontend-dapp — React 19 + Vite 7 + TanStack Query + Zustand + cosmes wallets.
  • Indexer: indexer — Rust Axum/SQLx; economic authority for fees.
  • No analytics module, feature-flag service, or third-party pixels in deps or index.html.
  • Production CSP connect-src is allowlisted (LCD/RPC/indexer/WalletConnect/Legal). Third-party analytics hosts are blocked unless CSP is updated (viteCsp.ts).

Surfaces in scope

Surface Route Page
Trade (swap) / (/swap alias) SwapPage.tsx
Limit / book /trade, /trade/:pairAddr, /limits TradePage.tsx, LimitOrdersPage.tsx
Wrap /wrap WrapPage.tsx
UST1 /ust1 Ust1Page.tsx
Create token /token/create CreateTokenPage.tsx
Create pair /create CreatePairPage.tsx

Wrap / UST1 / create-token nav items appear when the corresponding env addresses are set.

UTM / campaign

Grep finds no utm_, campaign_id, or cl8y_intent. SwapPage and CreatePairPage call setSearchParams(..., { replace: true }) with canonical keys only (from/to/amount or pair tokens). Inbound UTMs are stripped after first sync unless captured on land first.

Wallet, quotes, txs, share

  • Wallet: WalletModal → useWalletStore.connect → connectTerraWallet. Persistence keys cl8y_wallet_connection, cl8y_dev_sim.
  • Quotes: hybrid LCD + indexer route/solve on Swap — natural quote_requested hook.
  • Tx: terraBroadcast.ts phases signing → broadcasting → confirming (+ recovering). useTerraBroadcastMutation exposes phase + pendingTxHash; success data is typically the tx hash string. Maps to tx_sign_prompted / tx_broadcast / tx_succeeded / tx_failed.
  • Share: ShareLinkButton + shareOrCopyPageLink (Web Share → clipboard) on Swap, Trader, Portfolio. Shared URLs are canonical and campaign-free today.

Indexer join key (do not change FeeSource)

protocol_fee_events.tx_hash with UNIQUE (tx_hash, source, ordinal). Sources today: swap_amm, book_take, limit_place, wrap, unwrap, ust1_mint, ust1_redeem. Public API is aggregate (GET /api/v1/protocol/fees); there is no by-txhash HTTP API. Reconciliation is offline/SQL or a future marketing-core ingest.

Feature flags

No LaunchDarkly/Unleash. Behavior is VITE_*. There is no VITE_ANALYTICS_ENDPOINT or analytics kill-switch yet. Simulated Wallet exists only in VITE_DEV_MODE and must not hit a production sink.


Why a new implementation is needed

Phase 0 marketing cannot claim what acquires fee-paying flow. Without client events + durable campaign params, landing traffic cannot be joined to protocol_fee_events on txhash. Indexer aggregates cannot answer “which campaign”. Third-party pixels are the wrong default: production CSP blocks them, and raw wallets must not leave the first party.

Capture must run before Swap/Create Pair URL canonicalization, or campaign params are lost on the primary trade landing path.


Constraints and guardrails

  1. Indexer remains the sole fee economic authority. Client events are funnel/touch only. Do not extend FeeSource or put exact fees in analytics.
  2. Amounts in analytics are bucketed. Exact fees stay in the indexer.
  3. No raw wallet addresses in third-party payloads. Prefer omit wallet from all analytics payloads. Optional local actor_key with offline salt.
  4. First-party HTTPS sink preferred (future cl8y-marketing-core or a minimal Coolify collector). A vendor is allowed only if already privacy-reviewed and CSP/script allowlisted. None is in-tree today.
  5. Out of scope: indexer FeeSource / pair_creation (separate ticket), marketing Postgres schema in this repo, incrementality experiments, Telegram/X ↔ wallet joins, paid media pixels.
  6. Document ≤30d TTL and privacy basis for stored UTMs / cl8y_intent.
  7. wallet-exclusions applies on the reporting side, not in the client. Do not invent exclusion lists in the SPA.
  8. Simulated Wallet / VITE_DEV_MODE must not emit to the production sink.
  9. Share links stay canonical unless product later opts into explicit campaign share URLs. Attribution uses stored first/last touch, not rewritten share URLs.
  10. Do not treat first-touch or last-touch as causal lift.

Relevant files

Area Files
App bootstrap / routes frontend-dapp/src/main.tsx, App.tsx, components/common/navItems.ts
CSP / env frontend-dapp/viteCsp.ts, index.html, .env.example, package.json
URL strip risk frontend-dapp/src/pages/SwapPage.tsx, utils/swapQueryParams.ts, pages/CreatePairPage.tsx
Wallet frontend-dapp/src/hooks/useWallet.ts, components/wallet/WalletModal.tsx
Tx funnel frontend-dapp/src/hooks/useTerraBroadcastMutation.ts, services/terraclassic/terraBroadcast.ts
Quotes frontend-dapp/src/services/indexer/client.ts, Swap quote effects
Share frontend-dapp/src/components/ui/ShareLinkButton.tsx, utils/sharePageLink.ts
Other surfaces pages/TradePage.tsx, WrapPage.tsx, Ust1Page.tsx, CreateTokenPage.tsx, components/trade/TradeMarketOrderPanel.tsx, TradeOrderTicket.tsx
Fee join (read-only) indexer/src/indexer/protocol_fees.rs, indexer/migrations/20260821120000_protocol_fees.sql, indexer/src/api/protocol_fees.rs
Marketing spec cl8y-marketing/strategy/operating-system.md, 90-day-plan.md, fee-growth-model.md, technical-architecture.md, founder-decisions.md

  1. Capture-on-land first (synchronously, before Swap/Create Pair setSearchParams): parse utm_* + cl8y_intent (+ agreed campaign taxonomy keys); persist first-touch and last-touch in localStorage with ≤30d TTL.
  2. Thin first-party track(event, props) module, gated by env (VITE_ANALYTICS_ENDPOINT or equivalent kill-switch) so it can ship dark.
  3. Wire:
    • Layout/router → qualified_page_view
    • Quote effects → quote_requested (bucketed amounts)
    • Wallet modal/store → wallet_connect_started / wallet_connect_succeeded
    • useTerraBroadcastMutation / phase callbacks → tx_sign_prompted / tx_broadcast / tx_succeeded / tx_failed with txhash, action, pair/route, first+last campaign_id
    • ShareLinkButton → share_started / share_completed (if share UI exists on that surface; else defer that event only)
  4. Sink: first-party HTTPS endpoint. Add it to CSP connect-src. Do not add gtag/PostHog unless separately approved.
  5. Identity: random actor_key in local storage; never send raw wallet to third parties.
  6. Reconcile offline: marketing joins events → protocol_fee_events on txhash.
  7. Short DEX docs or PR note linking the marketing event contract. Author source-to-fee-instrumentation.md in marketing if still missing; do not block the client on that filename.

Acceptance criteria

  • Campaign params persist across wallet connect → sign → broadcast on the happy path (first + last touch available on tx_succeeded).
  • Capture runs before Swap/Create Pair URL canonicalization; UTMs are not lost on / or /create.
  • Minimum events from the marketing product event contract are emitted on in-scope surfaces: qualified_page_view, quote_requested, wallet_connect_started / wallet_connect_succeeded, tx_sign_prompted, tx_broadcast / tx_succeeded / tx_failed, and share events where share UI exists.
  • tx_broadcast / tx_succeeded include txhash, action, pair/route, first+last campaign_id.
  • Amounts in analytics are bucketed; exact fees are not sent.
  • ≥1 staging/mainnet test tx shows client tx_succeeded.txhash matching an indexer protocol_fee_events.tx_hash (or equivalent success/fee row).
  • Network log / schema: no raw wallets in third-party payloads (prefer no wallet in first-party payloads either).
  • Feature flag or env gate: can ship dark then enable. VITE_DEV_MODE / Simulated Wallet does not hit prod sink.
  • CSP allowlists only the approved first-party sink (or reviewed vendor).
  • Short note in DEX docs or PR linking the marketing spec.
  • Indexer FeeSource unchanged.

Test plan: all paths

  1. Land with UTMs + cl8y_intent on /: first-touch stored; Swap canonical URL rewrite does not drop stored campaign; qualified_page_view fires once per qualified land.
  2. Land with no params: events still fire; campaign ids empty/absent per spec; no throw.
  3. Last-touch update: second land with a different campaign updates last-touch only; first-touch unchanged until TTL.
  4. TTL: stored touches expire at ≤30d; documented.
  5. Quote: quote_requested on swap quote (success, empty route, LCD error) with bucketed amount.
  6. Wallet: started / succeeded / cancelled / failed connect; no address in payload.
  7. Tx happy path on swap, limit/book, wrap, UST1, create-token, create-pair (if in dApp): sign → broadcast → success with matching txhash.
  8. Tx fail paths: user reject sign, broadcast error, confirm timeout/fail — tx_failed with normalized stage; no fake success.
  9. Share: started/completed and cancel/failure; shared URL remains canonical (no forced UTM pollution).
  10. Dark flag: env unset ⇒ zero network beacons.
  11. Dev/sim wallet: no prod sink.
  12. CSP: production build connect-src includes the sink and still blocks arbitrary analytics hosts.
  13. Join: one real or captured txhash exists in protocol_fee_events.
  14. Surfaces without quote/share: events that do not apply are omitted, not stubbed with dummy pair ids.

Test plan: attack, hack, and abuse vectors

Vector Expected result
Hostile campaign_id / UTM (XSS, huge strings, unknown keys) Sanitize length/charset; drop unknown keys; never echo raw query into DOM or logs
Campaign spoofing / last-touch hijack Treated as descriptive only; server allowlist/taxonomy if sink exists; first-touch preferred for “source of land”
Fake tx_succeeded without chain confirm Emit only after confirmed phase / successful mutation; marketing join still requires indexer row
Wallet leakage in props, share text, or WC logs Redact; tests fail if terra1/0x address appears in analytics payload
Bot / self-traffic / Simulated Wallet Dev gate; reporting-side exclusions later; client must not special-case production wallets
Analytics sink SSRF / open beacon Fixed allowlisted URL from env; no user-controlled sink host
CSP / supply-chain (adding gtag/PostHog) Default is first-party beacon only; vendor requires explicit review
localStorage tampering Corrupt JSON ignored; no throw; no privilege gain
Share amplification of organic links Do not claim last-touch from canonical shares without campaign

Verification criteria

  • Unit tests for capture-before-canonicalize, TTL, sanitization, and payload schema (no wallet, bucketed amounts, txhash present on success).
  • Hook tests for wallet and useTerraBroadcastMutation phase mapping.
  • Playwright or equivalent: land with UTMs → connect (sim or stub) → swap (stub broadcast) → tx_succeeded payload still has first-touch campaign id after URL rewrite.
  • Production CSP snapshot includes only the approved sink.
  • Manual: one staging/mainnet txhash equals an indexer fee/success row.
  • Docs/PR links the marketing event contract and states indexer is fee truth.
  • make / existing frontend verify targets still pass; indexer tests untouched.
## Summary Marketing cannot measure source → fee. The DEX SPA (`frontend-dapp` on dex.cl8y.com) has no PostHog, gtag, Plausible, or other product-analytics SDK. There is no UTM / `campaign_id` / `cl8y_intent` persistence. Swap and Create Pair **replace** the query string with a canonical set after land, so inbound campaign params are wiped unless captured first. Add privacy-reviewed first-party product events so a `campaign_id` can survive from landing through `tx_succeeded`, joinable to indexer fee rows via `txhash`. Attribution is descriptive only — not causal ROI. The indexer remains the sole economic authority for fees. Do not change `FeeSource`. Pair-creation fee indexing stays a separate ticket. Related spec lives in `PlasticDigits/cl8y-marketing` (`strategy/operating-system.md` product event contract; Phase 0 item 6 in `strategy/90-day-plan.md`). There is no `strategy/source-to-fee-instrumentation.md` or `wallet-exclusions.md` in-tree yet. --- ## Current codebase ### Architecture ```text Browser SPA (Vite/React) ──LCD/RPC──▶ Terra Classic │ │ │ quotes/charts │ txs (sign → broadcast → poll) ▼ ▼ Indexer API On-chain events (Axum/Postgres) ──▶ protocol_fee_events (fee truth) ``` - **Client:** `frontend-dapp` — React 19 + Vite 7 + TanStack Query + Zustand + cosmes wallets. - **Indexer:** `indexer` — Rust Axum/SQLx; economic authority for fees. - **No analytics module, feature-flag service, or third-party pixels** in deps or `index.html`. - Production CSP `connect-src` is allowlisted (LCD/RPC/indexer/WalletConnect/Legal). Third-party analytics hosts are blocked unless CSP is updated (`viteCsp.ts`). ### Surfaces in scope | Surface | Route | Page | |---|---|---| | Trade (swap) | `/` (`/swap` alias) | `SwapPage.tsx` | | Limit / book | `/trade`, `/trade/:pairAddr`, `/limits` | `TradePage.tsx`, `LimitOrdersPage.tsx` | | Wrap | `/wrap` | `WrapPage.tsx` | | UST1 | `/ust1` | `Ust1Page.tsx` | | Create token | `/token/create` | `CreateTokenPage.tsx` | | Create pair | `/create` | `CreatePairPage.tsx` | Wrap / UST1 / create-token nav items appear when the corresponding env addresses are set. ### UTM / campaign Grep finds no `utm_`, `campaign_id`, or `cl8y_intent`. `SwapPage` and `CreatePairPage` call `setSearchParams(..., { replace: true })` with canonical keys only (`from`/`to`/amount or pair tokens). **Inbound UTMs are stripped after first sync** unless captured on land first. ### Wallet, quotes, txs, share - Wallet: `WalletModal` → `useWalletStore.connect` → `connectTerraWallet`. Persistence keys `cl8y_wallet_connection`, `cl8y_dev_sim`. - Quotes: hybrid LCD + indexer `route/solve` on Swap — natural `quote_requested` hook. - Tx: `terraBroadcast.ts` phases `signing` → `broadcasting` → `confirming` (+ `recovering`). `useTerraBroadcastMutation` exposes `phase` + `pendingTxHash`; success data is typically the tx hash string. Maps to `tx_sign_prompted` / `tx_broadcast` / `tx_succeeded` / `tx_failed`. - Share: `ShareLinkButton` + `shareOrCopyPageLink` (Web Share → clipboard) on Swap, Trader, Portfolio. Shared URLs are canonical and campaign-free today. ### Indexer join key (do not change FeeSource) `protocol_fee_events.tx_hash` with `UNIQUE (tx_hash, source, ordinal)`. Sources today: `swap_amm`, `book_take`, `limit_place`, `wrap`, `unwrap`, `ust1_mint`, `ust1_redeem`. Public API is aggregate (`GET /api/v1/protocol/fees`); there is no by-txhash HTTP API. Reconciliation is offline/SQL or a future marketing-core ingest. ### Feature flags No LaunchDarkly/Unleash. Behavior is `VITE_*`. There is no `VITE_ANALYTICS_ENDPOINT` or analytics kill-switch yet. Simulated Wallet exists only in `VITE_DEV_MODE` and must not hit a production sink. --- ## Why a new implementation is needed Phase 0 marketing cannot claim what acquires fee-paying flow. Without client events + durable campaign params, landing traffic cannot be joined to `protocol_fee_events` on `txhash`. Indexer aggregates cannot answer “which campaign”. Third-party pixels are the wrong default: production CSP blocks them, and raw wallets must not leave the first party. Capture must run **before** Swap/Create Pair URL canonicalization, or campaign params are lost on the primary trade landing path. --- ## Constraints and guardrails 1. Indexer remains the **sole fee economic authority**. Client events are funnel/touch only. Do not extend `FeeSource` or put exact fees in analytics. 2. Amounts in analytics are **bucketed**. Exact fees stay in the indexer. 3. **No raw wallet addresses** in third-party payloads. Prefer omit wallet from all analytics payloads. Optional local `actor_key` with offline salt. 4. First-party HTTPS sink preferred (future `cl8y-marketing-core` or a minimal Coolify collector). A vendor is allowed only if already privacy-reviewed **and** CSP/script allowlisted. None is in-tree today. 5. Out of scope: indexer `FeeSource` / pair_creation (separate ticket), marketing Postgres schema in this repo, incrementality experiments, Telegram/X ↔ wallet joins, paid media pixels. 6. Document ≤30d TTL and privacy basis for stored UTMs / `cl8y_intent`. 7. `wallet-exclusions` applies on the **reporting** side, not in the client. Do not invent exclusion lists in the SPA. 8. Simulated Wallet / `VITE_DEV_MODE` must not emit to the production sink. 9. Share links stay canonical unless product later opts into explicit campaign share URLs. Attribution uses stored first/last touch, not rewritten share URLs. 10. Do not treat first-touch or last-touch as causal lift. --- ## Relevant files | Area | Files | |---|---| | App bootstrap / routes | `frontend-dapp/src/main.tsx`, `App.tsx`, `components/common/navItems.ts` | | CSP / env | `frontend-dapp/viteCsp.ts`, `index.html`, `.env.example`, `package.json` | | URL strip risk | `frontend-dapp/src/pages/SwapPage.tsx`, `utils/swapQueryParams.ts`, `pages/CreatePairPage.tsx` | | Wallet | `frontend-dapp/src/hooks/useWallet.ts`, `components/wallet/WalletModal.tsx` | | Tx funnel | `frontend-dapp/src/hooks/useTerraBroadcastMutation.ts`, `services/terraclassic/terraBroadcast.ts` | | Quotes | `frontend-dapp/src/services/indexer/client.ts`, Swap quote effects | | Share | `frontend-dapp/src/components/ui/ShareLinkButton.tsx`, `utils/sharePageLink.ts` | | Other surfaces | `pages/TradePage.tsx`, `WrapPage.tsx`, `Ust1Page.tsx`, `CreateTokenPage.tsx`, `components/trade/TradeMarketOrderPanel.tsx`, `TradeOrderTicket.tsx` | | Fee join (read-only) | `indexer/src/indexer/protocol_fees.rs`, `indexer/migrations/20260821120000_protocol_fees.sql`, `indexer/src/api/protocol_fees.rs` | | Marketing spec | `cl8y-marketing/strategy/operating-system.md`, `90-day-plan.md`, `fee-growth-model.md`, `technical-architecture.md`, `founder-decisions.md` | --- ## Recommended direction 1. **Capture-on-land first** (synchronously, before Swap/Create Pair `setSearchParams`): parse `utm_*` + `cl8y_intent` (+ agreed campaign taxonomy keys); persist first-touch and last-touch in `localStorage` with **≤30d TTL**. 2. **Thin first-party `track(event, props)` module**, gated by env (`VITE_ANALYTICS_ENDPOINT` or equivalent kill-switch) so it can ship dark. 3. Wire: - Layout/router → `qualified_page_view` - Quote effects → `quote_requested` (bucketed amounts) - Wallet modal/store → `wallet_connect_started` / `wallet_connect_succeeded` - `useTerraBroadcastMutation` / phase callbacks → `tx_sign_prompted` / `tx_broadcast` / `tx_succeeded` / `tx_failed` with `txhash`, action, pair/route, first+last `campaign_id` - `ShareLinkButton` → `share_started` / `share_completed` (if share UI exists on that surface; else defer that event only) 4. **Sink:** first-party HTTPS endpoint. Add it to CSP `connect-src`. Do not add gtag/PostHog unless separately approved. 5. Identity: random `actor_key` in local storage; never send raw wallet to third parties. 6. Reconcile offline: marketing joins events → `protocol_fee_events` on `txhash`. 7. Short DEX docs or PR note linking the marketing event contract. Author `source-to-fee-instrumentation.md` in marketing if still missing; do not block the client on that filename. --- ## Acceptance criteria - [ ] Campaign params persist across wallet connect → sign → broadcast on the happy path (first + last touch available on `tx_succeeded`). - [ ] Capture runs before Swap/Create Pair URL canonicalization; UTMs are not lost on `/` or `/create`. - [ ] Minimum events from the marketing product event contract are emitted on in-scope surfaces: `qualified_page_view`, `quote_requested`, `wallet_connect_started` / `wallet_connect_succeeded`, `tx_sign_prompted`, `tx_broadcast` / `tx_succeeded` / `tx_failed`, and share events where share UI exists. - [ ] `tx_broadcast` / `tx_succeeded` include `txhash`, action, pair/route, first+last `campaign_id`. - [ ] Amounts in analytics are bucketed; exact fees are not sent. - [ ] ≥1 staging/mainnet test tx shows client `tx_succeeded.txhash` matching an indexer `protocol_fee_events.tx_hash` (or equivalent success/fee row). - [ ] Network log / schema: no raw wallets in third-party payloads (prefer no wallet in first-party payloads either). - [ ] Feature flag or env gate: can ship dark then enable. `VITE_DEV_MODE` / Simulated Wallet does not hit prod sink. - [ ] CSP allowlists only the approved first-party sink (or reviewed vendor). - [ ] Short note in DEX docs or PR linking the marketing spec. - [ ] Indexer `FeeSource` unchanged. --- ## Test plan: all paths 1. **Land with UTMs + `cl8y_intent` on `/`:** first-touch stored; Swap canonical URL rewrite does not drop stored campaign; `qualified_page_view` fires once per qualified land. 2. **Land with no params:** events still fire; campaign ids empty/absent per spec; no throw. 3. **Last-touch update:** second land with a different campaign updates last-touch only; first-touch unchanged until TTL. 4. **TTL:** stored touches expire at ≤30d; documented. 5. **Quote:** `quote_requested` on swap quote (success, empty route, LCD error) with bucketed amount. 6. **Wallet:** started / succeeded / cancelled / failed connect; no address in payload. 7. **Tx happy path** on swap, limit/book, wrap, UST1, create-token, create-pair (if in dApp): sign → broadcast → success with matching `txhash`. 8. **Tx fail paths:** user reject sign, broadcast error, confirm timeout/fail — `tx_failed` with normalized stage; no fake success. 9. **Share:** started/completed and cancel/failure; shared URL remains canonical (no forced UTM pollution). 10. **Dark flag:** env unset ⇒ zero network beacons. 11. **Dev/sim wallet:** no prod sink. 12. **CSP:** production build connect-src includes the sink and still blocks arbitrary analytics hosts. 13. **Join:** one real or captured txhash exists in `protocol_fee_events`. 14. **Surfaces without quote/share:** events that do not apply are omitted, not stubbed with dummy pair ids. --- ## Test plan: attack, hack, and abuse vectors | Vector | Expected result | |---|---| | Hostile `campaign_id` / UTM (XSS, huge strings, unknown keys) | Sanitize length/charset; drop unknown keys; never echo raw query into DOM or logs | | Campaign spoofing / last-touch hijack | Treated as descriptive only; server allowlist/taxonomy if sink exists; first-touch preferred for “source of land” | | Fake `tx_succeeded` without chain confirm | Emit only after confirmed phase / successful mutation; marketing join still requires indexer row | | Wallet leakage in props, share text, or WC logs | Redact; tests fail if `terra1`/`0x` address appears in analytics payload | | Bot / self-traffic / Simulated Wallet | Dev gate; reporting-side exclusions later; client must not special-case production wallets | | Analytics sink SSRF / open beacon | Fixed allowlisted URL from env; no user-controlled sink host | | CSP / supply-chain (adding gtag/PostHog) | Default is first-party beacon only; vendor requires explicit review | | localStorage tampering | Corrupt JSON ignored; no throw; no privilege gain | | Share amplification of organic links | Do not claim last-touch from canonical shares without campaign | --- ## Verification criteria - Unit tests for capture-before-canonicalize, TTL, sanitization, and payload schema (no wallet, bucketed amounts, txhash present on success). - Hook tests for wallet and `useTerraBroadcastMutation` phase mapping. - Playwright or equivalent: land with UTMs → connect (sim or stub) → swap (stub broadcast) → `tx_succeeded` payload still has first-touch campaign id after URL rewrite. - Production CSP snapshot includes only the approved sink. - Manual: one staging/mainnet txhash equals an indexer fee/success row. - Docs/PR links the marketing event contract and states indexer is fee truth. - `make` / existing frontend verify targets still pass; indexer tests untouched.
PlasticDigits changed title from Intake request — DEX client source-to-fee instrumentation (Phase 0 S1) From: Ma to feat(frontend): privacy-reviewed source-to-fee product events (Phase 0 S1) 2026-09-04 04:03:51 +00:00
Author
Owner

+1; S1 also requires ≥80% coverage of non-excluded successful trades on instrumented surfaces (join tx_succeeded.txhash to indexer fee rows).

+1; S1 also requires ≥80% coverage of non-excluded successful trades on instrumented surfaces (join tx_succeeded.txhash to indexer fee rows).

Privacy DoR (enqueue / ship hold) — Privacy policy alignment

Not legal advice. Gap IDs: G1 (block), G3 (high). Forwarded via Market sensor to Ops Sensor as enqueue hold.

Must be true before enqueue claims “ready” / before ship:

  1. Privacy Notice published (or a founder/counsel-approved interim notice linked from dex) that names S1 client-event collection, purposes, retention, and destinations. T&Cs-only is not enough (G1).
  2. Event schema written (names, fields, PII/pseudonym classes) with explicit no raw wallets to third-party vendors (product rule).
  3. Destinations listed: first-party only vs any third party; if any 3P, disclose in notice and get founder/counsel sign-off before enqueue.
  4. Opt-out / refuse decision recorded by founder: required or not; if UI offers refuse, processing must actually stop or down-scope (consent ≠ decoration).
  5. Founder “no analytics on dex today” remains true until (1)+(4) land — do not ship measurement IDs / SDKs ahead of notice.
  6. Redaction test plan: sample payloads show wallets absent or irreversibly redacted before any vendor hop.

Nice-to-have (not ship-blockers alone): align with bridge storage-consent schema (cl8y-bridge-monorepo#165) if S1 shares the same consent record; document retention/deletion owner.

Not blocked on PlasticDigits/cl8y-agent-control#65 (that issue is work-queue ranking research, not privacy).

## Privacy DoR (enqueue / ship hold) — Privacy policy alignment Not legal advice. Gap IDs: **G1** (block), **G3** (high). Forwarded via Market sensor to Ops Sensor as enqueue hold. **Must be true before enqueue claims “ready” / before ship:** 1. **Privacy Notice published** (or a founder/counsel-approved interim notice linked from dex) that names S1 client-event collection, purposes, retention, and destinations. T&Cs-only is not enough (G1). 2. **Event schema written** (names, fields, PII/pseudonym classes) with explicit **no raw wallets to third-party vendors** (product rule). 3. **Destinations listed:** first-party only vs any third party; if any 3P, disclose in notice and get founder/counsel sign-off before enqueue. 4. **Opt-out / refuse decision recorded** by founder: required or not; if UI offers refuse, processing must actually stop or down-scope (consent ≠ decoration). 5. **Founder “no analytics on dex today” remains true** until (1)+(4) land — do not ship measurement IDs / SDKs ahead of notice. 6. **Redaction test plan:** sample payloads show wallets absent or irreversibly redacted before any vendor hop. **Nice-to-have (not ship-blockers alone):** align with bridge storage-consent schema (cl8y-bridge-monorepo#165) if S1 shares the same consent record; document retention/deletion owner. **Not blocked on** PlasticDigits/cl8y-agent-control#65 (that issue is work-queue ranking research, not privacy).

DRAFT — DoR items 2 / 3 / 6 (schema · destinations · redaction)

Status: DRAFT — not ship approval / not legal advice. For founder/counsel + eng review. Full stub kept by Privacy policy alignment; Market sensor may send event-name deltas after field inventory.

2 — Event schema (field classes)

Aligned with source-to-fee-instrumentation (names provisional until eng finalizes):

Event Allowed Never to 3P
qualified_page_view route, referrer_class, campaign_id, cl8y_intent, device_class, session_id raw wallet, email, X handle, IP-as-stable-id
wallet_connect_started / succeeded wallet_type, actor_key_pseudo, session_id raw wallet address to vendors
tx_broadcast / tx_succeeded / tx_failed campaign_id (+ first/last touch), txhash, session_id, outcome raw wallet to vendors

actor_key_pseudo = first-party irreversible / session key — not the on-chain address in cleartext to third parties. Campaign persistence: first-party ≤30d TTL (plan rec); disclose when Privacy Notice (G1) ships.

3 — Destinations

Destination Class Rule
Coolify / marketing Postgres / marketing API (when built) 1P Preferred for S1 receipts
Indexer 1P fee authority Join via txhash; not a marketing vendor
PostHog / GA / etc. 3P — not approved today Founder: no analytics SDK on dex; do not add without Notice + counsel
WalletConnect / Coinbase CCA 3P product stack Separate from S1; do not piggyback wallets onto these

Any proposed 3P hop for S1 payloads → stop for founder/counsel + Notice update before enqueue.

6 — Redaction tests (sketches)

  1. No analytics SDK / measurement IDs in dex bundle.
  2. Network: no S1 POSTs off allowlisted 1P hosts.
  3. Golden payloads: no cleartext wallets toward 3P; actor_key_pseudo ≠ raw address.
  4. tx_succeeded carries campaign_id + txhash for indexer join without wallet on marketing hop.
  5. If founder requires refuse: must stop/down-scope S1 (pair with bridge#165 consent record if shared).

Still founder-gated: G1 Privacy Notice; DoR 4 opt-out decision.

## DRAFT — DoR items 2 / 3 / 6 (schema · destinations · redaction) **Status: DRAFT — not ship approval / not legal advice.** For founder/counsel + eng review. Full stub kept by Privacy policy alignment; Market sensor may send event-name deltas after field inventory. ### 2 — Event schema (field classes) Aligned with `source-to-fee-instrumentation` (names provisional until eng finalizes): | Event | Allowed | Never to 3P | |---|---|---| | `qualified_page_view` | route, referrer_class, campaign_id, cl8y_intent, device_class, session_id | raw wallet, email, X handle, IP-as-stable-id | | `wallet_connect_started` / `succeeded` | wallet_type, actor_key_pseudo, session_id | raw wallet address to vendors | | `tx_broadcast` / `tx_succeeded` / `tx_failed` | campaign_id (+ first/last touch), txhash, session_id, outcome | raw wallet to vendors | `actor_key_pseudo` = first-party irreversible / session key — **not** the on-chain address in cleartext to third parties. Campaign persistence: first-party ≤30d TTL (plan rec); disclose when Privacy Notice (G1) ships. ### 3 — Destinations | Destination | Class | Rule | |---|---|---| | Coolify / marketing Postgres / marketing API (when built) | **1P** | Preferred for S1 receipts | | Indexer | **1P fee authority** | Join via txhash; not a marketing vendor | | PostHog / GA / etc. | **3P — not approved today** | Founder: no analytics SDK on dex; do not add without Notice + counsel | | WalletConnect / Coinbase CCA | **3P product stack** | Separate from S1; do not piggyback wallets onto these | Any proposed **3P** hop for S1 payloads → stop for founder/counsel + Notice update before enqueue. ### 6 — Redaction tests (sketches) 1. No analytics SDK / measurement IDs in dex bundle. 2. Network: no S1 POSTs off allowlisted 1P hosts. 3. Golden payloads: no cleartext wallets toward 3P; `actor_key_pseudo` ≠ raw address. 4. `tx_succeeded` carries campaign_id + txhash for indexer join without wallet on marketing hop. 5. If founder requires refuse: must stop/down-scope S1 (pair with bridge#165 consent record if shared). **Still founder-gated:** G1 Privacy Notice; DoR 4 opt-out decision.

DRAFT delta — DoR 2 fuller S1 table (Market sensor inventory)

Status: DRAFT — not ship approval / not legal advice. Eng may trim event names. Supersedes the shorter event list in the prior DoR 2/3/6 draft comment for schema rows only; destinations + redaction sketches still apply.

Events (source-to-fee + OS; don’t invent beyond this)

Event Fields
qualified_page_view route, referrer_class, campaign_id, cl8y_intent, device_class + session_id
quote_requested action, pair_or_route, route_mode, amount_bucket, latency_ms, outcome + session_id, quote_id
wallet_connect_started wallet_type + session_id
wallet_connect_succeeded wallet_type, actor_key_pseudo + session_id
tx_sign_prompted action, pair_or_route, quote_id + session_id
tx_broadcast / tx_succeeded action, pair_or_route, txhash, first/last campaign_id + txhash join
tx_failed action, stage, reason_normalized + session_id
share_started / share_completed object_type, channel_if_known + session_id

Cross-cut: utm_*, cl8y_intent, campaign_id ≤30d, session_id, actor_key_pseudo. Amounts bucketed. No raw wallets to third parties.

Destination preference: first-party VITE_ANALYTICS_ENDPOINT / Coolify Postgres; indexer is fee-truth join only.

DoR 4 (opt-out): still OPEN — founder skipped opt-out widget; do not assume an answer.

## DRAFT delta — DoR 2 fuller S1 table (Market sensor inventory) **Status: DRAFT — not ship approval / not legal advice.** Eng may trim event names. Supersedes the shorter event list in the prior DoR 2/3/6 draft comment for **schema rows only**; destinations + redaction sketches still apply. ### Events (source-to-fee + OS; don’t invent beyond this) | Event | Fields | |---|---| | `qualified_page_view` | route, referrer_class, campaign_id, cl8y_intent, device_class + session_id | | `quote_requested` | action, pair_or_route, route_mode, amount_bucket, latency_ms, outcome + session_id, quote_id | | `wallet_connect_started` | wallet_type + session_id | | `wallet_connect_succeeded` | wallet_type, actor_key_pseudo + session_id | | `tx_sign_prompted` | action, pair_or_route, quote_id + session_id | | `tx_broadcast` / `tx_succeeded` | action, pair_or_route, txhash, first/last campaign_id + txhash join | | `tx_failed` | action, stage, reason_normalized + session_id | | `share_started` / `share_completed` | object_type, channel_if_known + session_id | **Cross-cut:** `utm_*`, `cl8y_intent`, `campaign_id` ≤30d, `session_id`, `actor_key_pseudo`. Amounts **bucketed**. **No raw wallets to third parties.** **Destination preference:** first-party `VITE_ANALYTICS_ENDPOINT` / Coolify Postgres; indexer is fee-truth join only. **DoR 4 (opt-out):** still **OPEN** — founder skipped opt-out widget; do not assume an answer.

DRAFT delta — fuller S1 event set (Market sensor)

Status: DRAFT — complements comment 44486; not ship approval. Canonical: PlasticDigits/cl8y-marketing/strategy/source-to-fee-instrumentation.md.

Also include (provisional until eng finalizes):

Event Required (class) Join
quote_requested action, pair_or_route, route_mode, amount_bucket, latency_ms, outcome session_id, quote_id
tx_sign_prompted action, pair_or_route, quote_id session_id
share_started / share_completed object_type, channel_if_known session_id

Same rules as 44486: amounts bucketed; no raw wallets to 3P; actor_key_pseudo first-party only; 1P sink preferred; indexer = txhash fee-truth join only.

Still open: G1 Privacy Notice; DoR 4 opt-out (founder skipped widget — no assumed answer).

## DRAFT delta — fuller S1 event set (Market sensor) **Status: DRAFT** — complements comment 44486; not ship approval. Canonical: `PlasticDigits/cl8y-marketing/strategy/source-to-fee-instrumentation.md`. Also include (provisional until eng finalizes): | Event | Required (class) | Join | |---|---|---| | `quote_requested` | action, pair_or_route, route_mode, amount_bucket, latency_ms, outcome | session_id, quote_id | | `tx_sign_prompted` | action, pair_or_route, quote_id | session_id | | `share_started` / `share_completed` | object_type, channel_if_known | session_id | Same rules as 44486: amounts bucketed; no raw wallets to 3P; `actor_key_pseudo` first-party only; 1P sink preferred; indexer = txhash fee-truth join only. **Still open:** G1 Privacy Notice; DoR 4 opt-out (founder skipped widget — no assumed answer).
Member

HOLD: privacy DoR / legal#35 — Market+ops overnight hold

HOLD: privacy DoR / legal#35 — Market+ops overnight hold
Member

Overnight privacy/#1202 security note (2026-09-06)

Private dual-pass audit filed under ops-worker/cl8y-security-audits → 2026-09-06/cl8y-dex-terraclassic-privacy-1202/ (ops-sensor mirror of kimi-k3-max + grok-4.6-high).

HOLD respected — no implement, no ready. Top process High: do not ship client product events until destinations + redaction DoR signed (campaign_id/txhash linkability, sink allowlist, no secrets in payloads). Formal kimi security pass when HOLD lifts.

## Overnight privacy/#1202 security note (2026-09-06) Private dual-pass audit filed under `ops-worker/cl8y-security-audits` → `2026-09-06/cl8y-dex-terraclassic-privacy-1202/` (ops-sensor mirror of kimi-k3-max + grok-4.6-high). **HOLD respected** — no implement, no `ready`. Top process High: do not ship client product events until destinations + redaction DoR signed (campaign_id/txhash linkability, sink allowlist, no secrets in payloads). Formal kimi security pass when HOLD lifts.
Author
Owner

S1 opt-out operator card on cl8y-pm /inbox was failed (“Could not understand the issue”). That was this engineering ticket, not a missing DEX AC.

A replacement phone card is queued: pick required (opt-out before product events go live) or not_required (events may go live without an opt-out widget). Completing that card is the unlock; it is not implement work on this issue.

S1 opt-out operator card on cl8y-pm `/inbox` was failed (“Could not understand the issue”). That was this engineering ticket, not a missing DEX AC. A replacement phone card is queued: pick **required** (opt-out before product events go live) or **not_required** (events may go live without an opt-out widget). Completing that card is the unlock; it is not implement work on this issue.
Author
Owner

Operator S1 decision on cl8y-pm /inbox: required. Ship an opt-out control before DEX product events go live. This is the unlock, not implement work on this ticket.

Operator S1 decision on cl8y-pm /inbox: **required**. Ship an opt-out control before DEX product events go live. This is the unlock, not implement work on this ticket.
Sign in to join this conversation.
No milestone
No project
No assignees
4 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#1202
No description provided.