Skip to content
Closed
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
122 changes: 122 additions & 0 deletions web/app/pricing/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
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; sign in for free to keep going, with up to 10,000 messages a month.',
alternates: {
canonical: absoluteUrl('/pricing'),
},
};

export default function PricingPage() {
return (
<div className={s.page}>
<SiteNav actions={<GitHubStarsBadge />} />

<main className={s.content}>
<h1 className={s.title}>Pricing</h1>
<p className={s.subtitle}>
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.
</p>

<section className={s.section}>
<h2>Plans</h2>
<div className={s.tableWrapper}>
<table className={s.table}>
<thead>
<tr>
<th>Plan</th>
<th>Messages</th>
<th>Other limits</th>
<th>Price</th>
</tr>
</thead>
<tbody>
{RELAY_PLANS.map((plan) => (
<tr key={plan.id} id={plan.id}>
<td>
<strong>{plan.name}</strong>
<br />
{plan.who}
</td>
<td>{plan.messages}</td>
<td>{plan.limits}</td>
<td>{plan.price}</td>
</tr>
))}
</tbody>
</table>
</div>
<p>
Monthly message figures are sizing guidance, not hard caps. The only hard message limit is the hosted
trial&rsquo;s 100 messages. API-call and per-minute request limits are enforced.
</p>
<p>
Paid prices are not published yet.{' '}
<a href={`mailto:${SITE_EMAIL}`} className={s.link}>
Contact us
</a>{' '}
for Pro or{' '}
<Link href="/enterprise" className={s.link}>
Enterprise
</Link>
.
</p>
</section>

<section className={s.section}>
<h2>Local and self-hosted</h2>
<p>
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.
</p>
</section>

<section className={s.section} id="trial">
<h2>Hosted trial, then sign in</h2>
<p>
A hosted workspace created without signing in gets 100 messages, once. That is enough to prove agents
on two machines can talk. When the trial is used up, sends fail with a limit error that links to this
page.
</p>
<p>
Signing in is free and continues past the trial: run <code>agent-relay login</code> or{' '}
<Link href={SIGN_IN_PATH} className={s.link}>
sign in
</Link>
, and keep going on Hosted Free with up to 10,000 messages a month.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
</p>
<p>
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.
</p>
</section>

<section className={s.section}>
<h2>When you reach a plan limit</h2>
<p>
When a workspace uses up the trial or a monthly plan quota, the API answers with HTTP 429 and the error
code <code>plan_limit_exceeded</code>. The error names the limit and carries an <code>upgrade_url</code>: this page for a used-up trial (with{' '}
<code>upgrade_action: sign_in</code>, meaning run <code>agent-relay login</code>), 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.
</p>
</section>
</main>

<SiteFooter />
</div>
);
}
6 changes: 6 additions & 0 deletions web/app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
65 changes: 65 additions & 0 deletions web/lib/pricing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* 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.
*
* 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: 'Unlimited agents and API calls, 30,000 requests a minute',
price: 'Free',
},
{
id: 'trial',
name: 'Hosted trial',
who: 'A hosted workspace created without signing in',
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: 'Up to 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: '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',
},
{
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 to /pricing, which links here. */
export const SIGN_IN_PATH = '/login';
59 changes: 59 additions & 0 deletions web/lib/test/pricing-page.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
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(<PricingPage />);

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('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"');
// 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', () => {
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(/(?<!Up to |up to )10,000 messages a month/);
});

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'));
});
});
Loading