Standalone Anarchy Online market-tracking bot
The aomarket Python package — everything that ships in the
aomarket-bot container.
| 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:
config.py — AppConfig (pydantic-settings): every environment
variable the bot reads, with defaults, loaded once in main.py.logging.py — structlog configuration (configure_logging) and
get_logger(), used throughout the codebase for structured log output.main.py — the process entry point: builds the DB engine, seeds
default settings, starts the bot thread if AO credentials are present,
and runs the FastAPI app under uvicorn.The process has (at most) two independently-running event loops:
uvicorn on the main 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.
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.