Skip to main content

max / makenotwork

server: remove the cost-allocation breakdown from /pricing Drop the "Where your tier fee goes" widget from the pricing page and strip its now-dead backing: the CostSegment/CostAllocation types + builders + the float_at helper and their two tests in tier_prices.rs, the AppState field and its main.rs construction, the [cost_allocation.*] blocks in assumptions.toml, and the .cost-* CSS. Also fix the economics page, which linked to /pricing#cost-allocation and described the breakdown — a dead anchor + false claim once the widget is gone; remove that sentence and the stale maintenance comment. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-08 16:38 UTC
Commit: bdd88fbb376c9c86fda18da9ef484b94d49ac571
Parent: ace9caa
9 files changed, +2 insertions, -348 deletions
@@ -239,62 +239,6 @@ chargeback_rate_tier_subs = 0.001 # A12: 0.1% for recurring (lower than one-
239 239 # - cap_count > 0, cap_months > 0
240 240
241 241
242 - # ─── Per-tier monthly fee allocation (Mode 1 widget on /pricing) ─────────
243 - #
244 - # Each row sums to the standard tier price. The widget renders standard-rate
245 - # numbers only; founder-rate creators pay 50% of standard with the subsidy
246 - # absorbed by reserves + earnback, so the allocation shape is the same but
247 - # the visible total scales down — a one-line note on the pricing page covers
248 - # that case.
249 - #
250 - # Model (per `site-docs/public/about/economics.md`):
251 - # - Stripe: 2.9% × price + $0.30 (Stripe's actual fee formula)
252 - # - Storage: at-rest + egress + virus-scan + backups + provider-price
253 - # cushion; ~5–15× the bare Hetzner at-rest math from
254 - # [creator_marginal] to absorb spikes
255 - # - Support: flat $5.00/creator — per-creator floor, doesn't scale
256 - # with tier (a Basic text creator and a $60 video creator
257 - # each get the same human availability)
258 - # - Engineering: flat $6.00/creator — engineering hours don't scale with
259 - # what tier someone bought; the same platform serves all
260 - # - Reserves: 12.5% of revenue (risk float for refunds/chargebacks/runway)
261 - # - Earn-back: residual; the surplus from higher tiers funds platform
262 - # development that benefits everyone
263 - #
264 - # Numbers below are pinned by `tier_prices::tests::cost_allocation_*`.
265 - [cost_allocation.basic]
266 - stripe = 0.76
267 - storage = 0.40
268 - support = 5.00
269 - engineering = 6.00
270 - reserves = 2.00
271 - earnback = 1.84
272 -
273 - [cost_allocation.small_files]
274 - stripe = 1.00
275 - storage = 1.00
276 - support = 5.00
277 - engineering = 6.00
278 - reserves = 3.00
279 - earnback = 8.00
280 -
281 - [cost_allocation.big_files]
282 - stripe = 1.34
283 - storage = 3.00
284 - support = 5.00
285 - engineering = 6.00
286 - reserves = 4.50
287 - earnback = 16.16
288 -
289 - [cost_allocation.everything]
290 - stripe = 2.04
291 - storage = 6.00
292 - support = 5.00
293 - engineering = 6.00
294 - reserves = 7.50
295 - earnback = 33.46
296 -
297 -
298 242 # ─── Runway disclosure (transparency block on /docs/about/economics) ─────
299 243 #
300 244 # Operator-set. Refreshed quarterly at sprint close, alongside the
@@ -91,7 +91,6 @@ pub struct AppState {
91 91 pub email: EmailClient,
92 92 pub docs: Arc<DocLoader>,
93 93 pub tier_prices: tier_prices::TierPrices,
94 - pub cost_allocation: tier_prices::CostAllocation,
95 94 pub runway_config: tier_prices::RunwayConfig,
96 95 /// Public `/pricing` comparison model (competitor fee tables + Stripe
97 96 /// fees), parsed from `assumptions.toml` at startup.
@@ -398,10 +398,6 @@ async fn main() {
398 398 email,
399 399 docs,
400 400 tier_prices: makenotwork::tier_prices::TierPrices::from_assumptions(&assumptions),
401 - cost_allocation: {
402 - let tp = makenotwork::tier_prices::TierPrices::from_assumptions(&assumptions);
403 - makenotwork::tier_prices::CostAllocation::from_assumptions(&assumptions, &tp)
404 - },
405 401 runway_config: makenotwork::tier_prices::RunwayConfig::from_assumptions(&assumptions),
406 402 pricing_comparison: makenotwork::pricing_comparison::PricingComparison::load(&assumptions_path),
407 403 scanner,
@@ -437,7 +437,6 @@ pub(super) async fn pricing_page(
437 437 PricingTemplate {
438 438 csrf_token: get_csrf_token(&session).await,
439 439 tier_prices: state.tier_prices.clone(),
440 - cost_allocation: state.cost_allocation.clone(),
441 440 comparison,
442 441 }
443 442 }
@@ -828,7 +828,6 @@ pub struct DocIndexTemplate {
828 828 pub struct PricingTemplate {
829 829 pub csrf_token: CsrfTokenOption,
830 830 pub tier_prices: crate::tier_prices::TierPrices,
831 - pub cost_allocation: crate::tier_prices::CostAllocation,
832 831 /// Server-computed initial comparison (default revenue + Basic tier). The
833 832 /// page re-fetches this partial via HTMX as the inputs change.
834 833 pub comparison: crate::pricing_comparison::ComparisonResult,
@@ -138,124 +138,6 @@ impl TierPrices {
138 138 }
139 139 }
140 140
141 - /// One segment of the per-tier monthly fee bar. Sized in cents so the
142 - /// template can drop it straight into `flex: <cents>` for a stacked
143 - /// horizontal bar that scales proportionally without per-segment math.
144 - #[derive(Clone, Debug)]
145 - pub struct CostSegment {
146 - /// CSS modifier and toml key — `"stripe"`, `"storage"`, `"support"`,
147 - /// `"engineering"`, `"reserves"`, `"earnback"`. Drives `.cost-bar-seg-*`.
148 - pub kind: &'static str,
149 - /// Human label e.g. `"Stripe processing"`.
150 - pub label: &'static str,
151 - /// Pre-formatted dollar string e.g. `"$0.76"` for the segment + tooltip.
152 - pub amount: String,
153 - /// Amount in cents — the flex weight for the stacked bar.
154 - pub cents: i32,
155 - /// Pre-formatted hover text (rendered into `title="…"`). Kept on the
156 - /// Rust side so copy reviews land here and not in the template.
157 - pub tooltip: String,
158 - }
159 -
160 - /// One row of the cost-allocation widget — a single tier and its six
161 - /// fee segments. The template iterates `rows` and per row iterates
162 - /// `segments`, so the markup is fully uniform.
163 - #[derive(Clone, Debug)]
164 - pub struct CostAllocationRow {
165 - pub tier_key: &'static str,
166 - pub tier_label: &'static str,
167 - /// Monthly price in dollars (whole-dollar tiers today).
168 - pub tier_price: i32,
169 - pub segments: Vec<CostSegment>,
170 - /// Pre-built `aria-label` for the bar — names every segment + value
171 - /// so screen readers get the breakdown.
172 - pub aria_label: String,
173 - }
174 -
175 - /// All four tier rows, in canonical Basic → Everything order.
176 - #[derive(Clone, Debug, Default)]
177 - pub struct CostAllocation {
178 - pub rows: Vec<CostAllocationRow>,
179 - }
180 -
181 - impl CostAllocation {
182 - pub fn from_assumptions(a: &Assumptions, tp: &TierPrices) -> Self {
183 - let rows = vec![
184 - row(a, "basic", "Basic", tp.basic_std),
185 - row(a, "small_files", "Small Files", tp.small_files_std),
186 - row(a, "big_files", "Big Files", tp.big_files_std),
187 - row(a, "everything", "Everything", tp.everything_std),
188 - ];
189 - Self { rows }
190 - }
191 - }
192 -
193 - fn row(a: &Assumptions, key: &'static str, label: &'static str, price: i32) -> CostAllocationRow {
194 - let segments = vec![
195 - seg(a, key, "stripe", "Stripe processing",
196 - "Stripe processing: {amount}. Stripe's fee on your monthly subscription."),
197 - seg(a, key, "storage", "Storage",
198 - "Storage: {amount}. At-rest storage, bandwidth, virus scanning, and \
199 - backups at your tier's typical fill, sized to absorb spikes and \
200 - provider price changes without a price increase to you."),
201 - seg(a, key, "support", "Human support time",
202 - "Human support time: {amount}. Identity recovery, billing disputes, \
203 - moderation, abuse, and legal — the work that can't be automated. \
204 - Per-creator floor; the same at every tier."),
205 - seg(a, key, "engineering", "Product engineering",
206 - "Product engineering: {amount}. Bug fixes and ongoing product work. \
207 - Per-creator floor; the same at every tier."),
208 - seg(a, key, "reserves", "Reserves",
209 - "Reserves: {amount}. Held against a bad month — refunds, chargebacks, \
210 - unexpected costs — so a single incident doesn't force a price change \
211 - or shutdown."),
212 - seg(a, key, "earnback", "Earn-back (returned to creators)",
213 - "Earn-back: {amount}. The margin above cost, reinvested to build the \
214 - platform out and lower prices as we grow, with the rest returned to \
215 - creators as earn-back credit (launching by 2027-01-01). It's how a \
216 - fair platform gets big and stays that way."),
217 - ];
218 - let aria_label = build_aria_label(label, price, &segments);
219 - CostAllocationRow {
220 - tier_key: key,
221 - tier_label: label,
222 - tier_price: price,
223 - segments,
224 - aria_label,
225 - }
226 - }
227 -
228 - fn seg(
229 - a: &Assumptions,
230 - tier_key: &str,
231 - seg_kind: &'static str,
232 - seg_label: &'static str,
233 - tooltip_template: &str,
234 - ) -> CostSegment {
235 - let dollars = float_at(a, &format!("cost_allocation.{tier_key}.{seg_kind}"));
236 - let cents = (dollars * 100.0).round() as i32;
237 - let amount = format!("${dollars:.2}");
238 - let tooltip = tooltip_template.replace("{amount}", &amount);
239 - CostSegment {
240 - kind: seg_kind,
241 - label: seg_label,
242 - amount,
243 - cents,
244 - tooltip,
245 - }
246 - }
247 -
248 - fn build_aria_label(tier_label: &str, price: i32, segments: &[CostSegment]) -> String {
249 - let mut s = format!("{tier_label} tier ${price}/mo allocation: ");
250 - for (i, seg) in segments.iter().enumerate() {
251 - if i > 0 { s.push_str(", "); }
252 - s.push_str(seg.label);
253 - s.push(' ');
254 - s.push_str(&seg.amount);
255 - }
256 - s
257 - }
258 -
259 141 /// Operator-edited runway figures, loaded once at startup. The
260 142 /// live paying-creator counts come from the DB at request time and
261 143 /// are NOT in this struct — see `db::creator_tiers::count_active_paying`
@@ -288,14 +170,6 @@ impl RunwayConfig {
288 170 }
289 171 }
290 172
291 - fn float_at(a: &Assumptions, key: &str) -> f64 {
292 - match a.get(key) {
293 - Some(LookupValue::Float(x)) => *x,
294 - Some(LookupValue::Int(n)) => *n as f64,
295 - other => panic!("expected number at {key}, got {other:?}"),
296 - }
297 - }
298 -
299 173 fn int_at(a: &Assumptions, key: &str) -> i32 {
300 174 match a.get(key) {
301 175 Some(LookupValue::Int(n)) => i32::try_from(*n)
@@ -318,52 +192,6 @@ mod tests {
318 192
319 193 const ASSUMPTIONS_PATH: &str = "docs/business/assumptions.toml";
320 194
321 - /// Guards every key TierPrices reads. If a future toml edit removes one of
322 - /// these or flips its type, the panic in `from_assumptions` will fire at
323 - /// startup; this test catches it at PR time instead.
324 - #[test]
325 - fn cost_allocation_from_canonical_assumptions_sums_to_tier_price() {
326 - // Each row's six segments must sum to the standard tier price.
327 - // A future toml edit that breaks that invariant — say a typo in
328 - // `cost_allocation.big_files.storage` — would render a stacked
329 - // bar whose visible total disagrees with the headline price.
330 - // Catch the divergence at PR time.
331 - let a = Assumptions::load(ASSUMPTIONS_PATH).expect("load canonical toml");
332 - let tp = TierPrices::from_assumptions(&a);
333 - let alloc = CostAllocation::from_assumptions(&a, &tp);
334 - assert_eq!(alloc.rows.len(), 4);
335 -
336 - let prices = [tp.basic_std, tp.small_files_std, tp.big_files_std, tp.everything_std];
337 - for (row, &price) in alloc.rows.iter().zip(prices.iter()) {
338 - assert_eq!(row.tier_price, price);
339 - let sum_cents: i32 = row.segments.iter().map(|s| s.cents).sum();
340 - assert_eq!(
341 - sum_cents,
342 - price * 100,
343 - "{tier} segments sum to {sum_cents}¢ but tier price is ${price} = {expected}¢",
344 - tier = row.tier_label,
345 - expected = price * 100,
346 - );
347 - // Six canonical segments in canonical order.
348 - let kinds: Vec<&str> = row.segments.iter().map(|s| s.kind).collect();
349 - assert_eq!(
350 - kinds,
351 - vec!["stripe", "storage", "support", "engineering", "reserves", "earnback"],
352 - );
353 - }
354 -
355 - // Spot-check the per-segment values match what we agreed in the
356 - // economics doc. A drift here means the toml was edited without
357 - // a corresponding rationale update.
358 - let basic = &alloc.rows[0];
359 - assert_eq!(basic.segments[0].amount, "$0.76"); // stripe
360 - assert_eq!(basic.segments[5].amount, "$1.84"); // earnback
361 - let everything = &alloc.rows[3];
362 - assert_eq!(everything.segments[2].amount, "$5.00"); // support flat
363 - assert_eq!(everything.segments[3].amount, "$6.00"); // engineering flat
364 - assert_eq!(everything.segments[4].amount, "$7.50"); // reserves 12.5% of $60
365 - }
366 -
367 195 #[test]
368 196 fn runway_config_loads_from_canonical_assumptions() {
369 197 // The presence of the [runway] block is the only enforced thing —
@@ -393,21 +221,6 @@ mod tests {
393 221 }
394 222
395 223 #[test]
396 - fn cost_allocation_aria_label_names_every_segment() {
397 - // Screen readers get the full breakdown via aria-label since the
398 - // colored bar carries no native semantics. Pin the format.
399 - let a = Assumptions::load(ASSUMPTIONS_PATH).expect("load canonical toml");
400 - let tp = TierPrices::from_assumptions(&a);
401 - let alloc = CostAllocation::from_assumptions(&a, &tp);
402 - let aria = &alloc.rows[0].aria_label;
403 - assert!(aria.starts_with("Basic tier $16/mo allocation: "));
404 - for label in ["Stripe processing", "Storage", "Human support time",
405 - "Product engineering", "Reserves", "Earn-back (returned to creators)"] {
406 - assert!(aria.contains(label), "aria-label missing {label}: {aria}");
407 - }
408 - }
409 -
410 - #[test]
411 224 fn from_canonical_assumptions_populates_every_field() {
412 225 let a = Assumptions::load(ASSUMPTIONS_PATH).expect("load canonical toml");
413 226 let p = TierPrices::from_assumptions(&a);
@@ -11158,70 +11158,6 @@ button.saved { border-color: var(--action); color: var(--action); }
11158 11158 .synckit-gauge-fill--warn { background: var(--warning); }
11159 11159 .synckit-gauge-fill--danger { background: var(--danger); }
11160 11160
11161 - /* Cost-allocation widget on /pricing — "Where your tier fee goes". */
11162 - .cost-allocation { margin-top: 0.5rem; }
11163 - .cost-allocation-intro {
11164 - font-size: 0.95rem;
11165 - opacity: 0.85;
11166 - margin-bottom: 1.5rem;
11167 - max-width: 60ch;
11168 - }
11169 - .cost-row {
11170 - display: grid;
11171 - grid-template-columns: 12rem 1fr;
11172 - gap: 1rem;
11173 - align-items: center;
11174 - margin-bottom: 1rem;
11175 - }
11176 - .cost-row-label {
11177 - font-family: var(--font-mono);
11178 - font-size: 0.9rem;
11179 - }
11180 - .cost-row-label strong { font-family: var(--font-heading); font-size: 1.05rem; display: block; }
11181 - .cost-bar {
11182 - display: flex;
11183 - height: 2.25rem;
11184 - border-radius: 4px;
11185 - overflow: hidden;
11186 - border: 1px solid var(--border, rgba(0,0,0,0.12));
11187 - }
11188 - .cost-bar-seg {
11189 - display: flex;
11190 - align-items: center;
11191 - justify-content: center;
11192 - font-family: var(--font-mono);
11193 - font-size: 0.75rem;
11194 - color: #fff;
11195 - cursor: help;
11196 - padding: 0 0.25rem;
11197 - overflow: hidden;
11198 - white-space: nowrap;
11199 - }
11200 - .cost-bar-seg-stripe { background: #5b6f8a; }
11201 - .cost-bar-seg-storage { background: #7d8c5e; }
11202 - .cost-bar-seg-support { background: #8b6b4a; }
11203 - .cost-bar-seg-engineering { background: #6b4a8b; }
11204 - .cost-bar-seg-reserves { background: #4a6b8b; }
11205 - .cost-bar-seg-earnback { background: #8b8b4a; }
11206 - .cost-legend {
11207 - display: flex;
11208 - flex-wrap: wrap;
11209 - gap: 0.5rem 1.25rem;
11210 - margin-top: 1.5rem;
11211 - font-size: 0.85rem;
11212 - }
11213 - .cost-legend-item { display: flex; align-items: center; gap: 0.4rem; }
11214 - .cost-legend-swatch {
11215 - display: inline-block;
11216 - width: 0.85rem;
11217 - height: 0.85rem;
11218 - border-radius: 2px;
11219 - }
11220 - @media (max-width: 640px) {
11221 - .cost-row { grid-template-columns: 1fr; gap: 0.4rem; }
11222 - .cost-bar-seg { font-size: 0; }
11223 - }
11224 -
11225 11161 /* ============================================================================
11226 11162 Carousel -- composable click-through widget (partials/carousel.html).
11227 11163 One frame visible at a time; viewer-driven (prev/next, dots, arrow keys).
@@ -7,8 +7,8 @@
7 7 inbound links.
8 8
9 9 Prose mirrors the previous economics.md content (deleted in the same
10 - commit). To edit prose, edit this file. To edit the cost-allocation
11 - model or the runway figure, edit docs/business/assumptions.toml. #}
10 + commit). To edit prose, edit this file. To edit the runway figure, edit
11 + docs/business/assumptions.toml. #}
12 12 {% extends "base.html" %}
13 13
14 14 {% block title %}Platform Economics - Makenot.work{% endblock %}
@@ -50,8 +50,6 @@
50 50
51 51 <p><strong>Returned to creators.</strong> Surplus beyond what cost-to-deliver and platform overhead require is earmarked to come back to you as earn-back credit. We've committed to launching that program no later than 2027-01-01.</p>
52 52
53 - <p>See the full breakdown on the <a href="/pricing#cost-allocation">pricing page</a> — every tier fee broken into Stripe processing, storage, human support time, product engineering, reserves, and earn-back surplus.</p>
54 -
55 53 <p>No surplus goes to investors, shareholders, dividends, executive bonuses, paid acquisition, or marketing spend. We have none of those things.</p>
56 54
57 55 <hr>
@@ -66,36 +66,6 @@
66 66 </div>
67 67 </div>
68 68
69 - <div class="tier-section cost-allocation" id="cost-allocation">
70 - <h2 class="section-label">Where your tier fee goes</h2>
71 - <p class="cost-allocation-intro">You're paying for a platform built to be fair and built to last. Here's where each monthly fee goes, averaged across creators on that tier. Hover any segment for detail.</p>
72 - <p class="cost-allocation-founder-note">Founder-rate creators pay exactly 50% of the standard tier. Per-creator fixed costs (Support, Engineering, Storage, Stripe) stay the same; the discount comes out of Earn-back and Reserves. The shape is unchanged, the totals halve.</p>
73 -
74 - {# Every dollar here round-trips through `cost_allocation.*` in
75 - `docs/business/assumptions.toml`. Tests in `tier_prices::tests`
76 - pin the per-segment values; tooltip copy lives in the Rust
77 - builder. #}
78 - {% for row in cost_allocation.rows %}
79 - <div class="cost-row">
80 - <div class="cost-row-label"><strong>{{ row.tier_label }}</strong>${{ row.tier_price }}/mo</div>
81 - <div class="cost-bar" role="img" aria-label="{{ row.aria_label }}">
82 - {% for seg in row.segments %}
83 - <div class="cost-bar-seg cost-bar-seg-{{ seg.kind }}" style="flex: {{ seg.cents }}" title="{{ seg.tooltip }}">{{ seg.amount }}</div>
84 - {% endfor %}
85 - </div>
86 - </div>
87 - {% endfor %}
88 -
89 - <div class="cost-legend" aria-hidden="true">
90 - <span class="cost-legend-item"><span class="cost-legend-swatch cost-bar-seg-stripe"></span>Stripe processing</span>
91 - <span class="cost-legend-item"><span class="cost-legend-swatch cost-bar-seg-storage"></span>Storage (typical fill)</span>
92 - <span class="cost-legend-item"><span class="cost-legend-swatch cost-bar-seg-support"></span>Human support time</span>
93 - <span class="cost-legend-item"><span class="cost-legend-swatch cost-bar-seg-engineering"></span>Product engineering</span>
94 - <span class="cost-legend-item"><span class="cost-legend-swatch cost-bar-seg-reserves"></span>Reserves</span>
95 - <span class="cost-legend-item"><span class="cost-legend-swatch cost-bar-seg-earnback"></span>Earn-back (returned to creators)</span>
96 - </div>
97 - </div>
98 -
99 69 <div class="landing-cta">
100 70 <a class="btn-primary btn--large" href="/join">Join the Alpha</a>
101 71 </div>