Repository navigation
Add a minimal /pricing page: unlimited self-hosted, hosted trial, signed-in free #244
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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’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. | ||
| </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> | ||
| ); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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')); | ||
| }); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.