Young's Mind
Product Spec

Pulse Account-Holder Features — API & Schema Spec

Author: Sol · Status: v0.95 — UI contracts LOCKED · Date: 2026-08-23 · Rev 2026-08-26: tier-economics annex (§9) folded in — source: Mira memo (task-6e24bf67 / rep-ca09da0e); supersedes draft pricing rep-5781dcdd. Pending Sol final sign-off. Consumers: Nova (UI/component specs), Ace (backend implementation) Design system: Pulse neon-green #39FF14 hard-edge, Nuxt 3 (pulse-tv-home-design.md) Precedent: Account gate pattern from pulse-split-sol-review.md (HMAC + localStorage approved for test phase)

Pulse Account-Holder Features — API & Schema Spec

Author: Sol · Status: v0.95 — UI contracts LOCKED · Date: 2026-08-23 · Rev 2026-08-26: tier-economics annex (§9) folded in — source: Mira memo (task-6e24bf67 / rep-ca09da0e); supersedes draft pricing rep-5781dcdd. Pending Sol final sign-off. Consumers: Nova (UI/component specs), Ace (backend implementation) Design system: Pulse neon-green #39FF14 hard-edge, Nuxt 3 (pulse-tv-home-design.md) Precedent: Account gate pattern from pulse-split-sol-review.md (HMAC + localStorage approved for test phase)


0. Locked Decisions (constrain UI)

#DecisionValue
D1Auth/sessionEmail+password, bcrypt server-side. Session = HMAC-signed bearer token in localStorage (matches existing gate). Exp 30d. HttpOnly cookie upgrade DEFERRED.
D2Tier enum"free" | "member" | "founding" — lowercase strings. Additive-only evolution, never rename.
D6PricingFREE $0 · PREMIUM A$29/mo or A$249/yr founding (~28% discount) · price review at 500 subs. Trial: 14-day card-required, auto-converts monthly unless cancelled (D-3/D-1/day-of reminders), one per account+payment instrument. Payments: Stripe AU (1.7%+$0.30). GST deferred until A$75k turnover.
D7Data feed gateLaunch on DELAYED ASX data (~20 min) only. "Real-time" banned sitewide until a live ASX entitlement licence is signed. Future PRO tier A$69–79/mo carries RT pass-through (roadmap, not launch).
D3WatchlistServer-persisted per account (cross-device sync). Cap by tier: free=10, member=50, founding=50 (rev 2026-08-26: free raised 5→10 per Mira tier-economics sanity check). Cap value comes from API (features.watchlistCap) — never hardcode in UI.
D4Paper tradingMirrors Min's engine ledger (moomoo SIMULATE). P&L invariant: realized / open / total are three separate fields — never conflated. Tranches always visible (1/3, 2/3, 3/3).
D5Reports gatingMetadata always returned. Body only when entitled. Server decides — UI never guesses entitlement.

1. Auth & Session

POST /api/v1/auth/register   {email, password, displayName}      → 201 {token, account}
POST /api/v1/auth/login      {email, password}                    → 200 {token, account}
GET  /api/v1/auth/me                                              → 200 {account, features}
// account
{ "id": "acc_…", "email": "…", "displayName": "…", "tier": "free", "createdAt": "ISO8601" }

// features block (drives ALL UI gating/caps)
{ "features": { "watchlistCap": 10, "paperPortfolios": 1, "pnlHistoryDays": 30,
                "quoteDelay": "delayed", "reportsUnlocked": false, "paperTrading": true } }

UI rule: read caps/features from /auth/me, never infer from tier name alone.


2. Watchlist

GET    /api/v1/watchlist                        → [item]
POST   /api/v1/watchlist        {ticker, exchange?}   → 201 item | 409 cap-reached {code:"WATCHLIST_CAP"}
DELETE /api/v1/watchlist/:itemId                      → 204
PATCH  /api/v1/watchlist/order  {orderedIds:[…]}      → 200
// item
{ "id": "wl_…", "ticker": "UUUU", "exchange": "NYSE",
  "sortOrder": 0, "note": "", "addedAt": "ISO8601" }

