eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1# πŸ€– Pricing bands β€” global pricing that subsidises, never interrogates
2
3Design doc, 2026-07-14. Status: **designed, not built.** Companion docs:
4`docs/free-pool.md` (what the Supporter band funds), `docs/demo-spaces.md`,
5`docs/tier-economics.md` (the cost-anchored base prices this doc multiplies).
6Decisions in this doc were made by Paul 2026-07-14; legal findings are
7research summaries, not legal advice β€” a human legal review gates Phase 1.
8
9## The design in one sentence
10
11> **Location suggests, never decides.** Every buyer sees the same four
12> prices; the only thing geography ever does is choose which one is
13> *pre-selected* β€” and stepping to any other band is one tap, no questions
14> asked, in both directions, with a sentence explaining what the step means.
15
16This single choice is simultaneously the honor mechanism ("click a link, get
17the lower price, instantly"), what makes VPN games pointless without any
18policing, and the legal defence (Β§ Law). It is what separates this from
19personalised/surveillance pricing: the country→band table is public, the
20suggestion can be computed client-side, and no IP is ever stored.
21
22## The bands
23
24Multipliers on the existing EUR base prices, charm-rounded:
25
26| Tier | Supporter Γ—1.4 | Base Γ—1.0 | Reduced Γ—0.5 | Solidarity Γ—0.25 |
27|---|---|---|---|---|
28| Family (annual) | €7.70/y | €5.50/y | €2.75/y | €1.40/y |
29| Community (annual, per member) | €0.15/member/y | €0.11/member/y | €0.055/member/y | €0.0275/member/y |
30| Business (annual, per seat) | €15/person/y | €11/person/y | €5.50/person/y | β€” |
31
32Community's €11 minimum needs no row (and no Stripe Price) of its own: the
33billed quantity floors at 100 members, so each band's minimum is just 100 Γ—
34its member rate β€” supporter €15, base €11, reduced €5.50, solidarity €2.75.
35
36- **Family and C&F prices are VAT-inclusive; Business prices are
37 VAT-exclusive** (decided 2026-07-14) β€” the standard B2C/B2B split: a
38 consumer's €5 means €5 with VAT inside; Business is quoted ex-VAT and tax
39 is added at checkout (or reverse-charged) β€” Β§ VAT below.
40- **Solidarity is annual-only**: Stripe's fixed ~€0.25/charge makes €1/m
41 fee-hostile (~26% of gross), and annual matches how those buyers think
42 about €10.
43- **Business went per-seat 2026-07-22** (€11/person/year, annual only β€”
44 `tier-economics.md Β§ Per-seat`): the band multipliers apply per seat, and
45 Solidarity is dropped for Business β€” a solidarity price aimed at
46 hardship-priced *businesses* never quite parsed, and Reduced at
47 €5.50/person covers nonprofits/education cleanly.
48- **C&F went per-member 2026-07-27** (€0.11/member/year, €11 minimum with the
49 first 100 members included β€” the elevens ladder): monthly billing is gone
50 ladder-wide (the fixed fee would eat it), and the per-member bands ride
51 Stripe's `unit_amount_decimal` β€” fractional cents are exactly what a
52 €0.055/member Reduced rate needs.
53- **All bands are always visible and any is selectable** β€” adjacent-only
54 stepping was considered and rejected; friction is the enemy of the story.
55- No band is structurally loss-making while hibernation holds (even
56 Solidarity Family nets ~€1 against a mostly-asleep space's ~€0.2/y
57 storage+noise); the real exposure remains media hoarding, already bounded
58 by tier storage caps.
59- Validation against revealed practice: Switzerland at 1.4Γ— the euro base is
60 *conservative* (Netflix/Spotify charge CH β‰ˆ1.5–1.65Γ— their German price);
61 Solidarity Γ—0.25 matches Steam's post-2022 India/CIS recommendations and
62 goes a step past the parity movement's 50–60%-off norm β€” because the free
63 pool, not the price, is the real floor here.
64
65## The index and the country→band table
66
67**Primary index: the World Bank price level ratio** (`PA.NUS.PPPC.RF` β€” PPP
68conversion factor Γ· market exchange rate; ~200 economies, annual, free bulk
69data). Not an income index: the number needed is *"what multiplier makes €5
70feel like €5 there"*, which is literally what a price index measures. The two
71diverge where it matters β€” the Gulf states and Singapore are rich by income
72but mid-priced by price level; a price index honestly puts them at Base.
73
74- Published vs US = 1.00; **rebased to a GDP-weighted euro-area anchor
75 (~0.73)** so everything reads as a multiplier on the EUR base.
76- Band edges: β‰₯1.15 β†’ Supporter Β· 0.75–1.15 β†’ Base Β· 0.45–0.75 β†’ Reduced Β·
77 <0.45 β†’ Solidarity.
78- **Fallback** for countries with no price-level datum (Taiwan, Cuba; stale
79 series): World Bank income classification β€” high β†’ Base, upper-middle β†’
80 Reduced, low/lower-middle β†’ Solidarity; neither datum β†’ Base. The honor
81 picker bounds the harm of any wrong default.
82- **Decided placements (2026-07-14):** **United States β†’ Supporter Γ—1.4**
83 (the index says 1.37; the one-tap step down is the escape hatch β€” this is
84 the boldest call in the scheme and the band-mix telemetry below is how we
85 check it). Band-edge UK (~1.16) and Canada (~1.15) β†’ **Base**; revisit at
86 the annual table regeneration. EU/EEA β†’ Base *by law*, Switzerland β†’
87 suggestion-only Supporter (Β§ Law).
88
89**The index never touches runtime code.** A small committed tool
90(`tools/pricing-bands.mjs`, human-legible) reads a vendored CSV of the index
91and emits a committed, reviewable country→band JSON table with the index name
92and vintage in a header (`"index": "wb-pli-2024"`). Swapping the index =
93regenerate from a different CSV, read the diff, commit. Regenerate annually;
94band *reassignments* reach existing subscribers only through the annual
95review (Β§ The 15% cap), which >15% jumps turn into hold+invite by
96construction. The table is public information β€” serve it on the pricing page;
97publishing it *is* the transparency story.
98
99## Law (the part that shaped the design)
100
101- **EU Geo-blocking Regulation 2018/302** (electronically supplied services β€”
102 hosting is squarely in scope): a trader may not *apply* different prices by
103 reason of the customer's residence, but is free to *offer* different
104 country price lists that anyone can access. Therefore: **one uniform
105 EU/EEA default (Base)** β€” Denmark/Luxembourg/Norway/Iceland forgo their
106 index-justified premium; the voluntary step-up remains available to them.
107- **Switzerland** has its own geo-price-discrimination ban (UCA Art. 3a, in
108 force 2022, from the Fair Price Initiative; civil enforcement only). A
109 *hard* Swiss surcharge would sit inside its text; a pre-selected suggestion
110 any Swiss customer can decline in one tap for the same product arguably
111 discriminates against no one. The Swiss premium ships **suggestion-only**
112 (as everywhere).
113- The open-band design is the belt *and* braces: inside the EU/EEA the
114 question never arises (uniform default); everywhere else, every price being
115 openly offered to every customer with no eligibility test is the defence.
116- Consumer transparency: EU rules require the total, VAT-inclusive price
117 before purchase β€” Β§ VAT below β€” and the one-line band explanations double
118 as the required transparency.
119
120**Sub-national pricing ("wealthier part of a country") β€” never as
121detection** (decided): no precedent exists for residence-based sub-national
122digital pricing; region-level IP geolocation is 55–80% accurate and exactly
123the interrogation this product refuses. The honor picker already solves it
124per-person, more accurately than any geo-database could β€” say so on the
125pricing page.
126
127## The picker (the honor mechanism, concretely)
128
1291. The plan picker shows the tier as today, with a **band row** beneath: four
130 prices, one pre-selected. Pre-selection: geo-suggestion (Phase 1, CDN
131 country header, evaluated in the page, never stored) for non-EU/EEA
132 visitors; otherwise Base.
1332. One quiet line under the selection: *"Prices are suggested by country.
134 Pick the one that's right for you β€” no questions asked."*
1353. **Deep links** (`…/upgrade?tier=family&band=reduced`) pre-select a band β€”
136 Paul's "click a link, get the lower price, instantly": newsletters, NGO
137 wikis, a friend's message. The server honours whatever band arrives. No
138 check, ever.
1394. **Not Stripe promotion codes** (considered, rejected): percent-off coupons
140 persist across Billing Portal plan switches, hide the band from
141 `tierByPrice()` and the ledger, and frame Reduced as a deal off *your*
142 price when it is simply *the* price for that band. **Separate Price IDs
143 per band** are architecturally native β€” the price book already maps
144 price→tier; it grows one dimension.
145
146### Local-currency display before checkout (added 2026-07-14)
147
148Show every picker price with an approximate local figure β€” *"€7/year
149(β‰ˆ $8.20)"* β€” **before** the buyer reaches Stripe:
150
151- **Display-only conversion.** Billing stays EUR (the tier-economics
152 EUR-settlement stance). The local figure is an estimate and is labelled:
153 *"billed in euros β€” β‰ˆ shown in your currency at today's rate."*
154- **Rates:** ECB daily reference rates (~30 currencies, free, no key). The
155 home service fetches once daily and serves a tiny cached JSON alongside the
156 price book, with the last-good snapshot as fallback β€” the client never
157 talks to a third party (CSP stays pinned).
158- **Currency choice comes from the browser locale** (`Intl.NumberFormat` /
159 `navigator.language`), not IP β€” privacy-consistent, works offline-ish, and
160 a small currency override sits next to the figure. Nothing is stored.
161- **Later, optional (Phase 3):** true multi-currency *charging* via Stripe
162 `currency_options` (one amount per currency, charm-rounded, curated
163 annually) β€” only if EUR-billing measurably hurts non-EU conversion. Note
164 `currency_options` cannot express bands *inside* a currency β€” one amount
165 per currency per Price β€” which is exactly why bands are separate Prices
166 and currency presentment is an independent layer on top. Stripe's
167 Adaptive Pricing was evaluated and rejected: it is FX localisation with a
168 2–4% customer-paid conversion fee, not PPP adjustment, and the hidden fee
169 is off-brand.
170
171## The +15% annual review cap (Paul's pricing promise)
172
173**The rule:** every live price β€” monthly or annual billing alike β€” is
174reviewed once a year (anchored to the subscription anniversary, so reviews
175stagger naturally); silent renewal can carry at most **+15%**; anything
176larger requires an explicit opt-in to a new subscription.
177
178The cap is **the sole propagation channel**: index updates, band
179reassignments, FX drift, and base-price changes reach existing subscribers
180only through reviews. New customers always pay the current table. Stripe
181grandfathers by default (a subscription renews on *its* Price until updated),
182so "current table" (the env price book) and "your price" (the subscription's
183Price id) separate cleanly; the ledger gains a pending-review notice
184(space id, from-price, to-price, effective date, consent-required flag).
185
186The review sweep, per subscription:
187
188- **Current ≀ subscribed** β†’ schedule the decrease at next renewal.
189 Unconditional, no ceremony, message it proudly. (The asymmetry β€” decreases
190 automatic, increases capped/consented β€” is deliberately public: German and
191 Italian courts voided Netflix's and Spotify's adjustment clauses partly for
192 being increase-only. Symmetry is the cure.)
193- **Gap ≀ 15%** β†’ schedule the increase at next renewal with in-product
194 notice and an easy cancel/step-down path.
195- **Gap > 15%** β†’ **hold + invite** (decided): no silent change, no
196 compounding 15-15-15 ladder; the subscriber stays on the old price until
197 they affirmatively opt in to the current table (or a lower band).
198- Discipline: the silent path only ever moves a subscription onto an
199 *existing catalog Price* (current or `"retired": true` vintages kept in the
200 price book) β€” never bespoke per-cohort amounts chasing "exactly +15%",
201 which would explode the book and break `tierByPrice()` legibility.
202
203**Honor-band subscribers track their own band's price** β€” Reduced is band C's
204real price, not a discount off band A β€” so there is no mechanism by which an
205honor price silently walks back up: a band reassignment is >15% by
206construction, hence always hold+invite. The invite surface may gently show
207the suggested band ("You're welcome on the reduced price as long as you need
208it β€” the suggested price for your country is €5"), keeping the social
209contract visible without enforcing it.
210
211**Notice without email** (accounts are anonymous passkeys; Checkout collects
212no email β€” verify nothing downstream assumes one): notices land in-product β€”
213(1) an inline row on the home page where Upgrade/Manage live ("Renews 12 Aug
214at €5.75 β€” review / change / cancel"), (2) the space's Admin-app update
215stream + admin push (the announce.go channel), (3) Eleven can explain any
216price from the public band table. "Explicit opt-in" = an authenticated
217affirmative act on the home page (same ownership gate as checkout), recorded
218in the ledger, then executed as a Stripe subscription update (never
219cancel+recreate β€” preserves the customer object, anniversary, and ledger
220row). Mechanics: plain subscription update with `proration_behavior=none`
221near renewal; `invoice.upcoming` webhook as the notice trigger.
222
223Regulatory shape-check: Ofcom's 2025 rule (in-contract rises must be
224disclosed in pounds-and-pence at point of sale) says state the cap **in
225euros on the picker** β€” "Family: never more than €0.75/yr more at renewal
226without your say-so" β€” not as a bare percentage. The terms need a
227transparent variation clause encoding cap + cadence + index reference +
228symmetric downside + cancel-anytime, drafted by a human lawyer before
229Phase 2 ships.
230
231## Stripe implementation
232
233**11 Prices** (nominally 3 tiers Γ— 4 bands, minus Business's solidarity β€”
234the elevens ladder, annual-only throughout), minted by the committed tool
235`tools/stripe-band-prices.mjs` β€” its `BAND_TABLE` is this doc's matrix as
236code, reviewed like any diff. The operator runs it from their own terminal
237(sandbox key first, then live; the key never transits a session):
238
239```sh
240STRIPE_SECRET_KEY=sk_… node tools/stripe-band-prices.mjs --book current.json # dry-run
241STRIPE_SECRET_KEY=sk_… node tools/stripe-band-prices.mjs --book current.json --apply
242```
243
244It reuses each tier's existing Stripe product where the current book has one
245(subscriber history stays put), creates every missing ladder Price β€” base and
246bands β€” idempotently (lookup keys `eleven11-<tier>-<band>`; re-runs create
247nothing; the old `eleven-*` prices stay untouched for lapsing subscribers),
248and prints the complete new price book β€” sealed into `/data/cloud.env` via
249the operator env flow. Per-unit tiers ride `unit_amount_decimal`, and the
250book carries the flags the cloud already understands (`per_seat`,
251`min_seats`, `business`):
252
253```json
254[{"key":"family","name":"Family",
255 "label":"€5.50/year","price":"price_baseFam",
256 "eur":5.5,"net_eur_year":5.168,
257 "bands":{
258 "supporter":{"label":"€7.70/year","price":"price_supFam","eur":7.7,"net_eur_year":7.335},
259 "reduced":{"label":"€2.75/year","price":"price_redFam","eur":2.75,"net_eur_year":2.459},
260 "solidarity":{"label":"€1.40/year","price":"price_solFam","eur":1.4,"net_eur_year":1.129}}},
261 {"key":"community","name":"Community & Friends",
262 "label":"€0.11/member/year","price":"price_baseCom",
263 "eur":0.11,"net_eur_year":0.108,"per_seat":true,"min_seats":100,
264 "bands":{"…":"…"}}]
265```
266
267(`net_eur_year` is the free pool's revenue input β€” see `docs/free-pool.md`.)
268
269Touch points:
270
271- `checkoutHandler`: optional `band` param (default `base`), resolve
272 `tier.bands[band].price`, stamp `metadata[band]`. Ownership gates,
273 no-customer-identity, webhook routing: unchanged.
274- Webhook: `tierByPrice` β†’ `tierBandByPrice` (still a flat scan over a few
275 dozen rows); the ledger records the band.
276- Billing Portal: four per-band portal configurations (portals are minted
277 per-session, so the cloud picks the customer's band's config); band
278 *changes* go through the home surface, which can message them β€” never the
279 portal.
280- Telemetry that respects the ethos: band mix by month from the ledger β€” no
281 per-user tracking β€” is how the US-at-Γ—1.4 call gets checked.
282
283### VAT β€” Stripe Tax (decided: enable; consumer-inclusive, Business-exclusive)
284
285Today nothing sets `automatic_tax`; the EUR figure is charged as-is and VAT
286is implicit. With bands multiplying the price book, this gets fixed properly.
287**The split (decided 2026-07-14): Family and C&F prices are VAT-inclusive;
288Business prices are VAT-exclusive** β€” the standard B2C/B2B convention.
289
290- **Code (this repo):** set `automatic_tax[enabled]=true` on Checkout
291 Sessions. **Family/C&F Prices** are created with `tax_behavior=inclusive`
292 β€” labels stay the total price (€5 means €5, VAT inside, satisfying EU
293 price-indication rules) and net revenue varies by buyer country (VAT
294 17–27% comes out of the sticker). **Business Prices** are created with
295 `tax_behavior=exclusive` β€” the sticker is ex-VAT, tax is added at
296 checkout, and Business checkout enables `tax_id_collection` so an EU
297 business with a valid VAT ID gets the **reverse charge** (no VAT
298 collected, they self-account). Picker labels for Business carry
299 "excl. VAT" wherever the price appears. Portal configs follow the same
300 behaviors.
301- **Bookkeeping:** consumer-tier `net_eur_year` figures become net-of-VAT
302 approximations across buyer countries (the free pool tolerates that
303 comfortably); Business `net_eur_year` is *more* stable than today β€”
304 €11/seat is €11/seat regardless of buyer country (per-seat since
305 2026-07-22, so the pool input scales it by the billed seats).
306- **Caveat:** ex-VAT display is standard for business-directed offers
307 (Slack/Notion-style), but EU price-indication rules technically demand
308 consumer-facing totals be inclusive β€” so the Business tier should present
309 as business-directed (it already does), show "excl. VAT" adjacent to
310 every price, and Checkout shows the final total before payment. Include
311 in the Phase 1 legal review.
312- **Dashboard (operator β€” Paul):** two places.
313 *Settings β†’ Tax* (dashboard.stripe.com/settings/tax): origin address +
314 defaults β€” note the "Include tax in prices" default (Automatic/Yes/No)
315 is **superseded by our explicit per-Price `tax_behavior`**, so its value
316 doesn't matter once the 20 Prices exist; its Automatic mode
317 (currency-based: USD/CAD exclusive, EUR inclusive) is *not* our
318 inclusive/exclusive split, which is per-tier, not per-currency.
319 *Tax β†’ Registrations* (dashboard.stripe.com/tax/registrations): **the
320 load-bearing step** β€” Stripe Tax only calculates where a registration is
321 added and merely monitors thresholds elsewhere. Needed eventually: the
322 home-country VAT registration + the **EU OSS (Union scheme)**, plus
323 others as the monitoring tab flags obligations. These are account-level
324 actions the code cannot and should not do β€” and until the code opts in
325 (`automatic_tax[enabled]=true` per session), a configured dashboard
326 still changes nothing at checkout.
327 **Registrations are deferred (Paul, 2026-07-14).** Phase 0 ships without
328 them and behaves gracefully: with zero registrations Stripe Tax
329 calculates €0 everywhere, so inclusive prices are unchanged for the
330 buyer and Business checkouts add nothing yet β€” the plumbing
331 (per-Price `tax_behavior`, `automatic_tax`, `tax_id_collection`) is all
332 in place, and adding a registration later activates it with no code
333 change. Stripe Tax's threshold-monitoring tab is the revisit trigger
334 (note: monitoring tracks configuration, not obligation β€” whether an
335 obligation already exists today is an accountant question, not a
336 dashboard one).
337- Existing subscriptions keep their current Prices; the new tax behaviors
338 apply as new Prices roll through checkout and reviews.
339
340## Messaging (drafts, not final copy β€” lock-and-key voice)
341
342The canonical cross-subsidy line (decided, aggregate form only):
343
344> **"Paying supports free spaces for those who can't."**
345
346- **Supporter:** "This is the supporter price for Switzerland. Paying
347 supports free spaces for those who can't β€” the difference doesn't go to
348 us, it goes to the free pool. If it's not right for you, pick the standard
349 price β€” no questions asked."
350- **Base:** "The standard price. It covers your space's costs with margin
351 for resilience β€” the books are public."
352- **Reduced / Solidarity:** "You're on the reduced price, carried by
353 supporters. It's yours as long as you need it." Never "discount", never
354 "eligible", never a country name on this side.
355- **The cap, everywhere prices appear:** "Reviewed once a year. Never up
356 more than 15% β€” for Family, at most €0.75 β€” never onto a new plan without
357 your explicit yes. Down happens by itself."
358- Never a per-space "your €7 funds N spaces" claim (decided): the aggregate
359 pool gauge ("paid spaces fund the free pool MΓ— over") is the verifiable
360 form. Precedent says transparent cross-subsidy works with an anchor, a
361 chosen default, and visible reciprocity (Guardian, solidarity tickets) and
362 backfires when hidden or anchorless (Amazon 2000, Panera) β€” this design
363 keeps the standard-price tap in the same glance as the supporter ask.
364
365## Phasing
366
367- **Phase 0 β€” bands, pure honor, no geolocation.** The 19 Prices; price-book
368 `bands` + `net_eur_year`; `tierBandByPrice`; checkout `band` param; picker
369 with all four bands, Base pre-selected for everyone; deep-link band param;
370 local-currency β‰ˆ display (ECB rates via home); Stripe Tax
371 (code + dashboard runbook above). Legally inert everywhere β€” it's four
372 prices β€” and already delivers "anyone, anywhere, one click to the lower
373 price" plus the supporter story. Measure band mix.
374 *Built (code side): the cloud rails (book/checkout/ledger/tax knob), the
375 home picker + deep links + β‰ˆ display, and the minting tool. What remains is
376 operational: run the tool (sandbox, then live), seal the new book into the
377 vault, and flip `LCHAT_STRIPE_AUTOMATIC_TAX=1` with it β€” everything renders
378 as today until the book carries bands.*
379- **Phase 1 β€” geo-suggested defaults.** Commit `tools/pricing-bands.mjs` +
380 the country→band table; pre-select by CDN country header (client-side,
381 never stored); EU/EEA pinned to Base; publish the table on the pricing
382 page. **Human legal review gates this phase.**
383- **Phase 2 β€” the annual review machinery.** Ledger review/consent fields;
384 the sweep; home inline notices + admin announce; decreases automatic,
385 ≀15% with notice, >15% hold+invite; terms variation clause (lawyer).
386- **Phase 3 β€” optional refinements:** true multi-currency charging via
387 `currency_options` (only if EUR-billing measurably hurts conversion);
388 annual index refresh cadence (Phase 2 already absorbs band moves);
389 free-pool gauge coupling on the pricing page.
390- **Never:** sub-national pricing as detection; promotion-code discounts;
391 Adaptive Pricing's customer-paid FX fee.
392