read.markets/app/templates/privacy.html
Giorgio Gilestro f1903e1e61 public: landing + pricing + legal pages, apex-ready, lawyer-reviewed
Adds the unauthenticated surface that's needed to invite outsiders:

  - Landing (/) — dual-purpose root: dashboard for logged-in users,
    landing for everyone else. New maybe_current_user soft-auth helper
    in app/auth.py supports it without disturbing the per-route
    require_token deps on /news, /log, /upload, /settings.
  - About, Pricing, Disclaimer, Terms, Privacy — own router
    (app/routers/public.py), no auth dep, shared public_base layout
    (brand link, thin nav, footer with legal links + ICO ref + date).
  - Editorial positioning: news aggregator with a macro brain; tagline
    "Understand markets. Don't gamble on them."; anti-trading-as-gambling
    stance carried through About and Landing.

Legal pass following an independent lawyer-style review:

  - Privacy: explicit UK-GDPR Art. 6 lawful-basis section; Art. 22
    automated-decision line; explicit consent for sessionStorage sync
    key (PECR); 30-day IP-log retention; Art. 21 objection right;
    Children clause; Art. 33/34 breach-notification clause;
    international-transfer mechanism (IDTA + UK Addendum). ICO
    registration ZC098928 surfaced at the top.
  - Pricing: paid-card AI-portfolio-analysis bullet rewritten to remove
    advice-shaped wording ("what would invalidate the posture" gone);
    added italic carve-out citing FSMA / FCA COBS.
  - Disclaimer: separate EU/EEA carve-out + MAR 596/2014 Art. 3(1)(34)
    commentator safe-harbour; "qualifies the Terms" line; hallucination
    wording fixed.
  - Terms: cl.4 explicit AI-training prohibition + harassment line;
    cl.5 CCR 2013 14-day cancellation; cl.7 softened AI copyright
    claim under CDPA s.9(3) ambiguity; cl.8 proportionate suspension +
    pro-rata refund for paid users; cl.10 CRA 2015 Pt 1 statutory-rights
    carve-out from the liability cap; cl.11 right to close account on
    material change; cl.12 non-exclusive jurisdiction + UK consumer
    local courts.

Code-side enforcement of the Privacy claim:

  - openrouter.py: outbound OpenRouter calls now carry
    X-OR-Allow-Training: false. DeepSeek doesn't expose a per-request
    flag; the Privacy page discloses this caveat verbatim.

Apex domain prep:

  - branding.APP_URL flipped to https://read.markets (was app.). DNS for
    the apex already resolves; pending operator NPM step is a cert that
    covers the bare apex + a 301 from app.read.markets. No hard-coded
    subdomain references remain in code (verified with grep).

Nav + chrome:

  - app dropdown gains Pricing / Terms / Privacy / Disclaimer links.
  - login.html gains a small legal-links footer for the
    highest-leverage moment to surface them.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 00:08:02 +02:00

311 lines
12 KiB
HTML