Quote snapshots (price/change%) come from the existing public quotes feed — watchlist endpoint does not embed live prices. Widget joins client-side.


3. Paper Trading

Base: /api/v1/paper. All amounts USD. Env is implicitly SIMULATE (v1 has no live).

Entities

// position
{ "id": "pos_…", "ticker": "UUUU", "style": "trend",          // momentum|swing|trend
  "tranches": [ { "no": 1, "entryPrice": 8.20, "sizeUsd": 2500 } ],
  "avgEntry": 8.20, "ltp": 8.40,
  "pnlOpenUsd": 62.00, "pnlOpenPct": 0.76,
  "levelAlert": "watch $9.00" }                               // null unless tripped/near

// trade (fill ledger row)
{ "id": "trd_…", "orderId": "ord_…", "ticker": "UUUU", "side": "buy",
  "qtyShares": 304, "price": 8.20, "trancheNo": 1, "strategy": "trend",
  "pnlRealizedUsd": null,                                     // set on closing sells only
  "filledAt": "ISO8601" }

// summary — THE P&L INVARIANT
{ "realizedUsd": …, "openUsd": …, "totalUsd": … }             // total = realized + open

Endpoints

POST /api/v1/paper/orders   {ticker, side: buy|sell, sizeUsd, trancheNo?, strategy?}
                            → 202 {order}  // async fill by engine
GET  /api/v1/paper/orders/:id                     → {status: pending|filled|rejected|cancelled, fill?}
GET  /api/v1/paper/positions                      → [position]
GET  /api/v1/paper/trades?limit=50                → [trade]
GET  /api/v1/paper/summary                        → summary (invariant above)
GET  /api/v1/paper/equity?range=7d|30d|90d|all    → [{ "t": ISO8601, "equity": 10234.50 }]
                                                    // daily granularity — feeds equity sparkline

4. Membership

GET  /api/v1/membership        → {tier, since, renewsAt|null, features}
POST /api/v1/membership/upgrade {targetTier}   → v1 STUB (payment provider = Mira lane, later)

5. Special Reports (gated)

GET /api/v1/reports            → [{meta..., access}]
GET /api/v1/reports/:id        → see below
// meta (always returned, list + detail)
{ "id": "rpt_…", "title": "…", "dek": "…", "publishedAt": "ISO8601",
  "minTier": "member", "readingMinutes": 8 }

// detail — entitled
{ "access": "granted", "body": "markdown" }

// detail — not entitled (HTTP 200, not 403 — simpler client branch)
{ "access": "locked",
  "teaser": "<first 2 paragraphs>",
  "upsell": { "requiredTier": "member", "membershipUrl": "/membership" } }

UI rule: render lock overlay purely off access === "locked" + teaser. Never compute entitlement client-side.


6. Conventions

  • Error envelope: { "error": { "code": "MACHINE_CODE", "message": "human" } }
  • All timestamps ISO8601 UTC. All money USD, decimal strings-safe numbers.
  • Rate limits (Ace): auth 10/min/IP, writes 60/min/account.
  • OpenAPI file to follow at same path (openapi.yaml) before Ace starts implementation.

7. Build Order (confirmed)

  1. Watchlist (auth-lite: works logged-in; widget + stock-page add button)
  2. /membership shell (tier display + upgrade stub — unblocks reports UI copy)
  3. Paper trading (trade box → orders; positions/P&L page + equity sparkline)
  4. Special reports (gated reader last — depends on membership shell)

Rationale: each step only depends on steps above it. Reports last because its UI copy references membership tiers.

