From 640a0cf92f1dd14e0a21e8981d50314588292dd3 Mon Sep 17 00:00:00 2001 From: khaliqgant Date: Sun, 25 Oct 2026 23:44:18 -0700 Subject: [PATCH 1/3] Add a minimal /pricing page for the hosted free tier split States local and self-hosted as free and unlimited, the hosted anonymous trial (100 messages, one time), signed-in Free (10,000 messages a month), and Pro/Enterprise with "Contact us" until paid prices are confirmed. Links sign-in at /login, explains the plan_limit_exceeded error and its upgrade_url, and adds /pricing to the sitemap. Co-Authored-By: Claude Opus 5.5 (1M context) --- web/app/pricing/page.tsx | 109 +++++++++++++++++++++++++++++ web/app/sitemap.ts | 6 ++ web/lib/pricing.ts | 62 ++++++++++++++++ web/lib/test/pricing-page.test.tsx | 43 ++++++++++++ 4 files changed, 220 insertions(+) create mode 100644 web/app/pricing/page.tsx create mode 100644 web/lib/pricing.ts create mode 100644 web/lib/test/pricing-page.test.tsx diff --git a/web/app/pricing/page.tsx b/web/app/pricing/page.tsx new file mode 100644 index 00000000..7af0397c --- /dev/null +++ b/web/app/pricing/page.tsx @@ -0,0 +1,109 @@ +import type { Metadata } from 'next'; +import Link from 'next/link'; + +import { GitHubStarsBadge } from '../../components/GitHubStars'; +import { SiteFooter } from '../../components/SiteFooter'; +import { SiteNav } from '../../components/SiteNav'; +import { RELAY_PLANS, SIGN_IN_PATH } from '../../lib/pricing'; +import { absoluteUrl, SITE_EMAIL } from '../../lib/site'; +import s from '../legal.module.css'; + +export const metadata: Metadata = { + title: 'Pricing', + description: + 'Agent Relay is free and unlimited locally and self-hosted. The hosted service has a 100-message trial without signing in and 10,000 messages a month free when you sign in.', + alternates: { + canonical: absoluteUrl('/pricing'), + }, +}; + +export default function PricingPage() { + return ( +
+ } /> + +
+

Pricing

+

+ Relay is free and unlimited when you run it yourself. The hosted service is free to try and free to keep + using once you sign in. +

+ +
+

Plans

+
+ + + + + + + + + + + {RELAY_PLANS.map((plan) => ( + + + + + + + ))} + +
PlanMessagesOther limitsPrice
+ {plan.name} +
+ {plan.who} +
{plan.messages}{plan.limits}{plan.price}
+
+

+ Paid prices are not published yet.{' '} + + Contact us + {' '} + for Pro or{' '} + + Enterprise + + . +

+
+ +
+

Local and self-hosted

+

+ Running Relay on your own machine or servers has no message, agent, or API limits, and no account is + needed. +

+
+ +
+

Hosted trial, then sign in

+

+ A hosted workspace created without signing in gets 100 messages, once. That is enough to prove agents + on two machines can talk. When it runs out, sends fail with a limit error that links to sign-in. +

+

+ + Sign in + {' '} + (free), or run agent-relay login, to get a workspace with 10,000 messages a month. +

+
+ +
+

When you reach a limit

+

+ The API answers with HTTP 429 and the error code plan_limit_exceeded. The error names the + limit and carries an upgrade_url: the sign-in page for a trial workspace, or billing + settings for a signed-in one. Monthly limits reset at the start of each UTC month; the trial allowance + does not reset. +

+
+
+ + +
+ ); +} diff --git a/web/app/sitemap.ts b/web/app/sitemap.ts index 21b8901c..2e543338 100644 --- a/web/app/sitemap.ts +++ b/web/app/sitemap.ts @@ -138,6 +138,12 @@ export default function sitemap(): MetadataRoute.Sitemap { changeFrequency: 'yearly', priority: 0.4, }, + { + url: absoluteUrl('/pricing'), + lastModified: now, + changeFrequency: 'monthly', + priority: 0.7, + }, { url: absoluteUrl('/support'), lastModified: now, diff --git a/web/lib/pricing.ts b/web/lib/pricing.ts new file mode 100644 index 00000000..51961f4d --- /dev/null +++ b/web/lib/pricing.ts @@ -0,0 +1,62 @@ +/** + * Relay plans as the hosted service enforces them. Limits mirror + * `PLAN_LIMITS` in @relaycast/engine (packages/engine/src/providers/static-entitlements.ts); + * keep them in step when the engine changes. + * + * Paid prices are not set yet. `price` stays "Contact us" until they are + * confirmed — do not fill in numbers here without that decision. + */ +export interface RelayPlan { + id: 'selfhost' | 'trial' | 'free' | 'pro' | 'enterprise'; + name: string; + who: string; + messages: string; + limits: string; + price: string; +} + +export const RELAY_PLANS: readonly RelayPlan[] = [ + { + id: 'selfhost', + name: 'Local and self-hosted', + who: 'Run Relay on your own machine or servers', + messages: 'Unlimited', + limits: 'No limits', + price: 'Free', + }, + { + id: 'trial', + name: 'Hosted trial', + who: 'A hosted workspace created without signing in', + messages: '100 messages, one time', + limits: 'Enough to connect agents on two machines', + price: 'Free', + }, + { + id: 'free', + name: 'Hosted Free', + who: 'Sign in with your account', + messages: '10,000 messages a month', + limits: '5 agents, 100,000 API calls a month, 300 requests a minute', + price: 'Free', + }, + { + id: 'pro', + name: 'Pro', + who: 'Teams running agents every day', + messages: '1,000,000 messages a month', + limits: '100 agents, 10,000,000 API calls a month, 6,000 requests a minute', + price: 'Contact us', + }, + { + id: 'enterprise', + name: 'Enterprise', + who: 'Custom limits, security review, and support', + messages: 'Unlimited', + limits: 'Unlimited agents and API calls, 30,000 requests a minute', + price: 'Contact us', + }, +]; + +/** Where an anonymous hosted workspace signs in (the trial's limit error links here). */ +export const SIGN_IN_PATH = '/login'; diff --git a/web/lib/test/pricing-page.test.tsx b/web/lib/test/pricing-page.test.tsx new file mode 100644 index 00000000..8b6c46f6 --- /dev/null +++ b/web/lib/test/pricing-page.test.tsx @@ -0,0 +1,43 @@ +import { renderToStaticMarkup } from 'react-dom/server'; +import { describe, expect, it, vi } from 'vitest'; + +vi.mock('../../components/GitHubStars', () => ({ GitHubStarsBadge: () => null })); +vi.mock('../../components/SiteFooter', () => ({ SiteFooter: () => null })); +vi.mock('../../components/SiteNav', () => ({ SiteNav: () => null })); + +import PricingPage from '../../app/pricing/page'; +import sitemap from '../../app/sitemap'; +import { RELAY_PLANS, SIGN_IN_PATH } from '../pricing'; +import { absoluteUrl } from '../site'; + +const html = renderToStaticMarkup(); + +describe('pricing page', () => { + it('states local and self-hosted use as unlimited and free', () => { + expect(html).toContain('Local and self-hosted'); + expect(html).toMatch(/Local and self-hosted[\s\S]*?Unlimited[\s\S]*?Free/); + }); + + it('states the hosted trial and the signed-in free tier', () => { + expect(html).toContain('100 messages, one time'); + expect(html).toContain('10,000 messages a month'); + expect(html).toContain(`href="${SIGN_IN_PATH}"`); + expect(html).toContain('agent-relay login'); + }); + + it('lists paid plans without inventing prices', () => { + for (const plan of RELAY_PLANS.filter((p) => p.id === 'pro' || p.id === 'enterprise')) { + expect(plan.price).toBe('Contact us'); + } + expect(html).not.toMatch(/\$\d/); + }); + + it('explains the limit error and its upgrade URL', () => { + expect(html).toContain('plan_limit_exceeded'); + expect(html).toContain('upgrade_url'); + }); + + it('is canonical and in the sitemap', () => { + expect(sitemap().map((entry) => entry.url)).toContain(absoluteUrl('/pricing')); + }); +}); From ad78a3b3192375e954ff8656c6c5f0c95af8689e Mon Sep 17 00:00:00 2001 From: khaliqgant Date: Mon, 26 Oct 2026 01:04:23 -0700 Subject: [PATCH 2/3] Pricing: monthly message figures are guidance; trial is the gate Free and Pro now read "up to" 10,000 / 1,000,000 messages a month and the page says monthly message figures are not hard caps, while API-call and rate limits are enforced. The 100-message hosted trial is marked as the hard limit, and the trial section (anchor #trial) explains that signing in, free, with `agent-relay login` continues past it; the trial's limit error links here. Co-Authored-By: Claude Opus 5.5 (1M context) --- web/app/pricing/page.tsx | 25 ++++++++++++++++--------- web/lib/pricing.ts | 15 +++++++++------ web/lib/test/pricing-page.test.tsx | 14 +++++++++++--- 3 files changed, 36 insertions(+), 18 deletions(-) diff --git a/web/app/pricing/page.tsx b/web/app/pricing/page.tsx index 7af0397c..c3e1c715 100644 --- a/web/app/pricing/page.tsx +++ b/web/app/pricing/page.tsx @@ -11,7 +11,7 @@ import s from '../legal.module.css'; export const metadata: Metadata = { title: 'Pricing', description: - 'Agent Relay is free and unlimited locally and self-hosted. The hosted service has a 100-message trial without signing in and 10,000 messages a month free when you sign in.', + 'Agent Relay is free and unlimited locally and self-hosted. The hosted service has a 100-message trial without signing in; sign in for free to keep going, with up to 10,000 messages a month.', alternates: { canonical: absoluteUrl('/pricing'), }, @@ -57,6 +57,10 @@ export default function PricingPage() { +

+ Monthly message figures are sizing guidance, not hard caps. The only hard message limit is the hosted + trial’s 100 messages. API-call and per-minute request limits are enforced. +

Paid prices are not published yet.{' '} @@ -78,17 +82,19 @@ export default function PricingPage() {

-
+

Hosted trial, then sign in

A hosted workspace created without signing in gets 100 messages, once. That is enough to prove agents - on two machines can talk. When it runs out, sends fail with a limit error that links to sign-in. + on two machines can talk. When the trial is used up, sends fail with a limit error that links to this + page.

+ Signing in is free and continues past the trial: run agent-relay login or{' '} - Sign in - {' '} - (free), or run agent-relay login, to get a workspace with 10,000 messages a month. + sign in + + , and keep going on Hosted Free with up to 10,000 messages a month.

@@ -96,9 +102,10 @@ export default function PricingPage() {

When you reach a limit

The API answers with HTTP 429 and the error code plan_limit_exceeded. The error names the - limit and carries an upgrade_url: the sign-in page for a trial workspace, or billing - settings for a signed-in one. Monthly limits reset at the start of each UTC month; the trial allowance - does not reset. + limit and carries an upgrade_url: this page for a used-up trial (with{' '} + upgrade_action: sign_in, meaning run agent-relay login), or billing settings + for a signed-in workspace. Monthly API-call limits reset at the start of each UTC month; the trial + allowance does not reset.

diff --git a/web/lib/pricing.ts b/web/lib/pricing.ts index 51961f4d..b6e12bc7 100644 --- a/web/lib/pricing.ts +++ b/web/lib/pricing.ts @@ -1,5 +1,8 @@ /** - * Relay plans as the hosted service enforces them. Limits mirror + * Relay plans. Monthly message figures are sizing guidance, not hard caps: + * Relaycast does not enforce a monthly message limit. The hard message gate is + * the hosted trial's one-time 100 messages. API-call and rate limits are + * enforced. Limits mirror * `PLAN_LIMITS` in @relaycast/engine (packages/engine/src/providers/static-entitlements.ts); * keep them in step when the engine changes. * @@ -28,15 +31,15 @@ export const RELAY_PLANS: readonly RelayPlan[] = [ id: 'trial', name: 'Hosted trial', who: 'A hosted workspace created without signing in', - messages: '100 messages, one time', - limits: 'Enough to connect agents on two machines', + messages: '100 messages, one time (hard limit)', + limits: 'Enough to connect agents on two machines. Sign in (free) to keep going', price: 'Free', }, { id: 'free', name: 'Hosted Free', who: 'Sign in with your account', - messages: '10,000 messages a month', + messages: 'Up to 10,000 messages a month', limits: '5 agents, 100,000 API calls a month, 300 requests a minute', price: 'Free', }, @@ -44,7 +47,7 @@ export const RELAY_PLANS: readonly RelayPlan[] = [ id: 'pro', name: 'Pro', who: 'Teams running agents every day', - messages: '1,000,000 messages a month', + messages: 'Up to 1,000,000 messages a month', limits: '100 agents, 10,000,000 API calls a month, 6,000 requests a minute', price: 'Contact us', }, @@ -58,5 +61,5 @@ export const RELAY_PLANS: readonly RelayPlan[] = [ }, ]; -/** Where an anonymous hosted workspace signs in (the trial's limit error links here). */ +/** Where an anonymous hosted workspace signs in. The trial's limit error links to /pricing, which links here. */ export const SIGN_IN_PATH = '/login'; diff --git a/web/lib/test/pricing-page.test.tsx b/web/lib/test/pricing-page.test.tsx index 8b6c46f6..b759b64f 100644 --- a/web/lib/test/pricing-page.test.tsx +++ b/web/lib/test/pricing-page.test.tsx @@ -18,11 +18,19 @@ describe('pricing page', () => { expect(html).toMatch(/Local and self-hosted[\s\S]*?Unlimited[\s\S]*?Free/); }); - it('states the hosted trial and the signed-in free tier', () => { - expect(html).toContain('100 messages, one time'); - expect(html).toContain('10,000 messages a month'); + it('makes the 100-message trial the hard gate and signing in the way past it', () => { + expect(html).toContain('100 messages, one time (hard limit)'); + expect(html).toContain('Signing in is free and continues past the trial'); expect(html).toContain(`href="${SIGN_IN_PATH}"`); expect(html).toContain('agent-relay login'); + expect(html).toContain('id="trial"'); + }); + + it('does not promise a hard monthly message cap', () => { + expect(html).toContain('Up to 10,000 messages a month'); + expect(html).toContain('Up to 1,000,000 messages a month'); + expect(html).toContain('not hard caps'); + expect(html).not.toMatch(/(? { From 400487a4b5b52bf2c695f3c51c0a569c779b5b7d Mon Sep 17 00:00:00 2001 From: khaliqgant Date: Mon, 26 Oct 2026 01:22:17 -0700 Subject: [PATCH 3/3] Pricing: say sign-in means a new workspace; scope the limit error Address review feedback: signing in gives a new account-bound workspace (there is no claim flow), so the trial section tells users to connect their agents to it. The limit-error section covers only the trial and monthly plan quotas, and self-hosted states its 30,000-requests-a-minute ceiling instead of "No limits". Co-Authored-By: Claude Opus 5.5 (1M context) --- web/app/pricing/page.tsx | 16 +++++++++++----- web/lib/pricing.ts | 2 +- web/lib/test/pricing-page.test.tsx | 8 ++++++++ 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/web/app/pricing/page.tsx b/web/app/pricing/page.tsx index c3e1c715..d0714263 100644 --- a/web/app/pricing/page.tsx +++ b/web/app/pricing/page.tsx @@ -77,8 +77,9 @@ export default function PricingPage() {

Local and self-hosted

- Running Relay on your own machine or servers has no message, agent, or API limits, and no account is - needed. + Running Relay on your own machine or servers has no message, agent, or monthly API-call limits, and no + account is needed. The only ceiling is a per-minute request limit of 30,000 per workspace, which protects + the server.

@@ -96,13 +97,18 @@ export default function PricingPage() { , and keep going on Hosted Free with up to 10,000 messages a month.

+

+ Signing in gives you a new hosted workspace bound to your account; it does not convert the trial + workspace. Connect your agents to the new workspace to keep sending. The trial workspace stays capped at + 100 messages. +

-

When you reach a limit

+

When you reach a plan limit

- The API answers with HTTP 429 and the error code plan_limit_exceeded. The error names the - limit and carries an upgrade_url: this page for a used-up trial (with{' '} + When a workspace uses up the trial or a monthly plan quota, the API answers with HTTP 429 and the error + code plan_limit_exceeded. The error names the limit and carries an upgrade_url: this page for a used-up trial (with{' '} upgrade_action: sign_in, meaning run agent-relay login), or billing settings for a signed-in workspace. Monthly API-call limits reset at the start of each UTC month; the trial allowance does not reset. diff --git a/web/lib/pricing.ts b/web/lib/pricing.ts index b6e12bc7..3e35384d 100644 --- a/web/lib/pricing.ts +++ b/web/lib/pricing.ts @@ -24,7 +24,7 @@ export const RELAY_PLANS: readonly RelayPlan[] = [ name: 'Local and self-hosted', who: 'Run Relay on your own machine or servers', messages: 'Unlimited', - limits: 'No limits', + limits: 'Unlimited agents and API calls, 30,000 requests a minute', price: 'Free', }, { diff --git a/web/lib/test/pricing-page.test.tsx b/web/lib/test/pricing-page.test.tsx index b759b64f..5274a4af 100644 --- a/web/lib/test/pricing-page.test.tsx +++ b/web/lib/test/pricing-page.test.tsx @@ -24,6 +24,14 @@ describe('pricing page', () => { expect(html).toContain(`href="${SIGN_IN_PATH}"`); expect(html).toContain('agent-relay login'); expect(html).toContain('id="trial"'); + // No claim flow: signing in means a new workspace, not the trial one. + expect(html).toContain('does not convert the trial'); + }); + + it('scopes plan_limit_exceeded to plan quotas and states the self-hosted request ceiling', () => { + expect(html).toContain('When a workspace uses up the trial or a monthly plan quota'); + expect(html).not.toContain('No limits'); + expect(html).toContain('30,000 requests a minute'); }); it('does not promise a hard monthly message cap', () => {