Skip to main content

max / makenotwork

Replace the platform fee table with a calculator the visitor drives The /pricing page held nine other platforms' fee parameters in assumptions.toml and rendered them as a sorted table. Three problems, all structural rather than editorial: it named competitors, every rate in it was an unmaintained claim about somebody else's pricing page, and a comparison whose contents we choose is a comparison we can rig. The other platform's fees are now two dials the visitor sets, so there is nothing to keep fresh and nothing to name. Inputs are item price, monthly sales, tier, and the other platform's percentage cut and per-sale fee, the latter two being its total deduction as read off a payout. Dropping "monthly revenue" for price times volume also removes a fudge: the old code estimated transaction count as revenue / an assumed average sale, so every per-transaction figure in the table was approximate. Sale count is an input now and the arithmetic is exact. The difference between the two sides is linear in sales, so the crossover is solved rather than searched for, and the branch where no volume ever closes the gap is a first-class result the page states plainly. Tests pin the flip one sale either side and the no-crossover case. Defaults open on a mid-market scenario, a $25 item at 40 sales a month against a common percentage-plus-flat-fee model, so the first render is realistic without touching a dial. Also removes the named comparisons from compare.md (rewritten around our own model and the crossover arithmetic), migration.md (reorganised by export class rather than by platform), pricing.md, merchant-of-record.md and tiers.md, plus two the audit had not listed: a competitor domain in the example profile layout and a competitor recommended as a tax service. Carries the lib.rs edit that 6fe9ee0f was waiting on, so the two restored files go back out with the module that replaces them.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-05 19:23 UTC
Signed with PGP, not checked
Commit: 8b6270a0297fcb32bdc6abdfa232033d6198f15f
Parent: 6fe9ee0
21 files changed, +1160 insertions, -1092 deletions
@@ -31,6 +31,7 @@
31 31 pub mod email;
32 32 pub mod error;
33 33 pub mod extractors;
34 + pub mod fee_calculator;
34 35 pub mod formatting;
35 36 pub mod git;
36 37 pub mod git_ssh;
@@ -45,7 +46,6 @@
45 46 pub mod openapi;
46 47 pub mod payments;
47 48 pub mod pricing;
48 - pub mod pricing_comparison;
49 49 pub mod rate_limit;
50 50 pub mod routes;
51 51 pub mod rss;
@@ -116,9 +116,9 @@
116 116 pub docs: Arc<DocLoader>,
117 117 pub tier_prices: tier_prices::TierPrices,
118 118 pub runway_config: tier_prices::RunwayConfig,
119 - /// Public `/pricing` comparison model (competitor fee tables + Stripe
120 - /// fees), parsed from `assumptions.toml` at startup.
121 - pub pricing_comparison: pricing_comparison::PricingComparison,
119 + /// Public `/pricing` fee calculator (Stripe's fees + the opening
120 + /// position of each dial), parsed from `assumptions.toml` at startup.
121 + pub fee_calculator: fee_calculator::FeeCalculator,
122 122 pub scanner: Option<Arc<ScanPipeline>>,
123 123 pub webauthn: Arc<Webauthn>,
124 124 pub syntax: Option<Arc<git::SyntaxHighlighter>>,
@@ -226,7 +226,7 @@
226 226 pub stripe: Option<Arc<dyn PaymentProvider>>,
227 227 pub tier_prices: tier_prices::TierPrices,
228 228 pub runway_config: tier_prices::RunwayConfig,
229 - pub pricing_comparison: pricing_comparison::PricingComparison,
229 + pub fee_calculator: fee_calculator::FeeCalculator,
230 230 }
231 231
232 232 impl FromRef<AppState> for Billing {
@@ -235,7 +235,7 @@
235 235 stripe: s.stripe.clone(),
236 236 tier_prices: s.tier_prices.clone(),
237 237 runway_config: s.runway_config.clone(),
238 - pricing_comparison: s.pricing_comparison.clone(),
238 + fee_calculator: s.fee_calculator.clone(),
239 239 }
240 240 }
241 241 }
@@ -386,7 +386,7 @@
386 386 pub docs: Arc<DocLoader>,
387 387 pub tier_prices: tier_prices::TierPrices,
388 388 pub runway_config: tier_prices::RunwayConfig,
389 - pub pricing_comparison: pricing_comparison::PricingComparison,
389 + pub fee_calculator: fee_calculator::FeeCalculator,
390 390 pub scanner: Option<Arc<ScanPipeline>>,
391 391 pub webauthn: Arc<Webauthn>,
392 392 pub syntax: Option<Arc<git::SyntaxHighlighter>>,
@@ -444,7 +444,7 @@
444 444 docs: parts.docs,
445 445 tier_prices: parts.tier_prices,
446 446 runway_config: parts.runway_config,
447 - pricing_comparison: parts.pricing_comparison,
447 + fee_calculator: parts.fee_calculator,
448 448 scanner: parts.scanner,
449 449 webauthn: parts.webauthn,
450 450 syntax: parts.syntax,
@@ -499,7 +499,7 @@
499 499 let tier_prices = makenotwork::tier_prices::TierPrices::from_assumptions(&assumptions);
500 500 tier_prices.clone().install_global();
501 501 let runway_config = makenotwork::tier_prices::RunwayConfig::from_assumptions(&assumptions);
502 - let pricing_comparison = makenotwork::pricing_comparison::PricingComparison::load(
502 + let fee_calculator = makenotwork::fee_calculator::FeeCalculator::load(
503 503 makenotwork::site_docs::assumptions_path(),
504 504 );
505 505
@@ -516,7 +516,7 @@
516 516 docs,
517 517 tier_prices,
518 518 runway_config,
519 - pricing_comparison,
519 + fee_calculator,
520 520 scanner,
521 521 webauthn,
522 522 syntax,
@@ -891,6 +891,13 @@
891 891 border-top: 1px solid var(--border);
892 892 }
893 893
894 + /* Marks the row a table is about, used across the dashboard and library
895 + tables. Lived in the pricing block until the fee calculator replaced it. */
896 + .data-table .highlight {
897 + background: var(--surface-overlay);
898 + border-left: 3px solid var(--action);
899 + }
900 +
894 901 /* ===========================================
895 902 COMPACT TABLES (dashboard/admin, lighter than .data-table)
896 903 =========================================== */
@@ -6325,51 +6332,163 @@
6325 6332 padding: calc(var(--gap-section) - 1px);
6326 6333 }
6327 6334
6328 - .mnw-summary {
6329 - text-align: center;
6330 - padding: var(--gap-pane);
6331 - background: var(--surface-overlay);
6332 - border: 1px solid var(--border);
6335 + /* Two dials side by side (price + volume, then the other platform's two).
6336 + Stacks on narrow screens via the mobile block below. */
6337 + .calc-dials {
6338 + display: grid;
6339 + grid-template-columns: 1fr 1fr;
6340 + gap: var(--gap-section);
6341 + align-items: end;
6333 6342 }
6334 6343
6335 - .mnw-keep {
6336 - font-family: var(--font-heading);
6337 - font-size: var(--text-display);
6338 - margin-bottom: var(--gap-peer);
6344 + .calc-dial {
6345 + display: flex;
6346 + flex-direction: column;
6347 + align-items: center;
6348 + gap: var(--gap-bound);
6339 6349 }
6340 6350
6341 - .mnw-detail {
6351 + .calc-dial-label {
6352 + font-family: var(--font-mono);
6342 6353 font-size: var(--text-note);
6343 6354 opacity: 0.7;
6344 6355 }
6345 6356
6346 - .data-table .highlight {
6357 + /* Who comes out ahead at the current dials. The three states are the only
6358 + place the calculator uses color to make a claim, so they carry the same
6359 + weight in both directions: losing is stated as plainly as winning. */
6360 + .calc-verdict {
6361 + font-family: var(--font-heading);
6362 + font-size: var(--text-title);
6363 + text-align: center;
6364 + padding: var(--gap-pane);
6347 6365 background: var(--surface-overlay);
6348 - border-left: 3px solid var(--action);
6366 + border: 1px solid var(--border);
6367 + border-left-width: 3px;
6349 6368 }
6350 6369
6351 - .savings-positive {
6352 - color: var(--success);
6353 - font-weight: 600;
6370 + .verdict--ahead {
6371 + border-left-color: var(--success);
6354 6372 }
6355 6373
6356 - .savings-negative {
6357 - color: var(--danger);
6374 + .verdict--even {
6375 + border-left-color: var(--border);
6358 6376 }
6359 6377
6360 - .pricing-disclaimer {
6378 + .verdict--behind {
6379 + border-left-color: var(--warning);
6380 + }
6381 +
6382 + .calc-table th[scope="row"] {
6383 + text-align: left;
6384 + font-weight: 400;
6385 + opacity: 0.7;
6386 + }
6387 +
6388 + .calc-table .calc-gross,
6389 + .calc-keep-row td {
6390 + font-family: var(--font-mono);
6391 + }
6392 +
6393 + .calc-keep-row td {
6394 + font-size: var(--text-title);
6395 + }
6396 +
6397 + /* The win-region bar. Two segments tile the full width; the server sends both
6398 + widths as percentages, so nothing here is computed in the browser. */
6399 + .calc-scale {
6400 + display: flex;
6401 + flex-direction: column;
6402 + gap: var(--gap-bound);
6403 + }
6404 +
6405 + .calc-bar {
6406 + position: relative;
6407 + display: flex;
6408 + height: 1.5rem;
6409 + border: 1px solid var(--border);
6410 + overflow: hidden;
6411 + }
6412 +
6413 + .calc-bar-seg {
6414 + height: 100%;
6415 + }
6416 +
6417 + .calc-bar-seg--other {
6418 + background: var(--warning);
6419 + opacity: 0.35;
6420 + }
6421 +
6422 + .calc-bar-seg--mnw {
6423 + background: var(--success);
6424 + opacity: 0.35;
6425 + }
6426 +
6427 + /* "You are here" on the sales axis. */
6428 + .calc-marker {
6429 + position: absolute;
6430 + top: 0;
6431 + bottom: 0;
6432 + width: 2px;
6433 + margin-left: -1px;
6434 + background: var(--content);
6435 + }
6436 +
6437 + .calc-crossover {
6438 + position: absolute;
6439 + top: 0;
6440 + bottom: 0;
6441 + width: 1px;
6442 + background: var(--content);
6443 + opacity: 0.4;
6444 + }
6445 +
6446 + .calc-axis {
6447 + display: flex;
6448 + justify-content: space-between;
6449 + gap: var(--gap-bound);
6450 + font-family: var(--font-mono);
6361 6451 font-size: var(--text-fine);
6362 - opacity: 0.55;
6363 - margin-top: var(--gap-section);
6364 - line-height: 1.5;
6452 + opacity: 0.6;
6365 6453 }
6366 6454
6367 - .breakeven-note {
6455 + .calc-axis-crossover {
6456 + opacity: 0.9;
6457 + }
6458 +
6459 + .calc-legend {
6460 + display: flex;
6461 + flex-wrap: wrap;
6462 + gap: var(--gap-section);
6463 + font-size: var(--text-fine);
6464 + opacity: 0.7;
6465 + }
6466 +
6467 + .calc-legend-item::before {
6468 + content: "";
6469 + display: inline-block;
6470 + width: 0.75rem;
6471 + height: 0.75rem;
6472 + margin-right: var(--gap-bound);
6473 + vertical-align: baseline;
6474 + }
6475 +
6476 + .calc-legend-item--other::before {
6477 + background: var(--warning);
6478 + opacity: 0.35;
6479 + }
6480 +
6481 + .calc-legend-item--mnw::before {
6482 + background: var(--success);
6483 + opacity: 0.35;
6484 + }
6485 +
6486 + .calc-note {
6368 6487 font-family: var(--font-mono);
6369 6488 font-size: var(--text-note);
6370 6489 padding: var(--gap-section);
6371 6490 background: var(--surface-overlay);
6372 - border-left: 3px solid var(--warning);
6491 + border-left: 3px solid var(--border);
6373 6492 line-height: 1.6;
6374 6493 }
6375 6494
@@ -7384,8 +7503,12 @@
7384 7503 font-size: var(--text-title);
7385 7504 }
7386 7505
7387 - .mnw-keep {
7388 - font-size: var(--text-title);
7506 + .calc-verdict {
7507 + font-size: var(--text-body);
7508 + }
7509 +
7510 + .calc-dials {
7511 + grid-template-columns: 1fr;
7389 7512 }
7390 7513
7391 7514 .secondary-links {
@@ -9930,10 +10053,6 @@
9930 10053 font-size: var(--text-lead);
9931 10054 margin: 0;
9932 10055 }
9933 - /* The MNW row's own name cell carries the emphasis; the row already marks
9934 - itself with .highlight, so the template needs no per-cell attribute. */
9935 - #comparison-body tr.highlight td:first-child { font-weight: 600; }
9936 -
9937 10056 .analytics-section--comparison { margin-bottom: var(--gap-page); }
9938 10057 .analytics-section--comparison h2 { margin-bottom: var(--gap-section); }
9939 10058 .analytics-table-wrap {
@@ -144,11 +144,6 @@
144 144 "10,000 characters",
145 145 "changelog field length, unrelated to the broadcast recipient cap",
146 146 ),
147 - (
148 - "about/compare.md",
149 - "1,000 sales",
150 - "illustrative transaction count in a revenue example, not the cohort cap",
151 - ),
152 147 (
153 148 "about/how-we-work.md",
154 149 "1,000 transactions",