Skip to main content

max / makenotwork

server,docengine: move assumptions.toml into the repo at server/docs/business/ The file has no secrets — Stripe rates (public), tier prices (already on /pricing), vendor costs (Hetzner/Industrious/Claude Code line items the economics docs already discuss), reserve targets, tier-mix assumptions, and worked break-even math. The transparency thesis covers all of it. Living in _private/ broke sando's cargo_test gate because sandod clones the bare MNW repo without _private/ alongside; the canonical-assumptions test panicked with "No such file or directory". Same issue would hit any CI that didn't mount the private store. Moves: _private/docs/mnw/server-internal/business/assumptions.toml -> server/docs/business/assumptions.toml Path updates: server/src/tier_prices.rs test const -> ../docs/business/... server/tests/assumptions.rs const -> ../docs/business/... server/src/main.rs ASSUMPTIONS_PATH default -> docs/business/... server/deploy/deploy.sh rsync source -> docs/business/... (prod destination $REMOTE_DIR/docs/assumptions.toml unchanged; prod's ASSUMPTIONS_PATH env stays valid) shared/docengine/src/assumptions.rs include_str! -> ../../../server/docs/business/... Bump 0.9.2 -> 0.9.4 (0.9.3 was burned by the reverted f7d1990). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-06-02 02:03 UTC
Commit: 96c1b99d01a7b4d2e7d2f29a2896cf14a7ea6204
Parent: ad63f4a
8 files changed, +253 insertions, -19 deletions
@@ -4140,7 +4140,7 @@ dependencies = [
4140 4140
4141 4141 [[package]]
4142 4142 name = "makenotwork"
4143 - version = "0.9.2"
4143 + version = "0.9.4"
4144 4144 dependencies = [
4145 4145 "anyhow",
4146 4146 "apple-codesign",
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makenotwork"
3 - version = "0.9.2"
3 + version = "0.9.4"
4 4 edition = "2024"
5 5 license-file = "LICENSE"
6 6
@@ -74,7 +74,7 @@ upload_config() {
74 74
75 75 # Business assumptions (source-of-truth for substituted figures in docs)
76 76 # Lives in the private docs store outside the repo (moved 2026-05-20).
77 - rsync -az ../../_private/docs/mnw/server-internal/business/assumptions.toml $SERVER:$REMOTE_DIR/docs/assumptions.toml
77 + rsync -az docs/business/assumptions.toml $SERVER:$REMOTE_DIR/docs/assumptions.toml
78 78
79 79 # Rustdoc (API reference for library crates)
80 80 echo "[config] Generating rustdoc..."
@@ -0,0 +1,239 @@
1 + # MNW Business Assumptions — Machine-Readable Source of Truth
2 + #
3 + # Status: draft / not yet consumed by tooling. Target consumer is a future
4 + # docengine feature that substitutes `{{ key }}` placeholders in markdown
5 + # at build time. See `MNW/server/docs/todo.md` § "Docengine — assumption
6 + # substitution" for the proposed implementation.
7 + #
8 + # Until that feature ships, this file is informational. The canonical
9 + # values still live in the markdown docs listed in each section header.
10 + #
11 + # Conventions:
12 + # - All currency in USD unless suffixed `_eur`.
13 + # - All time in months unless suffixed `_days` / `_years`.
14 + # - Percentages as decimals (0.029 not 2.9).
15 + # - Each section names the canonical markdown source.
16 + # - Open decisions tagged `# OPEN:` — these are placeholder values
17 + # pending founder approval per `pricing.md` §7 and `reserve_policy.md` §8.
18 + #
19 + # As of: 2026-05-16
20 + # Last manual-verified: 2026-05-16
21 +
22 +
23 + # ─── Expenses (canonical: expenses.md) ────────────────────────────────────
24 + [expenses]
25 + F_monthly = 580 # Total fixed monthly burn (rounded from $579.08)
26 + F_monthly_exact = 579.08
27 + as_of = "2026-05-16"
28 +
29 + [expenses.lines]
30 + hetzner = 31.00
31 + coworking_industrious = 332.00
32 + claude_code = 200.00
33 + fastmail = 5.00
34 + domains = 2.00
35 + cloudflare = 0.00
36 + postmark = 0.00
37 + apple_developer_amortized = 8.25 # $99/yr ÷ 12
38 + co_llc_periodic_amortized = 0.83 # $10/yr ÷ 12
39 +
40 +
41 + # ─── Stripe fees (canonical: stripe_fees.md) ──────────────────────────────
42 + [stripe]
43 + percent = 0.029 # US standard online card
44 + fixed = 0.30 # Per-transaction flat fee, USD
45 + dispute_fee = 15.00 # Charged once at dispute creation
46 + instant_payout_us_pct = 0.015 # US/AU/NZ/AE region (was incorrectly cited as flat 1%)
47 + instant_payout_intl_pct = 0.010 # CA/EU/UK/SG/NO/HK/MY region
48 + instant_payout_min = 0.50
49 + chargeback_protection_pct = 0.004 # Stripe Radar add-on (fraud-only)
50 + tax_pct = 0.005 # Stripe Tax per transaction
51 +
52 + [stripe.connect_standard]
53 + # What MNW currently uses for creator subs.
54 + per_account_fee = 0
55 + per_payout_fee = 0
56 + payout_volume_pct = 0
57 +
58 + [stripe.connect_express]
59 + # Reference only — not used by MNW for creators.
60 + per_active_account = 2.00
61 + per_payout = 0.25
62 + payout_volume_pct = 0.0025
63 +
64 +
65 + # ─── Hetzner prices (canonical: hetzner_prices.md) ────────────────────────
66 + [hetzner]
67 + fx_eur_to_usd = 1.085 # Verify when FX moves >5%
68 +
69 + # Compute SKUs (EUR/mo)
70 + ccx13_eur = 13.10 # 2 dedicated vCPU / 8 GB / 80 GB — production
71 + ccx23_eur = 30.00 # 4 dedicated vCPU / 16 GB / 160 GB
72 + cpx11_eur = 3.85 # 2 shared vCPU / 2 GB / 40 GB
73 + cpx31_eur = 13.10 # 4 shared vCPU / 8 GB / 160 GB
74 + cx22_eur = 4.59 # 2 shared vCPU / 4 GB / 40 GB
75 +
76 + # Storage and bandwidth
77 + object_storage_eur_per_tb = 5.99
78 + object_storage_included_tb = 1
79 + egress_eur_per_tb_overage = 1.00
80 + egress_included_per_server_tb = 1
81 + volume_eur_per_gb = 0.044
82 + ipv4_eur = 0.60
83 + backup_pct_of_server = 0.20
84 +
85 +
86 + # ─── Tier prices (canonical: pricing.md) ──────────────────────────────────
87 + [tiers.founding]
88 + # Founder pricing — exactly 50% of standard sticker, locked for life. Active
89 + # until the founder window closes (1,000 creators OR exit-beta, whichever
90 + # first). Decision 2026-05-18; raised to 8/12/18/30 on 2026-05-31 to track
91 + # the standard-tier raise (see memory `project_founder_pricing.md`).
92 + basic = 8
93 + small_files = 12
94 + big_files = 18
95 + everything = 30
96 +
97 + [tiers.standard]
98 + # Post-founder sticker — set 2026-05-31 to clear the irreducible
99 + # ~$5/creator/mo support floor at every tier with even-dollar prices.
100 + # Founder rate is exactly 50% of these. See launchplan_final.md §4.5.
101 + basic = 16
102 + small_files = 24
103 + big_files = 36
104 + everything = 60
105 +
106 + # ─── Tier envelope limits (storage caps + file size caps) ────────────────
107 + # Per-tier file-size envelope. Public-facing copy substitutes these via
108 + # {{ tier_limits.* }} so the toml is the single source of truth and a future
109 + # limit change is a one-line edit. Stored as display strings (no NBSP, no
110 + # trailing periods) because that's the form the docs always need.
111 + [tier_limits]
112 + basic_per_file = "10MB"
113 + basic_total = "50GB"
114 + small_files_per_file = "500MB"
115 + small_files_total = "250GB"
116 + big_files_per_file = "20GB"
117 + big_files_total = "500GB"
118 + everything_per_file = "20GB"
119 + everything_total = "500GB"
120 +
121 +
122 + [annual_discount]
123 + # Annual billing is 10% off the monthly × 12 total at every tier, founder
124 + # and standard. Two-digit discount, clean pitch. Decision 2026-05-18.
125 + #
126 + # What this actually covers:
127 + # - Stripe per-transaction fees ($0.30 each, charged 12x for monthly vs 1x
128 + # for annual): saves MNW ~$3.30/yr per customer regardless of tier.
129 + # - The 2.9% percent fee is identical either way (a wash).
130 + # - At Basic, 10% off ≈ the literal Stripe saving (close to pass-through).
131 + # - At Everything, 10% off ($36/yr) > the Stripe saving ($3.30/yr); MNW
132 + # absorbs the difference (~$15/yr per Everything customer at founder
133 + # pricing, ~$32/yr per Everything customer at sticker) as a cashflow +
134 + # reduced-billing-failure benefit. Defensible but not pure pass-through.
135 + #
136 + # Computed values (monthly × 12 × 0.9, rounded to nearest dollar):
137 + # Founder: $86 / $130 / $194 / $324
138 + # Standard: $173 / $259 / $389 / $648
139 + multiplier = 0.9
140 + months_equivalent_free = 1.2 # 10% of 12 months
141 +
142 +
143 + # ─── Tier mix (canonical: tier_mix.md) ────────────────────────────────────
144 + [tier_mix.assumed]
145 + # Pre-launch placeholder distribution. A4 in assumptions.md.
146 + # Updated monthly from SQL once creators exist.
147 + basic_pct = 0.40
148 + small_files_pct = 0.30
149 + big_files_pct = 0.20
150 + everything_pct = 0.10
151 +
152 +
153 + # ─── Reserve policy (canonical: reserve_policy.md) ────────────────────────
154 + [reserve]
155 + # OPEN: all values in this block per reserve_policy.md §8
156 +
157 + T_fixed_months = 12 # OPEN: §8 item 1
158 + S_legal = 50000 # OPEN: §8 item 2 — pre-quote
159 + S_shock = 5000 # OPEN: §8 item 3
160 +
161 + R_opp = 10000 # OPEN: §8 item 4
162 + rho_annual = 0.50 # OPEN: §8 item 5 — max % of R_opp/yr
163 + rho_incident = 0.25 # OPEN: §8 item 6 — max % of R_opp/decision
164 +
165 + surplus_split_reserve = 0.20 # OPEN: §8 item 7 — steady state
166 + surplus_split_earnback = 0.80
167 + # Sum must equal 1.0
168 +
169 + transition_buffer_pct = 1.00 # OPEN: §8 item 10 — multiplier on R_cap before personal-to-company transition
170 +
171 +
172 + # ─── Cohort (canonical: pricing.md) ───────────────────────────────────────
173 + [cohort]
174 + # OPEN: all values per pricing.md §7
175 +
176 + cap_count = 1000 # Decided 2026-05-31; raised from 500 to match public copy in tiers.md / pricing.md.
177 + cap_display = "1,000" # Display string with thousands separator; substituted via {{ cohort.cap_display }}. Keep in sync with cap_count.
178 + cap_months = 12 # OPEN: §7 item 1 — or first M months (whichever first)
179 + lock_duration = "lifetime" # OPEN: §7 item 4 — lifetime-of-subscription
180 + # Counting rule (cumulative vs active) — see sops/founding-cohort-tracking.md, founder decision pending
181 +
182 +
183 + # ─── Per-creator marginal cost inputs (canonical: assumptions.md A1-A10) ─
184 + # All currently unmeasured — pre-launch placeholders.
185 + [creator_marginal]
186 + storage_basic_gb = 0.1 # A1: ~100 MB
187 + storage_small_files_gb = 2 # A2: ~2 GB
188 + storage_big_files_gb = 10 # A3: ~10 GB
189 + storage_everything_gb = 30 # A10: tier-dependent, placeholder
190 + storage_cost_per_gb_per_month = 0.0065 # = €5.99/TB-mo × FX
191 + egress_origin_hit_assumed_pct = 0.10 # A6: Cloudflare cache absorbs ~90% (assumed)
192 + chargeback_rate_tier_subs = 0.001 # A12: 0.1% for recurring (lower than one-shot)
193 +
194 +
195 + # ─── Derived values (for docengine feature; NOT stored — computed) ────────
196 + #
197 + # These are documented here for the future implementation. The docengine
198 + # feature should compute them from the values above. Listed as commented
199 + # pseudo-code; actual implementation belongs in Rust.
200 + #
201 + # R_cap = T_fixed_months · F_monthly + S_legal + S_shock
202 + # = 12 · 580 + 50000 + 5000 = 61960
203 + #
204 + # ARPU_founding = Σ (tier_mix[k] · tiers.founding[k])
205 + # = 0.4·8 + 0.3·12 + 0.2·18 + 0.1·30 = 13.40
206 + #
207 + # ARPU_standard = Σ (tier_mix[k] · tiers.standard[k])
208 + # = 0.4·16 + 0.3·24 + 0.2·36 + 0.1·60 = 26.80
209 + #
210 + # stripe_fee(amount) = stripe.percent · amount + stripe.fixed
211 + # stripe_fee_basic_std = 0.029 · 16 + 0.30 = 0.76
212 + # stripe_fee_small_std = 0.029 · 24 + 0.30 = 1.00
213 + # stripe_fee_big_std = 0.029 · 36 + 0.30 = 1.34
214 + # stripe_fee_ev_std = 0.029 · 60 + 0.30 = 2.04
215 + #
216 + # break_even(rate_class) = F_monthly / (ARPU_{rate_class} − marginal_avg)
217 + # break_even_standard = 580 / (26.80 − 1) ≈ 22.5 creators
218 + # break_even_founding = 580 / (13.40 − 1) ≈ 46.8 creators
219 + #
220 + # surplus(N, rate_class) = N · (ARPU − marginal) − F_monthly
221 + # surplus(100, "standard") = 100 · 25.80 − 580 = 2000
222 + # surplus(500, "standard") = 500 · 25.80 − 580 = 12320
223 + #
224 + # fill_time_months(N) = (R_cap + R_opp) / surplus(N, "standard")
225 + # fill_time(100) = 71960 / 2000 ≈ 36 mo
226 + # fill_time(500) = 71960 / 12320 ≈ 6 mo
227 + #
228 + # infra_cost(tier, N) = storage_{tier}_gb · storage_cost_per_gb_per_month
229 + # + egress_share + stripe_fee(price) + F_monthly/N
230 + #
231 + # Validation rules the build step should enforce:
232 + # - 100 < F_monthly < 10000 (typo guard)
233 + # - sum(tier_mix.*) = 1.00 (tier mix must sum to 100%)
234 + # - surplus_split_reserve + surplus_split_earnback = 1.00
235 + # - 0 < rho_annual ≤ 1
236 + # - 0 < rho_incident ≤ 1
237 + # - rho_incident ≤ rho_annual (single decision ≤ annual budget)
238 + # - All `tiers.founding[k]` ≤ `tiers.standard[k]`
239 + # - cap_count > 0, cap_months > 0
@@ -188,7 +188,7 @@ async fn main() {
188 188 // Load business assumptions (single source of truth for figures in the docs).
189 189 // Validation failure aborts startup — we don't want to serve stale numbers.
190 190 let assumptions_path = std::env::var("ASSUMPTIONS_PATH")
191 - .unwrap_or_else(|_| "docs/internal/business/assumptions.toml".to_string());
191 + .unwrap_or_else(|_| "docs/business/assumptions.toml".to_string());
192 192 let assumptions = std::sync::Arc::new(
193 193 match Assumptions::load(&assumptions_path) {
194 194 Ok(a) => {
@@ -158,8 +158,7 @@ fn str_at(a: &Assumptions, key: &str) -> String {
158 158 mod tests {
159 159 use super::*;
160 160
161 - const ASSUMPTIONS_PATH: &str =
162 - "../../_private/docs/mnw/server-internal/business/assumptions.toml";
161 + const ASSUMPTIONS_PATH: &str = "../docs/business/assumptions.toml";
163 162
164 163 /// Guards every key TierPrices reads. If a future toml edit removes one of
165 164 /// these or flips its type, the panic in `from_assumptions` will fire at
@@ -1,7 +1,7 @@
1 1 //! CI guard for business assumptions.
2 2 //!
3 3 //! Catches at PR time what would otherwise only fail at prod boot:
4 - //! - `docs/internal/business/assumptions.toml` parses
4 + //! - `docs/business/assumptions.toml` parses
5 5 //! - All consistency rules pass (sums, bounds, founding ≤ standard)
6 6 //! - Every `{{ ... }}` marker in the live site-docs corpus resolves
7 7 //!
@@ -9,11 +9,9 @@
9 9
10 10 use docengine::Assumptions;
11 11
12 - // Canonical assumptions.toml moved to _private/ on 2026-05-20. Test runs from
13 - // the server crate's cwd (MNW/server/); one level up gets to MNW/, two gets
14 - // to ~/Code/. Production uses ASSUMPTIONS_PATH env override (see main.rs).
15 - const ASSUMPTIONS_PATH: &str =
16 - "../../_private/docs/mnw/server-internal/business/assumptions.toml";
12 + // Canonical assumptions.toml ships with the repo at server/docs/business/.
13 + // Test runs from the server crate's cwd; ../ gets to MNW/server/.
14 + const ASSUMPTIONS_PATH: &str = "../docs/business/assumptions.toml";
17 15 const SITE_DOCS_PATH: &str = "site-docs/public";
18 16
19 17 #[test]
@@ -563,14 +563,12 @@ fn insert_derived(t: &Typed, out: &mut HashMap<String, LookupValue>) {
563 563 mod tests {
564 564 use super::*;
565 565
566 - // Canonical assumptions.toml lives outside the MNW repo at
567 - // ~/Code/_private/docs/mnw/server-internal/business/assumptions.toml (moved
568 - // 2026-05-20). include_str! resolves relative to this source file. A clone
569 - // of the MNW repo without _private/ alongside will fail to build the
570 - // assumptions feature; production never hits this path because main.rs
571 - // uses runtime Assumptions::load with ASSUMPTIONS_PATH env override.
566 + // Canonical assumptions.toml ships with the MNW repo at
567 + // server/docs/business/assumptions.toml. include_str! resolves relative to
568 + // this source file (MNW/shared/docengine/src/), so we walk up to MNW/ then
569 + // into server/docs/business/.
572 570 const FIXTURE: &str = include_str!(
573 - "../../../../_private/docs/mnw/server-internal/business/assumptions.toml"
571 + "../../../server/docs/business/assumptions.toml"
574 572 );
575 573
576 574 fn loaded() -> Assumptions {