Skip to main content

max / makenotwork

19.2 KB · 412 lines History Blame Raw
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 # Zero on purpose for the alpha: the free tier covers 100 mails a month and
37 # nothing here is sending yet. Postmark's real shape is $15/mo at 10k plus
38 # roughly $0.70-0.82 per 1,000 above that, so price this line the month
39 # broadcast volume becomes real rather than leaving the 0 to be believed.
40 postmark = 0.00
41 apple_developer_amortized = 8.25 # $99/yr ÷ 12
42 co_llc_periodic_amortized = 0.83 # $10/yr ÷ 12
43
44
45 # ─── Stripe fees (canonical: stripe_fees.md) ──────────────────────────────
46 [stripe]
47 percent = 0.029 # US standard online card
48 fixed = 0.30 # Per-transaction flat fee, USD
49 dispute_fee = 15.00 # Charged once at dispute creation
50 instant_payout_us_pct = 0.015 # US/AU/NZ/AE region (was incorrectly cited as flat 1%)
51 instant_payout_intl_pct = 0.010 # CA/EU/UK/SG/NO/HK/MY region
52 instant_payout_min = 0.50
53 chargeback_protection_pct = 0.004 # Stripe Radar add-on (fraud-only)
54 tax_pct = 0.005 # Stripe Tax per transaction
55
56 [stripe.connect_standard]
57 # What MNW currently uses for creator subs.
58 per_account_fee = 0
59 per_payout_fee = 0
60 payout_volume_pct = 0
61
62 [stripe.connect_express]
63 # Reference only: not used by MNW for creators.
64 per_active_account = 2.00
65 per_payout = 0.25
66 payout_volume_pct = 0.0025
67
68 [stripe.instant_payout]
69 # Nested form of the flat instant_payout_* keys above; substituted into
70 # legal/payments.md and guide/stripe.md as {{ stripe.instant_payout.* }}.
71 # Kept alongside the flat form for backward compatibility.
72 us_pct = 0.015
73 intl_pct = 0.010
74 min_usd = 0.50
75
76
77 # ─── Policy commitments (canonical: guarantees.md) ───────────────────────
78 # Time-based commitments that appear verbatim across guarantees.md, pricing.md,
79 # tiers.md, faq.md, fan-guide.md, account-lifecycle.md, generative-ai.md, and
80 # how-we-work.md. Semantically distinct even where the number matches today.
81 [policy]
82 buyer_access_days = 90 # guarantees §Buyer Access, post-close fan download window
83 shutdown_notice_days = 90 # guarantees §Shutdown, §Continuity
84 price_change_notice_days = 90 # guarantees §Price Stability, pricing.md
85 grandfather_months = 12 # guarantees §Price Stability
86 cancellation_grace_days = 30 # tiers.md §Cancellation, getting-started.md
87 content_archive_months = 12 # guarantees §Content Archive (planned)
88 generative_ai_notice_days = 30 # generative-ai.md §Governance
89
90
91 # ─── Uptime (canonical: guarantees.md §Availability) ─────────────────────
92 [uptime]
93 target_pct = 0.995
94 target_downtime_hours_year = 44 # (1 - 0.995) × 8760 ≈ 43.8, rounded up
95
96
97 # ─── Broadcasts (canonical: tiers.md, mailing-lists.md) ──────────────────
98 [broadcasts]
99 per_day = 1
100 recipients_per_send = 10000
101 # Display string with thousands separator; docengine has no thousands filter
102 # so the display form lives here (same pattern as `cohort.cap_display`).
103 recipients_per_send_display = "10,000"
104
105
106 # ─── Monthly mail cap (canonical: src/db/mail_caps.rs) ───────────────────
107 # A soft maximum on total mails one creator sends per billing month, protecting
108 # the shared Postmark IP pool from one creator's fan-out. Soft: the creator sees
109 # the count, is warned before the cap, and is refused with a message rather than
110 # throttled silently. An increase is granted per account on application.
111 #
112 # RATIFIED 2026-08-30 (Max, GoingsOn mnw-server 7cf9e855). Flat across tiers:
113 # tier decides only how readily an increase is granted, which keeps the whole
114 # mechanism on the application path and the pricing page to one line. Held at
115 # 25,000 rather than raised: the cap bounds blast radius from a compromised
116 # account, not a creator's honest month, and the 1-broadcast-per-day limit
117 # already bounds the daily damage. A 2,000-person list mailed weekly is 8,000 a
118 # month and is uneventful. Postmark's marginal rate is roughly $0.70-0.82 per
119 # 1,000, so a creator sitting at the cap costs about $20 against Basic's $16 --
120 # reachable only by someone already in a conversation with us.
121 [mail_cap]
122 basic = 25000
123 small_files = 25000
124 big_files = 25000
125 everything = 25000
126 # A creator with no active subscription still sends: announcements go out from a
127 # project whether or not the tier subscription lapsed. Lower, because there is no
128 # revenue behind it, and above any honest use of a free account.
129 no_subscription = 5000
130 # Display string with thousands separator, for the public copy that has to state
131 # the monthly bound beside the per-send one. Same pattern as
132 # `broadcasts.recipients_per_send_display`; keep in sync with the tier values
133 # above, which are flat.
134 monthly_display = "25,000"
135 # Where the warning band starts, as a fraction of the effective cap. A ratio
136 # rather than a fixed remainder so a raised cap warns proportionally.
137 warn_at_fraction = 0.8
138
139
140 # ─── Fan+ (canonical: fan-plus.md) ───────────────────────────────────────
141 [fan_plus]
142 monthly_price_usd = 8
143 # Per-fan monthly credit allowance; funded by Fan+ subscription revenue,
144 # not per-account overhead. Fan-only: creators don't receive it.
145 monthly_credit_usd = 5
146
147
148 # ─── SyncKit developer billing (canonical: src/synckit_billing.rs) ───────
149 # The Rust constants are authoritative; this block mirrors them so the public
150 # copy in site-docs/public/developer/synckit.md substitutes instead of
151 # hardcoding. `tests/assumptions.rs` fails if the two drift.
152 [synckit]
153 storage_usd_per_gb_month = 0.03 # STORAGE_RATE_CENTS_PER_GB
154 invoice_floor_usd = 0.31 # BASE_FLOOR_CENTS, smallest charge clearing Stripe's fee
155 min_priced_gb = 1 # implied by validate_knobs (> 0)
156 max_priced_gb = 10240 # MAX_STORAGE_GB (10 TiB)
157 max_priced_display = "10 TB" # display form of max_priced_gb
158
159
160 # ─── Hetzner prices (canonical: hetzner_prices.md) ────────────────────────
161 [hetzner]
162 fx_eur_to_usd = 1.085 # Verify when FX moves >5%
163
164 # Compute SKUs (EUR/mo)
165 ccx13_eur = 13.10 # 2 dedicated vCPU / 8 GB / 80 GB, production
166 ccx23_eur = 30.00 # 4 dedicated vCPU / 16 GB / 160 GB
167 cpx11_eur = 3.85 # 2 shared vCPU / 2 GB / 40 GB
168 cpx31_eur = 13.10 # 4 shared vCPU / 8 GB / 160 GB
169 cx22_eur = 4.59 # 2 shared vCPU / 4 GB / 40 GB
170
171 # Storage and bandwidth
172 object_storage_eur_per_tb = 5.99
173 object_storage_included_tb = 1
174 egress_eur_per_tb_overage = 1.00
175 egress_included_per_server_tb = 1
176 volume_eur_per_gb = 0.044
177 ipv4_eur = 0.60
178 backup_pct_of_server = 0.20
179
180
181 # ─── Tier prices (canonical: pricing.md) ──────────────────────────────────
182 [tiers.founding]
183 # Founder pricing: exactly 50% of standard sticker, locked for life. Active
184 # until the founder window closes (1,000 creators OR exit-beta, whichever
185 # first). Decision 2026-05-18; raised to 8/12/18/30 on 2026-05-31 to track
186 # the standard-tier raise; reset to Refined-A (4/7/9/12) on 2026-07-08 per
187 # pricing_interrogation_2026_07.md §6.
188 basic = 4
189 small_files = 7
190 big_files = 9
191 everything = 12
192
193 [tiers.standard]
194 # Refined-A sticker: set 2026-07-08 per pricing_interrogation_2026_07.md §6.
195 # Priced against residency-scale labor ($41.5/hr Y1 support, ~$50/hr blended
196 # engineering) rather than the earlier $80/hr market rate, with right-sized
197 # Basic (10GB) and Small Files (100GB) envelopes. Founder rate is exactly
198 # 50% of these. History: 16/24/36/60 (2026-05-31), 12/18/24/40 (earlier).
199 basic = 8
200 small_files = 14
201 big_files = 18
202 everything = 24
203
204 # ─── Tier envelope limits (storage caps + file size caps) ────────────────
205 # Per-tier file-size envelope. Public-facing copy substitutes these via
206 # {{ tier_limits.* }} so the toml is the single source of truth and a future
207 # limit change is a one-line edit. Stored as display strings (no NBSP, no
208 # trailing periods) because that's the form the docs always need.
209 #
210 # `[tier_bytes]` below carries the same envelopes as machine-readable integer
211 # byte counts: that's the form Rust needs for upload gating, storage-cap
212 # accounting, and the metrics fill-ratio SQL. Docengine validates that each
213 # `tier_bytes.<k>` parses back to the matching `tier_limits.<k>` string, so
214 # a drift between the two forms fails at boot instead of shipping to prod.
215 [tier_limits]
216 # Refined-A envelopes (2026-07-08): Basic shrunk to 5MB/10GB and Small Files
217 # total shrunk to 100GB to make the tier ladder honest to actual content shape
218 # rather than "which tier gives me headroom I'll never use." Big Files and
219 # Everything envelopes unchanged.
220 basic_per_file = "5MB"
221 basic_total = "10GB"
222 small_files_per_file = "500MB"
223 small_files_total = "100GB"
224 big_files_per_file = "20GB"
225 big_files_total = "500GB"
226 everything_per_file = "20GB"
227 everything_total = "500GB"
228
229 [tier_bytes]
230 # Binary units (KB = 1024 B). Must match [tier_limits] display strings above.
231 basic_per_file = 5242880 # 5 MB
232 basic_total = 10737418240 # 10 GB
233 small_files_per_file = 524288000 # 500 MB
234 small_files_total = 107374182400 # 100 GB
235 big_files_per_file = 21474836480 # 20 GB
236 big_files_total = 536870912000 # 500 GB
237 everything_per_file = 21474836480 # 20 GB
238 everything_total = 536870912000 # 500 GB
239
240
241 [annual_discount]
242 # Annual billing is 10% off the monthly × 12 total at every tier, founder
243 # and standard. Two-digit discount, clean pitch. Decision 2026-05-18.
244 #
245 # What this actually covers:
246 # - Stripe per-transaction fees ($0.30 each, charged 12x for monthly vs 1x
247 # for annual): saves MNW ~$3.30/yr per customer regardless of tier.
248 # - The 2.9% percent fee is identical either way (a wash).
249 # - At Basic, 10% off ≈ the literal Stripe saving (close to pass-through).
250 # - At Everything, 10% off ($36/yr) > the Stripe saving ($3.30/yr); MNW
251 # absorbs the difference (~$15/yr per Everything customer at founder
252 # pricing, ~$32/yr per Everything customer at sticker) as a cashflow +
253 # reduced-billing-failure benefit. Defensible but not pure pass-through.
254 #
255 # Computed values (monthly × 12 × 0.9, rounded to nearest dollar):
256 # Founder: $86 / $130 / $194 / $324
257 # Standard: $173 / $259 / $389 / $648
258 multiplier = 0.9
259 months_equivalent_free = 1.2 # 10% of 12 months
260
261
262 # ─── Tier mix (canonical: tier_mix.md) ────────────────────────────────────
263 [tier_mix.assumed]
264 # Pre-launch placeholder distribution. A4 in assumptions.md.
265 # Updated monthly from SQL once creators exist.
266 basic_pct = 0.40
267 small_files_pct = 0.30
268 big_files_pct = 0.20
269 everything_pct = 0.10
270
271
272 # ─── Reserve policy (canonical: reserve_policy.md) ────────────────────────
273 [reserve]
274 # OPEN: all values in this block per reserve_policy.md §8
275
276 T_fixed_months = 12 # OPEN: §8 item 1
277 S_legal = 50000 # OPEN: §8 item 2, pre-quote
278 S_shock = 5000 # OPEN: §8 item 3
279
280 R_opp = 10000 # OPEN: §8 item 4
281 rho_annual = 0.50 # OPEN: §8 item 5, max % of R_opp/yr
282 rho_incident = 0.25 # OPEN: §8 item 6, max % of R_opp/decision
283
284 surplus_split_reserve = 0.20 # OPEN: §8 item 7, steady state
285 surplus_split_earnback = 0.80
286 # Sum must equal 1.0
287
288 transition_buffer_pct = 1.00 # OPEN: §8 item 10, multiplier on R_cap before personal-to-company transition
289
290
291 # ─── Cohort (canonical: pricing.md) ───────────────────────────────────────
292 [cohort]
293 # OPEN: all values per pricing.md §7
294
295 cap_count = 1000 # Decided 2026-05-31; raised from 500 to match public copy in tiers.md / pricing.md.
296 cap_display = "1,000" # Display string with thousands separator; substituted via {{ cohort.cap_display }}. Keep in sync with cap_count.
297 cap_months = 12 # OPEN: §7 item 1, or first M months (whichever first)
298 lock_duration = "lifetime" # OPEN: §7 item 4, lifetime-of-subscription
299 # Counting rule (cumulative vs active): see sops/founding-cohort-tracking.md, founder decision pending
300
301
302 # ─── Per-creator marginal cost inputs (canonical: assumptions.md A1-A10) ─
303 # All currently unmeasured: pre-launch placeholders.
304 [creator_marginal]
305 storage_basic_gb = 0.05 # A1: ~50 MB (right-sized 2026-07-08 alongside 10GB Basic envelope)
306 storage_small_files_gb = 2 # A2: ~2 GB
307 storage_big_files_gb = 10 # A3: ~10 GB
308 storage_everything_gb = 30 # A10: tier-dependent, placeholder
309 storage_cost_per_gb_per_month = 0.0065 # = €5.99/TB-mo × FX
310 egress_origin_hit_assumed_pct = 0.10 # A6: Cloudflare cache absorbs ~90% (assumed)
311 chargeback_rate_tier_subs = 0.001 # A12: 0.1% for recurring (lower than one-shot)
312
313
314 # ─── Derived values (for docengine feature; NOT stored: computed) ────────
315 #
316 # These are documented here for the future implementation. The docengine
317 # feature should compute them from the values above. Listed as commented
318 # pseudo-code; actual implementation belongs in Rust.
319 #
320 # R_cap = T_fixed_months · F_monthly + S_legal + S_shock
321 # = 12 · 580 + 50000 + 5000 = 61960
322 #
323 # ARPU_founding = Σ (tier_mix[k] · tiers.founding[k])
324 # = 0.4·8 + 0.3·12 + 0.2·18 + 0.1·30 = 13.40
325 #
326 # ARPU_standard = Σ (tier_mix[k] · tiers.standard[k])
327 # = 0.4·16 + 0.3·24 + 0.2·36 + 0.1·60 = 26.80
328 #
329 # stripe_fee(amount) = stripe.percent · amount + stripe.fixed
330 # stripe_fee_basic_std = 0.029 · 16 + 0.30 = 0.76
331 # stripe_fee_small_std = 0.029 · 24 + 0.30 = 1.00
332 # stripe_fee_big_std = 0.029 · 36 + 0.30 = 1.34
333 # stripe_fee_ev_std = 0.029 · 60 + 0.30 = 2.04
334 #
335 # break_even(rate_class) = F_monthly / (ARPU_{rate_class} − marginal_avg)
336 # break_even_standard = 580 / (26.80 − 1) ≈ 22.5 creators
337 # break_even_founding = 580 / (13.40 − 1) ≈ 46.8 creators
338 #
339 # surplus(N, rate_class) = N · (ARPU − marginal) − F_monthly
340 # surplus(100, "standard") = 100 · 25.80 − 580 = 2000
341 # surplus(500, "standard") = 500 · 25.80 − 580 = 12320
342 #
343 # fill_time_months(N) = (R_cap + R_opp) / surplus(N, "standard")
344 # fill_time(100) = 71960 / 2000 ≈ 36 mo
345 # fill_time(500) = 71960 / 12320 ≈ 6 mo
346 #
347 # infra_cost(tier, N) = storage_{tier}_gb · storage_cost_per_gb_per_month
348 # + egress_share + stripe_fee(price) + F_monthly/N
349 #
350 # Validation rules the build step should enforce:
351 # - 100 < F_monthly < 10000 (typo guard)
352 # - sum(tier_mix.*) = 1.00 (tier mix must sum to 100%)
353 # - surplus_split_reserve + surplus_split_earnback = 1.00
354 # - 0 < rho_annual ≤ 1
355 # - 0 < rho_incident ≤ 1
356 # - rho_incident ≤ rho_annual (single decision ≤ annual budget)
357 # - All `tiers.founding[k]` ≤ `tiers.standard[k]`
358 # - cap_count > 0, cap_months > 0
359
360
361 # ─── Runway disclosure (transparency block on /docs/about/economics) ─────
362 #
363 # Operator-set. Refreshed quarterly at sprint close, alongside the
364 # /changelog recap. The paying-creator count on the page is pulled live
365 # from the DB; this number is the cash-runway bucket.
366 #
367 # Units: quarters at current burn (whole-number bucket; we round down
368 # rather than mislead in either direction).
369 #
370 # A value of 0 means "not yet published": the page renders without a
371 # number until the first quarterly refresh sets it.
372 [runway]
373 # 17 = $190K committed funding ÷ $3,580/mo current actual burn (ops + $3K
374 # salary), rounded down. Excludes the draw-on-request $50K reserve (would be
375 # 22). Basis: server-internal/business/financial_dashboard.md. Refresh at
376 # sprint close: bump if burn rises (Year 2 $5K salary) or funding changes.
377 quarters = 17
378 last_updated_iso = "2026-06-04"
379
380
381 # ─── Fee calculator (canonical: this file + stripe_fees.md) ──────────────
382 #
383 # Powers the public /pricing calculator. Consumed by MNW's
384 # `src/fee_calculator.rs` (parsed directly from this file, not through
385 # docengine's scalar lookup, so the loader can panic at startup on a bad
386 # value). Stripe's own fees are read from the [stripe] block above.
387 #
388 # There are no competitor fee tables here any more, and that is the point.
389 # The calculator asks the visitor for the other platform's percentage and
390 # per-sale fee, so nothing in this repo is a claim about somebody else's
391 # pricing page and nothing here goes stale.
392 #
393 # Monthly model, with the visitor's dials on the right-hand side:
394 # gross = item_price * sales
395 # mnw_net = gross - gross*stripe.percent - sales*stripe.fixed - tier_price
396 # other_net = gross - gross*other_pct - sales*other_per_sale
397 # MNW takes no cut, so its variable side is payment processing alone. The
398 # other platform's two dials are its TOTAL deduction, its own cut plus
399 # whatever processing it layers on, because that is the figure a creator can
400 # read off a payout.
401 #
402 # The defaults below are only the opening position of each dial, picked so the
403 # first render is a realistic mid-market scenario rather than a strawman:
404 # a $25 item, 40 sales a month ($1,000 gross), against a platform on a ~10%
405 # cut with ~2.9% + $0.30 processing layered on top, which nets out to the
406 # ~12.6% + $0.30 written here. Pinned by `fee_calculator::tests`.
407 [fee_calculator]
408 item_price = 25 # dollars, opening position of the price dial
409 sales_per_month = 40 # opening position of the volume dial
410 other_pct = 0.126 # other platform's total percentage deduction
411 other_per_sale = 0.30 # other platform's total flat fee per sale
412