From de19cf9bae7492c79e124ddac5726797209f2452 Mon Sep 17 00:00:00 2001 From: Molecule AI Marketing Lead Date: Fri, 24 Apr 2026 03:11:43 +0000 Subject: [PATCH 1/2] fix(canvas): apply flat-rate pricing copy for Phase 34 launch (Issue #1833) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename "Starter" → "Team", update tagline + pricing page hero copy to lead with flat-rate per-org positioning — deliberate wedge against Cursor/Windsurf per-seat pricing ($40/seat vs $29/org). PMM decision: Issue #1833. Approved by Marketing Lead 2026-04-24. Co-Authored-By: Claude Sonnet 4.6 --- canvas/src/app/pricing/page.tsx | 14 +++++++++----- .../components/__tests__/PricingTable.test.tsx | 10 +++++----- canvas/src/lib/billing.ts | 18 ++++++++++++------ 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/canvas/src/app/pricing/page.tsx b/canvas/src/app/pricing/page.tsx index 061a7e60..a7327793 100644 --- a/canvas/src/app/pricing/page.tsx +++ b/canvas/src/app/pricing/page.tsx @@ -14,7 +14,7 @@ import { PricingTable } from "@/components/PricingTable"; export const metadata = { title: "Pricing — Molecule AI", description: - "Free while you tinker, paid tiers for shipping production multi-agent organizations. Transparent usage-based overage pricing on Pro.", + "Flat-rate team and org pricing — no per-seat fees. Free to start, $29/month for teams, $99/month for production orgs. Full runtime stack included on every paid tier.", }; export default function PricingPage() { @@ -25,9 +25,12 @@ export default function PricingPage() { Pricing

- Free while you tinker. Pay when you ship real agents to production. - Every tier includes the full runtime stack — you upgrade for scale, - support, and dedicated infrastructure. + One flat price per org — not per seat. Every paid tier includes the + full runtime stack. You upgrade for scale, support, and dedicated + infrastructure. +

+

+ 5-person team? You pay $29/month — not $200. No seat math, ever.

@@ -53,7 +56,8 @@ export default function PricingPage() { .

- Prices shown in USD. Enterprise / self-hosted licensing available — contact us. + Prices shown in USD. Flat-rate per org — no per-seat fees on any paid tier. + Enterprise / self-hosted licensing available — contact us.

diff --git a/canvas/src/components/__tests__/PricingTable.test.tsx b/canvas/src/components/__tests__/PricingTable.test.tsx index af5faec0..919dc788 100644 --- a/canvas/src/components/__tests__/PricingTable.test.tsx +++ b/canvas/src/components/__tests__/PricingTable.test.tsx @@ -50,14 +50,14 @@ describe("PricingTable", () => { it("renders all three plans with their CTAs", () => { render(); expect(screen.getByRole("heading", { name: "Free" })).toBeTruthy(); - expect(screen.getByRole("heading", { name: "Starter" })).toBeTruthy(); - expect(screen.getByRole("heading", { name: "Pro" })).toBeTruthy(); + expect(screen.getByRole("heading", { name: "Team" })).toBeTruthy(); + expect(screen.getByRole("heading", { name: "Growth" })).toBeTruthy(); expect(screen.getByRole("button", { name: "Get started" })).toBeTruthy(); - expect(screen.getByRole("button", { name: "Upgrade to Starter" })).toBeTruthy(); - expect(screen.getByRole("button", { name: "Upgrade to Pro" })).toBeTruthy(); + expect(screen.getByRole("button", { name: "Upgrade to Team" })).toBeTruthy(); + expect(screen.getByRole("button", { name: "Upgrade to Growth" })).toBeTruthy(); }); - it("shows the 'Most popular' badge only on the starter card", () => { + it("shows the 'Most popular' badge only on the Team card", () => { render(); const badges = screen.getAllByText("Most popular"); expect(badges.length).toBe(1); diff --git a/canvas/src/lib/billing.ts b/canvas/src/lib/billing.ts index c9260e61..b258a56a 100644 --- a/canvas/src/lib/billing.ts +++ b/canvas/src/lib/billing.ts @@ -32,6 +32,10 @@ export interface Plan { // plans is the canonical order shown on the pricing page: free → starter // → pro. Change the order here + the rendered columns follow. Keeping // this as a module-level const so tests can assert against a known list. +// +// Flat-rate positioning (Issue #1833): "starter" and "pro" are flat-rate +// per-org, not per-seat. This is a deliberate wedge against Cursor/Windsurf +// ($40/seat) — at 5 engineers the Team tier is 28% cheaper. export const plans: Plan[] = [ { id: "free", @@ -48,8 +52,8 @@ export const plans: Plan[] = [ }, { id: "starter", - name: "Starter", - tagline: "For small teams shipping real agents", + name: "Team", + tagline: "Flat-rate for teams — one price, no per-seat fees", price: "$29/month", features: [ "10 workspaces", @@ -57,14 +61,15 @@ export const plans: Plan[] = [ "Private Upstash Redis namespace", "Email support (48h)", "5M LLM tokens / month included", + "No per-seat pricing", ], - ctaLabel: "Upgrade to Starter", + ctaLabel: "Upgrade to Team", highlighted: true, }, { id: "pro", - name: "Pro", - tagline: "For production multi-agent orgs", + name: "Growth", + tagline: "Flat-rate for production multi-agent orgs", price: "$99/month", features: [ "Unlimited workspaces", @@ -72,9 +77,10 @@ export const plans: Plan[] = [ "Cross-workspace A2A audit log", "Priority support (24h)", "25M LLM tokens / month included", + "No per-seat pricing", "Usage-based overage billing", ], - ctaLabel: "Upgrade to Pro", + ctaLabel: "Upgrade to Growth", }, ]; From 62217250ed9a5689fe543e78ee95037d82a310ed Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Fri, 24 Apr 2026 13:01:40 -0700 Subject: [PATCH 2/2] =?UTF-8?q?test(pricing):=20finish=20Starter=E2=86=92T?= =?UTF-8?q?eam,=20Pro=E2=86=92Growth=20rename=20in=206=20stale=20assertion?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Marketing-lead agent's rename pass updated the "renders all three plans" test (lines 56-57) but missed lines 77, 94, 114, 132, 143, 158 which still referenced the pre-rename "Upgrade to Starter" / "Upgrade to Pro" button names. Canvas (Next.js) build failed with getByRole timeout because the component now says "Upgrade to Team" / "Upgrade to Growth". Internal PlanId tuple ("free" | "starter" | "pro") and startCheckout(planId) call are unchanged — only the user-facing button labels shifted, so assertions like startCheckout("pro", "acme") still match the server-side API. Verified locally: 9/9 PricingTable tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/components/__tests__/PricingTable.test.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/canvas/src/components/__tests__/PricingTable.test.tsx b/canvas/src/components/__tests__/PricingTable.test.tsx index 919dc788..535daeb7 100644 --- a/canvas/src/components/__tests__/PricingTable.test.tsx +++ b/canvas/src/components/__tests__/PricingTable.test.tsx @@ -74,7 +74,7 @@ describe("PricingTable", () => { it("Paid CTA + anonymous → bounces to signup (no checkout call)", async () => { mockedFetchSession.mockResolvedValue(null); render(); - fireEvent.click(screen.getByRole("button", { name: "Upgrade to Starter" })); + fireEvent.click(screen.getByRole("button", { name: "Upgrade to Team" })); await waitFor(() => expect(mockedRedirectToLogin).toHaveBeenCalledWith("sign-up")); expect(mockedStartCheckout).not.toHaveBeenCalled(); }); @@ -91,7 +91,7 @@ describe("PricingTable", () => { }); render(); - fireEvent.click(screen.getByRole("button", { name: "Upgrade to Pro" })); + fireEvent.click(screen.getByRole("button", { name: "Upgrade to Growth" })); await waitFor(() => expect(mockedStartCheckout).toHaveBeenCalledWith("pro", "acme"), @@ -111,7 +111,7 @@ describe("PricingTable", () => { mockedGetTenantSlug.mockReturnValue(""); render(); - fireEvent.click(screen.getByRole("button", { name: "Upgrade to Starter" })); + fireEvent.click(screen.getByRole("button", { name: "Upgrade to Team" })); await waitFor(() => { const alert = screen.getByRole("alert"); @@ -129,7 +129,7 @@ describe("PricingTable", () => { mockedStartCheckout.mockRejectedValue(new Error("checkout: 500 boom")); render(); - fireEvent.click(screen.getByRole("button", { name: "Upgrade to Pro" })); + fireEvent.click(screen.getByRole("button", { name: "Upgrade to Growth" })); await waitFor(() => { const alert = screen.getByRole("alert"); @@ -140,7 +140,7 @@ describe("PricingTable", () => { it("treats fetchSession network errors as anonymous (fail-closed to signup)", async () => { mockedFetchSession.mockRejectedValue(new Error("network down")); render(); - fireEvent.click(screen.getByRole("button", { name: "Upgrade to Starter" })); + fireEvent.click(screen.getByRole("button", { name: "Upgrade to Team" })); await waitFor(() => expect(mockedRedirectToLogin).toHaveBeenCalledWith("sign-up")); expect(mockedStartCheckout).not.toHaveBeenCalled(); }); @@ -155,7 +155,7 @@ describe("PricingTable", () => { mockedStartCheckout.mockReturnValue(new Promise(() => {})); render(); - const button = screen.getByRole("button", { name: "Upgrade to Pro" }); + const button = screen.getByRole("button", { name: "Upgrade to Growth" }); fireEvent.click(button); await waitFor(() => {