read.markets/app/templates/pricing.html
Giorgio Gilestro 47dce1a1a4 compliance: flag-gate AI portfolio + cloud sync + Stripe; de-risk prompts; harden reviewer
Implements docs/read-markets-compliance-changes.md as flag-gated changes
(no deletions) so paused features stay in the tree for future re-enable.
All four flags default False so a fresh deploy is compliance-safe.

- New env flags: PORTFOLIO_AI_ENABLED, PORTFOLIO_SYNC_ENABLED,
  TICKER_UNIVERSE_AGGREGATE_ENABLED, SUBSCRIPTIONS_ENABLED.
- Gates: /api/analyze, /api/portfolio/sync*, /api/stripe/*, /pricing,
  ticker_universe writes, portfolio_analysis.analyse(). is_paid_active()
  returns True for any auth'd user when subscriptions are paused.
- Prompts (PROMPT_VERSION 10): universal _COMPLIANCE_RIDER prepended to
  every system prompt; watch list removed; price-target / close-above-below
  / trigger / forward-state-as-description rules added; SPECULATIVE
  pivoted to regime-only scenarios; daily + weekly digests tightened.
- Reviewer: deterministic regex/lexicon pre-check fail-closed under the
  Haiku call; portfolio rider gated by PORTFOLIO_AI_ENABLED; base prompt
  sharpened for forward-state and MAR forward-opinion patterns;
  ReviewerVerdict audit table; generate_with_review retry helper.
- Migration 0026: purge portfolio_sync + ticker_universe; create
  reviewer_verdicts.
- Copy: MAR cite fixed to Art 3(1)(35) + Art 20 + Del Reg 2016/958;
  portfolio reframed as browser-only viewer in disclaimer / privacy /
  terms / about / pricing / landing (en + it). TODO(legal) marker for
  lawyer sign-off on disclaimer.
- Tests: 13 lexicon + 6 reviewer compliance regressions; conftest enables
  all flags so existing 402 tests still cover their code paths.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 19:57:12 +02:00

266 lines
12 KiB
HTML

{% extends "public_base.html" %}
{% block title %}{{ BRAND_NAME }} &middot; Pricing{% endblock %}
{% block main %}
<section class="public-section">
<h1 class="public-section__head">Pricing</h1>
<p>
Two tiers. The core editorial is free today &mdash; a rolling
6-hour news feed, the cross-asset indicator panels, and a strategic
log refreshed every six hours. Paid stretches the news feed to a
full 24 hours, runs the strategic log hourly, unlocks the follow-up
chat against past logs, adds a browser-only portfolio composition
viewer, and turns on the daily email digest on top of the Sunday
recap everyone gets.
</p>
</section>
<section class="tier-grid">
<div class="tier-card">
<h2 class="tier-card__name">Free</h2>
<div class="tier-card__tagline">The core editorial &mdash; news, indicators, and a strategic log every 6 hours.</div>
<div class="tier-card__price">&pound;0</div>
<div class="tier-card__price-hint">No card needed.</div>
<div class="tier-card__divider"></div>
<div class="tier-card__list-head">What you get</div>
<ul>
<li>News feed &mdash; <strong>headlines from the last 6 hours</strong>, auto-tagged by theme, click-to-filter</li>
<li>Cross-asset indicator panels (equities, rates, FX, commodities, credit, &hellip;) with a one-paragraph AI read on each tab</li>
<li>Strategic log &mdash; a single editorial interpretation of the day, <strong>refreshed every 6 hours</strong></li>
<li>Two reading levels: <em>Novice</em> (defines jargon) or <em>Intermediate</em> (terse, for fluent readers)</li>
<li><strong>Sunday weekly digest</strong> by email &mdash; week behind + week ahead, one-click unsubscribe</li>
</ul>
<div class="tier-card__more">
Need the full-day news feed, hourly strategic log, follow-up chat, daily digests, or the portfolio composition viewer? See <strong>Paid</strong> &rarr;
</div>
<div class="tier-card__cta">
{% if cu and (cu.user or cu.is_admin) %}
<a class="btn-secondary btn-block" href="/">Open dashboard</a>
{% else %}
<a class="btn-primary btn-block" href="/login">Sign up free</a>
{% endif %}
</div>
</div>
<div class="tier-card tier-card--featured">
<div class="tier-card__badge">Best value</div>
<h2 class="tier-card__name">Paid</h2>
<div class="tier-card__tagline">Full-day news feed, hourly strategic log, follow-up chat, and the browser-only portfolio composition viewer.</div>
<div class="tier-card__price">&pound;7<span class="tier-card__price-unit"> / month</span></div>
<div class="tier-card__price-hint">
Or <strong>&pound;70 / year</strong> &mdash; two months free, and
starts with a <strong>14-day free trial</strong> (cancel during
the trial and you are not charged). Monthly plans start
immediately. Prices in GBP, VAT where applicable.
</div>
<div class="tier-card__divider"></div>
<div class="tier-card__list-head">Everything in Free, plus</div>
<ul>
<li><strong>News feed: headlines from the last 24 hours</strong> instead of 6 &mdash; a whole session in view, nothing rolls off</li>
<li><strong>Strategic log refreshed every hour</strong> instead of every six &mdash; track intraday moves as they unfold</li>
<li><strong>Follow-up chat on any past log</strong> &mdash; ask the model a question against the day&rsquo;s full context</li>
<li><strong>Daily email digest</strong> (Mon&ndash;Sat) &mdash; ~600-word read of the session ahead, on top of the Sunday recap</li>
<li><strong>Browser-only portfolio composition viewer</strong> &mdash; drop a broker CSV and see your sector, currency, and concentration breakdown, computed entirely in your browser</li>
</ul>
<div class="tier-card__cta">
{% if paid %}
<a class="btn-secondary btn-block" href="/settings">Manage subscription</a>
{% elif cu and cu.user %}
<label class="tier-card__consent">
<input type="checkbox" id="tos-consent">
<span>
I agree to the <a href="/terms" target="_blank" rel="noopener">Terms of Service</a>.
For <strong>monthly plans</strong>, I expressly consent to begin
immediate access and acknowledge this waives my 14-day cancellation
right under Regulation 36 of the Consumer Contracts Regulations 2013.
(<strong>Annual plans</strong> include a 14-day free trial, so the
cancellation right is preserved differently &mdash; see
<a href="/terms" target="_blank" rel="noopener">Terms &sect;6</a>.)
</span>
</label>
<button class="btn-primary btn-block" type="button"
data-stripe-checkout="monthly" disabled>Subscribe &mdash; &pound;7/month</button>
<button class="btn-secondary btn-block" type="button"
data-stripe-checkout="annual" disabled
style="margin-top:10px;">or &pound;70/year (with 14-day free trial)</button>
{% else %}
<a class="btn-primary btn-block" href="/login?next=/pricing">Sign in to subscribe</a>
{% endif %}
</div>
</div>
</section>
<script>
(function () {
// Gate the Subscribe buttons on the ToS / Reg-36 consent checkbox.
// The buttons render with `disabled` set; toggling the checkbox is
// what enables them. Server-side, the Reg-36 waiver applies to
// monthly only — but the single checkbox covers both ToS agreement
// (always needed) and the monthly waiver, so it's required for any
// subscribe path.
var consent = document.getElementById('tos-consent');
var subscribeBtns = document.querySelectorAll('[data-stripe-checkout]');
if (consent) {
consent.addEventListener('change', function () {
subscribeBtns.forEach(function (b) { b.disabled = !consent.checked; });
});
}
// Wire the buttons to /api/stripe/checkout. Stripe returns a
// hosted-checkout URL; we just redirect there. No Stripe.js needed.
subscribeBtns.forEach(function (btn) {
btn.addEventListener('click', async function () {
if (consent && !consent.checked) {
alert('Please agree to the Terms of Service before subscribing.');
return;
}
var cadence = btn.getAttribute('data-stripe-checkout');
btn.disabled = true;
var prev = btn.textContent;
btn.textContent = 'Opening checkout…';
try {
var r = await fetch('/api/stripe/checkout', {
method: 'POST',
headers: {'content-type': 'application/json'},
body: JSON.stringify({cadence: cadence}),
credentials: 'same-origin',
});
if (!r.ok) {
var detail = '';
try { detail = (await r.json()).detail || ''; } catch (e) {}
throw new Error('Checkout failed: ' + (detail || r.status));
}
var data = await r.json();
window.location.href = data.url;
} catch (e) {
alert(e.message || 'Could not start checkout. Please try again.');
btn.disabled = (consent && !consent.checked);
btn.textContent = prev;
}
});
});
})();
</script>
<section class="public-section">
<h2 class="public-section__head">Free vs Paid at a glance</h2>
<table class="compare-table">
<thead>
<tr>
<th scope="col">Feature</th>
<th scope="col">Free</th>
<th scope="col">Paid</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">News feed &mdash; headlines from the last&hellip;</th>
<td class="compare-table__free">6 hours</td>
<td class="compare-table__paid"><strong>24 hours</strong></td>
</tr>
<tr>
<th scope="row">Strategic log refresh</th>
<td class="compare-table__free">Every 6 hours</td>
<td class="compare-table__paid"><strong>Every hour</strong></td>
</tr>
<tr>
<th scope="row">Cross-asset indicator panels</th>
<td class="compare-table__free">&check;</td>
<td class="compare-table__paid">&check;</td>
</tr>
<tr>
<th scope="row">Follow-up chat on past logs</th>
<td class="compare-table__none">&mdash;</td>
<td class="compare-table__paid"><strong>Included</strong></td>
</tr>
<tr>
<th scope="row">Email digest</th>
<td class="compare-table__free">Sunday only</td>
<td class="compare-table__paid"><strong>Sunday + daily Mon&ndash;Sat</strong></td>
</tr>
<tr>
<th scope="row">Browser-only portfolio composition viewer</th>
<td class="compare-table__none">&mdash;</td>
<td class="compare-table__paid"><strong>Included</strong></td>
</tr>
</tbody>
</table>
</section>
<section class="invite-callout">
<div class="invite-callout__icon" aria-hidden="true">&#x1F381;</div>
<div class="invite-callout__body">
<div class="invite-callout__eyebrow">Invite a friend</div>
<div class="invite-callout__headline">Both of you get <strong>45 days of paid access</strong></div>
<div class="invite-callout__sub">
Share your personal invite link from <a href="/settings">Settings</a>. The credit applies when they start a paid plan.
</div>
</div>
<button type="button" class="btn-secondary" id="invite-more">How it works</button>
</section>
<dialog id="invite-modal" class="text-modal" aria-label="How the referral works">
<button type="button" class="text-modal__close" aria-label="Close">&times;</button>
<h2 class="text-modal__title">Invite a friend</h2>
<p>
Every account gets an 8-character referral code and matching invite
link, both shown on your <a href="/settings">Settings</a> page. When
someone signs up through your link and starts a paid plan,
<strong>both of you get 45 days of paid access</strong> credited
to your account.
</p>
<h3 class="text-modal__head">How it works</h3>
<ol class="text-modal__list">
<li><strong>Sign up.</strong> Your code and link go live in Settings.</li>
<li><strong>Share.</strong> Send the link, or read the code &mdash; the alphabet drops <code>0/O</code> and <code>1/I/L</code> so it dictates cleanly.</li>
<li><strong>They sign up.</strong> The referral is recorded against your account when they verify their email.</li>
<li><strong>They subscribe.</strong> 45 days of paid access lands on both accounts &mdash; usable any time over the next month and a half.</li>
</ol>
<h3 class="text-modal__head">The fine print</h3>
<ul class="text-modal__list">
<li>One referral per new account &mdash; whichever link they used first.</li>
<li>No self-referral.</li>
<li>Credits stack: if you already have a credit window running, the new 45 days extend from its end, not from today.</li>
<li>Credits aren&rsquo;t refundable for cash &mdash; see <a href="/terms">Terms &amp; Conditions &sect; 6</a>.</li>
<li>Pending signups, conversions, and active credits are visible on the Settings page.</li>
</ul>
</dialog>
<script>
(function () {
var dlg = document.getElementById('invite-modal');
var open = document.getElementById('invite-more');
if (!dlg || !dlg.showModal || !open) return;
open.addEventListener('click', function () { dlg.showModal(); });
dlg.addEventListener('click', function (e) {
if (e.target === dlg) dlg.close();
});
dlg.querySelector('.text-modal__close').addEventListener('click', function () {
dlg.close();
});
})();
</script>
<section class="public-section">
<h2 class="public-section__head">How the data is handled</h2>
<p>
Your portfolio holdings live in your browser&rsquo;s local storage.
The CSV is parsed in your browser, the resulting pie is kept there,
and nothing about your holdings is sent to or stored on the server.
Full details on the <a href="/privacy">privacy page</a>.
</p>
</section>
<section class="public-section public-section--callout">
<p style="margin:0;">
<strong>Not investment advice.</strong> Every output here is an
interpretation of public data &mdash; not personalised advice, not a
recommendation, and not produced by a regulated entity. Read the full
<a href="/disclaimer">disclaimer</a> before relying on anything you see.
</p>
</section>
{% endblock %}