Merge pull request #2045 from Molecule-AI/feat/flat-rate-pricing-1833

feat(canvas): flat-rate pricing — rename Starter→Team, Pro→Growth (Issue #1833)
This commit is contained in:
Hongming Wang 2026-04-25 05:54:06 +00:00 committed by GitHub
commit 06c85bd185
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 32 additions and 22 deletions

View File

@ -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
</h1>
<p className="mx-auto mt-4 max-w-2xl text-lg text-zinc-300">
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.
</p>
<p className="mx-auto mt-2 max-w-xl text-sm text-zinc-400">
5-person team? You pay $29/month not $200. No seat math, ever.
</p>
</div>
@ -53,7 +56,8 @@ export default function PricingPage() {
.
</p>
<p className="mt-6 text-sm text-zinc-500">
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.
</p>
</section>

View File

@ -50,14 +50,14 @@ describe("PricingTable", () => {
it("renders all three plans with their CTAs", () => {
render(<PricingTable />);
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(<PricingTable />);
const badges = screen.getAllByText("Most popular");
expect(badges.length).toBe(1);
@ -74,7 +74,7 @@ describe("PricingTable", () => {
it("Paid CTA + anonymous → bounces to signup (no checkout call)", async () => {
mockedFetchSession.mockResolvedValue(null);
render(<PricingTable />);
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(<PricingTable />);
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(<PricingTable />);
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(<PricingTable />);
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(<PricingTable />);
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(<PricingTable />);
const button = screen.getByRole("button", { name: "Upgrade to Pro" });
const button = screen.getByRole("button", { name: "Upgrade to Growth" });
fireEvent.click(button);
await waitFor(() => {

View File

@ -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",
},
];