A/B variants
Serve different markdown twins to different AI agents and record which variant each agent read.
Variants let a GEO or AEO team test what agents read. Each route can list variants that target agents by name, vendor or purpose. The route’s own source is the control.
import { agentside, remote } from "@usenotra/agentside";
const twins = agentside({
routes: {
"/pricing": () => renderPricingMarkdown(),
},
variants: {
"/pricing": [
{ id: "pricing-openai", vendors: ["OpenAI"], source: remote("https://cms.acme.dev/pricing-v2.md") },
{ id: "pricing-search", purposes: ["search"], source: () => renderPricingMarkdown({ compact: true }) },
{ id: "pricing-claude-user", agents: ["Claude-User"], source: "# Pricing\n\nStarter is free." },
],
},
onServe: (event) => analytics.track("agent_read", event),
});
Matching
The key in variants is the route pattern, exactly as written in routes, so "/blog/[slug]" targets every blog post. For each request Agentside picks the first variant whose matcher fits the detected agent.
| Field | Matches when |
|---|---|
agents |
The agent name is in the list, compared case insensitively. |
vendors |
The agent vendor is in the list, compared case insensitively. |
purposes |
The agent purpose is in the list. |
A variant matches when any of its fields match. Requests without a known AI agent, such as a client that only sends Accept: text/markdown, always get the control. A variant’s source accepts everything a route accepts: a string, a function or remote().
Order variants from most to least specific because the first match wins. In the example above ChatGPT-User gets pricing-openai, PerplexityBot gets pricing-search and Claude-User gets pricing-claude-user.
Exposure events
onServe runs after every successful markdown response with a ServeEvent.
interface ServeEvent {
readonly path: string;
readonly variant: string;
readonly agent: Agent | null;
readonly status: number;
readonly cached: boolean;
readonly durationMs: number;
}
variant is the id that was actually served. When a variant fails and falls back, the event reports control, which keeps your exposure data honest. The same id is sent to the client in the X-Agentside-Variant header.
Keep onServe fast and non-blocking. Queue the event or hand it to your analytics client instead of awaiting a network call.
Failures
A variant that throws, times out or returns null falls back to the control twin. onError(error, path) receives the variant’s error so you can spot a broken experiment without agents noticing.