{% extends "public_base.html" %}
{% block title %}{{ BRAND_NAME }} &middot; Privacy{% endblock %}
{% block main %}
<section class="public-section">
<h1 class="public-section__head">Privacy notice</h1>
<p style="color: var(--muted); font-size: 13px;">
Last updated: 2026-05-24. The operator (data controller) is
{{ LEGAL_OPERATOR }}, {{ OPERATOR_JURISDICTION }}. Registered with
the UK Information Commissioner&rsquo;s Office under reference
<strong>ZC098928</strong>. Questions:
<a href="mailto:{{ OPERATOR_EMAIL }}">{{ OPERATOR_EMAIL }}</a>.
</p>
<p>
This page describes exactly what we collect, what we don&rsquo;t,
where it lives, and how long we keep it. It is written from the code,
not from a template &mdash; every claim corresponds to an explicit
code path we&rsquo;re happy to point a reviewer at.
</p>
</section>
<section class="public-section">
<h2 class="public-section__head">What we collect</h2>
<ul>
<li>
<strong>Your email address</strong>, when you sign in. We use it
only to send one-time login codes.
</li>
<li>
<strong>An argon2 hash of each login code</strong>, plus expiry
and attempt counts. The plaintext code is sent to your inbox and
never written to disk on our side.
</li>
<li>
<strong>A signed session cookie</strong> after you verify a code.
It contains your user id only and is signed so we can detect
tampering. Cookie is marked Secure and HttpOnly.
</li>
<li>
<strong>Anonymous ticker universe</strong>: when you upload a
portfolio CSV we record which Yahoo tickers appear, with
<em>no link</em> to your account. The same row would exist whether
any specific user holds the ticker or not &mdash; once a ticker is in
the universe, the row carries no signal as to whose import added it.
</li>
<li>
<strong>If you opt in to encrypted cloud sync</strong>: an opaque
blob of bytes per user. The blob is your portfolio, encrypted in
your browser with a PIN you choose, then wrapped a second time on
the server with a key only the server holds. We can&rsquo;t decrypt
the blob to plaintext without your PIN, and we can&rsquo;t recover
your PIN if you forget it. By enabling cloud sync you give your
consent (UK-GDPR Art. 6(1)(a)) to this processing; you can
withdraw consent at any time by disabling sync in Settings, which
also removes the server-side blob.
</li>
<li>
<strong>Anonymised cost ledger</strong> of AI calls (model, tokens,
cost). No portfolio or personal data is attached to ledger rows.
</li>
<li>
<strong>Referral linkage</strong>: if you signed up via an invite
link, we record which existing user&rsquo;s code you used so we can
apply the agreed referral credit later.
</li>
<li>
<strong>Job-run telemetry</strong>: success/failure timestamps for
the scheduled jobs that fetch market data and generate AI reads.
No user identifiers are attached.
</li>
</ul>
</section>
<section class="public-section">
<h2 class="public-section__head">What we don&rsquo;t collect</h2>
<ul>
<li>
<strong>Your portfolio holdings as plaintext on the server.</strong>
Parsed pies are returned to your browser and kept in
<code>localStorage</code>. The server&rsquo;s view is the anonymous
ticker universe described above.
</li>
<li>
<strong>Third-party analytics or ad cookies.</strong> No Google
Analytics, no Hotjar, no Segment, no Facebook pixel, no LinkedIn
tag. (You can verify by viewing-source on any page.)
</li>
<li>
<strong>Browser fingerprints.</strong>
</li>
<li>
<strong>IP-address joins to your user identity.</strong> IP
addresses are processed transiently by the reverse proxy for
security and access logging, retained for up to 30 days, and not
linked to your account record.
</li>
</ul>
</section>
<section class="public-section">
<h2 class="public-section__head">Lawful basis (UK-GDPR Art. 6)</h2>
<p>We rely on the following lawful bases:</p>
<ul>
<li>
<strong>Performance of a contract</strong> (Art. 6(1)(b)) &mdash; for
operating your account, the sign-in flow, paid features, and the
mechanics of encrypted cloud sync.
</li>
<li>
<strong>Legitimate interests</strong> (Art. 6(1)(f)) &mdash; for the
anonymous ticker universe, the anonymised cost ledger, job-run
telemetry, and reverse-proxy access logs. Our interest is the
secure, abuse-resistant, cost-controlled operation of a free
public service, balanced against the minimal and de-identified
nature of the data.
</li>
<li>
<strong>Consent</strong> (Art. 6(1)(a)) &mdash; where you opt in to
encrypted cloud sync (and the related caching of a derived
encryption key in your browser&rsquo;s <code>sessionStorage</code>).
You can withdraw consent at any time by disabling sync in
Settings; the cached key is cleared and the server-side blob is
removed.
</li>
</ul>
</section>
<section class="public-section">
<h2 class="public-section__head">Automated decisions and profiling</h2>
<p>
The Service does not make decisions about you that produce legal or
similarly significant effects in an automated way (UK-GDPR Art. 22).
The AI portfolio analysis is editorial commentary on the holdings
you upload; it does not approve, reject or rank you, and you remain
the sole decision-maker about anything in your account.
</p>
</section>
<section class="public-section">
<h2 class="public-section__head">Cookies and local storage</h2>
<ul>
<li>
<strong>Session cookie</strong> &mdash; strictly necessary for keeping
you signed in (PECR reg. 6(4)). No prior consent required.
</li>
<li>
<strong>Local preferences</strong> &mdash; your chosen theme (light /
dark) and reading level (Novice / Intermediate) are stored in
<code>localStorage</code> on your device. They never leave the
browser.
</li>
<li>
<strong>Local portfolio + cached sync key</strong> &mdash; parsed pies
live in <code>localStorage</code> on your device. If you enable
cloud sync, the derived encryption key is cached in
<code>sessionStorage</code> so you don&rsquo;t have to re-enter
your PIN on every navigation. This caching is performed only with
your consent (given when you enable sync); it is cleared when you
close the tab or disable sync.
</li>
</ul>
</section>
<section class="public-section">
<h2 class="public-section__head">Where the data lives, and international transfers</h2>
<p>
The server runs in {{ OPERATOR_JURISDICTION }}. Data is stored in a
MariaDB database on the same host, backed up locally.
</p>
<p>
Two flows can take personal data outside the UK:
</p>
<ul>
<li>
<strong>SMTP</strong> for sending one-time login codes. Operator-hosted,
currently inside the UK; if that changes we will update this notice.
</li>
<li>
<strong>AI provider calls</strong> for the strategic log, indicator
summaries, and (paid) portfolio analysis. Where the provider sits
outside the UK, we rely on the UK International Data Transfer
Agreement (IDTA) / the UK Addendum to the EU Standard Contractual
Clauses where no adequacy decision applies. Each outbound request
carries an explicit no-training opt-out header
(<code>X-OR-Allow-Training: false</code> on OpenRouter); see the
Third parties section below for the caveats.
</li>
</ul>
</section>
<section class="public-section">
<h2 class="public-section__head">Retention</h2>
<ul>
<li>
<strong>Login codes</strong>: expire after a few minutes; row
remains briefly to enforce single-use, then is purged.
</li>
<li>
<strong>Session cookies</strong>: expire automatically; you can
sign out at any time to revoke.
</li>
<li>
<strong>Ticker universe</strong>: rows untouched for 60 days are
evicted by a nightly job. Active tickers remain.
</li>
<li>
<strong>Encrypted portfolio blob</strong>: kept until you disable
cloud sync (one click in Settings) or delete your account. We hold
one row per user; new uploads overwrite the previous blob.
</li>
<li>
<strong>Account</strong>: held until you ask us to delete it.
Email <a href="mailto:{{ OPERATOR_EMAIL }}">{{ OPERATOR_EMAIL }}</a>.
</li>
<li>
<strong>Cost ledger and job telemetry</strong>: retained for
operational accounting; no personal data attached.
</li>
</ul>
</section>
<section class="public-section">
<h2 class="public-section__head">Third parties</h2>
<ul>
<li>
<strong>SMTP provider</strong>: an operator-hosted Mailu server
sends the one-time login codes. The provider sees your email
address and the code body (the code itself).
</li>
<li>
<strong>AI provider(s)</strong>: DeepSeek (primary) with OpenRouter
as a fallback. They see the prompt for the strategic log, the
indicator summaries, and the portfolio analysis call &mdash; which
contains your holdings only when you press
&ldquo;Generate AI analysis&rdquo; on a paid plan, and only for the
duration of that single call. The portfolio analysis output is not
persisted on the server.
<br>
<strong>No-training opt-out.</strong> Every OpenRouter request
carries the <code>X-OR-Allow-Training: false</code> header, which
signals to OpenRouter and any compatible upstream that the prompt
must not be used to train or improve models. DeepSeek does not
currently expose a per-request opt-out; if you do not want your
holdings to leave our server at all, do not use the AI portfolio
analysis feature. We do not control retention or training policies
on the provider side beyond the headers we set &mdash; the provider&rsquo;s
own published data policy is the binding statement on that point.
</li>
<li>
<strong>Market-data sources</strong>: Yahoo Finance and a small set
of public RSS feeds. We request prices and headlines; we don&rsquo;t
send them any user identifier.
</li>
</ul>
</section>
<section class="public-section">
<h2 class="public-section__head">Your rights (UK-GDPR)</h2>
<p>You have the right to:</p>
<ul>
<li>Ask what personal data we hold about you (Art. 15, right of access).</li>
<li>Have inaccurate data corrected (Art. 16, rectification).</li>
<li>Have your account and associated data deleted (Art. 17, erasure).</li>
<li>Export the data you can recognise (Art. 20, portability): your
email, any active encrypted blob, your referral linkage.</li>
<li>Restrict processing (Art. 18).</li>
<li>Object specifically to processing carried out on the basis of
legitimate interests (Art. 21), including any direct marketing.</li>
<li>Withdraw consent at any time where processing is based on
consent (Art. 7(3)), e.g. by disabling cloud sync.</li>
<li>Lodge a complaint with the
<a href="https://ico.org.uk/" target="_blank" rel="noopener">Information Commissioner&rsquo;s Office</a>
if you think we&rsquo;re mishandling your data.</li>
</ul>
<p>
Email <a href="mailto:{{ OPERATOR_EMAIL }}">{{ OPERATOR_EMAIL }}</a> to
exercise any of these.
</p>
</section>
<section class="public-section">
<h2 class="public-section__head">Children</h2>
<p>
The Service is not directed at, and is not intended for use by,
anyone under 18. Do not create an account if you are under 18. If
you believe a child has provided personal data to us, contact
<a href="mailto:{{ OPERATOR_EMAIL }}">{{ OPERATOR_EMAIL }}</a> and we
will delete it.
</p>
</section>
<section class="public-section">
<h2 class="public-section__head">Security incidents</h2>
<p>
If we discover a personal-data breach likely to result in a risk to
your rights and freedoms, we will notify the ICO within 72 hours of
becoming aware of it, as required by UK-GDPR Art. 33, and notify
affected users without undue delay where Art. 34 requires.
</p>
</section>
<section class="public-section">
<h2 class="public-section__head">Changes to this notice</h2>
<p>
Material changes will be flagged in-app and dated above. Trivial
edits (grammar, restructuring) won&rsquo;t.
</p>
</section>
{% endblock %}