diff --git a/package.json b/package.json index 4e41f298..44210ea7 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "test": "npm run test:plain && npm run test:ts", "test:security-headers": "node --experimental-strip-types --test tests/response-security.test.ts", "test:plain": "node --experimental-strip-types --test tests/adversarial-harness.test.ts tests/trust.test.ts tests/release-signing.test.ts tests/cli-install.test.ts tests/trust-attestation.test.ts", - "test:ts": "node --experimental-strip-types --test tests/site-stats.test.ts tests/certify.test.ts tests/prompt-injection-guard.test.ts tests/audit-skills.test.ts tests/runtime.test.ts tests/integrations.test.ts tests/growth-revenue-split.test.ts tests/trust-badge.test.ts tests/bounties.test.ts tests/trust-scoring.test.ts tests/adversarial-holdout.test.ts tests/adversarial-judge.test.ts tests/paired-benchmark.test.ts tests/format-rubric.test.ts tests/lang-detect.test.ts tests/university.test.ts tests/package-signature.test.ts", + "test:ts": "node --experimental-strip-types --test tests/site-stats.test.ts tests/certify.test.ts tests/prompt-injection-guard.test.ts tests/audit-skills.test.ts tests/runtime.test.ts tests/integrations.test.ts tests/growth-revenue-split.test.ts tests/trust-badge.test.ts tests/bounties.test.ts tests/trust-scoring.test.ts tests/adversarial-holdout.test.ts tests/adversarial-judge.test.ts tests/paired-benchmark.test.ts tests/format-rubric.test.ts tests/lang-detect.test.ts tests/university.test.ts tests/package-signature.test.ts tests/pricing-copy.test.ts", "test:all": "node --experimental-strip-types --test tests/*.test.ts", "lint:strict": "eslint .", "check:navigation": "node scripts/check-navigation.mjs", diff --git a/src/components/StripeEmbeddedCheckout.tsx b/src/components/StripeEmbeddedCheckout.tsx index 3d4004dd..a758bc91 100644 --- a/src/components/StripeEmbeddedCheckout.tsx +++ b/src/components/StripeEmbeddedCheckout.tsx @@ -7,10 +7,16 @@ import { createCheckoutSession } from "@/lib/payments.functions"; interface Props { priceId: string; quantity?: number; + adjustableQuantity?: boolean; returnUrl?: string; } -export function StripeEmbeddedCheckout({ priceId, quantity, returnUrl }: Props) { +export function StripeEmbeddedCheckout({ + priceId, + quantity, + adjustableQuantity, + returnUrl, +}: Props) { const [error, setError] = useState(null); const fetchClientSecret = useCallback(async (): Promise => { @@ -19,6 +25,7 @@ export function StripeEmbeddedCheckout({ priceId, quantity, returnUrl }: Props) data: { priceId, quantity, + adjustableQuantity, returnUrl: returnUrl || window.location.href, environment: getStripeEnvironment(), }, @@ -31,7 +38,7 @@ export function StripeEmbeddedCheckout({ priceId, quantity, returnUrl }: Props) setError(message); throw new Error(message); } - }, [priceId, quantity, returnUrl]); + }, [priceId, quantity, adjustableQuantity, returnUrl]); if (error) { return ( diff --git a/src/components/site/Nav.tsx b/src/components/site/Nav.tsx index 0dbddfa7..109b1978 100644 --- a/src/components/site/Nav.tsx +++ b/src/components/site/Nav.tsx @@ -53,7 +53,7 @@ const SKILLS: NavItem[] = [ const AGENTS_MENU: NavItem[] = [ { to: "/agents", label: "Agent Store", hint: "Ready-to-use corporate agents" }, - { to: "/agents/new", label: "Build an agent", hint: "Agent Factory (Agent Pass)" }, + { to: "/agents/new", label: "Build an agent", hint: "Agent Factory (Pro)" }, ]; const PUBLISH: NavItem[] = [ diff --git a/src/components/site/home/AgentFactory.tsx b/src/components/site/home/AgentFactory.tsx index 3875cdfe..15ad8e33 100644 --- a/src/components/site/home/AgentFactory.tsx +++ b/src/components/site/home/AgentFactory.tsx @@ -123,12 +123,12 @@ export function AgentFactory() { } className="inline-flex h-11 w-full items-center justify-center rounded-md border border-border px-5 text-sm font-medium transition-colors hover:bg-accent sm:w-auto" > - See Agent Pass + See Pro

- Custom agents are included in Agent Pass. Download as ZIP, single markdown file, or - install straight into your agent over MCP. + Custom agents are included in Pro. Download as ZIP, single markdown file, or install + straight into your agent over MCP.

diff --git a/src/components/site/home/Faq.tsx b/src/components/site/home/Faq.tsx index c487f4e0..e21aeb3d 100644 --- a/src/components/site/home/Faq.tsx +++ b/src/components/site/home/Faq.tsx @@ -1,6 +1,7 @@ import { Link } from "@tanstack/react-router"; import { useState } from "react"; import { + PRICE_AGENT_PASS, PRICE_MONTHLY, PRICE_YEARLY, PRICE_YEARLY_LIST, @@ -87,8 +88,10 @@ const ITEMS: { q: string; a: React.ReactNode }[] = [ Browsing and installing public capabilities is free — no account needed. Pro is{" "} {PRICE_YEARLY_LIST}{" "} {PRICE_YEARLY} per year ( - {PRO_YEARLY_DISCOUNT_PCT}% off) or {PRICE_MONTHLY} per month, and includes everything: the - Agent Factory, the University, unlimited reviews and the full registry.{" "} + {PRO_YEARLY_DISCOUNT_PCT}% off) or {PRICE_MONTHLY} per month for the human builder seat + (Agent Factory, Store, University, unlimited reviews) and includes 1 production agent. Extra + agents are Agent Pass at {PRICE_AGENT_PASS} per agent per month (standalone, monthly only — + no Pro required).{" "} See pricing diff --git a/src/components/site/home/PlansTeaser.tsx b/src/components/site/home/PlansTeaser.tsx index e6b2b547..678e4cdb 100644 --- a/src/components/site/home/PlansTeaser.tsx +++ b/src/components/site/home/PlansTeaser.tsx @@ -1,21 +1,29 @@ import { Link } from "@tanstack/react-router"; import { Check } from "lucide-react"; -import { PRICE_MONTHLY, PRICE_YEARLY, PRICE_YEARLY_LIST, SAVE_BADGE } from "@/lib/pricing-copy"; +import { + PRICE_AGENT_PASS, + PRICE_MONTHLY, + PRICE_YEARLY, + PRICE_YEARLY_LIST, + SAVE_BADGE, +} from "@/lib/pricing-copy"; -// Keep in sync with /pricing: Free, Pro, Enterprise. -const INCLUDED = [ +const PRO_INCLUDED = [ + "1 production agent included", + "Agent Factory, Agent Store and SAK University", "Unlimited skill reviews with full graded reports", - "The Agent Factory — build a custom corporate agent from a prompt", - "The Agent Store — 33 ready-made agents with soul, guardrails and skills", - "SAK University — diagnosis, adaptive curriculum, residency, credentials", "Adversarial evaluation tools and version-specific reports", - "Private packages, signed releases and mutual NDA on request", +]; + +const AGENT_PASS_INCLUDED = [ + "Standalone — no Pro required", + "Monthly only — unlimited upgrades per agent", + "SkillForge AI and weekly report per seat", ]; /** - * One decision, not three: Pro includes everything, billed yearly by default - * with a monthly escape hatch. The free registry is stated as a fact, not as - * a competing column. + * Landing teaser: Free + Pro + Agent Pass, with Enterprise as a footnote. + * Keep amounts in sync with /pricing via pricing-copy. */ export function PlansTeaser() { return ( @@ -24,34 +32,16 @@ export function PlansTeaser() {
Pricing

- One plan. Everything included. + Free, Pro, and Agent Pass.

- No tiers to compare and no add-ons. Browsing and installing public capabilities stays - free forever — Pro is for when you want your own capabilities tested, built and kept - current. + Public capabilities stay free forever. Pro includes 1 production agent. Agent Pass is + standalone (monthly only — no Pro required) for extra agents or agent-only use.

-
-
-
- Everything in Super Agent Skill -
-
    - {INCLUDED.map((f) => ( -
  • - - {f} -
  • - ))} -
-

- Solo or team. Cancel in one click. Everything you create stays yours. -

-
- -
+
+
Pro · billed yearly
@@ -66,15 +56,51 @@ export function PlansTeaser() { {SAVE_BADGE}

- Instead of {PRICE_YEARLY_LIST} / year billed - monthly at {PRICE_MONTHLY}, which stays available if you prefer the flexibility. + Or {PRICE_MONTHLY}/month. Human seat + 1 production agent: Factory, Store, University, + unlimited reviews.

+
    + {PRO_INCLUDED.map((f) => ( +
  • + + {f} +
  • + ))} +
Get Pro → +
+ +
+
+ Agent Pass · monthly · standalone +
+
+ {PRICE_AGENT_PASS} + / agent / month +
+

+ No Pro required. Buy Agent Pass alone if you only run agents — or add seats beyond + Pro's included 1. +

+
    + {AGENT_PASS_INCLUDED.map((f) => ( +
  • + + {f} +
  • + ))} +
+ + Get Agent Pass → + p.lookup_key === "sak_pro_monthly") ?? prices.data[0] ?? undefined; if (price) { out.price = { id: price.id, diff --git a/src/lib/checkout-quantity.ts b/src/lib/checkout-quantity.ts new file mode 100644 index 00000000..63e0043b --- /dev/null +++ b/src/lib/checkout-quantity.ts @@ -0,0 +1,24 @@ +/** Pure checkout quantity helpers (unit-tested; used by createCheckoutSession). */ + +export function validateCheckoutQuantity(quantity: unknown): number { + if (quantity === undefined || quantity === null) return 1; + if ( + typeof quantity !== "number" || + !Number.isInteger(quantity) || + quantity < 1 || + quantity > 100 + ) { + throw new Error("quantity must be an integer between 1 and 100"); + } + return quantity; +} + +export function shouldEnableAdjustableQuantity(input: { + priceId: string; + quantity?: number; + adjustableQuantity?: boolean; +}): boolean { + if (typeof input.adjustableQuantity === "boolean") return input.adjustableQuantity; + if (input.priceId.startsWith("sak_agent_pass_")) return true; + return typeof input.quantity === "number" && input.quantity > 0; +} diff --git a/src/lib/crm/copy.ts b/src/lib/crm/copy.ts index 1aabd972..804597c8 100644 --- a/src/lib/crm/copy.ts +++ b/src/lib/crm/copy.ts @@ -6,6 +6,7 @@ import type { CrmSnapshot } from "@/lib/crm/types"; import type { TriggerId } from "@/lib/crm/segments"; import { PATTERN_LABELS, patternHook, personalizedBullets } from "@/lib/crm/tool-profile"; +import { PRICE_MONTHLY, PRICE_YEARLY, PRO_PRICING } from "@/lib/pricing-copy"; export type CrmMetric = { label: string; value: string; note?: string }; @@ -181,12 +182,12 @@ export function buildMessage(trigger: TriggerId, s: CrmSnapshot): CrmMessage { return { trigger, subject: s.roi.headroom_monthly_usd - ? `Pro pays for itself ${Math.max(1, Math.round(s.roi.headroom_monthly_usd / 19))}x on your current usage` + ? `Pro pays for itself ${Math.max(1, Math.round(s.roi.headroom_monthly_usd / PRO_PRICING.monthly))}x on your current usage` : "Pro, priced against your own numbers", preheader: "Batch reviews, the Agent Store and unlimited residency rounds.", heading: "You are already past the break-even point", intro: [ - `You have run ${num(s.usage.reviews)} reviews. Pro is $19/month (or $140/year) and removes the limits you are hitting.`, + `You have run ${num(s.usage.reviews)} reviews. Pro is ${PRICE_MONTHLY}/month (or ${PRICE_YEARLY}/year) and removes the limits you are hitting.`, ...(s.roi.headroom_monthly_usd ? [ `Your own numbers show about ${usd(s.roi.headroom_monthly_usd)}/month of avoidable spend still in your skills.`, @@ -194,10 +195,10 @@ export function buildMessage(trigger: TriggerId, s: CrmSnapshot): CrmMessage { : []), ], metrics: [ - { label: "Pro monthly", value: "$19/month" }, + { label: "Pro monthly", value: `${PRICE_MONTHLY}/month` }, { label: "Pro yearly", - value: "$140/year", + value: `${PRICE_YEARLY}/year`, note: "auto-renews at the same discounted rate", }, ...roiMetrics(s).slice(0, 2), @@ -238,7 +239,7 @@ export function buildMessage(trigger: TriggerId, s: CrmSnapshot): CrmMessage { ? [{ label: `Where they land in ${tool.label}`, value: tool.path }] : [{ label: "Agent tools supported", value: "15" }]), { label: "How you use SAK", value: PATTERN_LABELS[s.pattern] }, - { label: "Pro", value: "$19/month", note: "or $140/year" }, + { label: "Pro", value: `${PRICE_MONTHLY}/month`, note: `or ${PRICE_YEARLY}/year` }, ...roiMetrics(s).slice(0, 1), ], bullets: personalizedBullets(s.pattern, tool), diff --git a/src/lib/payments.functions.ts b/src/lib/payments.functions.ts index ef00de19..474a61d5 100644 --- a/src/lib/payments.functions.ts +++ b/src/lib/payments.functions.ts @@ -1,10 +1,23 @@ import { createServerFn } from "@tanstack/react-start"; import { requireSupabaseAuth } from "@/integrations/supabase/auth-middleware"; +import { + shouldEnableAdjustableQuantity, + validateCheckoutQuantity, +} from "@/lib/checkout-quantity"; import { type StripeEnv, createStripeClient, getStripeErrorMessage } from "@/lib/stripe.server"; export type CheckoutSessionResult = { clientSecret: string } | { error: string }; export type PortalSessionResult = { url: string } | { error: string }; +export type CheckoutSessionInput = { + priceId: string; + quantity?: number; + /** When true, Checkout lets the buyer change quantity (1–100). */ + adjustableQuantity?: boolean; + returnUrl: string; + environment: StripeEnv; +}; + async function resolveOrCreateCustomer( stripe: ReturnType, options: { email?: string; userId?: string }, @@ -40,12 +53,11 @@ async function resolveOrCreateCustomer( export const createCheckoutSession = createServerFn({ method: "POST" }) .middleware([requireSupabaseAuth]) - .inputValidator( - (data: { priceId: string; quantity?: number; returnUrl: string; environment: StripeEnv }) => { - if (!/^[a-zA-Z0-9_-]+$/.test(data.priceId)) throw new Error("Invalid priceId"); - return data; - }, - ) + .inputValidator((data: CheckoutSessionInput) => { + if (!/^[a-zA-Z0-9_-]+$/.test(data.priceId)) throw new Error("Invalid priceId"); + const quantity = validateCheckoutQuantity(data.quantity); + return { ...data, quantity }; + }) .handler(async ({ data, context }): Promise => { // Always derive identity from the verified session — never trust client input. const { userId, claims } = context as { userId: string; claims?: { email?: string } }; @@ -70,13 +82,31 @@ export const createCheckoutSession = createServerFn({ method: "POST" }) productDescription = product.name; } + const quantity = data.quantity || 1; + const adjustable = shouldEnableAdjustableQuantity({ + priceId: data.priceId, + quantity, + adjustableQuantity: data.adjustableQuantity, + }); + const session = await stripe.checkout.sessions.create({ - line_items: [{ price: stripePrice.id, quantity: data.quantity || 1 }], + line_items: [ + { + price: stripePrice.id, + quantity, + ...(adjustable && + isRecurring && { + adjustable_quantity: { enabled: true, minimum: 1, maximum: 100 }, + }), + }, + ], mode: isRecurring ? "subscription" : "payment", // `embedded` was removed by Stripe — sessions 400 with it. ui_mode: "embedded_page", return_url: data.returnUrl, customer: customerId, + // Stripe Tax not enabled: GetTaxSettings returns unsupported for this account's country (BR). + // Re-enable automatic_tax + billing_address_collection when Stripe Tax is available. ...(!isRecurring && { payment_intent_data: { description: productDescription } }), metadata: { userId }, ...(isRecurring && { subscription_data: { metadata: { userId } } }), diff --git a/src/lib/pricing-copy.ts b/src/lib/pricing-copy.ts index ee076a26..c47d5cc7 100644 --- a/src/lib/pricing-copy.ts +++ b/src/lib/pricing-copy.ts @@ -1,24 +1,45 @@ /** - * Single source of truth for Pro pricing copy. + * Single source of truth for Super Agent Skill pricing copy. * Keeps the landing teaser, FAQ and /pricing page visually and textually in sync. + * + * Stripe lookup_keys for new checkout (live products must exist before publish): + * sak_pro_monthly | sak_pro_yearly | sak_agent_pass_monthly + * Legacy grandfather only (do not use on new checkout paths): + * agent_pass_pro_monthly | agent_pass_pro_yearly */ export const PRO_PRICING = { - monthly: 19, - yearly: 140, + monthly: 29, + yearly: 240, /** 12 x monthly — the anchor shown struck through next to the yearly price. */ - yearlyList: 228, + yearlyList: 348, } as const; -export const PRO_YEARLY_SAVINGS = PRO_PRICING.yearlyList - PRO_PRICING.yearly; // 88 +/** Per production-agent seat (quantity = number of agents). */ +export const AGENT_PASS_PRICING = { + monthlyPerAgent: 12, +} as const; + +/** Production agents included with a Pro subscription. Extra seats are Agent Pass. */ +export const PRO_INCLUDED_AGENTS = 1; + +/** + * Pro = human builder seat + 1 included production agent. + * Agent Pass = standalone monthly seat ($12/agent) — no Pro required; also covers extras beyond Pro's included 1. + */ +export const PRO_VS_AGENT_PASS = + `Pro is the human builder seat (Agent Factory, Store, University, reviews) and includes ${PRO_INCLUDED_AGENTS} production agent. Extra production agents beyond that are Agent Pass at $${AGENT_PASS_PRICING.monthlyPerAgent}/agent/mo. Agent Pass is also sold standalone (monthly only, no Pro required) for people who only run agents.`; + +export const PRO_YEARLY_SAVINGS = PRO_PRICING.yearlyList - PRO_PRICING.yearly; // 108 export const PRO_YEARLY_DISCOUNT_PCT = Math.round( (PRO_YEARLY_SAVINGS / PRO_PRICING.yearlyList) * 100, -); // 39 +); // 31 export const PRICE_MONTHLY = `$${PRO_PRICING.monthly}`; export const PRICE_YEARLY = `$${PRO_PRICING.yearly}`; export const PRICE_YEARLY_LIST = `$${PRO_PRICING.yearlyList}`; +export const PRICE_AGENT_PASS = `$${AGENT_PASS_PRICING.monthlyPerAgent}`; export const SAVE_BADGE = `Save $${PRO_YEARLY_SAVINGS} · ${PRO_YEARLY_DISCOUNT_PCT}% off`; export const SAVE_SHORT = `save $${PRO_YEARLY_SAVINGS}`; /** Canonical one-line answer reused in FAQ copy, meta descriptions and JSON-LD. */ -export const PRICING_SENTENCE = `Browsing and installing public capabilities is free — no account needed. Pro is ${PRICE_YEARLY} per year (down from ${PRICE_YEARLY_LIST}, ${PRO_YEARLY_DISCOUNT_PCT}% off) or ${PRICE_MONTHLY} per month and includes everything: the Agent Factory, the Agent Store, SAK University and unlimited tested reviews. Enterprise adds a private registry, SSO and audit logs.`; +export const PRICING_SENTENCE = `Browsing and installing public capabilities is free — no account needed. Pro is ${PRICE_YEARLY} per year (down from ${PRICE_YEARLY_LIST}, ${PRO_YEARLY_DISCOUNT_PCT}% off) or ${PRICE_MONTHLY} per month for the human builder seat (Agent Factory, Agent Store, SAK University and unlimited tested reviews) and includes ${PRO_INCLUDED_AGENTS} production agent. Extra production agents are Agent Pass at ${PRICE_AGENT_PASS} per agent per month (monthly only) — buy Agent Pass without Pro if you only run agents. Enterprise adds a private registry, SSO and audit logs.`; diff --git a/src/routes/account.billing.tsx b/src/routes/account.billing.tsx index 069cc497..dd005d48 100644 --- a/src/routes/account.billing.tsx +++ b/src/routes/account.billing.tsx @@ -8,6 +8,7 @@ import { useAuth } from "@/hooks/use-auth"; import { useSubscription } from "@/hooks/use-subscription"; import { useStripeCheckout } from "@/hooks/useStripeCheckout"; import { createPortalSession } from "@/lib/payments.functions"; +import { PRICE_MONTHLY } from "@/lib/pricing-copy"; import { toast } from "sonner"; export const Route = createFileRoute("/account/billing")({ @@ -54,7 +55,8 @@ function BillingPage() { const onSubscribe = () => { if (!user) return; openCheckout({ - priceId: "agent_pass_pro_monthly", + // New Pro SKU — legacy agent_pass_pro_* are grandfather-only. + priceId: "sak_pro_monthly", returnUrl: `${window.location.origin}/account/billing?checkout=success&session_id={CHECKOUT_SESSION_ID}`, }); }; @@ -103,7 +105,7 @@ function BillingPage() {
Current plan
- {isActive ? "Agent Pass — Pro" : "Hacker (Free)"} + {isActive ? "Pro" : "Free"}
{subscription?.current_period_end && (
@@ -136,7 +138,7 @@ function BillingPage() { onClick={onSubscribe} className="inline-flex h-10 items-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground hover:opacity-95" > - Upgrade to Pro — $19/mo + Upgrade to Pro — {PRICE_MONTHLY}/mo )}
diff --git a/src/routes/agents.$slug.tsx b/src/routes/agents.$slug.tsx index 7b44d4d1..c0f5a4c7 100644 --- a/src/routes/agents.$slug.tsx +++ b/src/routes/agents.$slug.tsx @@ -127,7 +127,7 @@ function AgentDetail() { {canDownload ? "Included in your plan. Choose how you want to install it." - : "The Agent Store is included with Agent Pass and Enterprise."} + : "The Agent Store is included with Pro and Enterprise."} diff --git a/src/routes/agents.index.tsx b/src/routes/agents.index.tsx index 0bc3b3b4..7d01c2a7 100644 --- a/src/routes/agents.index.tsx +++ b/src/routes/agents.index.tsx @@ -39,7 +39,7 @@ function AgentsIndex() {