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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
11 changes: 9 additions & 2 deletions src/components/StripeEmbeddedCheckout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | null>(null);

const fetchClientSecret = useCallback(async (): Promise<string> => {
Expand All @@ -19,6 +25,7 @@ export function StripeEmbeddedCheckout({ priceId, quantity, returnUrl }: Props)
data: {
priceId,
quantity,
adjustableQuantity,
returnUrl: returnUrl || window.location.href,
environment: getStripeEnvironment(),
},
Expand All @@ -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 (
Expand Down
2 changes: 1 addition & 1 deletion src/components/site/Nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [
Expand Down
6 changes: 3 additions & 3 deletions src/components/site/home/AgentFactory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
</Link>
</div>
<p className="mt-3 text-xs text-muted-foreground">
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.
</p>
</div>
</div>
Expand Down
7 changes: 5 additions & 2 deletions src/components/site/home/Faq.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -87,8 +88,10 @@ const ITEMS: { q: string; a: React.ReactNode }[] = [
Browsing and installing public capabilities is free — no account needed. Pro is{" "}
<span className="text-muted-foreground line-through">{PRICE_YEARLY_LIST}</span>{" "}
<span className="font-semibold text-foreground">{PRICE_YEARLY} per year</span> (
{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).{" "}
<Link to="/pricing" className="text-primary hover:underline">
See pricing
</Link>
Expand Down
96 changes: 61 additions & 35 deletions src/components/site/home/PlansTeaser.tsx
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -24,34 +32,16 @@ export function PlansTeaser() {
<div className="mx-auto max-w-2xl text-center">
<span className="font-mono text-xs uppercase tracking-[0.2em] text-primary">Pricing</span>
<h2 className="mt-3 text-balance text-3xl font-semibold tracking-tight md:text-5xl">
One plan. Everything included.
Free, Pro, and Agent Pass.
</h2>
<p className="mt-4 text-pretty text-base leading-relaxed text-muted-foreground md:text-lg">
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.
</p>
</div>

<div className="mt-12 overflow-hidden rounded-2xl border border-primary/40 bg-background shadow-elevated md:grid md:grid-cols-[1.2fr_1fr]">
<div className="p-7 md:p-9">
<div className="font-mono text-[11px] uppercase tracking-[0.18em] text-muted-foreground">
Everything in Super Agent Skill
</div>
<ul className="mt-5 space-y-3 text-sm">
{INCLUDED.map((f) => (
<li key={f} className="flex items-start gap-2.5">
<Check className="mt-0.5 h-4 w-4 shrink-0 text-signal" aria-hidden />
<span className="text-foreground/90">{f}</span>
</li>
))}
</ul>
<p className="mt-6 text-xs text-muted-foreground">
Solo or team. Cancel in one click. Everything you create stays yours.
</p>
</div>

<div className="border-t border-border bg-surface/60 p-7 md:border-l md:border-t-0 md:p-9">
<div className="mt-12 grid gap-5 md:grid-cols-2">
<div className="overflow-hidden rounded-2xl border border-primary/40 bg-background shadow-elevated p-7 md:p-9">
<div className="font-mono text-[11px] uppercase tracking-[0.18em] text-primary">
Pro · billed yearly
</div>
Expand All @@ -66,15 +56,51 @@ export function PlansTeaser() {
{SAVE_BADGE}
</div>
<p className="mt-2 text-sm text-muted-foreground">
Instead of <span className="text-foreground">{PRICE_YEARLY_LIST} / year</span> 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.
</p>
<ul className="mt-5 space-y-3 text-sm">
{PRO_INCLUDED.map((f) => (
<li key={f} className="flex items-start gap-2.5">
<Check className="mt-0.5 h-4 w-4 shrink-0 text-signal" aria-hidden />
<span className="text-foreground/90">{f}</span>
</li>
))}
</ul>
<Link
to="/pricing"
className="mt-6 inline-flex h-11 w-full items-center justify-center rounded-md bg-primary px-6 text-sm font-semibold text-primary-foreground transition-all hover:opacity-95"
>
Get Pro →
</Link>
</div>

<div className="overflow-hidden rounded-2xl border border-border bg-background p-7 md:p-9">
<div className="font-mono text-[11px] uppercase tracking-[0.18em] text-primary">
Agent Pass · monthly · standalone
</div>
<div className="mt-4 flex items-end gap-2">
<span className="text-5xl font-semibold tracking-tight">{PRICE_AGENT_PASS}</span>
<span className="pb-2 text-sm text-muted-foreground">/ agent / month</span>
</div>
<p className="mt-2 text-sm text-muted-foreground">
No Pro required. Buy Agent Pass alone if you only run agents — or add seats beyond
Pro&apos;s included 1.
</p>
<ul className="mt-5 space-y-3 text-sm">
{AGENT_PASS_INCLUDED.map((f) => (
<li key={f} className="flex items-start gap-2.5">
<Check className="mt-0.5 h-4 w-4 shrink-0 text-signal" aria-hidden />
<span className="text-foreground/90">{f}</span>
</li>
))}
</ul>
<Link
to="/pricing"
className="mt-6 inline-flex h-11 w-full items-center justify-center rounded-md border border-border bg-surface-elevated px-6 text-sm font-semibold transition-colors hover:bg-accent"
>
Get Agent Pass →
</Link>
<Link
to="/marketplace"
className="mt-3 inline-flex h-11 w-full items-center justify-center rounded-md border border-border bg-background px-6 text-sm font-medium transition-colors hover:bg-accent"
Expand Down
1 change: 1 addition & 0 deletions src/hooks/useStripeCheckout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { StripeEmbeddedCheckout } from "@/components/StripeEmbeddedCheckout";
interface CheckoutOptions {
priceId: string;
quantity?: number;
adjustableQuantity?: boolean;
returnUrl?: string;
}

Expand Down
11 changes: 8 additions & 3 deletions src/lib/admin/payments-health.functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { z } from "zod";
import { requireAdmin } from "@/lib/admin/middleware";
import { type StripeEnv, createStripeClient, getStripeErrorMessage } from "@/lib/stripe.server";

const PRO_PRICE = "agent_pass_pro_monthly";
/** New Pro SKU; also probe Agent Pass so missing live prices surface in admin health. */
const HEALTH_LOOKUP_KEYS = ["sak_pro_monthly", "sak_agent_pass_monthly"] as const;

export type EnvHealth = {
environment: StripeEnv;
Expand Down Expand Up @@ -50,8 +51,12 @@ async function envHealth(environment: StripeEnv, probe: boolean): Promise<EnvHea

try {
const stripe = createStripeClient(environment);
const prices = await stripe.prices.list({ lookup_keys: [PRO_PRICE], limit: 1 });
const price = prices.data[0];
const prices = await stripe.prices.list({
lookup_keys: [...HEALTH_LOOKUP_KEYS],
limit: HEALTH_LOOKUP_KEYS.length,
});
const price =
prices.data.find((p) => p.lookup_key === "sak_pro_monthly") ?? prices.data[0] ?? undefined;
if (price) {
out.price = {
id: price.id,
Expand Down
24 changes: 24 additions & 0 deletions src/lib/checkout-quantity.ts
Original file line number Diff line number Diff line change
@@ -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;
}
11 changes: 6 additions & 5 deletions src/lib/crm/copy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };

Expand Down Expand Up @@ -181,23 +182,23 @@ 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.`,
]
: []),
],
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),
Expand Down Expand Up @@ -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),
Expand Down
Loading
Loading