aomarket-bot

Standalone Anarchy Online market-tracking bot

View the Project on GitHub zznathans/aomarket-bot

aomarket

The aomarket Python package — everything that ships in the aomarket-bot container.

Module map

Module Purpose
aochat/ AO chat protocol client (login, tells, privgroup)
aodb/ aodb-api client — item lookup/search
gmi/ GMI client — live buy/sell order books
autotrack/ ao-stonks.com scraper for auto-tracking popular items
db/ SQLAlchemy models and repositories
market/ Core business logic: MarketService, chat commands, rendering
bot/ The AO chat session’s dedicated thread and background loops
api/ FastAPI control surface

Plus three top-level files with no submodule of their own:

Concurrency model

The process has (at most) two independently-running event loops:

  1. FastAPI’s loop, run by uvicorn on the main thread.
  2. The bot’s loop, run on a dedicated non-daemon thread (bot/runner.py) — only started at all if AO_LOGIN/AO_PASSWORD/AO_CHARACTER are all set. Without them, the process runs in API-only mode: no AO chat connection, bot_handle stays None, and every API route that would need the bot (/bot/*, and any MarketService ChatSink callback) either degrades to a no-op or returns 503.

The two loops never share state directly. The bridge is api.deps.call_on_bot(), which schedules a coroutine onto the bot thread’s loop with asyncio.run_coroutine_threadsafe and awaits the result from whichever loop called it. BotHandle (in bot/runner.py) is the only object shared between the two threads — its loop/bot fields are published once, after login, and are read-only from the API side after that; ready (a threading.Event) is the memory barrier that makes that publication safe to observe from the other thread.

Request/command flow

Both the chat command layer (market/commands.py) and the HTTP API (api/routes/) are thin presentation layers over the exact same MarketService methods — neither talks to db, aodb, gmi, or autotrack directly. A chat-originated market watch <item> and an API POST /watch/{aoid} end up calling the same code.