Skip to main content

max / makenotwork

Describe the creators page, table and all The fifth public document, and the first with a described table on a public page: four tiers by four columns, every price and storage figure read off TierPrices rather than written, so this page and /use-cases cannot disagree about what Basic costs. Node::Table states the column widths, so the narrow-viewport behaviour is the design system's rather than .wave-table's. Three readers again, splitting on a different axis than /fan-plus did: what the reader is allowed to do. can_create_projects comes off the session user the factory already resolved, so the branch costs no query. A creator is not sold an application they already hold. The Stripe settlement constraint keeps its link and its six currencies, with a test on it: it is the one piece of prose on this page somebody can lose money by not reading. Deletes templates/pages/creators.html, CreatorsTemplate, pages::creators_page and twenty .creators-page rules including .wave-table, which had no other user. The public route module's imports shrank to two lines with the handler gone.
Author: Max Johnson <me@maxj.phd> · 2026-09-01 13:21 UTC
Signed with PGP, not checked
Commit: de85a338522ed258ab2865c411d4f8c0c80ca929
Parent: fa1a268
7 files changed, +377 insertions, -228 deletions
@@ -406,7 +406,7 @@
406 406 .export-page .container { max-width: 800px; margin: 0 auto; }
407 407
408 408 .fan-plus-page main,
409 - .creators-page .container { max-width: 900px; margin: 0 auto; }
409 + .creators-page main { max-width: 900px; margin: 0 auto; }
410 410
411 411 /* READING COLUMN, the 680px measure every long-form surface uses. Narrower
412 412 than .container and with tighter horizontal padding, so it is its own
@@ -3009,8 +3009,7 @@
3009 3009 `Node::banner` with `Tone::Success`, the price is a `Figure`, and the button
3010 3010 is an `Act`. Nothing here is left to target.
3011 3011
3012 - The measure is on `.fan-plus-page .container` beside `.creators-page` and
3013 - moves to `main` when that page converts too. */
3012 + The measure is on `.fan-plus-page main` beside `.creators-page main`. */
3014 3013
3015 3014 /* ===========================================
3016 3015 PURCHASE PAGE
@@ -3659,45 +3658,11 @@
3659 3658
3660 3659 .user-page .project-description { font-size: var(--text-note); opacity: 0.8; }
3661 3660
3662 - /* Creators / become-a-creator page (templates/pages/creators.html). */
3663 - .creators-page h1 { font-size: var(--text-display); margin-bottom: var(--gap-peer); }
3664 - .creators-page h2 { font-size: var(--text-head); margin-top: var(--gap-page); margin-bottom: var(--gap-section); }
3665 -
3666 - .creators-page .how-it-works { margin-bottom: var(--gap-page); }
3667 - .creators-page .how-it-works ol { padding-left: var(--gap-pane); line-height: 1.8; }
3668 -
3669 - .creators-page .stats-row { gap: var(--gap-page); margin: var(--gap-page) 0; }
3670 - .creators-page .stat-box { padding: var(--gap-pane); }
3671 - .creators-page .stat-box .number { font-size: var(--text-title); }
3672 - .creators-page .stat-box .label { font-size: var(--text-note); margin-top: var(--gap-bound); }
3673 -
3674 - .creators-page .wave-table {
3675 - width: 100%;
3676 - border-collapse: collapse;
3677 - margin: var(--gap-section) 0;
3678 - }
3679 -
3680 - .creators-page .wave-table th,
3681 - .creators-page .wave-table td {
3682 - padding: var(--gap-section);
3683 - text-align: left;
3684 - border-bottom: 1px solid var(--border);
3685 - }
3686 -
3687 - .creators-page .wave-table th {
3688 - font-family: var(--font-mono);
3689 - font-size: var(--text-note);
3690 - opacity: 0.7;
3691 - }
3692 -
3693 - .creators-page .cta {
3694 - margin-top: var(--gap-page);
3695 - padding: var(--gap-pane);
3696 - background: var(--surface-overlay);
3697 - text-align: center;
3698 - }
3699 -
3700 - .creators-page .cta a { margin: 0 var(--gap-peer); }
3661 + /* The creators page moved to the description layer. Its headings, its ordered
3662 + list, its stat box, its four-column table and its call-to-action box are the
3663 + design system's now: the count is a `Figure`, the tiers are a `Node::Table`
3664 + with stated column widths, and the CTA is one or two `Act`s. `.wave-table`
3665 + went with it and had no other user. */
3701 3666
3702 3667 /* Tag tree browse page (templates/pages/tag_tree.html). */
3703 3668 .tag-tree-page .breadcrumbs {
@@ -10398,17 +10363,6 @@
10398 10363 /* Buyer contacts tab (templates/partials/tabs/buyer_contacts.html). */
10399 10364 .buyer-contacts .contacts-table { min-width: 500px; }
10400 10365
10401 - /* Creators page extras (templates/pages/creators.html). */
10402 - .creators-page .pricing-fineprint {
10403 - font-size: var(--text-note);
10404 - opacity: 0.8;
10405 - margin-top: var(--gap-peer);
10406 - }
10407 - .creators-page .trial-callout {
10408 - font-size: var(--text-note);
10409 - margin-top: var(--gap-pane);
10410 - }
10411 -
10412 10366 /* ===========================================
10413 10367 LIBRARY TABS
10414 10368 Shared classes for the library-dashboard tab partials:
@@ -40,6 +40,7 @@
40 40
41 41 pub mod blog_delete_act;
42 42 pub mod buyer_contacts;
43 + pub mod creators;
43 44 pub mod custom_page;
44 45 pub mod discover_search;
45 46 pub mod discover_typeahead;
@@ -388,8 +389,13 @@
388 389 /// mechanism: those screens resolve nothing per request and take a state built
389 390 /// once at startup. These resolve a session when there is one, so they carry a
390 391 /// per-request viewer and can mint a CSRF token for the form on them.
391 - pub const PUBLIC_DOCUMENT_PATHS: &[&str] =
392 - &[team::PATH, use_cases::PATH, policy::PATH, fan_plus::PATH];
392 + pub const PUBLIC_DOCUMENT_PATHS: &[&str] = &[
393 + team::PATH,
394 + use_cases::PATH,
395 + policy::PATH,
396 + fan_plus::PATH,
397 + creators::PATH,
398 + ];
393 399
394 400 /// Every screen's switch name, in the same order as [`PATHS`].
395 401 ///
@@ -583,6 +589,10 @@
583 589 fan_plus::PATH,
584 590 public_document_mount(app, fan_plus::PATH, fan_plus::screen, fan_plus::renderer),
585 591 ),
592 + (
593 + creators::PATH,
594 + public_document_mount(app, creators::PATH, creators::screen, creators::renderer),
595 + ),
586 596 ]
587 597 }
588 598
@@ -179,7 +179,6 @@
179 179 // Team
180 180 // Fan+
181 181 // Creator invite system
182 - CreatorsTemplate,
183 182 // Email & account
184 183 EmailResultTemplate,
185 184 EmailPreferencesTemplate,
@@ -880,19 +880,6 @@
880 880 pub trialing_or_grace: i64,
881 881 }
882 882
883 - // Creator Invite System
884 -
885 - /// Public page: creator signup, tier pricing, and the active-creator count.
886 - #[derive(Template)]
887 - #[template(path = "pages/creators.html")]
888 - pub struct CreatorsTemplate {
889 - pub csrf_token: CsrfTokenOption,
890 - pub session_user: Option<SessionUser>,
891 - pub total_creators: u32,
892 - pub is_creator: bool,
893 - pub tier_prices: crate::tier_prices::TierPrices,
894 - }
895 -
896 883 // Email & Account
897 884
898 885 /// Public page: email action result (verification, unsubscribe, etc.).
@@ -11,23 +11,11 @@
11 11 mod sitemap;
12 12 mod two_factor;
13 13
14 - use axum::{
15 - extract::State,
16 - response::{IntoResponse, Redirect},
17 - routing::get,
18 - };
19 - use sqlx::PgPool;
20 - use tower_sessions::Session;
14 + use axum::{response::Redirect, routing::get};
21 15
22 16 use crate::{
23 - AppState, Billing,
24 - auth::MaybeUserUnverified,
25 - constants,
17 + AppState, constants,
26 18 csrf::{CsrfRouter, post_csrf, post_csrf_skip, with_csrf_skip},
27 - db,
28 - error::Result,
29 - helpers::get_csrf_token,
30 - templates::CreatorsTemplate,
31 19 };
32 20
33 21 use tower_governor::GovernorLayer;
@@ -140,7 +128,6 @@
140 128 "/notify",
141 129 post_csrf(landing::notify).layer(GovernorLayer::new(join_rate_limit.clone())),
142 130 )
143 - .route_get("/creators", get(creators_page))
144 131 .route_get("/docs", get(docs::docs_index))
145 132 .route_get("/docs/search.json", get(docs::docs_search_index))
146 133 // Platform economics renders as Askama (live runway disclosure); the
@@ -165,26 +152,3 @@
165 152 .layer(GovernorLayer::new(twofa_rate_limit)),
166 153 )
167 154 }
168 -
169 - /// Render the public creators page: signup, tier pricing, active-creator count.
170 - #[tracing::instrument(skip_all, name = "pages::creators_page")]
171 - async fn creators_page(
172 - State(db): State<PgPool>,
173 - State(payments): State<Billing>,
174 - session: Session,
175 - MaybeUserUnverified(maybe_user): MaybeUserUnverified,
176 - ) -> Result<impl IntoResponse> {
177 - let csrf_token = get_csrf_token(&session).await;
178 -
179 - let total_creators = db::waitlist::count_active_creators(&db).await? as u32;
180 -
181 - let is_creator = maybe_user.as_ref().is_some_and(|u| u.can_create_projects);
182 -
183 - Ok(CreatorsTemplate {
184 - csrf_token,
185 - session_user: maybe_user,
186 - total_creators,
187 - is_creator,
188 - tier_prices: payments.tier_prices.clone(),
189 - })
190 - }
@@ -1,0 +1,356 @@
1 + //! The creator-application page at `/creators`, described.
2 + //!
3 + //! The fifth public document. It replaces `templates/pages/creators.html`,
4 + //! `CreatorsTemplate` and `pages::creators_page`.
5 + //!
6 + //! # The tier table is a table, and it is the first described one on a public page
7 + //!
8 + //! Four tiers by four columns, and every cell in two of those columns comes
9 + //! from [`TierPrices`](crate::tier_prices::TierPrices). `Node::Table` says it,
10 + //! the same member `/feed` uses for its item list, so the widths and the
11 + //! narrow-viewport behaviour are the design system's rather than
12 + //! `.wave-table`'s.
13 + //!
14 + //! Where `/use-cases` said the same prices as prose inside nine cards, this
15 + //! says them as a grid, which is what the shipped page did too. Both read the
16 + //! one `TierPrices`, so the two pages cannot disagree about what Basic costs.
17 + //!
18 + //! # Three readers again, and the third one is new
19 + //!
20 + //! [`super::Audience::Anyone`] carries a fourth kind of branch here. `/fan-plus`
21 + //! split on whether the reader had bought; this splits on what the reader is
22 + //! allowed to do:
23 + //!
24 + //! a visitor Join, and Login
25 + //! a reader Apply, from the dashboard
26 + //! a creator nothing to apply for; go to the dashboard
27 + //!
28 + //! `can_create_projects` is the flag, read off the session user the factory
29 + //! already resolved, so the branch costs no query.
30 + //!
31 + //! # The count is live and the page says so
32 + //!
33 + //! `total_creators` is read at request time. It is the one number on this page
34 + //! that is not a price, and the disclosure is the point: a person deciding
35 + //! whether to apply is told how many creators are actually here.
36 +
37 + use makeover_layout as layout;
38 + use quasi_router::screen::{Cell, Cells, Column, Figure};
39 + use quasi_router::{
40 + Action, Document, Node, RegionKind, Request, Response, RouteError, Screen as Described, Slot,
41 + };
42 + use quasi_webview::Webview;
43 +
44 + use crate::db;
45 + use crate::tier_prices::TierPrices;
46 +
47 + /// The address, registered whole. See [`super::public_document_mount`].
48 + pub const PATH: &str = "/creators";
49 +
50 + /// The page's own region, and what the skip link points at.
51 + pub const PAGE_REGION: &str = "creators";
52 +
53 + const MEASURE: layout::Measure = layout::Measure::Wide;
54 +
55 + /// One row of the tier table.
56 + ///
57 + /// `price` and `storage` are read off [`TierPrices`] rather than written here,
58 + /// for the reason `/use-cases` gives: a formatted price in a table is a price
59 + /// that goes stale on its own.
60 + struct Tier {
61 + name: &'static str,
62 + best_for: &'static str,
63 + price: fn(&TierPrices) -> i32,
64 + storage: fn(&TierPrices) -> String,
65 + }
66 +
67 + /// The four, in the order the shipped table listed them.
68 + const TIERS: &[Tier] = &[
69 + Tier {
70 + name: "Basic",
71 + best_for: "Text, blogs, newsletters",
72 + price: |p| p.basic_std,
73 + storage: |p| p.basic_total.clone(),
74 + },
75 + Tier {
76 + name: "Small Files",
77 + best_for: "Audio, plugins, small software",
78 + price: |p| p.small_files_std,
79 + storage: |p| p.small_files_total.clone(),
80 + },
81 + Tier {
82 + name: "Big Files",
83 + best_for: "Video, games, large software",
84 + price: |p| p.big_files_std,
85 + storage: |p| p.big_files_total.clone(),
86 + },
87 + Tier {
88 + name: "Everything",
89 + best_for: "All features, current and future",
90 + price: |p| p.everything_std,
91 + storage: |p| p.everything_total.clone(),
92 + },
93 + ];
94 +
95 + /// What this request knows about the reader's standing.
96 + enum Standing {
97 + /// Nobody signed in.
98 + Visitor,
99 + /// Signed in, not yet a creator.
100 + Reader,
101 + /// Already has creator access.
102 + Creator,
103 + }
104 +
105 + /// The page.
106 + pub fn screen(viewer: &super::Viewer, _request: Request) -> Result<Response, RouteError> {
107 + use axum::extract::FromRef as _;
108 +
109 + let total_creators = viewer
110 + .block_on(db::waitlist::count_active_creators(&viewer.app.db))
111 + .map_err(|_| RouteError::internal("the creator count could not be read"))?;
112 +
113 + let standing = match viewer.user.as_ref() {
114 + None => Standing::Visitor,
115 + Some(user) if user.can_create_projects => Standing::Creator,
116 + Some(_) => Standing::Reader,
117 + };
118 +
119 + let billing = crate::Billing::from_ref(&viewer.app);
120 +
121 + Ok(page_screen(&standing, total_creators, &billing.tier_prices).into())
122 + }
123 +
124 + /// The whole document: the title, the measure, the body.
125 + fn page_screen(standing: &Standing, total_creators: i64, prices: &TierPrices) -> Described {
126 + let page = Slot::new(PAGE_REGION, RegionKind::Pane)
127 + .with(Node::page("Become a Creator"))
128 + .with(Node::text(
129 + "Anyone can sign up to browse and buy. To create projects and sell your work, apply \
130 + for creator access. Most applications are approved within a few days. Makenotwork is \
131 + in private alpha; we're approving applications one cohort at a time.",
132 + ))
133 + .with(Node::section("How It Works"))
134 + .with(Node::rich(
135 + "1. **Sign up** and verify your email\n\
136 + 2. **Apply** from your dashboard: tell us what you make and which tier fits\n\
137 + 3. **Get approved**: we review applications individually, usually within a few days\n\
138 + \n\
139 + We review applications to make sure applicants are here to share and sell creative \
140 + work. If you make something and want to sell it, you'll likely get in. Link to your \
141 + existing work (a portfolio, channel, or profile elsewhere) to speed things up.\n\
142 + \n\
143 + **Important:** You sell in the currency your Stripe account settles in, and \
144 + receiving payouts requires a [Stripe](https://stripe.com/global) account in a \
145 + supported country that settles in one of the six we support: **USD, CAD, GBP, AUD, \
146 + NZD or EUR**. Check both with Stripe before applying.",
147 + ))
148 + .with(Node::stats([Figure::new(
149 + total_creators.to_string(),
150 + "Active Creators",
151 + )]))
152 + .with(Node::section("Pricing"))
153 + .with(Node::text(
154 + "Flat monthly fee. 0% cut of your revenue. The only deduction from fan payments is \
155 + the payment processor's fee (~3%).",
156 + ))
157 + .with(tier_table(prices))
158 + .with(Node::rich(
159 + "Every tier is the complete platform: `/u/username` profile, project and item pages, \
160 + project forum, Discover listing, memberships, pay-what-you-want, promo codes, RSS, \
161 + analytics, full data export, 2FA/passkeys. The tier picks the file-size envelope, \
162 + not the feature set. You sell in your Stripe account's currency (USD, CAD, GBP, AUD, \
163 + NZD or EUR); receiving payouts requires [Stripe](https://stripe.com/global) in a \
164 + supported country. [Full tier details](/docs/tiers) | \
165 + [Pricing models](/docs/pricing)",
166 + ))
167 + .with(Node::rich(
168 + "**Not ready to commit?** Request a **free trial** (2-6 weeks, no credit card) when \
169 + you apply. Or [try sandbox mode](/sandbox) to explore the dashboard without signing \
170 + up.",
171 + ))
172 + .with(Node::section("Who Runs This"))
173 + .with(Node::rich(
174 + "Makenotwork is built and operated by one person. No investors, no board, no outside \
175 + pressure. Decisions are fast and aligned with creators, but there's no large team \
176 + behind the scenes. Read the full picture in our \
177 + [continuity guarantee](/docs/guarantees#continuity) and \
178 + [platform economics](/docs/economics).",
179 + ));
180 +
181 + let page = call_to_action(page, standing);
182 +
183 + Described::single("Creators - Makenotwork")
184 + .measured(MEASURE)
185 + .documented(
186 + Document::default().classed(crate::shell::body_class(MEASURE, &["creators-page"])),
187 + )
188 + .summarised(
189 + "Apply for creator access: a flat monthly fee, no cut of your revenue, and four \
190 + tiers that pick a file-size envelope rather than a feature set.",
191 + )
192 + .with(page)
193 + }
194 +
195 + /// The four tiers, priced from the live figures.
196 + fn tier_table(prices: &TierPrices) -> Node {
197 + Node::Table {
198 + columns: vec![
199 + Column::new("Tier")
200 + .width(layout::Width::Content)
201 + .priority(layout::Priority::Essential),
202 + Column::new("Monthly").width(layout::Width::Content),
203 + Column::new("Best For").width(layout::Width::Fill),
204 + Column::new("Storage").width(layout::Width::Content),
205 + ],
206 + rows: TIERS
207 + .iter()
208 + .map(|tier| {
209 + Cells::new([
210 + Cell::new(tier.name),
211 + Cell::new(format!("${}", (tier.price)(prices))),
212 + Cell::new(tier.best_for),
213 + Cell::new((tier.storage)(prices)),
214 + ])
215 + })
216 + .collect(),
217 + more: None,
218 + }
219 + }
220 +
221 + /// What the page asks of this reader, which is the only thing on it that
222 + /// differs by who is asking.
223 + fn call_to_action(page: Slot, standing: &Standing) -> Slot {
224 + match standing {
225 + Standing::Creator => page
226 + .with(Node::text("You have creator access."))
227 + .with(Node::act(
228 + "Go to Dashboard",
229 + Action::get("/dashboard").navigating(),
230 + )),
231 + Standing::Reader => page.with(Node::text("Ready to create?")).with(Node::act(
232 + "Apply from Dashboard",
233 + Action::get("/dashboard?tab=settings&section=creator").navigating(),
234 + )),
235 + Standing::Visitor => page
236 + .with(Node::text("Join to get started."))
237 + .with(Node::act("Join", Action::get("/join").navigating()))
238 + .with(Node::act("Login", Action::get("/login").navigating())),
239 + }
240 + }
241 +
242 + /// The document this screen is drawn in.
243 + #[must_use]
244 + pub fn renderer(viewer: &super::Viewer) -> Webview {
245 + Webview::new().with_shell(viewer.document_shell().with_body_first(format!(
246 + "{}{}",
247 + crate::shell::skip_link(PAGE_REGION),
248 + crate::shell::site_header(viewer.user.as_ref(), Some(&viewer.csrf)),
249 + )))
250 + }
251 +
252 + #[cfg(test)]
253 + mod tests {
254 + use super::*;
255 +
256 + fn html(standing: &Standing) -> String {
257 + use quasi_axum::Serves as _;
258 +
259 + Webview::new().screen(&page_screen(standing, 7, &TierPrices::default()))
260 + }
261 +
262 + /// `2790e5c4`. Both classes were on the body already, so this is a copy.
263 + #[test]
264 + fn the_document_carries_the_classes_the_template_carried() {
265 + let screen = page_screen(&Standing::Visitor, 0, &TierPrices::default());
266 +
267 + assert_eq!(
268 + screen.document.body_class.as_deref(),
269 + Some("padded-page creators-page")
270 + );
271 + }
272 +
273 + /// Every tier the table listed is still listed, and its price is read
274 + /// rather than written.
275 + #[test]
276 + fn every_tier_is_priced_from_the_live_figures() {
277 + let mut prices = TierPrices::default();
278 + prices.basic_std = 4321;
279 + prices.small_files_std = 5678;
280 + prices.big_files_std = 8765;
281 + prices.everything_std = 9876;
282 +
283 + let html = {
284 + use quasi_axum::Serves as _;
285 + Webview::new().screen(&page_screen(&Standing::Visitor, 0, &prices))
286 + };
287 +
288 + assert_eq!(TIERS.len(), 4);
289 + for tier in TIERS {
290 + assert!(html.contains(tier.name), "{} missing", tier.name);
291 + assert!(html.contains(tier.best_for), "{} missing", tier.best_for);
292 + }
293 + for price in ["4321", "5678", "8765", "9876"] {
294 + assert!(html.contains(price), "{price} is not read from TierPrices");
295 + }
296 + }
297 +
298 + /// The live disclosure: how many creators are actually here.
299 + #[test]
300 + fn the_active_creator_count_is_shown() {
301 + assert!(html(&Standing::Visitor).contains('7'));
302 + assert!(html(&Standing::Visitor).contains("Active Creators"));
303 + }
304 +
305 + /// A visitor is offered an account, not an application they cannot file.
306 + #[test]
307 + fn a_visitor_is_offered_both_ways_in() {
308 + let html = html(&Standing::Visitor);
309 +
310 + assert!(html.contains(r#"href="/join""#), "{html}");
311 + assert!(html.contains(r#"href="/login""#), "{html}");
312 + assert!(!html.contains("tab=settings"), "{html}");
313 + }
314 +
315 + /// A signed-in reader is sent to the place the application lives.
316 + #[test]
317 + fn a_reader_is_sent_to_the_application() {
318 + let html = html(&Standing::Reader);
319 +
320 + assert!(html.contains("section=creator"), "{html}");
321 + assert!(!html.contains(r#"href="/join""#), "{html}");
322 + }
323 +
324 + /// A creator is not sold something they already have.
325 + #[test]
326 + fn a_creator_is_offered_the_dashboard_and_no_application() {
327 + let html = html(&Standing::Creator);
328 +
329 + assert!(html.contains("You have creator access"), "{html}");
330 + assert!(!html.contains("section=creator"), "{html}");
331 + assert!(!html.contains("Ready to create"), "{html}");
332 + }
333 +
334 + /// The payout constraint is the one piece of prose on this page somebody
335 + /// can lose money by not reading, so it keeps its link and its emphasis.
336 + #[test]
337 + fn the_stripe_settlement_warning_survives_intact() {
338 + let html = html(&Standing::Visitor);
339 +
340 + assert!(html.contains("https://stripe.com/global"), "{html}");
341 + assert!(
342 + html.contains("USD, CAD, GBP, AUD, NZD or EUR"),
343 + "the six settlement currencies are not stated: {html}"
344 + );
345 + }
346 +
347 + /// `736f45a5`: this screen's markup carries none of the four spellings.
348 + #[test]
349 + fn the_page_spells_no_spinner() {
350 + let html = html(&Standing::Visitor);
351 +
352 + for spelling in ["htmx-indicator", "spinner", "loading-text", "loading-state"] {
353 + assert!(!html.contains(spelling), "{spelling} survives in {html}");
354 + }
355 + }
356 + }
@@ -1,121 +1,0 @@
1 - {% extends "base.html" %}
2 -
3 - {% block title %}Creators - Makenotwork{% endblock %}
4 - {% block body_attrs %} class="{{ crate::shell::measure(crate::shell::Measure::Wide) }} creators-page"{% endblock %}
5 -
6 - {% block content %}
7 - {% include "partials/site_header.html" %}
8 -
9 - <div class="container">
10 - <h1 class="page-title">Become a Creator</h1>
11 -
12 - <div class="page-intro">
13 - Anyone can sign up to browse and buy. To create projects and sell your work,
14 - apply for creator access. Most applications are approved within a few days.
15 - Makenotwork is in private alpha; we're approving applications one cohort at a time.
16 - </div>
17 -
18 - <div class="how-it-works">
19 - <h2 class="section-header">How It Works</h2>
20 - <ol>
21 - <li><strong>Sign up</strong> and verify your email</li>
22 - <li><strong>Apply</strong> from your dashboard: tell us what you make and which tier fits</li>
23 - <li><strong>Get approved</strong>: we review applications individually, usually within a few days</li>
24 - </ol>
25 -
26 - <p class="mt-section">
27 - We review applications to make sure applicants are here to share and sell creative work.
28 - If you make something and want to sell it, you'll likely get in.
29 - Link to your existing work (a portfolio, channel, or profile elsewhere) to speed things up.
30 - </p>
31 -
32 - <p class="mt-section">
33 - <strong>Important:</strong> You sell in the currency your Stripe account settles in, and receiving
34 - payouts requires a <a href="https://stripe.com/global">Stripe</a> account in a supported country that
35 - settles in one of the six we support: <strong>USD, CAD, GBP, AUD, NZD or EUR</strong>. Check both with
36 - Stripe before applying.
37 - </p>
38 - </div>
39 -
40 - <div class="stats-row">
41 - <div class="stat-box">
42 - <div class="number">{{ total_creators }}</div>
43 - <div class="label">Active Creators</div>
44 - </div>
45 - </div>
46 -
47 - <h2 class="section-header">Pricing</h2>
48 - <p class="mb-section">Flat monthly fee. 0% cut of your revenue. The only deduction from fan payments is the payment processor's fee (~3%).</p>
49 - <table class="wave-table" aria-label="Creator plan pricing">
50 - <thead>
51 - <tr>
52 - <th>Tier</th>
53 - <th>Monthly</th>
54 - <th>Best For</th>
55 - <th>Storage</th>
56 - </tr>
57 - </thead>
58 - <tbody>
59 - <tr>
60 - <td>Basic</td>
61 - <td>${{ tier_prices.basic_std }}</td>
62 - <td>Text, blogs, newsletters</td>
63 - <td>{{ tier_prices.basic_total }}</td>
64 - </tr>
65 - <tr>
66 - <td>Small Files</td>
67 - <td>${{ tier_prices.small_files_std }}</td>
68 - <td>Audio, plugins, small software</td>
69 - <td>{{ tier_prices.small_files_total }}</td>
70 - </tr>
71 - <tr>
72 - <td>Big Files</td>
73 - <td>${{ tier_prices.big_files_std }}</td>
74 - <td>Video, games, large software</td>
75 - <td>{{ tier_prices.big_files_total }}</td>
76 - </tr>
77 - <tr>
78 - <td>Everything</td>
79 - <td>${{ tier_prices.everything_std }}</td>
80 - <td>All features, current and future</td>
81 - <td>{{ tier_prices.everything_total }}</td>
82 - </tr>
83 - </tbody>
84 - </table>
85 - <p class="pricing-fineprint">
86 - Every tier is the complete platform: <code>/u/username</code> profile, project and item pages, project forum, Discover listing, memberships, pay-what-you-want, promo codes, RSS, analytics, full data export, 2FA/passkeys. The tier picks the file-size envelope, not the feature set.
87 - You sell in your Stripe account's currency (USD, CAD, GBP, AUD, NZD or EUR); receiving payouts requires <a href="https://stripe.com/global">Stripe</a> in a supported country.
88 - <a href="/docs/tiers">Full tier details</a> | <a href="/docs/pricing">Pricing models</a>
89 - </p>
90 -
91 - <p class="trial-callout">
92 - <strong>Not ready to commit?</strong> Request a <strong>free trial</strong> (2-6 weeks, no credit card) when you apply.
93 - Or <a href="/sandbox">try sandbox mode</a> to explore the dashboard without signing up.
94 - </p>
95 -
96 - <h2 class="section-header">Who Runs This</h2>
97 - <p>
98 - Makenotwork is built and operated by one person. No investors, no board, no outside pressure.
99 - Decisions are fast and aligned with creators, but there's no large team behind the scenes.
100 - Read the full picture in our
101 - <a href="/docs/guarantees#continuity">continuity guarantee</a> and
102 - <a href="/docs/economics">platform economics</a>.
103 - </p>
104 -
105 - <div class="cta">
106 - {% if is_creator %}
107 - <p>You have creator access.</p>
108 - <a href="/dashboard" class="btn-primary">Go to Dashboard</a>
109 - {% else %}
110 - {% if let Some(user) = session_user %}
111 - <p>Ready to create?</p>
112 - <a href="/dashboard?tab=settings&amp;section=creator" class="btn-primary">Apply from Dashboard</a>
113 - {% else %}
114 - <p>Join to get started.</p>
115 - <a href="/join" class="btn-primary">Join</a>
116 - <a href="/login" class="btn-secondary">Login</a>
117 - {% endif %}
118 - {% endif %}
119 - </div>
120 - </div>
121 - {% endblock %}