# 🤖 Pricing bands — global pricing that subsidises, never interrogates

Design doc, 2026-07-14. Status: **designed, not built.** Companion docs:
`docs/free-pool.md` (what the Supporter band funds), `docs/demo-spaces.md`,
`docs/tier-economics.md` (the cost-anchored base prices this doc multiplies).
Decisions in this doc were made by Paul 2026-07-14; legal findings are
research summaries, not legal advice — a human legal review gates Phase 1.

## The design in one sentence

> **Location suggests, never decides.** Every buyer sees the same four
> prices; the only thing geography ever does is choose which one is
> *pre-selected* — and stepping to any other band is one tap, no questions
> asked, in both directions, with a sentence explaining what the step means.

This single choice is simultaneously the honor mechanism ("click a link, get
the lower price, instantly"), what makes VPN games pointless without any
policing, and the legal defence (§ Law). It is what separates this from
personalised/surveillance pricing: the country→band table is public, the
suggestion can be computed client-side, and no IP is ever stored.

## The bands

Multipliers on the existing EUR base prices, charm-rounded:

| Tier | Supporter ×1.4 | Base ×1.0 | Reduced ×0.5 | Solidarity ×0.25 |
|---|---|---|---|---|
| Family (annual) | €7.70/y | €5.50/y | €2.75/y | €1.40/y |
| Community (annual, per member) | €0.15/member/y | €0.11/member/y | €0.055/member/y | €0.0275/member/y |
| Business (annual, per seat) | €15/person/y | €11/person/y | €5.50/person/y | — |

Community's €11 minimum needs no row (and no Stripe Price) of its own: the
billed quantity floors at 100 members, so each band's minimum is just 100 ×
its member rate — supporter €15, base €11, reduced €5.50, solidarity €2.75.

- **Family and C&F prices are VAT-inclusive; Business prices are
  VAT-exclusive** (decided 2026-07-14) — the standard B2C/B2B split: a
  consumer's €5 means €5 with VAT inside; Business is quoted ex-VAT and tax
  is added at checkout (or reverse-charged) — § VAT below.
- **Solidarity is annual-only**: Stripe's fixed ~€0.25/charge makes €1/m
  fee-hostile (~26% of gross), and annual matches how those buyers think
  about €10.
- **Business went per-seat 2026-07-22** (€11/person/year, annual only —
  `tier-economics.md § Per-seat`): the band multipliers apply per seat, and
  Solidarity is dropped for Business — a solidarity price aimed at
  hardship-priced *businesses* never quite parsed, and Reduced at
  €5.50/person covers nonprofits/education cleanly.
- **C&F went per-member 2026-07-27** (€0.11/member/year, €11 minimum with the
  first 100 members included — the elevens ladder): monthly billing is gone
  ladder-wide (the fixed fee would eat it), and the per-member bands ride
  Stripe's `unit_amount_decimal` — fractional cents are exactly what a
  €0.055/member Reduced rate needs.
- **All bands are always visible and any is selectable** — adjacent-only
  stepping was considered and rejected; friction is the enemy of the story.
- No band is structurally loss-making while hibernation holds (even
  Solidarity Family nets ~€1 against a mostly-asleep space's ~€0.2/y
  storage+noise); the real exposure remains media hoarding, already bounded
  by tier storage caps.
- Validation against revealed practice: Switzerland at 1.4× the euro base is
  *conservative* (Netflix/Spotify charge CH ≈1.5–1.65× their German price);
  Solidarity ×0.25 matches Steam's post-2022 India/CIS recommendations and
  goes a step past the parity movement's 50–60%-off norm — because the free
  pool, not the price, is the real floor here.

## The index and the country→band table

**Primary index: the World Bank price level ratio** (`PA.NUS.PPPC.RF` — PPP
conversion factor ÷ market exchange rate; ~200 economies, annual, free bulk
data). Not an income index: the number needed is *"what multiplier makes €5
feel like €5 there"*, which is literally what a price index measures. The two
diverge where it matters — the Gulf states and Singapore are rich by income
but mid-priced by price level; a price index honestly puts them at Base.

- Published vs US = 1.00; **rebased to a GDP-weighted euro-area anchor
  (~0.73)** so everything reads as a multiplier on the EUR base.
- Band edges: ≥1.15 → Supporter · 0.75–1.15 → Base · 0.45–0.75 → Reduced ·
  <0.45 → Solidarity.
- **Fallback** for countries with no price-level datum (Taiwan, Cuba; stale
  series): World Bank income classification — high → Base, upper-middle →
  Reduced, low/lower-middle → Solidarity; neither datum → Base. The honor
  picker bounds the harm of any wrong default.
- **Decided placements (2026-07-14):** **United States → Supporter ×1.4**
  (the index says 1.37; the one-tap step down is the escape hatch — this is
  the boldest call in the scheme and the band-mix telemetry below is how we
  check it). Band-edge UK (~1.16) and Canada (~1.15) → **Base**; revisit at
  the annual table regeneration. EU/EEA → Base *by law*, Switzerland →
  suggestion-only Supporter (§ Law).

**The index never touches runtime code.** A small committed tool
(`tools/pricing-bands.mjs`, human-legible) reads a vendored CSV of the index
and emits a committed, reviewable country→band JSON table with the index name
and vintage in a header (`"index": "wb-pli-2024"`). Swapping the index =
regenerate from a different CSV, read the diff, commit. Regenerate annually;
band *reassignments* reach existing subscribers only through the annual
review (§ The 15% cap), which >15% jumps turn into hold+invite by
construction. The table is public information — serve it on the pricing page;
publishing it *is* the transparency story.

## Law (the part that shaped the design)

- **EU Geo-blocking Regulation 2018/302** (electronically supplied services —
  hosting is squarely in scope): a trader may not *apply* different prices by
  reason of the customer's residence, but is free to *offer* different
  country price lists that anyone can access. Therefore: **one uniform
  EU/EEA default (Base)** — Denmark/Luxembourg/Norway/Iceland forgo their
  index-justified premium; the voluntary step-up remains available to them.
- **Switzerland** has its own geo-price-discrimination ban (UCA Art. 3a, in
  force 2022, from the Fair Price Initiative; civil enforcement only). A
  *hard* Swiss surcharge would sit inside its text; a pre-selected suggestion
  any Swiss customer can decline in one tap for the same product arguably
  discriminates against no one. The Swiss premium ships **suggestion-only**
  (as everywhere).
- The open-band design is the belt *and* braces: inside the EU/EEA the
  question never arises (uniform default); everywhere else, every price being
  openly offered to every customer with no eligibility test is the defence.
- Consumer transparency: EU rules require the total, VAT-inclusive price
  before purchase — § VAT below — and the one-line band explanations double
  as the required transparency.

**Sub-national pricing ("wealthier part of a country") — never as
detection** (decided): no precedent exists for residence-based sub-national
digital pricing; region-level IP geolocation is 55–80% accurate and exactly
the interrogation this product refuses. The honor picker already solves it
per-person, more accurately than any geo-database could — say so on the
pricing page.

## The picker (the honor mechanism, concretely)

1. The plan picker shows the tier as today, with a **band row** beneath: four
   prices, one pre-selected. Pre-selection: geo-suggestion (Phase 1, CDN
   country header, evaluated in the page, never stored) for non-EU/EEA
   visitors; otherwise Base.
2. One quiet line under the selection: *"Prices are suggested by country.
   Pick the one that's right for you — no questions asked."*
3. **Deep links** (`…/upgrade?tier=family&band=reduced`) pre-select a band —
   Paul's "click a link, get the lower price, instantly": newsletters, NGO
   wikis, a friend's message. The server honours whatever band arrives. No
   check, ever.
4. **Not Stripe promotion codes** (considered, rejected): percent-off coupons
   persist across Billing Portal plan switches, hide the band from
   `tierByPrice()` and the ledger, and frame Reduced as a deal off *your*
   price when it is simply *the* price for that band. **Separate Price IDs
   per band** are architecturally native — the price book already maps
   price→tier; it grows one dimension.

### Local-currency display before checkout (added 2026-07-14)

Show every picker price with an approximate local figure — *"€7/year
(≈ $8.20)"* — **before** the buyer reaches Stripe:

- **Display-only conversion.** Billing stays EUR (the tier-economics
  EUR-settlement stance). The local figure is an estimate and is labelled:
  *"billed in euros — ≈ shown in your currency at today's rate."*
- **Rates:** ECB daily reference rates (~30 currencies, free, no key). The
  home service fetches once daily and serves a tiny cached JSON alongside the
  price book, with the last-good snapshot as fallback — the client never
  talks to a third party (CSP stays pinned).
- **Currency choice comes from the browser locale** (`Intl.NumberFormat` /
  `navigator.language`), not IP — privacy-consistent, works offline-ish, and
  a small currency override sits next to the figure. Nothing is stored.
- **Later, optional (Phase 3):** true multi-currency *charging* via Stripe
  `currency_options` (one amount per currency, charm-rounded, curated
  annually) — only if EUR-billing measurably hurts non-EU conversion. Note
  `currency_options` cannot express bands *inside* a currency — one amount
  per currency per Price — which is exactly why bands are separate Prices
  and currency presentment is an independent layer on top. Stripe's
  Adaptive Pricing was evaluated and rejected: it is FX localisation with a
  2–4% customer-paid conversion fee, not PPP adjustment, and the hidden fee
  is off-brand.

## The +15% annual review cap (Paul's pricing promise)

**The rule:** every live price — monthly or annual billing alike — is
reviewed once a year (anchored to the subscription anniversary, so reviews
stagger naturally); silent renewal can carry at most **+15%**; anything
larger requires an explicit opt-in to a new subscription.

The cap is **the sole propagation channel**: index updates, band
reassignments, FX drift, and base-price changes reach existing subscribers
only through reviews. New customers always pay the current table. Stripe
grandfathers by default (a subscription renews on *its* Price until updated),
so "current table" (the env price book) and "your price" (the subscription's
Price id) separate cleanly; the ledger gains a pending-review notice
(space id, from-price, to-price, effective date, consent-required flag).

The review sweep, per subscription:

- **Current ≤ subscribed** → schedule the decrease at next renewal.
  Unconditional, no ceremony, message it proudly. (The asymmetry — decreases
  automatic, increases capped/consented — is deliberately public: German and
  Italian courts voided Netflix's and Spotify's adjustment clauses partly for
  being increase-only. Symmetry is the cure.)
- **Gap ≤ 15%** → schedule the increase at next renewal with in-product
  notice and an easy cancel/step-down path.
- **Gap > 15%** → **hold + invite** (decided): no silent change, no
  compounding 15-15-15 ladder; the subscriber stays on the old price until
  they affirmatively opt in to the current table (or a lower band).
- Discipline: the silent path only ever moves a subscription onto an
  *existing catalog Price* (current or `"retired": true` vintages kept in the
  price book) — never bespoke per-cohort amounts chasing "exactly +15%",
  which would explode the book and break `tierByPrice()` legibility.

**Honor-band subscribers track their own band's price** — Reduced is band C's
real price, not a discount off band A — so there is no mechanism by which an
honor price silently walks back up: a band reassignment is >15% by
construction, hence always hold+invite. The invite surface may gently show
the suggested band ("You're welcome on the reduced price as long as you need
it — the suggested price for your country is €5"), keeping the social
contract visible without enforcing it.

**Notice without email** (accounts are anonymous passkeys; Checkout collects
no email — verify nothing downstream assumes one): notices land in-product —
(1) an inline row on the home page where Upgrade/Manage live ("Renews 12 Aug
at €5.75 — review / change / cancel"), (2) the space's Admin-app update
stream + admin push (the announce.go channel), (3) Eleven can explain any
price from the public band table. "Explicit opt-in" = an authenticated
affirmative act on the home page (same ownership gate as checkout), recorded
in the ledger, then executed as a Stripe subscription update (never
cancel+recreate — preserves the customer object, anniversary, and ledger
row). Mechanics: plain subscription update with `proration_behavior=none`
near renewal; `invoice.upcoming` webhook as the notice trigger.

Regulatory shape-check: Ofcom's 2025 rule (in-contract rises must be
disclosed in pounds-and-pence at point of sale) says state the cap **in
euros on the picker** — "Family: never more than €0.75/yr more at renewal
without your say-so" — not as a bare percentage. The terms need a
transparent variation clause encoding cap + cadence + index reference +
symmetric downside + cancel-anytime, drafted by a human lawyer before
Phase 2 ships.

## Stripe implementation

**11 Prices** (nominally 3 tiers × 4 bands, minus Business's solidarity —
the elevens ladder, annual-only throughout), minted by the committed tool
`tools/stripe-band-prices.mjs` — its `BAND_TABLE` is this doc's matrix as
code, reviewed like any diff. The operator runs it from their own terminal
(sandbox key first, then live; the key never transits a session):

```sh
STRIPE_SECRET_KEY=sk_… node tools/stripe-band-prices.mjs --book current.json          # dry-run
STRIPE_SECRET_KEY=sk_… node tools/stripe-band-prices.mjs --book current.json --apply
```

It reuses each tier's existing Stripe product where the current book has one
(subscriber history stays put), creates every missing ladder Price — base and
bands — idempotently (lookup keys `eleven11-<tier>-<band>`; re-runs create
nothing; the old `eleven-*` prices stay untouched for lapsing subscribers),
and prints the complete new price book — sealed into `/data/cloud.env` via
the operator env flow. Per-unit tiers ride `unit_amount_decimal`, and the
book carries the flags the cloud already understands (`per_seat`,
`min_seats`, `business`):

```json
[{"key":"family","name":"Family",
  "label":"€5.50/year","price":"price_baseFam",
  "eur":5.5,"net_eur_year":5.168,
  "bands":{
    "supporter":{"label":"€7.70/year","price":"price_supFam","eur":7.7,"net_eur_year":7.335},
    "reduced":{"label":"€2.75/year","price":"price_redFam","eur":2.75,"net_eur_year":2.459},
    "solidarity":{"label":"€1.40/year","price":"price_solFam","eur":1.4,"net_eur_year":1.129}}},
 {"key":"community","name":"Community & Friends",
  "label":"€0.11/member/year","price":"price_baseCom",
  "eur":0.11,"net_eur_year":0.108,"per_seat":true,"min_seats":100,
  "bands":{"…":"…"}}]
```

(`net_eur_year` is the free pool's revenue input — see `docs/free-pool.md`.)

Touch points:

- `checkoutHandler`: optional `band` param (default `base`), resolve
  `tier.bands[band].price`, stamp `metadata[band]`. Ownership gates,
  no-customer-identity, webhook routing: unchanged.
- Webhook: `tierByPrice` → `tierBandByPrice` (still a flat scan over a few
  dozen rows); the ledger records the band.
- Billing Portal: four per-band portal configurations (portals are minted
  per-session, so the cloud picks the customer's band's config); band
  *changes* go through the home surface, which can message them — never the
  portal.
- Telemetry that respects the ethos: band mix by month from the ledger — no
  per-user tracking — is how the US-at-×1.4 call gets checked.

### VAT — Stripe Tax (decided: enable; consumer-inclusive, Business-exclusive)

Today nothing sets `automatic_tax`; the EUR figure is charged as-is and VAT
is implicit. With bands multiplying the price book, this gets fixed properly.
**The split (decided 2026-07-14): Family and C&F prices are VAT-inclusive;
Business prices are VAT-exclusive** — the standard B2C/B2B convention.

- **Code (this repo):** set `automatic_tax[enabled]=true` on Checkout
  Sessions. **Family/C&F Prices** are created with `tax_behavior=inclusive`
  — labels stay the total price (€5 means €5, VAT inside, satisfying EU
  price-indication rules) and net revenue varies by buyer country (VAT
  17–27% comes out of the sticker). **Business Prices** are created with
  `tax_behavior=exclusive` — the sticker is ex-VAT, tax is added at
  checkout, and Business checkout enables `tax_id_collection` so an EU
  business with a valid VAT ID gets the **reverse charge** (no VAT
  collected, they self-account). Picker labels for Business carry
  "excl. VAT" wherever the price appears. Portal configs follow the same
  behaviors.
- **Bookkeeping:** consumer-tier `net_eur_year` figures become net-of-VAT
  approximations across buyer countries (the free pool tolerates that
  comfortably); Business `net_eur_year` is *more* stable than today —
  €11/seat is €11/seat regardless of buyer country (per-seat since
  2026-07-22, so the pool input scales it by the billed seats).
- **Caveat:** ex-VAT display is standard for business-directed offers
  (Slack/Notion-style), but EU price-indication rules technically demand
  consumer-facing totals be inclusive — so the Business tier should present
  as business-directed (it already does), show "excl. VAT" adjacent to
  every price, and Checkout shows the final total before payment. Include
  in the Phase 1 legal review.
- **Dashboard (operator — Paul):** two places.
  *Settings → Tax* (dashboard.stripe.com/settings/tax): origin address +
  defaults — note the "Include tax in prices" default (Automatic/Yes/No)
  is **superseded by our explicit per-Price `tax_behavior`**, so its value
  doesn't matter once the 20 Prices exist; its Automatic mode
  (currency-based: USD/CAD exclusive, EUR inclusive) is *not* our
  inclusive/exclusive split, which is per-tier, not per-currency.
  *Tax → Registrations* (dashboard.stripe.com/tax/registrations): **the
  load-bearing step** — Stripe Tax only calculates where a registration is
  added and merely monitors thresholds elsewhere. Needed eventually: the
  home-country VAT registration + the **EU OSS (Union scheme)**, plus
  others as the monitoring tab flags obligations. These are account-level
  actions the code cannot and should not do — and until the code opts in
  (`automatic_tax[enabled]=true` per session), a configured dashboard
  still changes nothing at checkout.
  **Registrations are deferred (Paul, 2026-07-14).** Phase 0 ships without
  them and behaves gracefully: with zero registrations Stripe Tax
  calculates €0 everywhere, so inclusive prices are unchanged for the
  buyer and Business checkouts add nothing yet — the plumbing
  (per-Price `tax_behavior`, `automatic_tax`, `tax_id_collection`) is all
  in place, and adding a registration later activates it with no code
  change. Stripe Tax's threshold-monitoring tab is the revisit trigger
  (note: monitoring tracks configuration, not obligation — whether an
  obligation already exists today is an accountant question, not a
  dashboard one).
- Existing subscriptions keep their current Prices; the new tax behaviors
  apply as new Prices roll through checkout and reviews.

## Messaging (drafts, not final copy — lock-and-key voice)

The canonical cross-subsidy line (decided, aggregate form only):

> **"Paying supports free spaces for those who can't."**

- **Supporter:** "This is the supporter price for Switzerland. Paying
  supports free spaces for those who can't — the difference doesn't go to
  us, it goes to the free pool. If it's not right for you, pick the standard
  price — no questions asked."
- **Base:** "The standard price. It covers your space's costs with margin
  for resilience — the books are public."
- **Reduced / Solidarity:** "You're on the reduced price, carried by
  supporters. It's yours as long as you need it." Never "discount", never
  "eligible", never a country name on this side.
- **The cap, everywhere prices appear:** "Reviewed once a year. Never up
  more than 15% — for Family, at most €0.75 — never onto a new plan without
  your explicit yes. Down happens by itself."
- Never a per-space "your €7 funds N spaces" claim (decided): the aggregate
  pool gauge ("paid spaces fund the free pool M× over") is the verifiable
  form. Precedent says transparent cross-subsidy works with an anchor, a
  chosen default, and visible reciprocity (Guardian, solidarity tickets) and
  backfires when hidden or anchorless (Amazon 2000, Panera) — this design
  keeps the standard-price tap in the same glance as the supporter ask.

## Phasing

- **Phase 0 — bands, pure honor, no geolocation.** The 19 Prices; price-book
  `bands` + `net_eur_year`; `tierBandByPrice`; checkout `band` param; picker
  with all four bands, Base pre-selected for everyone; deep-link band param;
  local-currency ≈ display (ECB rates via home); Stripe Tax
  (code + dashboard runbook above). Legally inert everywhere — it's four
  prices — and already delivers "anyone, anywhere, one click to the lower
  price" plus the supporter story. Measure band mix.
  *Built (code side): the cloud rails (book/checkout/ledger/tax knob), the
  home picker + deep links + ≈ display, and the minting tool. What remains is
  operational: run the tool (sandbox, then live), seal the new book into the
  vault, and flip `LCHAT_STRIPE_AUTOMATIC_TAX=1` with it — everything renders
  as today until the book carries bands.*
- **Phase 1 — geo-suggested defaults.** Commit `tools/pricing-bands.mjs` +
  the country→band table; pre-select by CDN country header (client-side,
  never stored); EU/EEA pinned to Base; publish the table on the pricing
  page. **Human legal review gates this phase.**
- **Phase 2 — the annual review machinery.** Ledger review/consent fields;
  the sweep; home inline notices + admin announce; decreases automatic,
  ≤15% with notice, >15% hold+invite; terms variation clause (lawyer).
- **Phase 3 — optional refinements:** true multi-currency charging via
  `currency_options` (only if EUR-billing measurably hurts conversion);
  annual index refresh cadence (Phase 2 already absorbs band moves);
  free-pool gauge coupling on the pricing page.
- **Never:** sub-national pricing as detection; promotion-code discounts;
  Adaptive Pricing's customer-paid FX fee.
