pricing: land £7/£70 paid tier and make behaviour match

Marketing + behaviour pass to get the site ready for Paddle approval.

Pricing page
- £7/month, £70/year headline (was "Coming soon").
- Bigger tier names (was 11px uppercase mono — looked like chips).
- Real CTAs (button base styles were only scoped to .hero__ctas).
- "Best value" badge + drop-shadow on the Paid card; full-width
  block CTAs that align across both cards.
- "Free vs Paid at a glance" comparison table beneath the cards.
- Compact "Invite a friend — both get 50% off for 3 months"
  callout with the detail explanation behind a <dialog> popup.

Tier copy + behaviour now consistent
- Free strategic-log refresh is every 6 hours, not hourly. New
  read-side filter on /api/log/{latest,by-date} restricts free
  users to logs at boundary hours (00/06/12/18 UTC); paid users
  still see the most recent.
- Follow-up chat is paid-only. /api/chat returns 402 for free;
  the chat sidebar on /log is replaced with a locked aside and
  chat.js no longer loads at all for free users.
- Dashboard meta lines + landing copy softened so they no longer
  promise hourly to everyone.

Future-proofing copy on public pages
- Dropped "free forever" wording (we may close the free tier).
- "Trading 212 CSV" became "broker CSV (Trading 212 today; more
  planned)" on pricing + landing; the actual import UIs stay
  T212-specific.

Terms
- Renamed Terms of Service -> Terms and Conditions (Paddle
  expectation), bumped last-updated to 2026-05-26.
- New §6 Refunds covering the 14-day cooling off, post-window
  cancellation, termination-by-us refunds, statutory rights, and
  how to request a refund.
- Renumbered §7-§14 and fixed the disclaimer link labels.

Tests
- 6 new tests in tests/test_chat_and_log_gates.py cover the
  chat 402 + the boundary-hour filter on both log endpoints.
- Full suite: 205 passed, 5 skipped, 0 failed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Giorgio Gilestro 2026-05-26 11:34:37 +02:00
parent 70cf6148ce
commit 2297f9b2ed
11 changed files with 757 additions and 117 deletions

View file

@ -61,7 +61,9 @@
<section id="log-panel" class="panel">
<div class="panel-header">
<span class="title">Strategic Log</span>
<span class="meta">generated hourly @ :20 UTC</span>
<span class="meta">
{% if paid %}refreshed hourly @ :20 UTC{% else %}refreshed every 6 hours &middot; <a href="/pricing">hourly on Paid</a>{% endif %}
</span>
</div>
<div class="panel-body"
hx-get="/api/log/latest?as=html"
@ -74,7 +76,9 @@
<section id="news-panel" class="panel">
<div class="panel-header">
<span class="title">Flash News</span>
<span class="meta">last 24h · ingest hourly @ :10 UTC</span>
<span class="meta">
{% if paid %}last 24h &middot; ingest hourly @ :10 UTC{% else %}last 6h &middot; <a href="/pricing">full 24h on Paid</a>{% endif %}
</span>
</div>
<div class="panel-body panel-body--scroll"
hx-get="/api/news?as=html&limit=40"

View file

@ -12,7 +12,7 @@
</p>
<p style="margin-top:6px; font-size:12px; color: var(--muted);">
This page is part of, and qualifies, the
<a href="/terms">Terms of Service</a>.
<a href="/terms">Terms and Conditions</a>.
</p>
</section>
@ -95,7 +95,7 @@
The service is provided &ldquo;as is&rdquo; without warranties of any
kind. To the maximum extent permitted by applicable law, the operator
excludes liability for any loss arising from use of, or reliance on,
the content. See the <a href="/terms">Terms of Service</a> for the
the content. See the <a href="/terms">Terms and Conditions</a> for the
full limitation of liability.
</p>
</section>

View file