8. Component Contract Implications (for Nova)

  • Watchlist widget → GET /watchlist + client-side quote join; empty state ≤ cap; cap-hit state consumes WATCHLIST_CAP error code.
  • Stock-page add button → optimistic add, rollback on 4xx.
  • Trade box → submit returns 202 + order id; UI shows pending→filled transition (poll orders/:id).
  • Positions page → positions[] renders tranches inline (never collapse to single entry); sparkline consumes equity[].
  • /membership → fully driven by features block.
  • Gated reader → binary branch on access; teaser + upsell CTA from payload.

9. Tier Economics Annex (Mira — task-6e24bf67 / rep-ca09da0e, folded 2026-08-26)

Source: Mira's Pulse freemium tier-economics memo, delivered to Min for master-spec fold-in. Supersedes draft pricing (rep-5781dcdd: A$19/A$180). Status: pending Sol final sign-off.

9.1 Pricing

TierPriceEntitlements
FREE$0Watchlist ×10 · ≤10 open paper positions · 30-day P&L history · delayed quotes · no reports / membership
PREMIUMA$29/mo or A$249/yr founding (~28% annual discount)Full entitlements; founding price locks early cohort + cash upfront
  • Price review at 500 subscribers.
  • Rationale: sits below the A$39–49 anchor set by Stockspot/SelfWealth-class AU competitors, high enough to signal value. ~90% contribution margin ex-acquisition.

9.2 Trial

  • 14-day Premium trial. Card required upfront, auto-converts to monthly unless cancelled.
  • Reminder sequence D-3 / D-1 / day-of. One trial per account + payment instrument.
  • Benchmark: card-required trials convert 2–3× higher than opt-in freemium upgrades.

9.3 Payments

  • Stripe confirmed for AU SaaS: domestic pricing 1.7% + $0.30 AUD; native wallets (Apple/Google Pay lift mobile checkout); Stripe Billing for dunning/involuntary-churn recovery; GST tax-inclusive display built in.
  • Alternative Paddle rejected (MoR 5% + $0.50 uneconomic at our volume).
  • GST registration deferred until A$75k turnover threshold.
  • Implementation note: POST /membership/upgrade stub (§4/§4.4) becomes a Stripe webhook flipping users.tier — guard + UI read tier live, nothing upstream changes (per Sol v1 architecture spec).

9.4 Gate-limit amendments (conversion-lever logic: limit → friction → habit → paywall)

LeverFreePremium
Watchlist×10×50
Open paper positions≤10unlimited
Paper portfolios1 (new lever per Mira)3
P&L history30 daysunlimited
Special reportsnewest report free-to-read window; archive Premium-onlyfull archive
Quotesdelayed (~20 min)delayed at launch
  • Watchlist ×10/×50 and positions/P&L gates approved as proposed.
  • Multi-paper-portfolio is a NEW premium lever: cheap to serve, strong upgrade trigger.
  • Reports gating reframed as marketing engine, not just a wall: newest report free window on Free drives habitual readership; archive behind Premium.
  • Launch on DELAYED ASX data only (~20 min).
  • Do NOT market "real-time alerts" until a live ASX entitlement licence is signed. Non-pro real-time feed ≈ A$20–26/sub/mo (INDICATIVE — web search was down this session; MUST verify with ASX directly before any commitment).
  • RT inside the A$29 tier is margin-negative → roadmap item: future PRO tier A$69–79/mo carrying RT pass-through.
  • Marketing copy gate: "real-time" banned sitewide until licence executed.

9.6 Spec deltas required by §9 (for Ace/Nova)

  1. features block gains paperPortfolios, pnlHistoryDays, quoteDelay keys (§1 example updated).
  2. Paper-trading schema: paper_accounts becomes multi-row (UNIQUE(user_id) → UNIQUE(user_id, portfolio_no)) when portfolio lever ships — additive migration, not launch-blocking.
  3. Position/P&L-history caps enforced server-side via features block; UI never hardcodes caps (existing D3 rule extended to all limits).
  4. Reports pipeline (§5): add free-window logic — newest report readable on Free until next publish; archive requires member tier.
  5. Copy gate: all marketing/UI copy lint-checked against banned phrase "real-time" until D7 cleared.
Copyright © 2026