Pulse Account-Holder Features — API & Schema 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)
0. Locked Decisions (constrain UI)
| # | Decision | Value |
|---|---|---|
| D1 | Auth/session | Email+password, bcrypt server-side. Session = HMAC-signed bearer token in localStorage (matches existing gate). Exp 30d. HttpOnly cookie upgrade DEFERRED. |
| D2 | Tier enum | "free" | "member" | "founding" — lowercase strings. Additive-only evolution, never rename. |
| D6 | Pricing | FREE $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. |
| D7 | Data feed gate | Launch 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). |
| D3 | Watchlist | Server-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. |
| D4 | Paper trading | Mirrors 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). |
| D5 | Reports gating | Metadata 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)
- Watchlist (auth-lite: works logged-in; widget + stock-page add button)
- /membership shell (tier display + upgrade stub — unblocks reports UI copy)
- Paper trading (trade box → orders; positions/P&L page + equity sparkline)
- 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 consumesWATCHLIST_CAPerror 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 consumesequity[]. - /membership → fully driven by
featuresblock. - 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
| Tier | Price | Entitlements |
|---|---|---|
| FREE | $0 | Watchlist ×10 · ≤10 open paper positions · 30-day P&L history · delayed quotes · no reports / membership |
| PREMIUM | A$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/upgradestub (§4/§4.4) becomes a Stripe webhook flippingusers.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)
| Lever | Free | Premium |
|---|---|---|
| Watchlist | ×10 | ×50 |
| Open paper positions | ≤10 | unlimited |
| Paper portfolios | 1 (new lever per Mira) | 3 |
| P&L history | 30 days | unlimited |
| Special reports | newest report free-to-read window; archive Premium-only | full archive |
| Quotes | delayed (~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.
9.5 Data feed & legal flag ⚠️
- 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)
featuresblock gainspaperPortfolios,pnlHistoryDays,quoteDelaykeys (§1 example updated).- 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. - Position/P&L-history caps enforced server-side via
featuresblock; UI never hardcodes caps (existing D3 rule extended to all limits). - Reports pipeline (§5): add free-window logic — newest report readable on Free until next publish; archive requires member tier.
- Copy gate: all marketing/UI copy lint-checked against banned phrase "real-time" until D7 cleared.
Pulse V2 — Product Spec (Google Finance-style)
Version: 2.0 · Owner: Min · Status: Approved by Young (2026-08-06) Delegated: Nova = frontend design & build · Ace = backend build & deploy Baseline: V1 live at https://pulse.youngle.au (/home/ubuntu/pulse-site) — upgrade, don't break. V1 spec: PULSE-SPEC.md.
Eve Privacy Policy
Effective: 2026-08-09 · Project: ~/.eve (Eve — Michio Kushi Macrobiotic Counsellor)

