- Support the build. Get coins early.
+ Help keep Atomic Notes independent.
- Atomic Notes is built by one developer, with no ads and no trackers. Coins aren't sold in the app
- yet. Support the project on Patreon or Ko-fi, and the developer sends Atomic Coins to your account as an
- early-supporter reward.
+ Atomic Notes is a source-available, local-first notes app built by one developer, with no ads, no
+ trackers, and no AI. Your support funds sync infrastructure, security work, real-device testing, and
+ future releases. As a thank-you, supporters get Atomic Coins.
+
Your notes stay yours, whether or not you support.
+ Pick an amount and see your Atomic Coins. $2, $4, and $10 are the monthly memberships on Patreon. Any
+ other amount is a one-time payment on Ko-fi.
+
+
+
+
+
+
+
+
+
+
+
MONTHLY ON PATREON
+
+ Choose your orbit.
+
+
+ Three memberships, each with Atomic Coins for every month that's paid. Cancel on Patreon at any time.
+
- The reward is sent to the email you write, not to your Patreon or Ko-fi name. A wrong or misspelled
- email means the coins can't reach you. If you signed in with more than one Google account, use the
- one that holds your notes. On Ko-fi, mark the message private so your email isn't shown on the
- public page.
+ Coins go to the email you write, not to your Patreon or Ko-fi name. A wrong or misspelled email means
+ the coins can't reach you. If you signed in with more than one Google account, use the one that
+ holds your notes. On Ko-fi, mark the message private so your email isn't shown on the public page.
@@ -165,7 +278,7 @@ export default function SupportPage() {
Patreon or Ko-fi handles the payment. Atomic Notes never sees your card, PayPal, or bank details.
Each platform charges its own fees, under its own terms.
-
The developer sends rewards by hand, so they can take a little time to arrive.
+
Coins are added by hand after the payment is checked, so they can take a little time.
Writing notes stays free. Coins only buy sync energy and room for more notes.
@@ -197,14 +310,40 @@ export default function SupportPage() {
+ A monthly membership: {usd} a month and {q.coins} Atomic Coins each paid month. Cancel any time on
+ Patreon. After joining, send the message with your Atomic Notes account email. Coins are added by hand
+ after the developer checks the payment.
+
+ A one-time payment. On Ko-fi, enter {usd}, paste the message, add your Atomic Notes account email, and
+ mark it private. Coins are added by hand after the developer checks the payment.
+
+ >
+ )}
+
PAYMENT ON {where.toUpperCase()} · THIS PAGE ONLY PREVIEWS THE REWARD
+
+
+
+
+ );
+}
diff --git a/src/lib/content.ts b/src/lib/content.ts
index 29b4490..208a99f 100644
--- a/src/lib/content.ts
+++ b/src/lib/content.ts
@@ -56,6 +56,8 @@ export const REPO_URL = "https://github.com/DevBehindYou/Atomic-Notes-App-V0.2";
/** Where supporters go. Early supporters get Atomic Coins by hand (see /support-atomic-notes). */
export const PATREON_URL = "https://www.patreon.com/cw/DevBehindYou";
+/** Patreon's membership page, where a supporter picks a tier. */
+export const PATREON_JOIN_URL = "https://www.patreon.com/16654968/join";
export const KOFI_URL = "https://ko-fi.com/devbehindyou";
/** The answer-first definition, used in the page and in structured data. */
diff --git a/src/lib/supportPricing.ts b/src/lib/supportPricing.ts
new file mode 100644
index 0000000..3c4980f
--- /dev/null
+++ b/src/lib/supportPricing.ts
@@ -0,0 +1,139 @@
+/**
+ * Support rewards: Atomic Coins for each whole-dollar amount from $1 to $10, and where to pay.
+ *
+ * This is a versioned table, not a formula. Amounts are in integer US cents.
+ * - $2, $4 and $10 are the Patreon memberships (Spark, Catalyst, Reactor): the same coins each paid month.
+ * - Every other amount is a one-time payment on Ko-fi, where the supporter enters the amount.
+ *
+ * The page only previews rewards. Coins are granted by hand on the Server, after the payment is
+ * checked, against the amount actually paid. Change a reward here and bump the version.
+ */
+
+export const SUPPORT_PRICING_VERSION = "2026-10-11.v2";
+
+/** Coins per dollar before any bonus. */
+export const BASE_COINS_PER_DOLLAR = 10;
+
+export type MembershipId = "spark" | "catalyst" | "reactor";
+export type Channel = "patreon" | "kofi";
+
+export type RewardStep = {
+ cents: number;
+ coins: number;
+ /** Set when this amount is a Patreon membership; otherwise it's a one-time Ko-fi payment. */
+ membership?: MembershipId;
+};
+
+export const REWARD_STEPS: readonly RewardStep[] = [
+ { cents: 100, coins: 10 },
+ { cents: 200, coins: 20, membership: "spark" },
+ { cents: 300, coins: 30 },
+ { cents: 400, coins: 50, membership: "catalyst" },
+ { cents: 500, coins: 65 },
+ { cents: 600, coins: 80 },
+ { cents: 700, coins: 95 },
+ { cents: 800, coins: 110 },
+ { cents: 900, coins: 130 },
+ { cents: 1000, coins: 150, membership: "reactor" },
+];
+
+export const MIN_CENTS = REWARD_STEPS[0].cents;
+export const MAX_CENTS = REWARD_STEPS[REWARD_STEPS.length - 1].cents;
+export const STEP_CENTS = 100;
+export const DEFAULT_CENTS = 400;
+export const PRESET_CENTS: readonly number[] = [200, 400, 1000];
+
+export type Quote = {
+ cents: number;
+ coins: number;
+ bonusCoins: number;
+ channel: Channel;
+ membership: Membership | null;
+ version: string;
+};
+
+export type Membership = {
+ id: MembershipId;
+ level: string;
+ name: string;
+ tagline: string;
+ cents: number;
+ coins: number;
+ impact: string;
+ benefits: string[];
+ cta: string;
+ cover: string;
+ badge?: string;
+};
+
+export const MEMBERSHIPS: readonly Membership[] = [
+ {
+ id: "spark",
+ level: "01",
+ name: "Atomic Spark",
+ tagline: "Ignite the build",
+ cents: 200,
+ coins: 20,
+ impact: "A small, steady push that keeps independent development moving.",
+ benefits: ["20 Atomic Coins each paid month", "Supporter development updates"],
+ cta: "Ignite the spark",
+ cover: "/support/atomic-spark-cover.webp",
+ },
+ {
+ id: "catalyst",
+ level: "02",
+ name: "Atomic Catalyst",
+ tagline: "Keep the orbit moving",
+ cents: 400,
+ coins: 50,
+ impact: "Helps cover sync infrastructure, debugging, and real-device testing.",
+ benefits: ["50 Atomic Coins each paid month", "Supporter development updates"],
+ cta: "Become a catalyst",
+ cover: "/support/atomic-catalyst-cover.webp",
+ badge: "Good balance",
+ },
+ {
+ id: "reactor",
+ level: "03",
+ name: "Atomic Reactor",
+ tagline: "Power the build",
+ cents: 1000,
+ coins: 150,
+ impact: "Backs the deep work: security, sync reliability, testing, and future releases.",
+ benefits: [
+ "150 Atomic Coins each paid month",
+ "Supporter development updates",
+ "A personal thank-you from the developer when you join",
+ "Your feature ideas read when planning what comes next",
+ ],
+ cta: "Power the reactor",
+ cover: "/support/atomic-reactor-cover.webp",
+ },
+];
+
+/** The reward and payment route for an exact amount. Throws for any amount not in the table. */
+export function quote(cents: number): Quote {
+ const step = REWARD_STEPS.find((s) => s.cents === cents);
+ if (!Number.isSafeInteger(cents) || !step) throw new Error(`Unsupported contribution amount: ${cents}`);
+ const membership = step.membership ? (MEMBERSHIPS.find((m) => m.id === step.membership) ?? null) : null;
+ return {
+ cents,
+ coins: step.coins,
+ bonusCoins: Math.max(0, step.coins - (step.cents / 100) * BASE_COINS_PER_DOLLAR),
+ channel: membership ? "patreon" : "kofi",
+ membership,
+ version: SUPPORT_PRICING_VERSION,
+ };
+}
+
+export function formatUsd(cents: number): string {
+ return `$${(cents / 100).toFixed(2)}`;
+}
+
+/** The text a supporter sends with their payment, so the reward can be matched by hand. */
+export function supportMessage(q: Quote): string {
+ const what = q.membership
+ ? `${q.membership.name} membership (${formatUsd(q.cents)} a month, ${q.coins} Atomic Coins each paid month)`
+ : `${formatUsd(q.cents)} one-time support for ${q.coins} Atomic Coins`;
+ return `Atomic Notes: ${what}, rewards ${q.version}. My Atomic Notes account email: `;
+}
diff --git a/tests/support-pricing.test.mjs b/tests/support-pricing.test.mjs
new file mode 100644
index 0000000..d4db240
--- /dev/null
+++ b/tests/support-pricing.test.mjs
@@ -0,0 +1,75 @@
+// Run with: npm run test:pricing (Node 22+, using --experimental-strip-types).
+import { test } from "node:test";
+import assert from "node:assert/strict";
+import {
+ DEFAULT_CENTS,
+ MEMBERSHIPS,
+ PRESET_CENTS,
+ REWARD_STEPS,
+ formatUsd,
+ quote,
+ supportMessage,
+} from "../src/lib/supportPricing.ts";
+
+// The table from 03_COIN_PRICING_IMPLEMENTATION.md, confirmed by the owner on 2026-10-11.
+const TABLE = { 100: 10, 200: 20, 300: 30, 400: 50, 500: 65, 600: 80, 700: 95, 800: 110, 900: 130, 1000: 150 };
+
+test("every whole-dollar amount from $1 to $10 gives the exact table reward", () => {
+ for (const [cents, coins] of Object.entries(TABLE)) assert.equal(quote(Number(cents)).coins, coins, `${cents} cents`);
+});
+
+test("bonus is measured against 10 coins per dollar", () => {
+ assert.deepEqual(
+ Object.keys(TABLE).map((c) => quote(Number(c)).bonusCoins),
+ [0, 0, 0, 10, 15, 20, 25, 30, 40, 50],
+ );
+});
+
+test("$2, $4 and $10 route to the Patreon memberships", () => {
+ assert.deepEqual(
+ [200, 400, 1000].map((c) => [quote(c).channel, quote(c).membership?.name]),
+ [
+ ["patreon", "Atomic Spark"],
+ ["patreon", "Atomic Catalyst"],
+ ["patreon", "Atomic Reactor"],
+ ],
+ );
+});
+
+test("every other amount routes to a one-time Ko-fi payment", () => {
+ for (const cents of [100, 300, 500, 600, 700, 800, 900]) {
+ const q = quote(cents);
+ assert.equal(q.channel, "kofi");
+ assert.equal(q.membership, null);
+ }
+});
+
+test("membership rewards equal the table rewards for the same price", () => {
+ for (const m of MEMBERSHIPS) assert.equal(m.coins, TABLE[m.cents], m.name);
+});
+
+test("amounts outside the table are rejected, never interpolated", () => {
+ for (const cents of [0, 50, 150, 450, 1100, -100, 400.5, Number.NaN]) {
+ assert.throws(() => quote(cents), /Unsupported/);
+ }
+});
+
+test("the table covers every dollar from $1 to $10, in order", () => {
+ assert.deepEqual(REWARD_STEPS.map((s) => s.cents), Object.keys(TABLE).map(Number));
+});
+
+test("presets are the three memberships, and the default is Catalyst", () => {
+ assert.deepEqual(PRESET_CENTS, [200, 400, 1000]);
+ assert.equal(quote(DEFAULT_CENTS).membership?.id, "catalyst");
+});
+
+test("the copied message names the route, amount, coins, and version", () => {
+ assert.match(supportMessage(quote(700)), /\$7\.00 one-time support for 95 Atomic Coins, rewards 2026-10-11\.v2/);
+ assert.match(supportMessage(quote(400)), /Atomic Catalyst membership \(\$4\.00 a month, 50 Atomic Coins each paid month\)/);
+ assert.match(supportMessage(quote(100)), /account email: $/);
+ assert.equal(formatUsd(1000), "$10.00");
+});
+
+test("copy never calls Atomic Notes open source", () => {
+ assert.doesNotMatch(JSON.stringify(MEMBERSHIPS), /open[ -]source/i);
+});