@ -11,8 +11,8 @@
tune out the high-frequency noise that comes from treating markets
like a casino. We aggregate cross-asset news and macro signals,
then write a plain-English read of what the underlying fundamentals
justify versus what the crowd is doing. Refreshed hourly. A media
service, not a financial one.
justify versus what the crowd is doing. Refreshed through the
trading day. A media service, not a financial one.
</p>
<div class="hero__ctas">
{% if cu and (cu.user or cu.is_admin) %}
@ -75,7 +75,7 @@
</div>
<div class="feature-card">
<div class="feature-card__tag">The hourly read</div>
<div class="feature-card__tag">The strategic read</div>
<h3 class="feature-card__title">Rational vs irrational, every paragraph</h3>
<p class="feature-card__body">
We tie the day&rsquo;s headlines and the cross-asset signals into
@ -88,8 +88,8 @@
</p>
<button class="shot feature-card__shot"
data-full="{{ url_for('static', path='/images/strategic-log.png') }}"
data-alt="Strategic log — the hourly AI read"
data-caption="The strategic log. The model writes a fresh interpretation each hour, taking yesterday's draft as context so it updates rather than starts over. Sections are typed: date header, TL;DR, what moved, what to watch, system temperature.">
data-alt="Strategic log — the editorial AI read"
data-caption="The strategic log. The model writes a fresh interpretation through the trading day, taking the previous draft as context so it updates rather than starts over. Sections are typed: date header, TL;DR, what moved, what to watch, system temperature. Paid users get a refresh every hour; free users get one every six.">
<img src="{{ url_for('static', path='/images/strategic-log.png') }}"
alt="Strategic log thumbnail" loading="lazy">
</button>
@ -115,10 +115,11 @@
<section class="public-section">
<p style="font-size: 13.5px; color: var(--muted);">
Paid users can also drop a Trading 212 pie CSV for an AI
sense-check on concentration, regime fit, and currency exposure.
Holdings stay in your browser by default; opt in to encrypted cloud
sync to restore on another device.
Paid users can also drop a portfolio CSV from their broker
(Trading 212 today, more brokers planned) for an AI sense-check on
concentration, regime fit, and currency exposure. Holdings stay in
your browser by default; opt in to encrypted cloud sync to restore
on another device.
</p>
</section>

View file

@ -30,6 +30,7 @@
<div class="empty">loading log…</div>
</article>
{% if paid %}
<aside id="chat-sidebar" class="log-page__chat">
<div class="chat-header">
<span class="chat-title">Ask Cassandra</span>
@ -49,7 +50,24 @@
<button id="chat-send" type="submit">Send</button>
</form>
</aside>
{% else %}
<aside class="log-page__chat log-page__chat--locked">
<div class="chat-header">
<span class="chat-title">Ask Cassandra</span>
<span class="chat-hint">paid-tier feature</span>
</div>
<div class="chat-locked">
<p>
<strong>Follow-up chat is a paid feature.</strong>
Ask the model a question about any past log &mdash; it sees the
full day's context: the strategic log, live market readings, and
the last 24 hours of thesis-filtered headlines.
</p>
<a class="btn-primary" href="/pricing">See pricing</a>
</div>
</aside>
{% endif %}
</div>
</section>
<script src="{{ url_for('static', path='/js/chat.js') }}" defer></script>
{% if paid %}<script src="{{ url_for('static', path='/js/chat.js') }}" defer></script>{% endif %}
{% endblock %}

View file

