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.

Copy 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();

Copy 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}'`,
},
];

Copy 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
},
}
);

Copy link to headingThe transcript format problem

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

Copy 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;
};
}

Copy 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 };
}

Copy link to headingStage 4: Summary and brand extraction

Copy 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);
}

Copy link to headingWhat we’ve learned

Copy link to headingWhat’s next

Ready to deploy?