10 KiB
Nexa — Frontend Browser Extension Spec
1. Overview
Nexa is a blockchain-powered trading companion that helps traders manage the emotional/behavioral side of trading rather than the trades themselves. Because trades are already visible on-chain, Nexa doesn't need the user to manually log anything — it observes wallet activity and reacts to it.
This spec covers only the frontend: an open-source browser extension. The backend (wallet/RPC listening, loss detection, auth) is a separate private service and is treated here as an external API the extension talks to.
Target browser: built and tested on Zen (Firefox fork), but must be cross-platform (Firefox + Chromium-based via WebExtensions API / manifest v3 where possible). Avoid browser-specific APIs unless behind a compatibility shim.
Current milestone: the "Lockout" feature — disabling quick-buy interactions on supported trading sites after a qualifying loss, with a visible reason/notification, built in a modular way so more sites and more intervention types can be added later.
2. Core Concept Recap (for context, not build scope)
- User sets their own loss thresholds while calm (Ulysses-contract style).
- Backend watches the user's wallet (Solana RPC / Helius) and detects when a threshold-qualifying loss occurs.
- Backend pushes a lock/unlock signal to the extension (WebSocket).
- Extension enforces the lock in the DOM of the trading site the user is currently on.
- Escalation model: first disable new-position buy actions only (sell/manage stays enabled); if the user tries to circumvent it, escalate to fully blocking the site.
3. Scope of This Spec: The Lockout Feature (v1)
3.1 Behavior
When the extension receives a "locked" state (from backend, or a mocked/local state for UI dev purposes):
- Detect buy-action elements on the current page for the active site adapter (see Site Adapters below).
- Visually disable each detected buy element — do not just hide it. The user should see that an action exists but is blocked, not that it vanished. Preferred treatment: blur + reduced opacity +
pointer-events: none, with a small lock icon overlay on hover/tap. - Intercept any click that somehow still reaches the underlying element (defense in depth) and prevent default/propagation.
- Show a reason. On lock activation, surface a non-intrusive extension notification/popup (e.g. toast anchored near the browser action icon, or an injected banner) stating why it's locked, e.g. "Locked: -$62 loss on SOL/PIXELCAT crossed your $50 threshold." Include a way to view details (opens extension popup with more info) but no way to silently dismiss-and-unlock from that toast — unlocking is a deliberate action.
- Sell/manage actions remain untouched — only buy-side elements are targeted.
- When state changes to "unlocked," reverse all DOM modifications cleanly (no leftover classes/overlays).
3.2 Modularity Requirement
This must be built so that:
- Adding a new supported site = adding a new "site adapter" module, not touching core logic.
- Adding a new intervention type (e.g. full-site block, delayed buy with countdown, confirmation modal) = adding a new "intervention strategy" module, not touching site adapters.
- The lock/unlock decision (when to lock, for how long, why) is fully decoupled from how it's enforced on the page and from how it's detected on the page.
Suggested architecture:
/src
/background - extension background/service worker: owns WebSocket connection to backend, holds current lock state, messages content scripts
/content-scripts
/adapters
axiom.ts - site-specific selectors + DOM strategy for axiom.trade
<future-site>.ts
adapter-interface.ts - shared type/interface all adapters implement
/interventions
blur-disable.ts - visual lock treatment (v1 default)
full-block.ts - escalation: full page block overlay (stub for v1, structure only)
intervention-interface.ts
observer.ts - generic mutation observer that watches for adapter-declared selectors appearing/disappearing (SPA-safe)
content-index.ts - wires adapter + intervention + background messages together
/popup - extension popup UI: current status, reason for lock, threshold settings link
/notifications - toast/banner component injected into page or shown via browser notification API
/shared
messaging.ts - typed message contracts between background <-> content scripts <-> popup
types.ts
manifest.json
3.3 Site Adapter: Axiom.trade (first target)
An adapter declares:
matches: URL pattern(s) this adapter applies to.findBuyElements(): returns all current buy-action DOM nodes on the page.getContainer(el): given a buy element, returns the stable ancestor container to apply lock styling to (so we style the whole control, not just an inner span).
Known selectors/patterns to encode (from live markup on axiom.trade):
Quick Buy button (small pill-style buy amount button, e.g. "0.04"):
<div class="flex min-h-[28px] flex-1 cursor-pointer items-center justify-center rounded-full text-[14px] font-medium leading-[18px] outline-none border border-increase/50 text-increase hover:bg-increase/5 hover:text-increaseHover touch-callout-none select-none">0.04</div>
- Detection: the reliable anchor is the ancestor with class
buy-click-container— walk up from any.border-increase\/50.text-increasecandidate, or more robustly, query.buy-click-containerdirectly and treat it (or its clickable child) as the target. - The 4th ancestor being
buy-click-containeris a current DOM depth fact, not something to hardcode as "4 levels up" — instead useclosest('.buy-click-container')so it survives markup depth changes.
Noob Mode Buy button (the big "Buy {token}" button):
<div class="flex min-h-[4px] flex-1 flex-row items-center justify-center px-[16px] pb-[16px]">
<button type="button" class="bg-increase hover:bg-increase/90 rounded-8 flex max-h-[36px] min-h-[36px] flex-1 flex-row items-center justify-center rounded-full p-[4px] undefined">
<span class="text-[14px] font-bold leading-[18px] text-background">
<span class="text-[#090909]">Buy pixelcat</span>
</span>
</button>
</div>
- Detection: match
buttonelements whose class list includesbg-increaseand whose text content matches/^Buy\s/i. Do not hardcode the token name ("pixelcat") — match the "Buy " prefix pattern instead, since it changes per token page. - Fallback/secondary check: the button's own class
bg-increase(their "increase/green" action color) combined with being inside a quick-buy/trade panel region, to avoid accidentally matching unrelated green buttons elsewhere on the page.
Note for Claude Code: Axiom.trade is a React SPA — buy elements mount/unmount as the user navigates between tokens without full page reloads. Use a MutationObserver on a stable root (e.g. document.body or the main app root) rather than a one-time querySelectorAll on load, and re-apply/remove the lock treatment whenever matching elements appear.
3.4 Lock Enforcement — Visual Treatment (v1: blur-disable)
For each detected buy container:
- Apply a wrapper/overlay (don't mutate the site's own classes destructively — wrap or overlay so original markup can be restored exactly on unlock).
- Visual:
filter: blur(2-3px), reduced opacity (~0.5), cursornot-allowed. - Overlay a small lock icon, centered, not blurred, so it reads clearly as "locked" rather than "broken."
pointer-events: noneon the original element or a transparent click-catcher div on top of it.- Click-catcher (if used) triggers the same "show reason" notification on click/tap, so users who try to click get immediate feedback rather than nothing happening.
3.5 Notification/Reason UI
- Small, dismissible toast, injected into the page (top-right corner, non-blocking) or via the extension's own popup badge/notification.
- Content: short reason (e.g. threshold crossed, amount, token if relevant) + a "View details" affordance that opens the extension popup.
- Must not offer a one-click unlock from the toast itself.
- Extension popup (separate, already-scoped-elsewhere UI surface) shows: current lock status, reason, time locked, link to adjust thresholds (thresholds config itself is out of scope for this spec — just leave a hook/placeholder).
3.6 State Source for This Milestone
Since backend integration (WebSocket, RPC loss detection) is separate work:
- Build the content-script/background lock state as consuming a simple typed interface, e.g.
{ locked: boolean, reason?: string, scope: 'buy-only' | 'full-block' }. - For UI development purposes, wire this to a mock/dev toggle (e.g. via the extension popup, or a
chrome.storage/browser.storageflag) so the lock/unlock visuals can be demoed and iterated on before the real backend WebSocket is wired in. - The background script should have a clearly isolated function/module (
getLockState()/setLockState()) that's the single seam where the future WebSocket client will plug in.
4. Non-Goals for This Milestone
- No real backend connection yet (mock state only).
- No cost-basis tracking logic (backend concern).
- No full-site-block implementation detail beyond a stub module (structure only, so it's easy to fill in later).
- No threshold-setting UI (placeholder link only).
- Only one site adapter (axiom.trade) needs to be functional; architecture just needs to make adding more trivial.
5. Deliverable for This Pass
A working browser extension (loadable unpacked in Zen/Firefox, and ideally Chromium) that:
- On axiom.trade, detects quick-buy and noob-mode buy elements per section 3.3.
- Applies the blur/disable visual treatment per 3.4 when a dev-mode "locked" toggle is on.
- Shows a reason toast per 3.5 when lock activates, and on click of a locked element.
- Cleanly reverses all changes when toggled back to unlocked.
- Is structured per the modular architecture in 3.2 so a second site adapter and a second intervention type could be added without touching existing files beyond registration.
6. Repo / Licensing Context
- This frontend repo is the OSS component; description: "Your blockchain powered Agent to help with your trading emotions."
- License: Apache 2.0 (consistent with other OSS projects, and chosen partly to build trust given the extension interacts with a trading wallet).
- Backend stays private/closed-source.