Skip to main content
Glama

Path of Exile 2 Build Optimizer MCP

trade_api.py20.5 kB
""" Path of Exile Trade API Client Searches the official trade market for items Enhanced with gear evaluation for intelligent upgrade recommendations. """ import httpx import asyncio import logging from typing import Dict, List, Optional, Any try: from ..config import settings from .rate_limiter import RateLimiter from .cache_manager import CacheManager except ImportError: from src.config import settings from src.api.rate_limiter import RateLimiter from src.api.cache_manager import CacheManager logger = logging.getLogger(__name__) class TradeAPI: """ Official Path of Exile Trade API client Searches for items on the trade market """ def __init__( self, cache_manager: Optional[CacheManager] = None, rate_limiter: Optional[RateLimiter] = None, poesessid: Optional[str] = None ): self.base_url = "https://www.pathofexile.com" self.cache_manager = cache_manager self.rate_limiter = rate_limiter or RateLimiter(rate_limit=2) # Very conservative for trade API # Use provided poesessid, or fall back to config self.poesessid = poesessid or settings.POESESSID if not self.poesessid: logger.warning( "No POESESSID cookie configured. Trade API searches may be limited or fail.\n" "Use the 'setup_trade_auth' MCP tool for automated setup.\n" "Or see .env.example for manual cookie extraction instructions." ) self.client = httpx.AsyncClient( timeout=30.0, follow_redirects=True, headers={ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0", "Accept": "*/*", "Accept-Language": "en-US,en;q=0.5", "X-Requested-With": "XMLHttpRequest", "Origin": "https://www.pathofexile.com", "DNT": "1", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", } ) # Add session cookie if provided if self.poesessid: self.client.cookies.set("POESESSID", self.poesessid, domain="www.pathofexile.com") async def search_items( self, league: str, filters: Dict[str, Any], limit: int = 10 ) -> List[Dict[str, Any]]: """ Search for items on the trade market Args: league: League name (e.g., "Abyss", "Standard") filters: Search filters (mods, stats, type, etc.) limit: Maximum number of results Returns: List of item listings with pricing and details """ try: await self.rate_limiter.acquire() # Build search query query = self._build_search_query(filters) # Perform search - Note: /api/trade2/search/poe2/{league} search_url = f"{self.base_url}/api/trade2/search/poe2/{league}" # Add referer header for this specific request headers = {"Referer": f"{self.base_url}/trade2/search/poe2/{league}"} logger.info(f"Searching trade market in {league}") logger.debug(f"Query: {query}") response = await self.client.post(search_url, json=query, headers=headers) response.raise_for_status() search_result = response.json() result_ids = search_result.get("result", [])[:limit] query_id = search_result.get("id") # Get query ID for fetching if not result_ids: logger.info("No items found matching criteria") return [] logger.info(f"Found {len(result_ids)} items, fetching details...") # Fetch item details await asyncio.sleep(0.5) # Rate limiting between requests items = await self._fetch_item_details(result_ids, query_id) return items except httpx.HTTPStatusError as e: logger.error(f"Trade API HTTP error: {e.response.status_code} - {e.response.text}") return [] except Exception as e: logger.error(f"Trade API error: {e}") return [] async def _fetch_item_details(self, item_ids: List[str], query_id: str = None) -> List[Dict[str, Any]]: """Fetch full details for items by their IDs""" try: await self.rate_limiter.acquire() # Join IDs with commas id_string = ",".join(item_ids[:10]) # Max 10 at a time # PoE2 trade API uses /api/trade2/fetch/ fetch_url = f"{self.base_url}/api/trade2/fetch/{id_string}" # Add query parameter if we have a query_id if query_id: fetch_url += f"?query={query_id}" response = await self.client.get(fetch_url) response.raise_for_status() data = response.json() items = [] for item_data in data.get("result", []): item = self._parse_item_listing(item_data) if item: items.append(item) return items except Exception as e: logger.error(f"Error fetching item details: {e}") return [] def _parse_item_listing(self, raw_data: Dict) -> Optional[Dict[str, Any]]: """Parse raw item listing data into structured format""" try: listing = raw_data.get("listing", {}) item = raw_data.get("item", {}) return { "id": raw_data.get("id"), "name": item.get("name", ""), "type": item.get("typeLine", ""), "base_type": item.get("baseType", ""), "item_level": item.get("ilvl", 0), "corrupted": item.get("corrupted", False), "price": { "amount": listing.get("price", {}).get("amount"), "currency": listing.get("price", {}).get("currency"), "type": listing.get("price", {}).get("type"), }, "seller": { "account": listing.get("account", {}).get("name"), "character": listing.get("account", {}).get("lastCharacterName"), "online": listing.get("account", {}).get("online", False), }, "properties": item.get("properties", []), "requirements": item.get("requirements", []), "explicit_mods": item.get("explicitMods", []), "implicit_mods": item.get("implicitMods", []), "enchant_mods": item.get("enchantMods", []), "sockets": item.get("sockets", []), "links": self._count_links(item.get("sockets", [])), "listed_time": listing.get("indexed"), } except Exception as e: logger.error(f"Error parsing item: {e}") return None def _count_links(self, sockets: List[Dict]) -> int: """Count maximum linked sockets""" if not sockets: return 0 max_links = 0 for socket_group in sockets: group_size = socket_group.get("group", 0) if group_size > max_links: max_links = group_size return max_links + 1 if max_links > 0 else 0 def _build_search_query(self, filters: Dict[str, Any]) -> Dict[str, Any]: """Build trade API search query from filters""" query = { "query": { "status": {"option": "securable"}, # PoE2 uses "securable" not "online" "stats": [{"type": "and", "filters": []}], }, "sort": { "price": "asc" # Cheapest first } } # Text search (item name/type) if "term" in filters: query["query"]["term"] = filters["term"] # Item type filter (specific type like "Amulet") if "type" in filters: query["query"]["type"] = filters["type"] # Name filter (specific unique name) if "name" in filters: query["query"]["name"] = filters["name"] # Stats/mods filter if "stats" in filters and filters["stats"]: query["query"]["stats"] = self._build_stat_filters(filters["stats"]) # Item filters (sockets, links, etc.) if "item_filters" in filters: query["query"]["filters"] = filters["item_filters"] return query def _build_stat_filters(self, stats: List[Dict[str, Any]]) -> List[Dict[str, Any]]: """Build stat filter groups for the query""" stat_filters = [] for stat in stats: stat_filter = { "type": "and", "filters": [] } if "id" in stat: stat_filter["filters"].append({ "id": stat["id"], "value": { "min": stat.get("min"), "max": stat.get("max") } }) if stat_filter["filters"]: stat_filters.append(stat_filter) return stat_filters async def close(self): """Close the HTTP client""" await self.client.aclose() async def search_for_upgrades( self, league: str, character_needs: Dict[str, Any], max_price_chaos: Optional[int] = None ) -> Dict[str, List[Dict[str, Any]]]: """ Search for items that address character deficiencies Args: league: League name character_needs: Dict with keys like: - missing_resistances: {"fire": 2, "cold": 8} - needs_life: bool - needs_es: bool - item_slots: List of slots to search (e.g., ["charm", "amulet", "helmet"]) max_price_chaos: Maximum price filter Returns: Dict of item_type -> List of matching items """ results = {} # Extract needs missing_res = character_needs.get("missing_resistances", {}) needs_life = character_needs.get("needs_life", False) needs_es = character_needs.get("needs_es", False) item_slots = character_needs.get("item_slots", ["charm", "amulet", "helmet"]) # Search for charms if resistances are needed if "charm" in item_slots and missing_res: logger.info("Searching for resistance charms...") charm_results = await self._search_resistance_charms( league, missing_res, max_price_chaos ) if charm_results: results["charms"] = charm_results # Search for amulets if needed if "amulet" in item_slots: logger.info("Searching for amulets...") amulet_results = await self._search_amulets_with_stats( league, missing_res, needs_life, max_price_chaos ) if amulet_results: results["amulets"] = amulet_results # Search for helmets if needed if "helmet" in item_slots and (needs_life or needs_es or missing_res): logger.info("Searching for helmets...") helmet_results = await self._search_helmets_with_defenses( league, missing_res, needs_life, needs_es, max_price_chaos ) if helmet_results: results["helmets"] = helmet_results return results async def _search_resistance_charms( self, league: str, missing_res: Dict[str, int], max_price_chaos: Optional[int] = None ) -> List[Dict[str, Any]]: """Search for charms with resistances""" filters = {"term": "charm resistance"} items = await self.search_items(league, filters, limit=20) # Filter for charms with needed resistances filtered = [] for item in items: if max_price_chaos: price = item.get("price", {}) if price.get("currency") == "chaos" and price.get("amount", 999) > max_price_chaos: continue # Check if has multiple resistances mods = item.get("explicit_mods", []) + item.get("implicit_mods", []) res_count = sum(1 for mod in mods if "Resistance" in mod) if res_count >= 2: filtered.append(item) return filtered[:10] async def _search_amulets_with_stats( self, league: str, missing_res: Dict[str, int], needs_life: bool, max_price_chaos: Optional[int] = None ) -> List[Dict[str, Any]]: """Search for amulets with spell levels and resistances""" filters = {"term": "amulet spell"} items = await self.search_items(league, filters, limit=20) filtered = [] for item in items: if max_price_chaos: price = item.get("price", {}) if price.get("currency") == "chaos" and price.get("amount", 999) > max_price_chaos: continue mods = item.get("explicit_mods", []) # Check for spell levels has_spell_levels = any("+#" in mod and "Spell" in mod and "Level" in mod for mod in mods) # Check for resistances has_res = sum(1 for mod in mods if "Resistance" in mod) >= 2 # Check for life if needed has_life = any("Life" in mod and "Maximum" in mod for mod in mods) if has_spell_levels and has_res and (has_life or not needs_life): filtered.append(item) return filtered[:10] async def _search_helmets_with_defenses( self, league: str, missing_res: Dict[str, int], needs_life: bool, needs_es: bool, max_price_chaos: Optional[int] = None ) -> List[Dict[str, Any]]: """Search for helmets with life/ES and resistances""" filters = {"term": "helmet life"} items = await self.search_items(league, filters, limit=20) filtered = [] for item in items: if max_price_chaos: price = item.get("price", {}) if price.get("currency") == "chaos" and price.get("amount", 999) > max_price_chaos: continue mods = item.get("explicit_mods", []) # Check for life has_life = any("Life" in mod and "Maximum" in mod for mod in mods) # Check for ES has_es = any("Energy Shield" in mod for mod in mods) # Check for resistances res_count = sum(1 for mod in mods if "Resistance" in mod) meets_requirements = True if needs_life and not has_life: meets_requirements = False if needs_es and not has_es: meets_requirements = False if res_count < 2: meets_requirements = False if meets_requirements: filtered.append(item) return filtered[:10] async def search_with_analysis( self, league: str, character_needs: Dict[str, Any], current_gear: Dict[str, Any], base_character_stats: Dict[str, Any], max_price_chaos: Optional[int] = None ) -> Dict[str, List[Dict[str, Any]]]: """ Enhanced search with upgrade value analysis. Wraps search_for_upgrades and adds priority scoring and upgrade recommendations for each item. Args: league: League name character_needs: Character deficiencies current_gear: Dict of slot -> GearStats base_character_stats: Base character stats max_price_chaos: Maximum price Returns: Dict with analyzed items sorted by priority score """ # Get raw search results raw_results = await self.search_for_upgrades( league=league, character_needs=character_needs, max_price_chaos=max_price_chaos ) # For now, return raw results # Full integration with GearEvaluator will be in next update logger.info("Trade search with analysis completed (basic mode)") return raw_results # Helper functions for common searches async def search_amulet_with_spell_levels_and_resistances( league: str, min_spell_levels: int = 2, min_life: int = 50, min_total_res: int = 80, max_price_chaos: int = 100 ) -> List[Dict[str, Any]]: """Search for amulets with spell levels and resistances""" trade_api = TradeAPI() try: filters = { "type": "Amulet", "stats": [ { "id": "explicit.stat_3988349707", # +# to Level of all Spell Skills "min": min_spell_levels, }, ], "item_filters": { "misc_filters": { "filters": { "ilvl": {"min": 75} } } } } results = await trade_api.search_items(league, filters, limit=20) # Filter by resistances and life in code (easier than building complex query) filtered_results = [] for item in results: # Check price price = item.get("price", {}) if price.get("currency") == "chaos" and price.get("amount", 999) > max_price_chaos: continue # Check for life/ES and resistances in mods mods = item.get("explicit_mods", []) has_life = any("Life" in mod and "Maximum" in mod for mod in mods) has_res = sum(1 for mod in mods if "Resistance" in mod) if (has_life or min_life == 0) and has_res >= 2: filtered_results.append(item) return filtered_results[:10] finally: await trade_api.close() async def search_helmet_with_life_es_resistances( league: str, min_life: int = 100, min_es: int = 100, min_total_res: int = 60, max_price_chaos: int = 100 ) -> List[Dict[str, Any]]: """Search for helmets with life, ES, and resistances""" trade_api = TradeAPI() try: filters = { "type": "Helmet", "item_filters": { "misc_filters": { "filters": { "ilvl": {"min": 75} } } } } results = await trade_api.search_items(league, filters, limit=20) # Filter in code filtered_results = [] for item in results: # Check price price = item.get("price", {}) if price.get("currency") == "chaos" and price.get("amount", 999) > max_price_chaos: continue mods = item.get("explicit_mods", []) has_life = any(str(min_life) in mod or "Life" in mod for mod in mods) has_es = any("Energy Shield" in mod for mod in mods) res_count = sum(1 for mod in mods if "Resistance" in mod) if has_life and has_es and res_count >= 2: filtered_results.append(item) return filtered_results[:10] finally: await trade_api.close() async def search_resistance_charms( league: str, min_total_res: int = 30, max_price_chaos: int = 20 ) -> List[Dict[str, Any]]: """Search for charms with resistances""" trade_api = TradeAPI() try: filters = { "type": "Charm", } results = await trade_api.search_items(league, filters, limit=30) # Filter for multi-resistance charms filtered_results = [] for item in results: price = item.get("price", {}) if price.get("currency") == "chaos" and price.get("amount", 999) > max_price_chaos: continue mods = item.get("explicit_mods", []) + item.get("implicit_mods", []) res_count = sum(1 for mod in mods if "Resistance" in mod) if res_count >= 2: # At least 2 different resistances filtered_results.append(item) return filtered_results[:10] finally: await trade_api.close()

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/HivemindOverlord/poe2-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server