POE2MarketMCP
POE2MarketMCP
A local, single-user tool that tracks Path of Exile 2 market data and exposes it to an LLM over MCP: currency prices, per-league price history, and stash valuation.
It runs on your own machine. A background collector polls market data on a schedule and stores it in a local SQLite file; an MCP server reads that file to answer your LLM's questions instantly, with no network call at query time. Nothing is hosted, shared, or sent anywhere — it's your data, on your box.
flowchart LR
NINJA["poe.ninja<br/>currency prices"]
GGG["GGG trade API<br/>items and stash"]
subgraph local["your machine"]
direction LR
COL["collector daemon<br/>background, 24/7"]
DB[("market.db<br/>SQLite")]
MCP["MCP server<br/>15 tools"]
COL -->|writes| DB
DB -->|reads| MCP
end
LLM(["your LLM"])
NINJA -->|hourly| COL
GGG -.->|on demand| MCP
MCP <--> LLMData sources: currency prices come from poe.ninja (the in-game Currency Exchange, where PoE2 currency actually trades); item and stash lookups come from GGG's trade API. The background collector hits only poe.ninja, so it never touches your in-game trade rate budget; GGG is called only when you run a stash or listing lookup yourself.
History can't be backfilled — the sources serve only current data, so price history exists only for the period the collector has been running. Start it early in a league.
Quick start
uv venv && uv pip install -e .
poe2market init # interactive: contact email + accountinit writes config/config.toml for you — your contact email (GGG requires a
contact in the User-Agent; use a real one) and, optionally, your account handle
for stash valuation. leagues defaults to ["@current"], following league
rollover automatically.
poe2market collect --once # one pass of every due job
poe2market validate # check every watch target matches listings
poe2market status # what has been collected
poe2market install-daemon # run it continuously via launchdRun validate before each league: an over-constrained target never errors, it
just records an empty series until you notice.
Rate limits
poe.ninja (currency) — CDN-cached ~30 min; the collector polls hourly, matching how often the data actually changes. Independent of GGG, so background collection never affects your in-game trade.
GGG trade API (items, stash) — called only on demand. Limits are advertised per response and enforced per IP, the same IP your game client uses:
Endpoint | Limit |
| 600 / 21600s |
| 1000 / 21600s (10 listings each) |
| 30 / 300s |
Because the budget is shared with your game, live lookups are deliberately sparing, and a cross-process ledger in SQLite keeps the collector and MCP server from double-spending it.
Prices carry confidence, not just a number
Thin books are common, and a wide bid/ask spread makes a midpoint misleading.
Every quote carries spread_pct and a confidence label. Conversions are
side-aware: buying uses the ask, stash valuation uses the bid.
Watchlists
Currency comes from poe.ninja automatically. Named items are declared in
config/watchlists/*.toml, each with its own cadence and priority, so the
budget can be steered at whatever matters this week:
List | Cadence | Purpose |
| 20 min | High-demand items while prices move hourly |
| 2 h | Mageblood, Headhunter, Astramentis… |
| 90 min | High-ilvl rare bases |
Three ways to specify a target, in increasing power:
[[target]] # a named unique
key = "uniq:mageblood"
kind = "unique"
name = "Mageblood"
type = "Utility Belt"
[[target]] # a base type, narrowed by filters
key = "base:stellar-amulet-i82"
kind = "base"
type = "Stellar Amulet"
[target.filters.misc_filters.filters.ilvl]
min = 82
[[target]] # anything else: a raw trade2 query
key = "base:tri-res-amulet"
kind = "raw"
[target.raw_query.query]
status = { option = "online" }
type = "Stellar Amulet"Disable league-start (enabled = false) once prices settle; its 20-minute
cadence is deliberately aggressive and eats the search budget.
MCP tools
History (local database — instant and free)
Tool | Purpose |
| Latest price with spread and confidence |
| OHLC candles for charting |
| Largest percentage moves |
| Discovery and health |
Live (spends the shared rate budget)
Tool | Purpose |
| Current listings plus whisper text |
| Best listing, price-checked against history |
| Currencies whose bid exceeds their ask |
| Leagues on the trade API |
Stash — get_stash_value, get_stash_history, list_stash_items
Documentation
Split by audience — see docs/README.md.
docs/agent/ — served to connecting clients as MCP
resources. What a client reads is exactly these files.
Document | Covers |
All 15 tools: signatures, return shapes, examples, cost | |
Install, config, daemon, watchlists, stash, troubleshooting | |
Answering correctly: confidence, units, empty-vs-absent | |
Price semantics, junk rejection, candles | |
Budgets, etiquette, compliance |
docs/maintainers/ — not exposed over MCP.
Document | Covers |
Architecture, invariants, how to extend, testing | |
What was measured live, with numbers |
Clients also get connect-time instructions and a live poe2market://state
resource (active league, coverage, remaining rate budget).
On executing trades
No API executes a PoE2 trade. fetch returns a whisper string; the trade
itself is a manual whisper → party → trade window. Automating in-game input
violates GGG's terms and risks a ban.
So this server takes it to the line and stops: it finds listings, prices them,
ranks them, checks them against history, and hands you the exact whisper. A
human sends it. prepare_trade never contacts anyone.
Stash valuation
Set your account handle; the tool reads your public trade listings and values them at poe.ninja prices.
stash_account = "Name#1234" # config.toml, or POE2MARKET_ACCOUNTpoe2market stashTo expose your stash, set the tab public with a price in game — name it
~price 5 exalted (or ~b/o 5 exalted), or set the tab's price field. Only
items in a priced, public tab are indexed and visible; held or unpriced items
are not, and it reflects GGG's last crawl of your account (log in to refresh).
See docs/agent/SETUP.md.
Storage
Three tiers, because raw ticks reach ~46M rows/year:
Table | Retention | Read by |
| 14 days | Recent detail, arbitrage |
| forever | Charts under ~14 days |
| forever | League-long charts, movers |
Charts never scan raw ticks, which is what keeps a multi-league history fast in
a single file. DuckDB can ATTACH this database directly if heavier analytics
are ever wanted — no migration required.
Operating the daemon
poe2market install-daemon # load the launchd agent
poe2market status # recent runs, coverage
tail -f logs/collector.log
poe2market uninstall-daemon # stop and removeTests
pytest -qCovers rate-limit header parsing and cross-process budget sharing, bid/ask book maths, rollup OHLC correctness and idempotency, and the currency-unit separation that keeps divine-denominated listings out of exalted candles.
License
MIT — see LICENSE.