Workers

AEO tracker

AI Engine Optimization tracker. For each saved query plus each enabled model, asks the model the query, parses the answer for brand mentions and prominence, classifies sentiment when a brand is mentioned, persists. Runs through an AI Gateway slug for caching, logs, and rate-limit policy.

Updated today

Overview

At a glance
Worker
aeo
Workflow
AEOTrackerWorkflow
Gateway slug
Configured at the platform dashboard, used per-call
D1
ultron-operations
Tables owned
brands, aeo_queries, aeo_runs, aeo_responses, aeo_mentions, aeo_sentiment
Step granularity
One per (query, model) pair

Operators care about where their brand shows up when their customers ask LLMs questions like "best CRM for small SaaS" or "which CRM integrates with HubSpot". The AEO worker answers that systematically: a workspace defines its brands plus its saved queries plus its model list, the worker fires them on a schedule, the results show up in the AEO dashboard.

Worker shape

wrangler.toml (key bindings)toml
1name = "aeo"
2main = "src/index.ts"
3
4[[workflows]]
5name = "aeo-tracker"
6binding = "AEO_TRACKER"
7class_name = "AEOTrackerWorkflow"
8
9[[d1_databases]]
10binding = "OPS_DB"
11database_name = "ultron-operations"
12
13[[kv_namespaces]]
14binding = "SHARED_CONFIG"
15
16[ai]
17binding = "AI"

Workflow steps

AEOTrackerWorkflow.run sketchts
1async run(event, step) {
2 // 1. Load context
3 const { queries, brands, modelDefs } = await step.do('load-context',
4 async () => loadContext(this.env, event.payload)
5 )
6
7 if (!queries.length || !modelDefs.length) {
8 await markRunSucceeded(event.payload.runId)
9 return
10 }
11
12 // 2. Per (query, model) pair, one checkpointed step
13 for (const query of queries) {
14 for (const model of modelDefs) {
15 const stableKey = sha1(`${runId}:${query.id}:${model.slug}`)
16
17 await step.do(`q-${query.id}-m-${model.slug}`, async () => {
18 const answer = await callModel(this.env, query.prompt, model)
19 const mentions = findMentions(answer.text, brands)
20
21 await persistResponse(this.env, stableKey, query, model, answer)
22 for (const m of mentions) {
23 const sentiment = await classifySentiment(this.env, m, answer.text)
24 await persistMention(this.env, stableKey, m, sentiment)
25 }
26 })
27 }
28 }
29}

Mention parsing

Brand surface-form matching with position awareness.

Each brand carries a primary name plus an aliases array (e.g. HubSpot Marketing Hub, Hubspot, HubSpot.com). The mention parser does a case-insensitive scan over the answer text, records every position match, then computes a prominence score per mention based on:

SignalEffect
Position in the answer (earlier = higher)Linear decay across the answer length
Mention countLog-scaled
Surrounding context (e.g. 'recommended', 'avoid')Negative phrases drop prominence; positive phrases raise it
Mention in a list (numbered / bulleted)Position in list weighted by ordinal

Sentiment classification

Only fires when a mention is detected.

The sentiment classifier sees a window of the answer text centred on the mention (roughly +/- 200 chars), prompted to return one of positive, neutral, negative, or mixed. Output is stored per mention in aeo_sentiment.

Output rows

ultron-operations (AEO schema)sql
1CREATE TABLE aeo_responses (
2 id TEXT PRIMARY KEY, -- deterministic per (run, query, model)
3 run_id TEXT NOT NULL,
4 query_id TEXT NOT NULL,
5 model_slug TEXT NOT NULL,
6 prompt TEXT NOT NULL,
7 response_text TEXT NOT NULL,
8 created_at TEXT NOT NULL DEFAULT (datetime('now'))
9);
10
11CREATE TABLE aeo_mentions (
12 id TEXT PRIMARY KEY,
13 response_id TEXT NOT NULL REFERENCES aeo_responses(id),
14 brand_id TEXT NOT NULL REFERENCES brands(id),
15 prominence REAL NOT NULL, -- 0.0 to 1.0
16 position INTEGER NOT NULL,
17 context TEXT NOT NULL
18);
19
20CREATE TABLE aeo_sentiment (
21 mention_id TEXT PRIMARY KEY REFERENCES aeo_mentions(id),
22 label TEXT NOT NULL, -- positive | neutral | negative | mixed
23 confidence REAL NOT NULL
24);

File map

cf-workers/aeo
wrangler.toml
src
index.ts
workflow.tsAEOTrackerWorkflow with per-pair steps
models.tsenabledModels(env, override)
parse.tsfindMentions + classifySentiment
types.ts
migrations
0001_brands_queries.sql
0002_responses.sql
0003_mentions.sql