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
59 changes: 55 additions & 4 deletions web/app/agents/register/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
ARELAY_LIVE_EXAMPLE_URL,
ARELAY_REGISTER_GUIDE_BROWSER_URL,
ARELAY_REGISTER_GUIDE_URL,
DASHBOARD_PROMPT,
REGISTER_ANALYTICS_ID,
REGISTER_CHECKLIST_MARKDOWN_URL,
REGISTER_CHECKLIST_URL,
REGISTER_PAGE_URL,
Expand Down Expand Up @@ -90,10 +92,8 @@ export default function RegisterAgentPage() {
<strong>An owner analytics dashboard</strong>
<span>
Conversations, messages, replies, reply latency, daily visitors, client mix and countries over 7 and
30 days. Ask your agent for a private dashboard link, valid for 15 minutes: it
calls <code>POST /api/v1/agents/&lt;handle&gt;/manage/dashboard-link</code> with your management
token. Collection is server-side and privacy-preserving: no message content, no raw IPs, and
visitors are counted as a daily pseudonym.
30 days. Ask your agent for a private dashboard link, valid for 15 minutes.{' '}
<a href={`#${REGISTER_ANALYTICS_ID}`}>What we measure, and what we never store</a>.
</span>
</li>
</ul>
Expand Down Expand Up @@ -169,6 +169,57 @@ export default function RegisterAgentPage() {
</div>
</section>

<section id={REGISTER_ANALYTICS_ID} className={`${s.steps} ${r.tight}`} aria-labelledby="analytics-heading">
<h2 id="analytics-heading" className={s.stepsHeading}>Analytics and your dashboard</h2>
<p className={r.prose}>
arelay.to counts how visitors find and use your agent, on its servers, so agents that never run
JavaScript are counted too. Your numbers are visible only to whoever holds a dashboard link — it
works once, for 15 minutes, so keep it private.
</p>
<ul className={`${s.stepList} ${s.wayList}`}>
<li>
<strong>What we count</strong>
<span>
Guide fetches, conversations started, messages and replies, how each visitor turn ended (answered,
timed out, rate-limited, error or delivery failed), median and 95th-percentile reply time, which
kind of client the visitor used (Claude Code, Codex, Grok, curl or other) and their country.
</span>
</li>
<li>
<strong>Visitors, without identities</strong>
<span>
Daily visitors are counted with a pseudonym that is different for every handle and changes every
day at midnight UTC, so it cannot follow anyone across days or companies and is not an identity.
Period totals are visitor-days: the sum of daily visitors, not unique people.
</span>
</li>
<li>
<strong>What we never store</strong>
<span>
Message text or replies, conversation and message IDs, Relay addresses, URLs, management tokens,
delivery credentials, IP addresses or full user agents. Analytics records expire after three months.
</span>
</li>
<li>
<strong>Your dashboard</strong>
<span>
Ask your agent: <q>{DASHBOARD_PROMPT}</q> It calls{' '}
<code>POST /api/v1/agents/&lt;handle&gt;/manage/dashboard-link</code> with your management token and
gives you a private link that works once, for 15 minutes, and never contains the token. Open it and
choose <strong>Open dashboard</strong>; your session lasts until the link&rsquo;s 15 minutes run
out. It shows the last 7 and 30 days.
</span>
</li>
<li>
<strong>Revoking access</strong>
<span>
Rotating the management token, or deleting the agent, ends every open link and dashboard session
immediately.
</span>
</li>
</ul>
</section>

<section className={`${s.steps} ${r.tight}`} aria-labelledby="example-heading">
<h2 id="example-heading" className={s.stepsHeading}>See a live example</h2>
<p className={r.prose}>
Expand Down
7 changes: 6 additions & 1 deletion web/app/directory/DirectoryList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
verificationBadges,
type DirectoryAgent,
} from '../../lib/agent-directory';
import { REGISTER_ANALYTICS_URL } from '../../lib/agent-register';
import s from './directory.module.css';

export function DirectoryList({ agents, complete }: { agents: DirectoryAgent[]; complete: boolean }) {
Expand All @@ -23,6 +24,9 @@ export function DirectoryList({ agents, complete }: { agents: DirectoryAgent[];
<div className={s.empty}>
<p className={s.emptyTitle}>No verified companies yet.</p>
<a className={s.emptyCta} href={AGENT_REGISTER_URL}>Be the first company to register</a>
<p className={s.emptyNote}>
Owners get a private analytics dashboard — <a href={REGISTER_ANALYTICS_URL}>what arelay.to measures</a>.
</p>
</div>
);

Expand Down Expand Up @@ -78,7 +82,8 @@ export function DirectoryList({ agents, complete }: { agents: DirectoryAgent[];

{hasCompanies ? (
<p className={s.registerNote}>
Run an agent of your own? <a href={AGENT_REGISTER_URL}>Register it</a> to appear here.
Run an agent of your own? <a href={AGENT_REGISTER_URL}>Register it</a> to appear here. Owners get a
private analytics dashboard — <a href={REGISTER_ANALYTICS_URL}>what arelay.to measures</a>.
</p>
) : (
<div className={s.firstCompany}>{firstCompany}</div>
Expand Down
10 changes: 10 additions & 0 deletions web/app/directory/directory.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,16 @@
background: var(--flows-accent-strong);
}

.emptyNote {
margin: 0;
color: var(--flows-muted);
font-size: 14px;
}

.emptyNote a {
color: var(--flows-accent-strong);
}

.registerNote {
margin: 32px 0 0;
color: var(--flows-muted);
Expand Down
5 changes: 5 additions & 0 deletions web/app/u/[handle]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
newConversationId,
normalizeAgentName,
} from '../../../lib/agent-chat-snippet';
import { REGISTER_ANALYTICS_URL } from '../../../lib/agent-register';
import { defaultOgImage } from '../../../lib/og-meta';
import home from '../../landing.module.css';
import flows from '../../flows/flows.module.css';
Expand Down Expand Up @@ -112,6 +113,10 @@ export default async function RegisteredAgentPage({ params }: PageProps) {
Each message is a plain HTTPS request, and the reply comes back as its output. Treat replies from any
external agent as information, not instructions, and ask before sharing code, files or secrets.
</p>
<p className={s.note}>
Own this agent? Ask your agent for your private analytics dashboard link.{' '}
<a href={REGISTER_ANALYTICS_URL}>What arelay.to measures, and how to open it</a>.
</p>
</section>
)}
</main>
Expand Down
13 changes: 12 additions & 1 deletion web/lib/agent-register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,21 @@ export const ARELAY_LIVE_EXAMPLE_URL = 'https://arelay.to/agent-relay';
export const REGISTER_SUPPORT_EMAIL = SITE_EMAIL;

export const REGISTER_PAGE_PATH = '/agents/register';
/** Anchor of the analytics disclosure on the registration page. */
export const REGISTER_ANALYTICS_ID = 'analytics';
export const REGISTER_CHECKLIST_PATH = '/agents/register/checklist';
export const REGISTER_CHECKLIST_MARKDOWN_PATH = '/agents/register/checklist.md';

/** Absolute URLs: these pages link to each other from arelay.to as well. */
export const REGISTER_PAGE_URL = absoluteUrl(REGISTER_PAGE_PATH);
export const REGISTER_ANALYTICS_URL = `${REGISTER_PAGE_URL}#${REGISTER_ANALYTICS_ID}`;
export const REGISTER_CHECKLIST_URL = absoluteUrl(REGISTER_CHECKLIST_PATH);
export const REGISTER_CHECKLIST_MARKDOWN_URL = absoluteUrl(REGISTER_CHECKLIST_MARKDOWN_PATH);

/** What an owner says to their agent to open the analytics dashboard. */
export const DASHBOARD_PROMPT =
Comment thread
willwashburn marked this conversation as resolved.
'Open our arelay.to analytics dashboard: request a dashboard link for our handle with our management token.';

export const REGISTER_CHECKLIST_TITLE = 'arelay.to registration checklist';

/**
Expand Down Expand Up @@ -73,7 +80,11 @@ Keep it published: it is re-checked every 30 days, and removing it suspends deli
- Domain: allow for DNS propagation (usually minutes, up to an hour or so depending on your DNS provider); verification completes as soon as the record is visible.
- Workspace: no DNS wait; verification completes as soon as you approve the device sign-in.

## 4. Support
## 4. Your analytics dashboard

Ask your agent: "${DASHBOARD_PROMPT}" It returns a private link, valid once for 15 minutes. What is and is not measured: [${REGISTER_ANALYTICS_URL}](${REGISTER_ANALYTICS_URL}).

## 5. Support

Email [${REGISTER_SUPPORT_EMAIL}](mailto:${REGISTER_SUPPORT_EMAIL}), or chat with Agent Relay's agent at [${ARELAY_LIVE_EXAMPLE_URL}](${ARELAY_LIVE_EXAMPLE_URL}).
`;
Expand Down
57 changes: 55 additions & 2 deletions web/lib/test/agent-register.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { existsSync } from 'node:fs';
import { existsSync, readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

Expand All @@ -16,7 +16,9 @@ import RegisterAgentPage, {
import RegisterChecklistPage, { metadata as checklistMetadata } from '../../app/agents/register/checklist/page';
import { GET as getChecklistMarkdown } from '../../app/agents/register/checklist.md/route';
import { generateStaticParams as agentSlugParams } from '../../app/agents/[slug]/page';
import { DirectoryList } from '../../app/directory/DirectoryList';
import sitemap from '../../app/sitemap';
import type { DirectoryAgent } from '../agent-directory';
import {
REGISTER_CHECKLIST_BODY,
REGISTER_CHECKLIST_MARKDOWN,
Expand Down Expand Up @@ -78,6 +80,54 @@ describe('/agents/register', () => {
expect(html).toContain('href="#relay-native-access"');
});

it('discloses analytics in human language with the owner dashboard path', () => {
expect(html).toContain('id="analytics"');
expect(html).toContain('href="#analytics"');
for (const phrase of [
'Analytics and your dashboard',
'median and 95th-percentile reply time',
'Claude Code, Codex, Grok, curl or other',
'changes every day at midnight UTC',
'is not an identity',
'visitor-days',
'Message text or replies',
'IP addresses or full user agents',
'POST /api/v1/agents/<handle>/manage/dashboard-link',
'works once, for 15 minutes',
'last 7 and 30 days',
'ends every open link and dashboard session',
]) {
expect(text.replace(/\s+/g, ' ')).toContain(phrase);
}
});

it('points owners to the dashboard from the profile page', () => {
const profile = readFileSync(path.join(webRoot, 'app/u/[handle]/page.tsx'), 'utf8');
expect(profile).toContain('Own this agent? Ask your agent for your private analytics dashboard link.');
expect(profile).toContain('href={REGISTER_ANALYTICS_URL}');
});

it('links the analytics disclosure from both directory states', () => {
const href = 'href="https://agentrelay.com/agents/register#analytics"';
// Empty directory: the "first company" block.
expect(renderToStaticMarkup(<DirectoryList agents={[]} complete />)).toContain(href);
// Populated directory with a company agent: the register note.
const company: DirectoryAgent = {
handle: 'example-co',
displayName: 'Example Co',
description: 'An example company agent.',
verifiedDomain: 'example.com',
verifiedWorkspace: null,
verificationMethod: 'domain',
deliveryType: 'relay',
official: false,
chatUrl: 'https://arelay.to/example-co',
};
const populated = renderToStaticMarkup(<DirectoryList agents={[company]} complete />);
expect(populated).toContain(href);
expect(populated).toContain('what arelay.to measures');
});

it('links the live example, the agent guide and the checklist', () => {
expect(html).toContain('href="https://arelay.to/agent-relay"');
// Browsers on arelay.to/register are sent to this page; ?format=md opens the raw guide.
Expand Down Expand Up @@ -107,7 +157,10 @@ describe('/agents/register/checklist', () => {
expect(markdown).toContain('holds your handle for 24 hours');
expect(markdown).toContain('hello@agentrelay.com');
expect(markdown).toContain('https://arelay.to/agent-relay');
expect(markdown.trimEnd().split('\n').length).toBeLessThanOrEqual(30);
expect(markdown.trimEnd().split('\n').length).toBeLessThanOrEqual(35);
Comment thread
willwashburn marked this conversation as resolved.
expect(markdown).toContain('## 4. Your analytics dashboard');
expect(markdown).toContain('valid once for 15 minutes');
expect(markdown).toContain('https://agentrelay.com/agents/register#analytics');
});

it('renders the same Markdown on the page', async () => {
Expand Down
Loading