+ );
+}
diff --git a/web/app/[locale]/pricing/page.tsx b/web/app/[locale]/pricing/page.tsx
new file mode 100644
index 0000000000..6c55761875
--- /dev/null
+++ b/web/app/[locale]/pricing/page.tsx
@@ -0,0 +1,63 @@
+import Link from "next/link";
+import { APP_LOGIN_URL } from "@/lib/i18n/links";
+import { buildPageMetadata } from "@/lib/page-meta";
+
+export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
+ const { locale } = await params;
+ const isZh = locale === "zh";
+ return buildPageMetadata({
+ path: "/pricing",
+ locale,
+ title: isZh ? "价格 · Codewhale" : "Pricing · Codewhale",
+ description: isZh
+ ? "Codewhale 开源运行时免费。托管会员计费已实现但尚未对公众开放。"
+ : "The Codewhale open-source runtime is free. Hosted membership billing is built but not for sale yet.",
+ });
+}
+
+export default async function PricingPage({ params }: { params: Promise<{ locale: string }> }) {
+ const { locale } = await params;
+ const isZh = locale === "zh";
+ return (
+
+
+
+
{isZh ? "价格" : "Pricing"}
+
{isZh ? "现在没有可以付款的套餐。" : "There is nothing to pay for yet."}
+
+ {isZh
+ ? "开源运行时免费,自带模型密钥(BYOK)。托管计算的 $10/月会员已经写进产品,但生产环境的计费开关仍是休眠状态——没有结账按钮,也就不会误收一笔钱。"
+ : "The open-source runtime is free. Bring your own model key. The $10/month Member plan for hosted compute is implemented in the product, but production billing is dormant — so there is no checkout button that could charge anyone by accident."}
+
+
+
+ {isZh ? "开源 / 自托管" : "Open source / self-hosted"}
+ {isZh ? "免费。在你的机器上运行,使用你自己的模型密钥。" : "Free. Run on your machine with your own model keys."}
+
+
+ {isZh ? "托管会员" : "Hosted Member"}
+
+ {isZh
+ ? "$10/月、每期 $10 可结转的托管计算额度——已实现,尚未对公众销售。"
+ : "$10/month with $10 of rollover hosted-compute credits each paid period — built, not for sale to the public."}
+
+
+
+
+ {isZh
+ ? "模型用量由你连接的提供商计费,Codewhale 不加价。我们不会在计费休眠期间展示一个会失败的“立即购买”。"
+ : "Model tokens are billed by the provider you connect. Codewhale does not mark them up. We will not show a Buy button that 503s while billing is dormant."}
+
+ {legal.map((item) => {item.label})}
{GITEE_ENABLED && Gitee}
CNBnpmmirror
diff --git a/web/lib/docs-ia.test.ts b/web/lib/docs-ia.test.ts
index 2bc0090fad..6ade4a86be 100644
--- a/web/lib/docs-ia.test.ts
+++ b/web/lib/docs-ia.test.ts
@@ -17,6 +17,7 @@ import { contentLocalesForPath } from "./i18n/content-locales";
import { getChrome, getHome } from "./i18n/dictionaries";
import {
currentNavHref,
+ footerLegalLinks,
footerProductLinks,
footerProjectLinks,
navLinks as buildNavLinks,
@@ -88,7 +89,13 @@ describe("sitemap and hreflang preservation", () => {
});
it("keeps sitemap and hreflang output aligned with real translation coverage", () => {
- expect(sitemapEntries).toHaveLength(78);
+ expect(sitemapEntries).toHaveLength(84);
+ for (const path of ["/pricing", "/legal/terms", "/legal/privacy"]) {
+ expect(
+ sitemapEntries.some((entry) => entry.url === `${SITE_URL}/en${path}`),
+ path,
+ ).toBe(true);
+ }
for (const [path, expectedLocales] of [
["/", locales],
["/docs/guide", contentLocalesForPath("/docs/guide")],
@@ -199,6 +206,11 @@ describe("navigation parity and accessibility", () => {
`/${locale}/contribute`,
"https://github.com/Hmbown/CodeWhale/blob/main/LICENSE",
]);
+ expect(footerLegalLinks(locale).map((l) => l.href), `${locale} footer legal`).toEqual([
+ `/${locale}/pricing`,
+ `/${locale}/legal/terms`,
+ `/${locale}/legal/privacy`,
+ ]);
}
});
@@ -283,6 +295,7 @@ describe("navigation parity and accessibility", () => {
});
expect(footer).toContain("footerProductLinks(locale, chrome)");
expect(footer).toContain("footerProjectLinks(locale, chrome)");
+ expect(footer).toContain("footerLegalLinks(locale)");
});
});
diff --git a/web/lib/i18n/links.ts b/web/lib/i18n/links.ts
index c7cb4923c9..fe6ba8c58e 100644
--- a/web/lib/i18n/links.ts
+++ b/web/lib/i18n/links.ts
@@ -80,6 +80,15 @@ export function footerProjectLinks(locale: string, chrome: ChromeDict): ChromeLi
];
}
+/** Footer legal / pricing destinations — reachable, never a 404. */
+export function footerLegalLinks(locale: string): ChromeLink[] {
+ return [
+ { href: `/${locale}/pricing`, label: "Pricing" },
+ { href: `/${locale}/legal/terms`, label: "Terms" },
+ { href: `/${locale}/legal/privacy`, label: "Privacy" },
+ ];
+}
+
/**
* The one link in a chrome set that names the page currently being viewed.
*
diff --git a/web/lib/legal-copy.ts b/web/lib/legal-copy.ts
new file mode 100644
index 0000000000..3eb663fdbb
--- /dev/null
+++ b/web/lib/legal-copy.ts
@@ -0,0 +1,65 @@
+/** Binding legal text for Shannon Labs / Codewhale. Same body as app.codewhale.net/legal. */
+
+export const LEGAL_UPDATED = "July 23, 2026";
+
+export const TERMS_SECTIONS = [
+ {
+ title: "Your account",
+ body: "You are responsible for your account, connected providers, repositories, runners, and credentials. Keep access methods secure and provide accurate information. Do not share authority you are not permitted to grant.",
+ },
+ {
+ title: "Acceptable use",
+ body: "Do not use Codewhale to break the law, harm people or systems, evade access controls, distribute malware, interfere with the service, or process data you lack authority to use. Automated actions remain subject to the permissions, reviews, and stop conditions shown in the product.",
+ },
+ {
+ title: "Your content and connected services",
+ body: "You retain ownership of your content. You give Shannon Labs the limited permission needed to process it to provide Codewhale. Model, repository, compute, and other third-party providers have their own terms. You are responsible for charges you authorize with those providers.",
+ },
+ {
+ title: "Plans and charges",
+ body: "Features described as free do not create a Codewhale payment obligation. If paid Codewhale features become active, the product will show the price and obtain authorization before charging. Provider-billed model usage remains outside Codewhale charges.",
+ },
+ {
+ title: "Service changes and termination",
+ body: "We may change, suspend, or discontinue features and may restrict accounts that violate these terms or threaten the service. You may stop using Codewhale and use the available account deletion process. Required security, audit, deletion, and financial receipts may survive account deletion as described in the privacy policy.",
+ },
+ {
+ title: "Disclaimers and liability",
+ body: "Codewhale is provided on an “as is” and “as available” basis to the extent permitted by law. Software agents can make mistakes; review important changes and keep independent backups. Shannon Labs does not promise uninterrupted or error-free operation and is not responsible for third-party services outside its control.",
+ },
+ {
+ title: "Changes",
+ body: "We may update these terms. The effective date above identifies the current version. Continued use after an update means you accept the revised terms.",
+ },
+] as const;
+
+export const PRIVACY_SECTIONS = [
+ {
+ title: "Information we collect",
+ body: "We collect the identity information needed to create and protect your account, including your GitHub identity, display name, and a primary verified email when GitHub makes one available. We also store product information you create, such as preferences, projects, conversations, Work runs, approvals, and security or operational receipts.",
+ },
+ {
+ title: "How we use it",
+ body: "We use this information to authenticate you, operate and secure Codewhale, preserve your requested product state, provide support, and—only when you have enabled the relevant communication—send product or waitlist updates. We do not sell personal information.",
+ },
+ {
+ title: "Storage and processing",
+ body: "Codewhale is one global product. There is no residency selector, no region-specific account, and no promise that your account data stays in a particular jurisdiction. Account authentication and session state is stored in Cloudflare Durable Objects, today configured in Cloudflare’s US jurisdiction; that placement is an operational choice we may change, and we will update this policy when we do. Durable product state is owned by Codewhale’s private product runtime. Cloudflare may perform TLS, request routing, and cryptographic processing on its global edge, so we do not describe edge processing as US-only. Hosted compute is a separate system from account storage and, when enabled, identifies its placement before launch.",
+ },
+ {
+ title: "Model providers and repositories",
+ body: "Codewhale sends content to a model provider only when you choose or connect that provider. Bring-your-own provider credentials remain separate from Codewhale billing. Repository access is limited to the grants you approve with the source provider.",
+ },
+ {
+ title: "Retention and deletion",
+ body: "You can review export and deletion controls after signing in under Account, Data & privacy. Content is deleted according to the displayed retention and deletion process. Privacy-minimal security, audit, deletion, and financial receipts may be retained when necessary to prove an action, prevent abuse, or meet legal obligations. Provider-backed resources are not treated as deleted until the provider confirms deletion.",
+ },
+ {
+ title: "Questions and requests",
+ body: "Use Account, Data & privacy after signing in to request an export or deletion. If you cannot sign in, use the public support channel shown in the product once it is verified. Codewhale does not present an unverified mailbox as monitored.",
+ },
+ {
+ title: "Changes",
+ body: "We may update this policy as the product and its processors change. The effective date above identifies the version that applies.",
+ },
+] as const;
diff --git a/web/lib/llms-txt.test.ts b/web/lib/llms-txt.test.ts
index ffade42950..8b4de3073e 100644
--- a/web/lib/llms-txt.test.ts
+++ b/web/lib/llms-txt.test.ts
@@ -35,6 +35,9 @@ describe("llms.txt", () => {
"/en/community",
"/en/contribute",
"/en/docs",
+ "/en/pricing",
+ "/en/legal/terms",
+ "/en/legal/privacy",
]) {
expect(body, path).toContain(`${SITE_URL}${path}`);
}
diff --git a/web/lib/llms-txt.ts b/web/lib/llms-txt.ts
index 6848a54948..cb069d4dc8 100644
--- a/web/lib/llms-txt.ts
+++ b/web/lib/llms-txt.ts
@@ -56,6 +56,21 @@ const EXTRA_PAGES: readonly { path: string; title: string; description: string }
title: "Contribute",
description: "The pull-request workflow: scoped issue, fork, test the change, explain the result.",
},
+ {
+ path: "/pricing",
+ title: "Pricing",
+ description: "The open-source runtime is free. Hosted membership billing is built but not for sale yet.",
+ },
+ {
+ path: "/legal/terms",
+ title: "Terms of service",
+ description: "Terms that govern your use of Codewhale, a Shannon Labs product.",
+ },
+ {
+ path: "/legal/privacy",
+ title: "Privacy policy",
+ description: "How Shannon Labs handles information when you use Codewhale.",
+ },
];
function topicSitePath(topic: (typeof DOC_TOPICS)[number]): string | null {
diff --git a/web/lib/page-meta.test.ts b/web/lib/page-meta.test.ts
index 27fa0ede54..9f383a5c3a 100644
--- a/web/lib/page-meta.test.ts
+++ b/web/lib/page-meta.test.ts
@@ -112,6 +112,9 @@ describe("page metadata", () => {
["faq", "/faq"],
["feed", "/feed"],
["roadmap", "/roadmap"],
+ ["pricing", "/pricing"],
+ ["legal/terms", "/legal/terms"],
+ ["legal/privacy", "/legal/privacy"],
]) {
const source = readFileSync(
new URL(`../app/[locale]/${route}/page.tsx`, import.meta.url),
diff --git a/web/lib/public-billing-legal-routes.test.ts b/web/lib/public-billing-legal-routes.test.ts
new file mode 100644
index 0000000000..8c23418739
--- /dev/null
+++ b/web/lib/public-billing-legal-routes.test.ts
@@ -0,0 +1,31 @@
+import { existsSync } from "node:fs";
+import { describe, expect, it } from "vitest";
+import { footerLegalLinks } from "./i18n/links";
+import { LEGAL_UPDATED, PRIVACY_SECTIONS, TERMS_SECTIONS } from "./legal-copy";
+
+const webRoot = new URL("../", import.meta.url);
+
+describe("marketing pricing and legal routes", () => {
+ it("ships real pages for the URLs that used to 404", () => {
+ for (const path of [
+ "app/[locale]/pricing/page.tsx",
+ "app/[locale]/legal/terms/page.tsx",
+ "app/[locale]/legal/privacy/page.tsx",
+ "app/[locale]/privacy/page.tsx",
+ "app/[locale]/terms/page.tsx",
+ ]) {
+ expect(existsSync(new URL(path, webRoot)), path).toBe(true);
+ }
+ });
+
+ it("aliases /privacy and /terms onto the legal paths instead of inventing a second policy", () => {
+ expect(footerLegalLinks("en").map((l) => l.href)).toEqual([
+ "/en/pricing",
+ "/en/legal/terms",
+ "/en/legal/privacy",
+ ]);
+ expect(LEGAL_UPDATED).toBe("July 23, 2026");
+ expect(TERMS_SECTIONS.some((s) => s.title === "Plans and charges")).toBe(true);
+ expect(PRIVACY_SECTIONS.some((s) => s.title === "Retention and deletion")).toBe(true);
+ });
+});