eleven / messenger Public
π€ 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)
- 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.
- One quiet line under the selection: "Prices are suggested by country. Pick the one that's right for you β no questions asked."
- 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. - 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. Notecurrency_optionscannot 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": truevintages kept in the price book) β never bespoke per-cohort amounts chasing "exactly +15%", which would explode the book and breaktierByPrice()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):
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 --applyIt 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):
[{"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: optionalbandparam (defaultbase), resolvetier.bands[band].price, stampmetadata[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]=trueon Checkout Sessions. Family/C&F Prices are created withtax_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 withtax_behavior=exclusiveβ the sticker is ex-VAT, tax is added at checkout, and Business checkout enablestax_id_collectionso 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_yearfigures become net-of-VAT approximations across buyer countries (the free pool tolerates that comfortably); Businessnet_eur_yearis 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]=trueper 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-Pricetax_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; checkoutbandparam; 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 flipLCHAT_STRIPE_AUTOMATIC_TAX=1with 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.