@ -6,78 +6,190 @@
<section class="public-section">
<h1 class="public-section__head">Pricing</h1>
<p>
Two tiers. Most of the editorial stays free &mdash; the news
aggregator, the hourly AI read, the indicator panels, the
follow-up chat. Paid extends the news window from 6 hours to a
full 24, unlocks portfolio import &amp; analysis, and adds a
daily email digest on top of the Sunday recap everyone gets.
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 portfolio import with AI analysis, 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">
<div class="tier-card__name">Free</div>
<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">Forever. No card needed.</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 aggregator &mdash; last 6 hours, auto-tagged by theme, click-to-filter</li>
<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>Hourly strategic log &mdash; a single editorial interpretation of the day, updated each hour</li>
<li>Ask follow-up questions on any past log &mdash; the chat has the day&rsquo;s context loaded</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 by email</strong> &mdash; the week behind + the week ahead, one-click unsubscribe</li>
<li class="tier-card__excluded">Portfolio import &amp; analysis</li>
<li class="tier-card__excluded">Encrypted cloud sync</li>
<li class="tier-card__excluded">Daily email digest</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 portfolio analysis? See <strong>Paid</strong> &rarr;
</div>
<div class="tier-card__cta">
{% if cu and (cu.user or cu.is_admin) %}
<a class="btn-secondary" href="/">Open dashboard</a>
<a class="btn-secondary btn-block" href="/">Open dashboard</a>
{% else %}
<a class="btn-primary" href="/login">Sign up free</a>
<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__name">Paid</div>
<div class="tier-card__price">Coming soon</div>
<div class="tier-card__price-hint">Checkout opens with our payments rollout.</div>
<ul>
<li>Everything in Free</li>
<li><strong>News aggregator &mdash; full 24 hours</strong> (vs. 6 hours on Free)</li>
<li><strong>Daily email digest</strong> (Mon&ndash;Sat) &mdash; a ~600-word read of the session ahead, in addition to the Sunday recap</li>
<li>Portfolio import from a Trading 212 CSV</li>
<li>AI commentary on diversification, sector and currency concentration, and macro-regime fit for the holdings you upload</li>
<li>Optional encrypted cloud sync &mdash; PIN-derived encryption in your browser, second-layer wrap on the server, no plaintext holdings server-side</li>
</ul>
<div class="tier-card__cta">
{% if cu and (cu.user or cu.is_admin) %}
<a class="btn-secondary" href="/settings">Manage account</a>
{% else %}
<a class="btn-primary" href="/login">Sign up &mdash; paid unlocks soon</a>
{% endif %}
<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 AI portfolio analysis.</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. Prices
in GBP, VAT where applicable. Checkout opens with the payments
rollout.
</div>
<p style="margin-top:14px; font-size:11.5px; color: var(--muted); font-style: italic; line-height:1.55;">
<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>Portfolio import</strong> from a broker CSV (Trading 212 supported today; more brokers planned)</li>
<li><strong>AI portfolio read</strong> &mdash; diversification, sector and currency concentration, macro-regime fit on your holdings</li>
<li><strong>Optional encrypted cloud sync</strong> &mdash; PIN-derived encryption in your browser, second-layer wrap on the server, no plaintext holdings server-side</li>
</ul>
<p class="tier-card__more" style="font-style: italic;">
The portfolio feature does not produce buy, sell or hold
recommendations. It does not consider your wider finances, debts,
recommendations and does not consider your wider finances, debts,
tax position or objectives. It is not regulated investment advice
or a personal recommendation under FSMA / FCA COBS.
</p>
<div class="tier-card__cta">
{% if cu and (cu.user or cu.is_admin) %}
<a class="btn-secondary btn-block" href="/settings">Manage account</a>
{% else %}
<a class="btn-primary btn-block" href="/login">Sign up &mdash; paid unlocks soon</a>
{% endif %}
</div>
</div>
</section>
<section class="public-section">
<p style="font-size: 13px; color: var(--muted);">
<strong>Invite a friend.</strong> Every account gets a personal
invite link from the Settings page. When friends sign up through
it, we credit you toward the paid tier &mdash; the credit ledger
is live; the rate kicks in with the payments rollout.
</p>
<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">Portfolio import (broker CSV)</th>
<td class="compare-table__none">&mdash;</td>
<td class="compare-table__paid"><strong>Included</strong></td>
</tr>
<tr>
<th scope="row">AI portfolio read</th>
<td class="compare-table__none">&mdash;</td>
<td class="compare-table__paid"><strong>Included</strong></td>
</tr>
<tr>
<th scope="row">Encrypted cloud sync</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>50% off for 3 months</strong></div>
<div class="invite-callout__sub">
Share your personal invite link from <a href="/settings">Settings</a>. The discount 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 50% off for the next three months</strong>.
</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> The discount applies to their next bill and credits against yours.</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>The credit ledger is live today; the cash value kicks in when paid checkout opens. Referrals logged in the meantime are honoured.</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>

View file

