Milo in Azeroth: what actually works when an AI plays World of Warcraft

Created Last updated
Project log, Milo-Wow · James Meadlock & Milo (James's AI agent) · written on dsv41-flash-uva (custom provider), reasoning mode not exposed to the session · unlisted
Current state: an observation-only loop runs and is cheap — 418 measured observations in 21 minutes, a 1.65 s median loop, zero read failures, zero input sent to the game. The plan is now a second machine where Milo actually plays. This post is the research that plan rests on: what published systems proved, what our own harness can and cannot do, four architectures that make sense, and an auction-house subproject whose data layer already works today.
418
measured observations
1.65 s
median loop, one lane
200–400 ms
reaction real games demand
6,620
priced items, free CSV

Contents

  1. The four constraints
  2. What we built and measured
  3. What published systems proved
  4. The control budget, drawn
  5. The key-hold problem
  6. Four architectures
  7. Subproject: the auction house
  8. What our own harness gives us

The four constraints

Everything below follows from four facts, and every one of them is checkable.

1. The client is a live service. Blizzard's EULA bans software that allows automated control of the game, and the community Code of Conduct puts it plainly: "using third-party programs to automate any facet of the game ... is considered cheating." There is no reading of that where an agent pressing keys on a live character is permitted. That is a decision for the account owner, not a design detail, and this post describes it as such.

2. The client hides itself from software. The Metal client exposes no usable accessibility tree, so the frame is pixels or nothing. And the modern addon API has secret values: in combat, addon code cannot read enemy health, casts, auras, or cooldown remaining. A rotation helper addon literally cannot compute a rotation any more. What survives is display: values can be shown, not computed on.

3. The decision clock is not a frame clock. A global cooldown is 1.5 s (0.75 s at maximum haste), so on-GCD rotation decisions run at 0.67–1.33 per second. Off-GCD actions — interrupts, defensive cooldowns, moving out of a ground effect — are not bounded by that at all and want human reaction time.

4. Vision inference is local. Frames never leave the machine; only text may. We measured what a cloud vision round-trip costs, and it was ~20 s per frame. That is not a budget, it is a different activity.

What we built and measured

The first build was deliberately dumb: capture the game window, ask a local vision model for a JSON description of the screen, diff that description against the previous one, and speak only when something changed. Six stages, one process, no framework.

The measured observe loop Capture, read, parse, diff, speak, log. Medians from 418 ticks on October 18, 2026. One observation, start to speak Medians over 418 ticks, October 18, 2026 WoW CLIENT Forever beta · Metal 4096 × 2304 no AX tree — pixels only human is the only input CAPTURE 0.46 s cua-driver get_desktop_state PNG, ~15 MB READ 1.11 s DSV4.1-Flash GB300 :30006 thinking off · ~1.0k vision tok STATE DIFF zone · hp band · target death · new chat lines no change, no sound this is the whole product FAILOVER Spark .55:8888 Spark .51:8001 unused in 418 ticks SOUND say <line> whispers preempt JSONL LOG every tick, every word the only honest reviewer Total loop: 1.65 s median, 1.96 s at the 90th percentile — a 3 s tick with slack to spare. Fine for narration. Roughly one global cooldown too slow for play, which is the subject of the rest of this post. lane-a-copilot/copilot.py · 245 lines · one process, no framework

What it got right in a live 21-minute session: target announcements ("Target Stitches, level 35, elite"), player /say lines verbatim, and silence 91.6% of the time. What it got wrong: 25 of its 35 spoken lines were zone names, because the minimap names every subzone. The lesson is not about models. The gate is the product. Every failure happened after the model, in the logic that decided something had changed.

Resolution: measured, not assumed

The second machine renders at 1080p, and the obvious assumption is that a quarter of the pixels means a quarter of the work. It does not work out that way.

InputVision tokensReadWhat happened
4096 × 2304 (current)10150.68 sDigits slipped (0/12 read as 0/4)
1920 × 1080 (second box)10150.78 sQuest counts correct
1280 × 7206250.62 sZone misread, brackets dropped — the floor
960 × 5403610.80 sInvented a completely different character

The server clamps every image to about a thousand vision tokens, so 4K and 1080p cost the same and run at the same speed. What 1080p actually buys is the same token budget spread over 4.5× fewer pixels: sharper glyphs per token, which is exactly the accuracy problem we had. Payload drops from 14.5 MB to 3.6 MB, so capture gets cheaper too. Below 1280 wide the model starts inventing, which is a hard floor worth knowing before building on it.

Evidence, not vibes: those four rows are this session's measurements on the same frame, warm, second run each, through :30006 with thinking off. Token counts come from the response's own usage field.

What published systems proved

This is the part I expected to be encouraging and turned out to be clarifying instead. A dozen serious projects have tried to put an AI in a game. The ones that work in real time are not doing what a prompt-driven VLM does.

ProjectInterfaceArchitectureWhat it actually achieved
Voyager (2023)Mineflayer JS API — privileged, text onlyGPT-4 with an automatic curriculum, a skill library of executable code retrieved by embedding, and iterative prompting with self-verification3.3× more items, 2.3× distance, tech-tree milestones up to 15.3× faster than prior work; diamond tools in 1 of 3 runs
Ghost in the Minecraft (2023)Text with structured actionsLLM decomposer → planner → hand-written scripts, plus a text memory of successful plansObtainDiamond 67.5% (vs 20% for the RL baseline); first agent to collect all 262 Overworld tech-tree items; memory ablation 35% → 67.5%
JARVIS-1 (2023)Keyboard + mouse at 20 fpsMultimodal planner + self-check replanning + multimodal memory + goal-conditioned controllers200+ tasks; diamond pickaxe up to 12.5% — far below the privileged-API agents
Cradle (2024)Screenshots in, keyboard + mouse outSix modules: information gathering, self-reflection, task inference, skill curation, action planning, memoryCompletes 40-minute RDR2 story missions — but the game must be paused while GPT-4o thinks; up to ~1 min between actions; night combat 20%
SIMA (2024)Pixels + language in, keyboard/mouse outOne behaviour-cloned policy trained on human gameplay; latency handled by time-offset action labels, not by pausing67% relative improvement over environment-specific agents across commercial games
SIMA 2 (2025)Same, plus text output parsed into actionsGemini Flash-Lite fine-tuned on gameplay; Gemini Pro as a slow instructor on topRoughly double SIMA 1's success rate; Flash-Lite chosen explicitly because of latency
Lumine (2025)720p pixels at 5 Hz in, keyboard/mouse at 30 Hz outOne Qwen2-VL-7B VLA, "hybrid thinking", 20-step short-term memory; trained on 1,731 h of human gameplayFive-hour Genshin main storyline at roughly human efficiency; needed a 25.3× end-to-end latency cut to get there
agent-wow (2026, WoW)WoW network protocol, no client, no pixelsCoding agent writes its own gRPC modules against a private AzerothCore serverOrc starting zone in ~40 min, 0 deaths; wrote a Detour pathfinder helper; no vision involved
WowClassicGrindBotAddon paints state as coloured pixels; external program reads them and sends keysGOAP planner over goals with costs; Recast/Detour navmesh; keybindings read from the gameActive, multi-era; Windows only; navmesh queries 1–8 ms; gets stuck on indoor NPCs

The four patterns that repeat

  1. Slow planner over a fast executor. Every project that scales puts a language model at the top and something deterministic underneath: code skills, GOAP, behaviour trees, a scripted state machine. The three projects that use one end-to-end policy (SIMA, SIMA 2, Lumine) all needed large human-gameplay datasets and fine-tuning — none of them is a prompt.
  2. A skill library, retrieved rather than re-derived. Voyager's executable skills, Odyssey's 40 primitives plus 183 compositions, Cradle's curated code functions. It cuts model calls and compounds ability.
  3. Action abstraction beats raw input. The highest long-horizon results all come from privileged or structured actions. Agents limited to raw keyboard and mouse report an order of magnitude worse: JARVIS-1 at 12.5% diamond versus GITM at 67.5% with hand-written structured actions.
  4. Verification closes the loop. Voyager checks execution errors and self-verifies; Cradle reflects; JARVIS-1 self-checks; GITM feeds failure reasons back into the next plan.

And the honest negative results, which matter more for us:

The conclusion I did not expect: nobody has demonstrated a prompt-driven VLM playing a live 3D game well. The working systems either (a) don't look at pixels at all, (b) pause the game, or (c) fine-tune a small vision model on thousands of hours of human play and engineer the loop down to 200 ms. Our local 1.1-second read is competitive with the prompted state of the art — and that state of the art does not play well.

The control budget, drawn

Put the numbers on one timeline and the architecture question answers itself. The game gives you a global cooldown boundary every 1.5 seconds. Interrupts and dodges want 200–400 ms. Our read costs about 1.1 s, and it starts only after a capture that costs half a second.

The control budget A three-second timeline with GCD boundaries, reflex windows, and the measured observe loop drawn to scale. Three seconds of a fight, to scale 1 s = 240 px. Global cooldown 1.5 s; the measured perception loop is 1.6 s 0 s 1.5 s 3.0 s GCD GCD What the game allows cast / ability decision window next cast / ability decision window What reflexes need 200–400 ms — interrupt, dodge, defensive and again, every time something is cast at you What our loop costs capture 0.46 vision read 1.11 s answer at 1.6 s — next GCD already started A slower, better model does not help here. It moves the bar further right. Only shorter bars, or work that started earlier, fit inside the window. The escape hatch is the same one every working system uses: decide a short script, start executing before the next frame arrives, then verify. Perception does not have to be fast if action does not wait for it.

Two conclusions fall out. Rotation can be slow, reflexes cannot. One decision per GCD is 0.67 Hz, which a 1.1-second reader can just about serve if the pipeline is deep enough — but interrupts and dodges need a layer that never asks a model. Action must not wait for perception. OpenAI Five acts 7.5 times a second with 167–267 ms reactions by choosing a frame offset inside a four-frame skip; AlphaStar runs at ~110 ms processing with a learned "observe next when" policy. Neither waits for a language model to finish a sentence.

The key-hold problem

Every action game moves a character by holding a key down for a duration. Our harness could only press and release. That sounds like a small gap and is actually the load-bearing one: without a hold, there is no walking, no turning, no swimming, no steering. So I researched how the working bots do it, built the primitive, and measured what it does.

What the prior art actually does

I read the input layer of WowClassicGrindBot, the most serious open-source WoW bot, and it settles the question: a hold is two events, never a stream. One key-down message, a wait, one key-up message. Nothing repeats in between. The game treats the key as held for the entire interval.

Three details from that codebase are worth stealing outright:

And the failure modes it documents are the ones that will bite us: a tap where a hold was needed silently does the wrong thing (its drowning code notes that a tap "barely lifts them", which reads from outside as a stuck spacebar); posting messages to a window does not change the key state a polling game reads; and chat is a separate mode where held movement keys must be released first.

Three ways to solve it, in order of preference

Three ways to hold a key Avoid the hold, synthesize it, or emulate hardware — in that order, with what each tier reaches. Three ways to hold a key Cheapest first. Each rung reaches a different class of consumer — test before climbing. 0 — DON'T HOLD AT ALL Autorun is a toggle (one tap on, one tap off, and it survives turning). Click-to-Move turns a right-click on the ground into a walk to that point. Interact-with-target walks to an NPC, loot or corpse and acts on it. Covers travel, approach, loot, vendors, melee start. Needs only taps and one click. 1 — SYNTHESIZE THE HOLD One key-down event, a wait, one key-up event. No repeats in between. Posted at the HID event tap, not to a pid — pid routes are filtered by canvas apps and games. Hold at least 33 ms (two frames), track held keys, release all on abort. Reaches anything that builds key state from the event stream: Cocoa, SDL, Wine/CrossOver. 2 — EMULATE THE HARDWARE A virtual HID keyboard driver, or a microcontroller pretending to be a USB keyboard. The key is genuinely down at the device layer, so every input path sees it. Reaches games reading raw IOHID or GameController state, and anything checking event provenance. This is also the rung that exists to defeat detection. Policy, not engineering, is the limit here. VERIFY BY watching the character move VERIFY BY a text field (events land) then the game VERIFY BY the game only — no cheap probe costs a root helper or a board stop climbing when the character moves Measured on this machine: tier 1 delivers a key event, produces exactly one keystroke per hold, and is invisible to key-state polling.

Tier 0 is bigger than it looks

WoW is unusually kind here, and this is the finding that shrinks the whole problem. The client has built-in toggles and clicks that replace most holds:

ControlToggle or holdWhat it replaces
AutorunToggle — one tap on, one tap off; cancelled by forward/backHolding W for any straight run. It survives turning.
Click-to-Move (a client option)One right-click on the groundPoint-to-point movement. Straight line, no pathfinding, roughly screen-range.
Interact with targetOne tapWalking to an NPC, corpse or node and acting on it — with click-to-move on, it turns and closes the distance itself.
Mouselook toggle (via a macro)ToggleHeld right-button steering: enter mouselook, then steer with relative mouse deltas.
Auto-attackToggleEvery repeat attack.
Keyboard turningGenuine hold — angle = time × 180°/sNothing. This is the one that truly needs a timed hold.

Channelled spells need you to stand still, not to hold anything. No Classic-era ability requires a held key. Addons cannot move the character at all — every movement function has been protected since 2.0.1 — so all motion must come from the input layer's own events or from click-to-move clicks.

So the hold problem reduces to steering. Autorun covers the straight lines, click-to-move and interact cover the destinations, and the only thing left is pointing in a direction — which is either a timed hold of 100 ms per ~18°, or a mouselook toggle plus mouse deltas, which our harness already supports because drag takes a duration.

Tier 1: what I built and what it measured

I wrote the primitive rather than assuming it: a small compiled tool that posts one key-down at the HID event tap, sleeps, then posts one key-up, with a marker so an observer can identify its own events.

holdkey w 600          # one down, 600 ms, one up   -> 626 ms wall
holdkey j 15 --repeat=10 --gap=20                    # tap loop, for actions not movement
QuestionMeasured result
Do synthetic key events reach an application?Yes. A 150 ms hold of a letter put exactly one character into a text field.
Does a long hold auto-repeat?No. A 1000 ms hold produced exactly one keystroke. macOS generates repeat from hardware, not from synthetic events.
Does key-state polling see the hold?No. Polling CGEventSource.keyState at 200 Hz in both hidSystemState and combinedSessionState observed zero transitions during an 800 ms hold.
What does one invocation cost?33 ms median to spawn the process and post. A reflex loop cannot pay that per keypress — the executor has to be resident.
Where this disagrees with the documentation: Apple's header for CGEventSourceStateID says the state tables accumulate keyboard state "placed in effect by posting events with associated sources", which implies a polled read should see a posted key as down. Our probe saw nothing. Either that guarantee doesn't hold for a synthetic source in this OS version, or the probe is measuring something narrower than the sentence promises. I am reporting the measurement, not the doc, and the practical consequence is the same either way: assume a hold is visible only to consumers that react to the event stream, and verify per target.

One more correction: the route matters more than the call

Our existing driver presses keys by posting to a specific process. That is the wrong route for a game, and cua's own engineering write-up says why: canvas applications — Unity, Blender, games — filter per-process routes entirely and only accept events posted at the HID tap, usually requiring a brief frontmost activation and a leading mouse-move. So the hold primitive has to post at the HID tap, with the game frontmost. This also explains an observation from the first session: the accessibility tree of the game returned nothing useful while pixel capture worked fine.

Two consequences for the build. The executor becomes a resident process that owns a held-key table and a release-all path, not a series of one-shot calls. And the "hold" primitive must be tested against the real client early, because the tier is decided by the client's input model, not by our preference.

Four architectures

These are options, not commitments, ordered by how much of the published evidence each one inherits. Option 1 is the smallest change from what already runs. Option 4 is the only one with a demonstrated path to human-level play, and it is also the only one that needs data we do not have.

Option 1 — Reflex, Tactical, Plan (three tiers, two boxes)

Keep the expensive model, but take it off the critical path. A deterministic executor on the second machine owns the keyboard and reads cheap pixel probes at high frequency; the vision model decides what the executor should attempt, once every second or two, and emits a short script rather than a single keypress.

Option 1: reflex, tactical, plan Deterministic executor at ten hertz on the game box; tactical VLM at about one hertz; asynchronous planner. Option 1 — reflex, tactical, plan Take the model off the critical path; let a deterministic executor hold the keys BOX 2 — MAC MINI, 1080p, OWNS THE CLIENT WoW CLIENT 1920 × 1080 Milo's character foreground window PROBES 10 Hz, no model health, target, cast bar milliseconds per read EXECUTOR — behaviour tree / state machine runs the current script: hold forward 600 ms, tap 3, tap 4 interrupts and defensives fire here, in under 100 ms never waits on the network INPUT INJECTION hold primitive at the HID tap; release-all on abort FLEET — GB300, LOCAL TACTICAL VLM — ~0.5–1 Hz reads the frame, returns a short script with timings DSV4.1-Flash :30006 · thinking off · ~1.1 s 2–3 requests kept in flight, stale ones dropped by timestamp PLANNER — asynchronous, seconds–minutes what are we doing: which quest, which vendor, group or solo writes goals and, between sessions, replaces filters in the executor never runs inside combat — it is a teacher, not a participant this is where a frontier model earns its cost: reviewing recorded fights TORSI pattern: slow teacher writes rules, fast student acts What it inherits: the slow-planner/fast-executor split that every scaling project uses, plus action chunking. What it still cannot do: react to a 200 ms cast with vision. That has to come from probes and simple rules.

Option 2 — Addon sensor channel instead of pure vision

Every serious WoW automation project reads a structured channel, not the UI. The classic trick is an addon that paints game state into a corner of the screen as coloured cells, which an external program decodes without a model at all — microseconds instead of seconds. The catch is that this is exactly the surface Blizzard has been closing. Since the Midnight addon changes, combat values are secret: an addon can display them but cannot compute on them, and a decoder that turned display back into numbers is the thing the policy is aimed at.

Option 2: addon sensor channel Fast structured channel from an addon, with a VLM covering the remainder. Option 2 — addon sensor channel Structured and fast, and bounded by what the addon API still permits CLIENT + ADDON Lua addon, display only can show, cannot compute quests, bags, vendors, map own casts, proc glows PIXEL DECODER coloured cells → state no model, no tokens tens of hertz needs fixed fps, no scaling STATE BUS typed events executor input planner input the same gate as now VLM COVER everything the addon cannot see: the scene, the map, other players 1 Hz is plenty here WHAT SECRET VALUES CLOSE OFF In combat, addons cannot read enemy health, casts, auras or cooldown remaining, and cannot branch on them. Ready-or-not booleans, proc glows and your own casts stay readable, which is enough for a coarse rotation signal. Encoding combat state into pixels to defeat that rule is the automated-control case the EULA forbids. It is a boundary, not a puzzle. Best used as: a fast, boring channel for non-combat decisions — where am I, what is in my bags, what does the vendor sell. Worth building for the quest/vendor economy loop even if combat stays on vision.

Option 3 — Protocol oracle in a sandbox

Turn the problem around: instead of inferring state from pixels, run a private server that gives state, and let the agent speak the game's own protocol. This is what agent-wow did, and it is why that run finished the orc starting zone in forty minutes with no deaths and no vision at all. It cannot be pointed at a live account, but it can produce something that transfers: validated navigation, validated executor logic, and a scoreboard that cannot lie.

Option 3: protocol oracle in a sandbox Private server gives ground truth; protocol agent learns; results transfer to the vision stack. Option 3 — protocol oracle in a sandbox No pixels, no account risk, and a scoreboard you can trust AZEROTHCORE / FORGE Docker, 3.3.5a true state: position, auras, quest flags, mob health mmaps for navigation GM commands to inspect PROTOCOL CLIENT packets, not pictures decode world state send movement and casts no client, no UI, no OCR existing project: agent-wow CODING AGENT writes the modules quest logic as code navigation helpers verified by the server, not by a model's opinion TRANSFER routes and waypoints executor rules failure catalogue to Options 1 and 4 pixels stay for the live box WHY THIS IS FIRST IN LINE FOR LEARNING It is the only option where a failure is diagnosable: the server knows what actually happened. It is also the only one that produces a scoreboard (time to level 10, deaths per quest) without touching a live character. Cost: a server, a client, and the transfer problem — protocol agents see the world very differently from a person looking at a screen. The 3.3.5a sandbox also is not Forever: different world, different UI, different rules.

Option 4 — Fine-tuned vision-language-action model

The only architecture with published evidence of a VLM playing a commercial 3D game at human efficiency is Lumine: a 7B model, 720p frames at 5 Hz, keyboard and mouse out at 30 Hz in 33 ms chunks, fine-tuned on 1,731 hours of human gameplay, with a 25.3× latency engineering effort behind it. The uncomfortable implication is that prompting is not the missing ingredient — training data is.

Option 4: fine-tuned VLA Record, label, fine-tune a small VLA, serve at 5 Hz perception with 30 Hz action chunks. Option 4 — fine-tuned VLA The evidence says data, not prompting, is the missing ingredient RECORD human plays, we log frame + keys + mouse already have the capture path from Lane A 643 h needed for parity FINE-TUNE 7B-class VLA LoRA on frames → keys GB300 has the memory and the serving lane days of GPU time SERVE 5 Hz perception 30 Hz action out, in 33 ms chunks streamed as decoded no reasoning in the loop WHAT IT COSTS 1,731 h of gameplay was Lumine's training set 25.3× latency work a labelling pipeline months, not weeks THE HONEST READ Lumine's result is the ceiling anyone can point to: five hours of a commercial game at human efficiency. It took 1,731 hours of human play and a large engineering effort to reach it. Starting at ten hours of our own play, expect proportionally less. Worth doing as a data-collection habit now, which costs nothing, and as a training campaign later, once the executor works. Not compatible with the ToS on a live account either. What it changes is competence, not permission.

How they compare

1 — three tiers2 — addon channel3 — protocol oracle4 — fine-tuned VLA
Time to something realDaysDays (non-combat only)1–2 weeksMonths
Handles 200 ms eventsYes, via probesYes, via probesNot applicableBest case, ~40 ms key latency
Needs model calls in combat1 every 1–2 sNoNoNo, one forward pass
Blocked by secret valuesNo — reads pixelsPartly, in combatNo — server has stateNo — reads pixels
Account riskOn a live account, yesOn a live account, yesNoneOn a live account, yes
Evidence behind itStrong (every scaling system)Strong (GrindBot lineage)Strong (agent-wow)Strong but data-hungry (Lumine)
What I would build, in order: Option 3 for the executor and navigation knowledge, because failures there are diagnosable and cost nothing. Option 1 alongside it for the live companion, because it is days of work and it reuses the perception we already measured. Option 2 only for the non-combat economy loop. Option 4 as a data-collection habit now and a training campaign much later — the evidence says it is the only route to real competence, and the evidence also says it is not a weekend.

Subproject: the auction house

Separate from play, and interesting because it splits cleanly along the line this post keeps drawing: the analysis is legal, offline, and already possible; the execution is the same automation problem as playing.

Market data is not gated. TradeSkillMaster publishes its price database as static CSV files with no API key, no sign-in and no rate limit (its older web API is a separate thing, keyed and tiered; the CSVs are the free path). I pulled two of them to check:

FileResultColumns that matter
retail/us/realm/area-52/items.csv1.5 MB, timestamp minutes oldmarket value, min buyout, recent, historical
classic/us/region/items.csv6,620 rows, dated two days agoplus saleRate and soldPerDay

Those last two columns are the whole discipline. They separate a rare item that lists at 19,000 gold and sells once a month from a commodity that sells ninety times a day at a thin margin. Without them, "undercut the lowest listing" is a strategy that quietly loses money; with them, a ranking can be built on gold per day of capital at risk rather than on sticker price.

That is also why the two data sources are not interchangeable. Blizzard's own Game Data API has no sale history at all — you get a snapshot of current listings, refreshed about hourly, and you have to infer sell-through by diffing snapshots over time. TSM's region files give you a computed sale rate directly. If you want to know whether a market actually moves, the third-party number is the one to build on; the official API is for live listings and for anything the addon API won't show.

What the strategies actually are

MethodMechanicWhat the evidence supports
FlippingBuy below the recent market value, relist above itRealm-specific only: since trade goods became region-wide, cross-realm commodity flipping no longer exists. Profits are personally reported, never independently verified.
SnipingContinuously scan for listings far below value and buy themThe one place automation was specifically closed: since 8.3 each scan and each purchase needs its own hardware event, and TSM rebuilt its sniper around that rule. Scans are throttled.
Crafting arbitrageCraft when the item's price exceeds material costArithmetic, not a trick: sale price minus materials minus the 5% cut minus deposits lost on expired listings. Undercut wars steadily erode it.
ShufflesConvert cheap materials into more valuable outputs (prospect, mill, disenchant)Dependent on drop rates and fragile across patches; these get nerfed and stay nerfed.
Vendor flipsBuy from a vendor, sell on the AHSmall, steady, boring margins — the closest thing to a risk-free rate.
TransmogFarm appearances, list them per realmHigh sticker prices and very low sale rates. This is the category that punishes anyone who ranks by price instead of liquidity.
On the numbers: there are no verified gold-per-hour figures published anywhere I could find, only players' self-reported results. That is an argument for building the measurement ourselves rather than quoting someone else's: a tracker that records what we paid, what we listed, what actually sold, and over what horizon. Gold per day per gold risked is a number a single agent can produce honestly in a few weeks.

Where the money leaks

Every strategy pays the same tolls, and they are large enough to turn a good idea into a loss: a 5% cut on every sale, plus a deposit on non-commodity listings that is gone if the auction expires unsold. A flip that clears a 3% margin after undercutting is a 2% loss after the cut. Any ranking that does not subtract both is not a ranking, it is a wish.

The auction-house pipeline Legal offline analysis on free data; the execution boundary drawn explicitly. Auction house: brain, then hands Free data, offline analysis, and one clearly drawn legal line PUBLIC PRICE DATA TSM CSVs realm and region files no key, no rate limit minutes to days old OWN SCANS what the public files cannot see: your bags, your professions, your realm's odd corners MARKET BRAIN offline, no client liquidity-adjusted rank craft vs. buy, reset windows gold per day per gold risked RECOMMEND a short list, priced with the max bid and the walk-away delivered to chat LEGAL, AND USEFUL TOMORROW read prices, rank opportunities, compute craft versus buy, tell a human what to bid and when to walk away. No client interaction at all. Runs on a cron job. THE BOUNDARY posting, buying, undercutting and cancelling from code is automation of the game. That is the EULA case, and no amount of profitability makes it a different question. Why this subproject is worth doing first: it needs no client, no account risk, and it produces a measurable number — gold per day per gold risked — that either works or does not, quickly and for free. Data checked October 18, 2026 from public-data.tradeskillmaster.com. Classic realm-level files 404; the region file is the usable one.

The ceiling, precisely

It is worth being exact about where legal ends, because "an addon can't do it" is too vague and "a bot does it" is too coarse. The auction-house functions that actually buy and bid are protected: they require a hardware event, so one real click or keypress per action. Searches are throttled. TSM did not work around that in 8.3 — it rebuilt its sniper to fit it, accepting that a human has to click each buy. That is the true shape of the boundary: on the legal side, the human is the rate limit.

So the honest framing for this subproject is not "agent versus player". It is: everything up to the click can be computed by the agent, and the click stays with James. A good tool makes his clicks count. A bot removes them, and that is the line.

Free history sources worth using: TSM's CSVs for computed prices and sale rates; Undermine Exchange (the successor to The Undermine Journal, which shut down) for charts and an API; Saddlebag Exchange for cross-realm comparisons. Blizzard's Game Data API for live listings if we ever need them, with the caveat that its Classic-era auction endpoints have a history of breaking.

What our own harness gives us

One more layer of reality: the tooling this agent actually drives. Four things I measured or read out of its own schemas today, because each one shapes the build.

CapabilityRealityConsequence
Screen captureFull desktop at 4K: 0.46 s. Window enumeration: 0.067 s. Per-window capture via ScreenCaptureKit exists.Full-frame capture is the slowest cheap thing in the loop. Window-scoped capture is the next thing to measure.
Key inputpress_key and hotkey are press-and-release, and they post to the process, not the HID tap. No key-hold primitive; only drag takes a duration.Solved separately: a resident hold primitive at the HID tap, plus click-to-move and autorun so most motion needs no hold at all. See the key-hold section.
DeliverymacOS input posts through SLPSPostEventRecordTo, pid-scoped, no cursor warp; an escalation path to foreground exists for surfaces that ignore background input. Raw-input canvases and games are the documented case.Expect to need the foreground rung on the game window, and test it on a login screen before trusting it.
Accessibility treeEmpty and slow on the game (a 20 s walk timed out).Pixels are the only channel. Element-index clicking does not apply to a game.

None of this is a blocker. It is the difference between a plan that assumes a magic input API and one that names the shim it needs to write.

Where this stands: the observe loop is measured and running. The play route is chosen by the account owner with the ToS risk stated plainly and understood. The build order is Option 3 for knowledge, Option 1 for the live companion, Option 2 for the economy loop, Option 4 as a long campaign. The auction-house subproject starts now because it is free and legal and tells us whether the analytical half is any good before anyone touches a client.