Skip to content
Dashboard

How we built AEO tracking for coding agents

For end users on our marketing team, responses are consistently formatted across coding agents.
For end users on our marketing team, responses are consistently formatted across coding agents.

Link to headingThe coding agent AEO lifecycle

import { Sandbox } from "@vercel/sandbox";
// Step 1: Create the sandbox
sandbox = await Sandbox.create({
resources: { vcpus: 2 },
timeout: 10 * 60 * 1000
});
// Step 2: Install the agent CLI
for (const setupCmd of agent.setupCommands) {
await sandbox.runCommand("sh", ["-c", setupCmd]);
}
// Step 3: Inject AI Gateway credentials (via env vars in step 4)
// Step 4: Run the agent
const fullCommand = `AI_GATEWAY_API_KEY='${aiGatewayKey}' ${agent.command}`;
const result = await sandbox.runCommand("sh", ["-c", fullCommand]);
// Step 5: Capture transcript (agent-specific — see next section)
// Step 6: Tear down
await sandbox.stop();

Link to headingAgents as config

export const AGENTS: Agent[] = [
{
id: "anthropic/claude-code",
name: "Claude Code",
setupCommands: ["npm install -g @anthropic-ai/claude-code"],
buildCommand: (prompt) => `echo '${prompt}' | claude --print`,
},
{
id: "openai/codex",
name: "OpenAI Codex",
setupCommands: ["npm install -g @openai/codex"],
buildCommand: (prompt) => `codex exec -y -S '${prompt}'`,
},
];

Link to headingUsing the AI Gateway for routing

const claudeResult = await sandbox.runCommand(
'claude',
['-p', '-m', options.model, '-y', options.prompt]
{
env: {
ANTHROPIC_BASE_URL: AI_GATEWAY.baseUrl,
ANTHROPIC_AUTH_TOKEN: options.apiKey,
ANTHROPIC_API_KEY: '', // intentionally blank as AI Gateway handles auth
},
}
);

Link to headingThe transcript format problem

Link to headingStage 1: Transcript capture

async function captureTranscript(sandbox) {
const workdir = sandbox.getWorkingDirectory();
const projectPath = workdir.replace(/\\//g, '-');
const claudeProjectDir = `~/.claude/projects/${projectPath}`;
// Find the most recent .jsonl file
const findResult = await sandbox.runShell(
`ls -t ${claudeProjectDir}/*.jsonl 2>/dev/null | head -1`
);
const transcriptPath = findResult.stdout.trim();
return await sandbox.readFile(transcriptPath);
}

function extractTranscriptFromOutput(output: string) {
const lines = output.split('\\n').filter(line => {
const trimmed = line.trim();
return trimmed.startsWith('{') && trimmed.endsWith('}');
});
return lines.join('\\n');
}

Link to headingStage 2: Parsing tool names and message shapes

export type ToolName =
| 'file_read' | 'file_write' | 'file_edit'
| 'shell' | 'web_fetch' | 'web_search'
| 'glob' | 'grep' | 'list_dir'
| 'agent_task' | 'unknown';
const claudeToolMap = {
Read: 'file_read', Write: 'file_write', Bash: 'shell',
WebFetch: 'web_fetch', Glob: 'glob', Grep: 'grep', /* ... */
};
const codexToolMap = {
read_file: 'file_read', write_file: 'file_write', shell: 'shell',
patch_file: 'file_edit', /* ... */
};
const opencodeToolMap = {
read: 'file_read', write: 'file_write', bash: 'shell',
rg: 'grep', patch: 'file_edit', /* ... */
};

export interface TranscriptEvent {
timestamp?: string;
type: 'message' | 'tool_call' | 'tool_result' | 'thinking' | 'error';
role?: 'user' | 'assistant' | 'system';
content?: string;
tool?: {
name: ToolName; // Canonical name
originalName: string; // Agent-specific name (for debugging)
args?: Record<string, unknown>;
result?: unknown;
};
}

Link to headingStage 3: Enrichment

if (['file_read', 'file_write', 'file_edit'].includes(event.tool.name)) {
const path = extractFilePath(args);
if (path) event.tool.args = { ...args, _extractedPath: path };
}
if (event.tool.name === 'web_fetch') {
const url = extractUrl(args);
if (url) event.tool.args = { ...args, _extractedUrl: url };
}

Link to headingStage 4: Summary and brand extraction

Link to headingOrchestration with Vercel Workflow

export async function probeTopicWorkflow(topicId: string) {
"use workflow";
const agentPromises = AGENTS.map((agent, index) => {
const command = agent.buildCommand(topicData.text);
return queryAgentAndSave(topicData.text, run.id, {
id: agent.id,
name: agent.name,
setupCommands: agent.setupCommands,
command,
}, index + 1, totalQueries);
});
const results = await Promise.all(agentPromises);
}

Link to headingWhat we’ve learned

Link to headingWhat’s next

Ready to deploy?