feat(frontend): privacy-reviewed source-to-fee product events (Phase 0 S1) #1202
Labels
No labels
agent:fix_bugfix
agent:fix_conflicts
agent:fix_security
agent:gap_analysis
agent:implement
agent:implement
agent:implement
agent:open_issues
agent:ready
agent:research
agent:security_audit
agent:verify
architecture
backend
blocker:hybrid
blocker:launch
blocker:limit-orders
blocker:v2
block:log_only
block:security
bug
ci
contracts
correctness
deploy
dev
devops
docs
documentation
duplicate
e2e
enhancement
epic
feature
frontend
functional-completion
gas
good first issue
governance
help wanted
high-risk
hooks
hybrid
indexer
infra
infrastructure
integrators
invalid
launch-blocker
limit-orders
localnet
localterra
low priority
missing-implementation
needs-design
ops
performance
priority
high
priority
medium
product
qa
QA
question
ready
ready
research
scripts
security
security-hardening
smartcontracts
tech-debt
testing
ux
UX
v2
verification
wontfix
No milestone
No project
No assignees
4 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
code/cl8y-dex-terraclassic#1202
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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.
Summary
Marketing cannot measure source → fee. The DEX SPA (
frontend-dappon dex.cl8y.com) has no PostHog, gtag, Plausible, or other product-analytics SDK. There is no UTM /campaign_id/cl8y_intentpersistence. 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_idcan survive from landing throughtx_succeeded, joinable to indexer fee rows viatxhash. Attribution is descriptive only — not causal ROI. The indexer remains the sole economic authority for fees. Do not changeFeeSource. Pair-creation fee indexing stays a separate ticket.Related spec lives in
PlasticDigits/cl8y-marketing(strategy/operating-system.mdproduct event contract; Phase 0 item 6 instrategy/90-day-plan.md). There is nostrategy/source-to-fee-instrumentation.mdorwallet-exclusions.mdin-tree yet.Current codebase
Architecture
frontend-dapp— React 19 + Vite 7 + TanStack Query + Zustand + cosmes wallets.indexer— Rust Axum/SQLx; economic authority for fees.index.html.connect-srcis allowlisted (LCD/RPC/indexer/WalletConnect/Legal). Third-party analytics hosts are blocked unless CSP is updated (viteCsp.ts).Surfaces in scope
/(/swapalias)SwapPage.tsx/trade,/trade/:pairAddr,/limitsTradePage.tsx,LimitOrdersPage.tsx/wrapWrapPage.tsx/ust1Ust1Page.tsx/token/createCreateTokenPage.tsx/createCreatePairPage.tsxWrap / UST1 / create-token nav items appear when the corresponding env addresses are set.
UTM / campaign
Grep finds no
utm_,campaign_id, orcl8y_intent.SwapPageandCreatePairPagecallsetSearchParams(..., { 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
WalletModal→useWalletStore.connect→connectTerraWallet. Persistence keyscl8y_wallet_connection,cl8y_dev_sim.route/solveon Swap — naturalquote_requestedhook.terraBroadcast.tsphasessigning→broadcasting→confirming(+recovering).useTerraBroadcastMutationexposesphase+pendingTxHash; success data is typically the tx hash string. Maps totx_sign_prompted/tx_broadcast/tx_succeeded/tx_failed.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_hashwithUNIQUE (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 noVITE_ANALYTICS_ENDPOINTor analytics kill-switch yet. Simulated Wallet exists only inVITE_DEV_MODEand 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_eventsontxhash. 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
FeeSourceor put exact fees in analytics.actor_keywith offline salt.cl8y-marketing-coreor a minimal Coolify collector). A vendor is allowed only if already privacy-reviewed and CSP/script allowlisted. None is in-tree today.FeeSource/ pair_creation (separate ticket), marketing Postgres schema in this repo, incrementality experiments, Telegram/X ↔ wallet joins, paid media pixels.cl8y_intent.wallet-exclusionsapplies on the reporting side, not in the client. Do not invent exclusion lists in the SPA.VITE_DEV_MODEmust not emit to the production sink.Relevant files
frontend-dapp/src/main.tsx,App.tsx,components/common/navItems.tsfrontend-dapp/viteCsp.ts,index.html,.env.example,package.jsonfrontend-dapp/src/pages/SwapPage.tsx,utils/swapQueryParams.ts,pages/CreatePairPage.tsxfrontend-dapp/src/hooks/useWallet.ts,components/wallet/WalletModal.tsxfrontend-dapp/src/hooks/useTerraBroadcastMutation.ts,services/terraclassic/terraBroadcast.tsfrontend-dapp/src/services/indexer/client.ts, Swap quote effectsfrontend-dapp/src/components/ui/ShareLinkButton.tsx,utils/sharePageLink.tspages/TradePage.tsx,WrapPage.tsx,Ust1Page.tsx,CreateTokenPage.tsx,components/trade/TradeMarketOrderPanel.tsx,TradeOrderTicket.tsxindexer/src/indexer/protocol_fees.rs,indexer/migrations/20260821120000_protocol_fees.sql,indexer/src/api/protocol_fees.rscl8y-marketing/strategy/operating-system.md,90-day-plan.md,fee-growth-model.md,technical-architecture.md,founder-decisions.mdRecommended direction
setSearchParams): parseutm_*+cl8y_intent(+ agreed campaign taxonomy keys); persist first-touch and last-touch inlocalStoragewith ≤30d TTL.track(event, props)module, gated by env (VITE_ANALYTICS_ENDPOINTor equivalent kill-switch) so it can ship dark.qualified_page_viewquote_requested(bucketed amounts)wallet_connect_started/wallet_connect_succeededuseTerraBroadcastMutation/ phase callbacks →tx_sign_prompted/tx_broadcast/tx_succeeded/tx_failedwithtxhash, action, pair/route, first+lastcampaign_idShareLinkButton→share_started/share_completed(if share UI exists on that surface; else defer that event only)connect-src. Do not add gtag/PostHog unless separately approved.actor_keyin local storage; never send raw wallet to third parties.protocol_fee_eventsontxhash.source-to-fee-instrumentation.mdin marketing if still missing; do not block the client on that filename.Acceptance criteria
tx_succeeded)./or/create.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_succeededincludetxhash, action, pair/route, first+lastcampaign_id.tx_succeeded.txhashmatching an indexerprotocol_fee_events.tx_hash(or equivalent success/fee row).VITE_DEV_MODE/ Simulated Wallet does not hit prod sink.FeeSourceunchanged.Test plan: all paths
cl8y_intenton/: first-touch stored; Swap canonical URL rewrite does not drop stored campaign;qualified_page_viewfires once per qualified land.quote_requestedon swap quote (success, empty route, LCD error) with bucketed amount.txhash.tx_failedwith normalized stage; no fake success.protocol_fee_events.Test plan: attack, hack, and abuse vectors
campaign_id/ UTM (XSS, huge strings, unknown keys)tx_succeededwithout chain confirmterra1/0xaddress appears in analytics payloadVerification criteria
useTerraBroadcastMutationphase mapping.tx_succeededpayload still has first-touch campaign id after URL rewrite.make/ existing frontend verify targets still pass; indexer tests untouched.Intake request — DEX client source-to-fee instrumentation (Phase 0 S1) From: Mato feat(frontend): privacy-reviewed source-to-fee product events (Phase 0 S1)+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:
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):qualified_page_viewwallet_connect_started/succeededtx_broadcast/tx_succeeded/tx_failedactor_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
Any proposed 3P hop for S1 payloads → stop for founder/counsel + Notice update before enqueue.
6 — Redaction tests (sketches)
actor_key_pseudo≠ raw address.tx_succeededcarries campaign_id + txhash for indexer join without wallet on marketing hop.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)
qualified_page_viewquote_requestedwallet_connect_startedwallet_connect_succeededtx_sign_promptedtx_broadcast/tx_succeededtx_failedshare_started/share_completedCross-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):
quote_requestedtx_sign_promptedshare_started/share_completedSame rules as 44486: amounts bucketed; no raw wallets to 3P;
actor_key_pseudofirst-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).
HOLD: privacy DoR / legal#35 — Market+ops overnight hold
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.S1 opt-out operator card on cl8y-pm
/inboxwas 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.
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.