@ -1,12 +1,12 @@
{% extends "public_base.html" %}
{% block title %}{{ BRAND_NAME }} &middot; Terms of Service{% endblock %}
{% block title %}{{ BRAND_NAME }} &middot; Terms and Conditions{% endblock %}
{% block main %}
<section class="public-section">
<h1 class="public-section__head">Terms of Service</h1>
<h1 class="public-section__head">Terms and Conditions</h1>
<p style="color: var(--muted); font-size: 13px;">
Last updated: 2026-05-24. {{ LEGAL_OPERATOR }} operates {{ BRAND_NAME }}
Last updated: 2026-05-26. {{ LEGAL_OPERATOR }} operates {{ BRAND_NAME }}
(the &ldquo;Service&rdquo;) from {{ OPERATOR_JURISDICTION }}.
</p>
</section>
@ -96,7 +96,55 @@
</section>
<section class="public-section">
<h2 class="public-section__head">6. Service availability</h2>
<h2 class="public-section__head">6. Refunds</h2>
<p>
<strong>14-day cooling-off (UK / EU consumers).</strong> If you buy a
paid subscription as a consumer, you have 14 days from the day of
purchase to cancel and receive a full refund of that purchase, under
the Consumer Contracts (Information, Cancellation and Additional
Charges) Regulations 2013. As noted in clause 5, if you start using
a paid feature inside the cancellation window you lose the right to
cancel in respect of digital content already delivered.
</p>
<p>
<strong>Cancellation after the cooling-off window.</strong> You can
cancel a paid subscription at any time. Cancellation takes effect at
the end of the current billing period; we do not pro-rate refunds
for the unused portion of a period you have already started, unless
a separate paragraph below applies.
</p>
<p>
<strong>Termination by us without fault on your part.</strong> If we
terminate or materially reduce a paid feature for reasons that are
not a breach by you (including a service shutdown), we will refund
the unused portion of any prepaid fees on a pro-rata basis. The same
applies under clause 8 if we terminate for a breach you did not
cause, and under clause 11 if you close your account because you do
not accept a material change to these Terms.
</p>
<p>
<strong>Service faults.</strong> Nothing in this clause limits your
statutory rights as a UK consumer under Part 1 of the Consumer
Rights Act 2015. If a paid feature is not supplied with reasonable
care and skill, you may be entitled to a repeat performance or a
price reduction (which can be a full refund) under that Act.
</p>
<p>
<strong>How to request a refund.</strong> Email
<a href="mailto:{{ OPERATOR_EMAIL }}">{{ OPERATOR_EMAIL }}</a> from
the address tied to your account, with the order reference if you
have one. We aim to acknowledge within 5 working days. Refunds are
returned to the original payment method and typically arrive within
14 days of approval, subject to your bank&rsquo;s processing time.
</p>
<p>
Referral credits and any other non-cash credits applied to your
account are not refundable for cash.
</p>
</section>
<section class="public-section">
<h2 class="public-section__head">7. Service availability</h2>
<p>
The Service is provided on a best-effort basis. There is no service
level agreement: outages, data delays, and feature changes may occur
@ -105,7 +153,7 @@
</section>
<section class="public-section">
<h2 class="public-section__head">7. Content &amp; ownership</h2>
<h2 class="public-section__head">8. Content &amp; ownership</h2>
<p>
The Service&rsquo;s code, design, indicator selection, and prompts
are owned or licensed by {{ LEGAL_OPERATOR }}. To the extent any
@ -126,7 +174,7 @@
</section>
<section class="public-section">
<h2 class="public-section__head">8. Suspension &amp; termination</h2>
<h2 class="public-section__head">9. Suspension &amp; termination</h2>
<p>
We may suspend or terminate access without notice for violation of
these Terms or for activity that risks the integrity, security, or
@ -138,12 +186,13 @@
respond, unless immediate suspension is necessary to protect the
Service, its users, or any third party. If we terminate a paid
subscription for a breach you did not cause, we will refund the
unused portion of any prepaid fees on a pro-rata basis.
unused portion of any prepaid fees on a pro-rata basis (see
clause 6).
</p>
</section>
<section class="public-section">
<h2 class="public-section__head">9. No warranty</h2>
<h2 class="public-section__head">10. No warranty</h2>
<p>
The Service is provided &ldquo;as is&rdquo; and &ldquo;as
available&rdquo;, without warranties of any kind, express or implied,
@ -153,7 +202,7 @@
</section>
<section class="public-section">
<h2 class="public-section__head">10. Limitation of liability</h2>
<h2 class="public-section__head">11. Limitation of liability</h2>
<p>
To the maximum extent permitted by law, {{ LEGAL_OPERATOR }} is not
liable for any indirect, incidental, special, consequential, or
@ -182,7 +231,7 @@
</section>
<section class="public-section">
<h2 class="public-section__head">11. Changes</h2>
<h2 class="public-section__head">12. Changes</h2>
<p>
These Terms may change. Material changes will be flagged in-app or
by email. Continued use after a change means you accept the updated
@ -193,7 +242,7 @@
</section>
<section class="public-section">
<h2 class="public-section__head">12. Governing law and jurisdiction</h2>
<h2 class="public-section__head">13. Governing law and jurisdiction</h2>
<p>
These Terms are governed by the laws of England and Wales. Subject
to any mandatory law of the consumer&rsquo;s country of residence,
@ -205,7 +254,7 @@
</section>
<section class="public-section">
<h2 class="public-section__head">13. Contact</h2>
<h2 class="public-section__head">14. Contact</h2>
<p>
<a href="mailto:{{ OPERATOR_EMAIL }}">{{ OPERATOR_EMAIL }}</a>
</p>