# How Azeroth Auction Assassin Monitors Auction Houses: A Technical Deep Dive

> Discover how Azeroth Auction Assassin monitors World of Warcraft auction houses using Blizzard API polling, Last-Modified headers, and parallel threads for efficient data updates.

- Repository: [FF14 Advanced Market Search/azerothauctionassassin](https://github.com/ff14-advanced-market-search/azerothauctionassassin)
- Tags: deep-dive
- Published: 2026-03-01

---

**Azeroth Auction Assassin continuously monitors World of Warcraft auction houses by polling the Blizzard Auction House API during calculated scan windows, using Last-Modified headers to skip unchanged data and parallel threads to process multiple realms simultaneously.**

The `ff14-advanced-market-search/azerothauctionassassin` repository provides an open-source Python solution for tracking underpriced items across World of Warcraft's auction houses. To monitor auction houses efficiently across hundreds of connected realms without exhausting API quotas, the tool implements a time-windowed polling architecture centered on the `MegaData` configuration class and the `Alerts` worker thread. This system coordinates OAuth-authenticated requests, intelligent data deduplication, and Discord webhook notifications into a continuous monitoring loop.

## Configuration and Upload Timer Initialization

Monitoring begins with the `MegaData` class defined in [`utils/mega_data_setup.py`](https://github.com/ff14-advanced-market-search/azerothauctionassassin/blob/main/utils/mega_data_setup.py). During initialization (lines 33-43), the class reads [`AzerothAuctionAssassinData/mega_data.json`](https://github.com/ff14-advanced-market-search/azerothauctionassassin/blob/main/AzerothAuctionAssassinData/mega_data.json) to load region settings, faction preferences, desired items/pets, and scan-window parameters. The configuration includes the **Saddlebag upload-timers** backup URL, which provides the exact minute when each realm's auction house data refreshes.

At startup, `MegaData` optionally calls `get_update_timers_backup` from [`utils/api_requests.py`](https://github.com/ff14-advanced-market-search/azerothauctionassassin/blob/main/utils/api_requests.py) (lines 101-115) to populate `self.upload_timers`. This dictionary maps each `dataSetID` (representing a connected realm or commodity group) to its `lastUploadMinute`, enabling the system to predict when Blizzard publishes new auction snapshots (typically once per hour).

## Scan Window Detection and Scheduling

Every minute, the `Alerts` thread in [`mega_alerts.py`](https://github.com/ff14-advanced-market-search/azerothauctionassassin/blob/main/mega_alerts.py) determines which realms require scanning. The `is_in_scan_window` function (lines 227-249) compares the current minute against each realm's known `lastUploadMinute`, respecting user-defined `SCAN_TIME_MIN` and `SCAN_TIME_MAX` offsets. This creates a dynamic window—typically starting 2 minutes before and extending 5 minutes after the upload time—during which the tool actively polls for new data.

Realms outside their designated windows are skipped, minimizing unnecessary API calls. When the current minute falls within a realm's window, the system flags the corresponding `dataSetID` for immediate processing.

## Parallel API Request Execution

For each realm entering its scan window, the main loop in [`mega_alerts.py`](https://github.com/ff14-advanced-market-search/azerothauctionassassin/blob/main/mega_alerts.py) (lines 260-285) spawns a `ThreadPoolExecutor` with a configurable number of workers (`mega_data.THREADS`). The executor submits `pull_single_realm_data` tasks for every matching `connectedRealmId`, enabling concurrent monitoring of multiple auction houses.

The `pull_single_realm_data` function invokes `mega_data.get_listings_single` (lines 595-639 in [`utils/mega_data_setup.py`](https://github.com/ff14-advanced-market-search/azerothauctionassassin/blob/main/utils/mega_data_setup.py)), which constructs the appropriate Blizzard API endpoint. For standard realms, it builds a region-specific URL; for **commodity items** (cross-realm trading), it uses special IDs `-1` or `-2` to hit the commodity endpoints.

```python

# utils/mega_data_setup.py – decide which API to call

def get_listings_single(self, connectedRealmId: int):
    if connectedRealmId in [-1, -2]:          # commodity endpoint

        auction_info = self.make_commodity_ah_api_request()
    else:
        url = self.construct_api_url(connectedRealmId, "")
        auction_info = self.make_ah_api_request(url, connectedRealmId)
    # “skipped” means the Last‑Modified header didn’t change

    if auction_info.get("skipped"):
        return None
    return auction_info["auctions"]

```

## Intelligent Change Detection

Before parsing auction data, the system checks for actual updates using HTTP header inspection. Both `make_ah_api_request` and `make_commodity_ah_api_request` in [`utils/mega_data_setup.py`](https://github.com/ff14-advanced-market-search/azerothauctionassassin/blob/main/utils/mega_data_setup.py) (lines 663-698 and 726-757) capture the **`Last-Modified`** header from Blizzard's response.

If the timestamp matches the cached value in `self.upload_timers` for that `dataSetID`, the function immediately returns `{"auctions": [], "skipped": True}`, causing the realm to be bypassed. When the timestamp differs, indicating fresh data, `update_local_timers` refreshes the cache and the JSON payload proceeds to parsing. This mechanism prevents processing stale data and respects API rate limits.

```python

# utils/mega_data_setup.py – skip unchanged data

def make_ah_api_request(self, url, connectedRealmId):
    req = requests.get(url, headers={"Authorization": f"Bearer {self.check_access_token()}"})
    if "Last-Modified" in req.headers:
        lastUploadTimeRaw = req.headers["Last-Modified"]
        if self.upload_timers.get(connectedRealmId, {}).get("lastUploadTimeRaw") == lastUploadTimeRaw:
            return {"auctions": [], "skipped": True}
        self.update_local_timers(connectedRealmId, lastUploadTimeRaw)
    return req.json()

```

## Data Processing and Discord Alerts

New auction data passes through `clean_listing_data` in [`mega_alerts.py`](https://github.com/ff14-advanced-market-search/azerothauctionassassin/blob/main/mega_alerts.py) (lines 250-340), which filters listings against user criteria including maximum price, item level, and specific item IDs. Matching auctions are formatted as Discord embeds or plain messages and dispatched via the configured webhook URL.

After processing all active realms, the loop sleeps for 5 seconds if work was performed or 20 seconds if all realms were skipped, then resumes at the next minute mark. This creates a continuous monitoring cycle that reacts immediately to Blizzard's hourly data refreshes.

```python

# mega_alerts.py – main monitoring loop

while self.running:
    current_min = int(datetime.now().minute)
    matching_realms = [realm["dataSetID"]
        for realm in mega_data.get_upload_time_list()
        if is_in_scan_window(current_min,
                              realm["lastUploadMinute"],
                              mega_data.SCAN_TIME_MIN,
                              mega_data.SCAN_TIME_MAX)]
    if matching_realms:
        pool = ThreadPoolExecutor(max_workers=mega_data.THREADS)
        for cid in matching_realms:
            pool.submit(pull_single_realm_data, cid)
        pool.shutdown(wait=True)

```

## Practical Code Examples

### Running a One-Shot Debug Scan

To execute a single scan across all configured realms without starting the continuous loop:

```python
from utils.mega_data_setup import MegaData
from mega_alerts import Alerts

# Initialise MegaData with default JSON files

mega = MegaData()                 # reads AzerothAuctionAssassinData/mega_data.json

# Run a single fast scan across all realms

alerts = Alerts()
alerts.path_to_data_files = None  # use defaults

alerts.run()                      # will invoke main_fast() → pull_single_realm_data for every realm

```

### Fetching Listings for a Specific Realm

To manually retrieve auctions for a single connected realm and check for new data:

```python
from utils.mega_data_setup import MegaData

mega = MegaData()
connected_realm_id = 1234                 # ID from AzerothAuctionAssassinData/*-wow-connected-realm-ids.json

auctions = mega.get_listings_single(connected_realm_id)

if auctions:
    print(f"Found {len(auctions)} listings in realm {connected_realm_id}")
else:
    print("No new data – Last‑Modified unchanged")

```

### Testing Scan Window Logic

To verify whether a specific minute falls within a realm's monitoring window:

```python
from mega_alerts import is_in_scan_window

current = 3          # current minute of the hour

last_upload = 58     # realm reported upload at minute 58

min_before = -2      # start scanning 2 minutes before upload

max_after = 5        # stop scanning 5 minutes after upload

print(is_in_scan_window(current, last_upload, min_before, max_after))

# → True (window wraps around the hour boundary)

```

## Summary

- **Azeroth Auction Assassin** monitors auction houses by polling the Blizzard API only during calculated scan windows around each realm's known upload time.
- The **`Last-Modified`** header check in `make_ah_api_request` eliminates redundant processing of unchanged data.
- **`ThreadPoolExecutor`** enables parallel scanning of multiple realms while respecting rate limits.
- The **`MegaData`** class centralizes configuration, OAuth token management, and timer caching.
- Detected deals are filtered through `clean_listing_data` and immediately posted to Discord via webhooks.

## Frequently Asked Questions

### How often does Azeroth Auction Assassin check the auction house?

The main loop evaluates realms every minute, but only initiates API requests during the configured scan window (typically 2-5 minutes before and after the realm's hourly data upload). Outside these windows, realms are skipped entirely to conserve API quota.

### What prevents the tool from hitting Blizzard API rate limits?

Two mechanisms protect against rate limiting: the **`Last-Modified`** header comparison that skips unchanged data, and the scan window logic that concentrates requests only when new data is expected. Additionally, the `ThreadPoolExecutor` uses a configurable worker count (`THREADS` in [`mega_data.json`](https://github.com/ff14-advanced-market-search/azerothauctionassassin/blob/main/mega_data.json)) to limit concurrent connections.

### How does AAA know when a realm's auction data updates?

The system references the **Saddlebag upload-timers** API or local `upload_timers` cache containing each `dataSetID`'s `lastUploadMinute`. Blizzard typically refreshes auction house data once per hour at consistent minutes (e.g., minute 58), and AAA polls aggressively only during the expected refresh window.

### Can the tool monitor commodity items separately from specific realms?

Yes. In [`utils/mega_data_setup.py`](https://github.com/ff14-advanced-market-search/azerothauctionassassin/blob/main/utils/mega_data_setup.py), the `get_listings_single` method treats IDs `-1` and `-2` as commodity endpoints, routing them to `make_commodity_ah_api_request` instead of the standard realm-specific `make_ah_api_request`. This allows monitoring cross-realm commodity markets independently from individual server auction houses.