Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions canvas/src/app/pricing/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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>

Expand All @@ -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>

Expand Down
22 changes: 11 additions & 11 deletions canvas/src/components/__tests__/PricingTable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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();
});
Expand All @@ -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"),
Expand All @@ -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");
Expand All @@ -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");
Expand All @@ -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();
});
Expand All @@ -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(() => {
Expand Down
18 changes: 12 additions & 6 deletions canvas/src/lib/billing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -48,33 +52,35 @@ 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",
"All runtimes + plugins",
"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",
"Dedicated Fly Machine per tenant",
"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",
},
];

Expand Down
Loading