{"openapi":"3.0.0","paths":{"/item-prices/average":{"get":{"operationId":"getAveragePrice","summary":"Average price for one item","description":"Market price statistics (in HR) for one item, identified by `typeCode`, `baseCode` and/or `itemName` (at least one required). Prices are deduplicated per listing per day; `averagePrice` is a 15%-trimmed mean. Filters by listing update time (`hours`, `startDate`, `endDate`) by default, or by ingestion time when any `crawledAt*` param is given. The current day is always excluded. Includes the latest daily price, 7-day moving average and 7-day change. Grouped items (Rainbow Facets, Hellfire Torch, Spirit Ward) are priced per variant; target a variant via its baseCode and read `groupingData`/`groupingLabel`. Results are cached up to 24h.","parameters":[{"name":"typeCode","required":false,"in":"query","description":"Item type code (exact match). At least one of typeCode, baseCode or itemName is required.","schema":{"example":"rune","type":"string"}},{"name":"baseCode","required":false,"in":"query","description":"Base code (exact match), e.g. `r30` (Ber), `r31` (Jah), `harlequin_crest`. Grouped items have one code per variant, e.g. `rainbow_facet_cold_5_5`, `hellfire_torch_druid`.","schema":{"example":"r30","type":"string"}},{"name":"itemName","required":false,"in":"query","description":"Item name, exact case-insensitive match (e.g. `Ber Rune`, `Harlequin Crest`). For grouped items (Rainbow Facet, Hellfire Torch) prefer baseCode to target one variant.","schema":{"example":"Ber Rune","type":"string"}},{"name":"isLadder","required":false,"in":"query","description":"Ladder listings only (`true`) or non-ladder only (`false`). Omit to match listings from both modes.","schema":{"example":true,"type":"boolean"}},{"name":"isHardcore","required":false,"in":"query","description":"Hardcore (`true`) or softcore (`false`) listings. Omit to match listings from both modes.","schema":{"example":false,"type":"boolean"}},{"name":"quality","required":false,"in":"query","description":"Item quality filter (exact match), e.g. `Unique`, `Set`.","schema":{"example":"Unique","type":"string"}},{"name":"startDate","required":false,"in":"query","description":"Only listings updated at/after this time. When combined with `hours`, acts as a lower clamp (e.g. season start).","schema":{"format":"date-time","example":"2026-04-24T00:00:00.000Z","type":"string"}},{"name":"endDate","required":false,"in":"query","description":"Only listings updated up to this time. Ignored when `hours` is set. Values today or later are capped at the start of today.","schema":{"format":"date-time","example":"2026-10-15T00:00:00.000Z","type":"string"}},{"name":"hours","required":false,"in":"query","description":"Rolling window: listings updated in the last N hours. Without any date params all history is used. The current (incomplete) day is always excluded.","schema":{"minimum":1,"example":168,"type":"number"}},{"name":"crawledAtStartDate","required":false,"in":"query","description":"Like `startDate` but on ingestion time. Any crawledAt* param switches filtering to ingestion time and ignores hours/startDate/endDate.","schema":{"format":"date-time","type":"string"}},{"name":"crawledAtEndDate","required":false,"in":"query","description":"Like `endDate` but on ingestion time.","schema":{"format":"date-time","type":"string"}},{"name":"crawledAtHours","required":false,"in":"query","description":"Like `hours` but on ingestion time.","schema":{"minimum":1,"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AveragePriceResponse"}}}},"400":{"description":"No identifier supplied (`Either typeCode, baseCode, or itemName must be provided`) or an unparseable date param (`startDate must be an ISO date`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PriceErrorResponse"}}}},"404":{"description":"No data matched (`No price data found for the specified criteria`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PriceErrorResponse"}}}},"500":{"description":"Internal error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PriceErrorResponse"}}}}},"tags":["Prices"],"security":[{},{"apiKey":[]}]}},"/item-prices/average/batch":{"post":{"operationId":"getAveragePricesBatch","summary":"Average prices for many items","description":"Same statistics as `getAveragePrice` for each requested baseCode, in one call. Codes with no data are omitted, so `count` may be lower than requested. Results are cached up to 24h.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchPriceQueryDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AveragePriceListResponse"}}}},"400":{"description":"Missing/empty `baseCodes` (`baseCodes array is required and must not be empty`) or an unparseable date field (`startDate must be an ISO date`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PriceErrorResponse"}}}},"500":{"description":"Internal error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PriceErrorResponse"}}}}},"tags":["Prices"],"security":[{},{"apiKey":[]}]}},"/item-prices/average/all":{"get":{"operationId":"listAveragePrices","summary":"Average prices for every tracked item","description":"Runs `getAveragePrice` for every distinct baseCode stored for the selected mode and returns all items with data, sorted by `averagePrice` descending. Large response; cached up to 24h.","parameters":[{"name":"isLadder","required":false,"in":"query","description":"Ladder (`true`) or non-ladder (`false`) listings. Omit to match both modes.","schema":{"example":true,"type":"boolean"}},{"name":"isHardcore","required":false,"in":"query","description":"Hardcore (`true`) or softcore (`false`) listings. Omit to match both modes.","schema":{"example":false,"type":"boolean"}},{"name":"hours","required":false,"in":"query","description":"Rolling window on listing update time. Omit to use all stored history (excluding today).","schema":{"example":168,"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AveragePriceListResponse"}}}},"500":{"description":"Internal error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PriceErrorResponse"}}}}},"tags":["Prices"],"security":[{},{"apiKey":[]}]}},"/item-prices/history/{baseCode}":{"get":{"operationId":"getPriceHistory","summary":"Daily price history for an item","description":"One point per day (midnight UTC, oldest first): trimmed-mean price of distinct listings that day, listing count and 7-day moving average, all in HR. Days are bucketed by listing update time; the current day is excluded. Each day is computed on its season's daily series: a price that is a statistical outlier against the whole season is replaced by the median of neighbouring days, so a day reports the same values for every range or limit. Cached up to 24h.","parameters":[{"name":"baseCode","required":true,"in":"path","description":"Base code, e.g. `r30`, `harlequin_crest`, `rainbow_facet_cold_5_5`.","schema":{"example":"r30","type":"string"}},{"name":"typeCode","required":false,"in":"query","description":"Type code filter. When omitted it is looked up from stored data for the baseCode.","schema":{"example":"rune","type":"string"}},{"name":"isLadder","required":false,"in":"query","description":"Ladder (`true`) or non-ladder (`false`) listings. Omit to match both modes.","schema":{"example":true,"type":"boolean"}},{"name":"isHardcore","required":false,"in":"query","description":"Hardcore (`true`) or softcore (`false`) listings. Omit to match both modes.","schema":{"example":false,"type":"boolean"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"default":100},"description":"Maximum number of days returned (default 100): the most recent days with data within the `startDate`/`endDate` range, still ordered oldest-first."},{"name":"startDate","required":false,"in":"query","schema":{"type":"string","format":"date"},"description":"Include days from this date (inclusive), e.g. season start."},{"name":"endDate","required":false,"in":"query","schema":{"type":"string","format":"date"},"description":"Include days up to the end of this date. Defaults to yesterday."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PriceHistoryResponse"}}}},"500":{"description":"Internal error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PriceErrorResponse"}}}}},"tags":["Prices"],"security":[{},{"apiKey":[]}]}},"/item-prices/corruption-prices":{"get":{"operationId":"getCorruptionPrices","summary":"Prices by corruption for a unique item","description":"For corrupted listings of one item, groups prices (in HR) by corruption outcome (e.g. sockets, +skills), sorted by median price descending. Socket corruptions include a per-socket-count breakdown. Identify the item by `itemName`, or by `baseCode` (optionally with `typeCode`). Uses a rolling `hours` window (default 168) unless `startDate`/`endDate` is given; the current day is excluded. An unknown item yields an empty `corruptionPrices` list rather than an error.","parameters":[{"name":"typeCode","required":false,"in":"query","description":"Type code (optional extra filter).","schema":{"example":"helm","type":"string"}},{"name":"baseCode","required":false,"in":"query","description":"Base code of the item.","schema":{"example":"harlequin_crest","type":"string"}},{"name":"itemName","required":false,"in":"query","description":"Item name, exact case-insensitive match. Used to resolve the baseCode when baseCode is not given.","schema":{"example":"Harlequin Crest","type":"string"}},{"name":"isLadder","required":false,"in":"query","description":"Ladder (`true`) or non-ladder (`false`) listings. Omit to match both modes.","schema":{"example":true,"type":"boolean"}},{"name":"isHardcore","required":false,"in":"query","description":"Hardcore (`true`) or softcore (`false`) listings. Omit to match both modes.","schema":{"example":false,"type":"boolean"}},{"name":"hours","required":false,"in":"query","schema":{"type":"integer","minimum":1,"default":168},"description":"Rolling window in hours on listing update time (default 168 = 7 days). Ignored when startDate/endDate is set."},{"name":"startDate","required":false,"in":"query","schema":{"type":"string","format":"date"},"description":"Include listings updated from this date."},{"name":"endDate","required":false,"in":"query","schema":{"type":"string","format":"date"},"description":"Include listings updated up to the end of this date. Defaults to yesterday."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CorruptionPricesResponse"}}}},"400":{"description":"Neither baseCode nor itemName supplied (`Either baseCode or itemName must be provided`) or an unparseable date param (`startDate must be an ISO date`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PriceErrorResponse"}}}},"500":{"description":"Internal error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PriceErrorResponse"}}}}},"tags":["Prices"],"security":[{},{"apiKey":[]}]}},"/api/seasons":{"get":{"operationId":"listSeasons","summary":"List seasons","description":"Returns every season that has started (newest first) and the id of the current season. Use season dates as `startDate`/`endDate` on economy and price endpoints.","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeasonsListResponse"}}}}},"tags":["Seasons"],"security":[{},{"apiKey":[]}]}},"/api/leaderboard":{"get":{"operationId":"getLeaderboard","summary":"Get a leaderboard","description":"Ranked opted-in users for a board (`gained` = HR gained in the timeframe, `wealth` = 7-day median value, `session` = best HR/hour session) in one mode and season.\n\nPublic, no auth; responses are cached for ~5 minutes and rate limited. Values are in HR (High Rune; 1 HR = 1 Lo rune).","parameters":[{"name":"offset","required":false,"in":"query","schema":{"type":"integer","minimum":0,"maximum":100000,"default":0},"description":"Entries to skip (pagination)."},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"Max entries (1–100)."},{"name":"seasonId","required":false,"in":"query","description":"Season id (see `GET /api/seasons`); defaults to the current season.","schema":{"example":"s14","type":"string"}},{"name":"isHardcore","required":false,"in":"query","schema":{"type":"boolean","default":false},"description":"Hardcore mode."},{"name":"isLadder","required":false,"in":"query","schema":{"type":"boolean","default":true},"description":"Ladder mode."},{"name":"timeframe","required":false,"in":"query","schema":{"type":"string","enum":["7d","30d","season"],"default":"season"},"description":"Ranking window."},{"name":"basis","required":false,"in":"query","schema":{"type":"string","enum":["liquid","total"],"default":"liquid"},"description":"`liquid` = runes + Worldstone Shards at fixed reference values; `total` = full Net Worth."},{"name":"board","required":false,"in":"query","schema":{"type":"string","enum":["gained","wealth","session"],"default":"gained"},"description":"Board to read."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LeaderboardResponseDto"}}}},"400":{"description":"Invalid query parameter (unknown enum value, non-boolean flag, out-of-range number, missing required value)."}},"tags":["Leaderboards"],"security":[{},{"apiKey":[]}]}},"/api/profile/{slug}":{"get":{"operationId":"getProfile","summary":"Get a public profile","description":"Public profile of an opted-in user: latest values, daily history, ranks, percentile, badges, best session, recent feed and (if shared) holdings. When `isLadder`/`isHardcore` are omitted, the user's mode with the most recent data is served.\n\nPublic, no auth; responses are cached for ~5 minutes and rate limited. Values are in HR (High Rune; 1 HR = 1 Lo rune).","parameters":[{"name":"slug","required":true,"in":"path","description":"Profile slug (from leaderboard entries).","schema":{"example":"gheed","type":"string"}},{"name":"timeframe","required":false,"in":"query","schema":{"type":"string","enum":["7d","30d","season"],"default":"season"},"description":"Timeframe used for the rank cards."},{"name":"basis","required":false,"in":"query","schema":{"type":"string","enum":["liquid","total"],"default":"liquid"},"description":"Basis used for the rank cards."},{"name":"seasonId","required":false,"in":"query","description":"Season id (see `GET /api/seasons`); defaults to the current season.","schema":{"example":"s14","type":"string"}},{"name":"isHardcore","required":false,"in":"query","description":"Hardcore mode; omitted → the user's most recent mode.","schema":{"type":"boolean"}},{"name":"isLadder","required":false,"in":"query","description":"Ladder mode; omitted → the user's most recent mode.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileResponseDto"}}}},"400":{"description":"Invalid query parameter (unknown enum value, non-boolean flag, out-of-range number, missing required value)."},"404":{"description":"No public profile with this slug."}},"tags":["Leaderboards"],"security":[{},{"apiKey":[]}]}},"/api/feed":{"get":{"operationId":"getFeed","summary":"Get the finds feed (\"New in stash\" + rare finds)","description":"Recent notable item gains (worth ≥ 1 HR) and rare finds (top-3% Keep rares, each item announced once per season and mode) by opted-in users in one mode and season, newest first.\n\nPublic, no auth; responses are cached for ~5 minutes and rate limited. Values are in HR (High Rune; 1 HR = 1 Lo rune).","parameters":[{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"Max entries (1–100)."},{"name":"seasonId","required":false,"in":"query","description":"Season id (see `GET /api/seasons`); defaults to the current season.","schema":{"example":"s14","type":"string"}},{"name":"isHardcore","required":false,"in":"query","schema":{"type":"boolean","default":false},"description":"Hardcore mode."},{"name":"isLadder","required":false,"in":"query","schema":{"type":"boolean","default":true},"description":"Ladder mode."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedResponseDto"}}}},"400":{"description":"Invalid query parameter (unknown enum value, non-boolean flag, out-of-range number, missing required value)."}},"tags":["Leaderboards"],"security":[{},{"apiKey":[]}]}},"/api/hall-of-fame":{"get":{"operationId":"getHallOfFame","summary":"Get a season hall of fame","description":"Frozen final top 25 per board (basis liquid, timeframe season) of an ended season, plus the best rare find per slot. Returns 404 for running seasons or when nothing is frozen.\n\nPublic, no auth; responses are cached for ~5 minutes and rate limited. Values are in HR (High Rune; 1 HR = 1 Lo rune).","parameters":[{"name":"isHardcore","required":false,"in":"query","schema":{"type":"boolean","default":false},"description":"Hardcore mode."},{"name":"isLadder","required":false,"in":"query","schema":{"type":"boolean","default":true},"description":"Ladder mode."},{"name":"seasonId","required":true,"in":"query","description":"Ended season id (see `GET /api/seasons`).","schema":{"example":"s13","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HallOfFameResponseDto"}}}},"400":{"description":"Invalid query parameter (unknown enum value, non-boolean flag, out-of-range number, missing required value)."},"404":{"description":"Season still running, or nothing frozen for this season and mode."}},"tags":["Leaderboards"],"security":[{},{"apiKey":[]}]}},"/api/rares/guide":{"get":{"operationId":"getRareGuide","summary":"What to look for on rares","description":"Per item slot (version 2): the stats that multiply a rare's asking price, each with its value per season phase (`early` = fortnights `earlyFortnights`, `late` = after, `season` = whole season, timing controlled): threshold (`keyFrom`), stars (1 = ×1.25–2, 2 = ×2–4, 3 = ×4+), multiplier with its 95% CI and a tier ladder (unreliable rungs flagged); roll range, corruption-only and prevalence, early→late `trend`. Plus archetypes (stat combinations that mark top items), real top listings, the asking-price ladder (softcore, hardcore when enough listings), a quality-adjusted price index by fortnight and `statOptions` (stats the item checker offers). `labelTemplates` maps feature keys to text with `#` standing for the value. Static per season; cached for a day.","parameters":[],"responses":{"200":{"description":"The rare guide."},"404":{"description":"Rare appraisal not live yet."}},"tags":["Rares"],"security":[{},{"apiKey":[]}]}},"/api/rares/appraisal":{"post":{"operationId":"appraiseRare","summary":"Appraise a rare or crafted item from its clipboard text or from structured stats","description":"`text` is the item exactly as Project Diablo 2 copies it with Ctrl+C (its JSON: `name`, `quality`, `type`, `iLevel`, `stats[]`…). Rare items are rebuilt as a market listing (base from `type`, each stat matched to the market label it shows as) and scored like Net Worth stash rares: verdict keep / check / vendor, rank in its slot, starred key stats (of the current season phase, `appraisal.phase`) and the asking-price band of similar listings. `history` lists earlier market listings of the same item (exact content match, else same stat text) in the requested mode; hide it when `weak`. Instead of `text`, the item checker sends `item`: a `baseCode` (or a `slot`, appraised on the slot's most listed base) and `stats` as guide feature keys with values; each stat is written as the listing line that carries it, so the appraisal equals that of the real item; `history` is then null. Optional `phase` (`early` / `late` / `season`, the guide's season toggle) appraises the item for that period instead of today: key stats, stars, rank and price band come from that phase, evaluated at its representative fortnight (early = middle of the early fortnights, late = the current fortnight once the season is late, else the newest trained one, season = middle of the trained season with the guide's whole-season key stats); `appraisal.phase` echoes it. Without `phase` the appraisal is for today. Crafted items (clipboard `quality` `Crafted`, or `item.quality: 'Crafted'`) are scored by the crafted model: `rank`, `market` and the verdict are against crafted listings of the slot, key stats and stars are the rare ones. Their response adds `quality: 'Crafted'` and `recipe` (inferred craft recipe, null when none matches), the appraisal adds `quality`, `recipe` and `confidence` (`normal` / `low`, the slot model's backtest confidence), and `history` is null; rare responses carry none of these fields. `reason` explains a null `appraisal` (`not a rare`, `unknown base`, `unparseable item text`, `unsupported item type`, `appraisal unavailable`, `crafted slot unreliable: <why>` for crafted items of a slot the crafted model does not appraise reliably, `crafted appraisal unavailable`) or a partial one (`unparsed stats: n`).","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string","minLength":1,"maxLength":20000,"description":"Raw clipboard text of the item (PD2 item JSON). Exactly one of text / item."},"item":{"type":"object","description":"Structured item (item checker). Exactly one of text / item.","required":["stats"],"anyOf":[{"required":["slot"]},{"required":["baseCode"]}],"properties":{"slot":{"type":"string","enum":["ring","amul","circ","jewl","glov","boot","belt","tors","helm","shield","quiver","weapon"]},"baseCode":{"type":"string","example":"rin"},"stats":{"type":"array","maxItems":24,"items":{"type":"object","required":["key","value"],"properties":{"key":{"type":"string","example":"item_fastercastrate"},"value":{"type":"number","example":10},"range":{"type":"object","properties":{"min":{"type":"number"},"max":{"type":"number"}}}}}},"sockets":{"type":"integer","minimum":0,"maximum":6},"ethereal":{"type":"boolean"},"corrupted":{"type":"boolean"},"corruptedKeys":{"type":"array","items":{"type":"string"}},"quality":{"type":"string","enum":["Rare","Crafted"],"default":"Rare","description":"Crafted appraises the item with the crafted model."}}},"isLadder":{"type":"boolean","default":true},"isHardcore":{"type":"boolean","default":false},"phase":{"type":"string","enum":["early","late","season"],"description":"Season phase to appraise for (item checker toggle). Omitted: the phase of today."}}}}}},"responses":{"200":{"description":"`{ appraisal, history, reason }`; crafted items add `quality: 'Crafted'` and `recipe`."},"400":{"description":"Invalid body: unknown field, missing or oversized `text`, both or neither of text / item, non-boolean flag, unknown `phase`; for `item`, `message` lists each invalid field as `path: problem` (`item.stats[2].key: unknown stat \"x\"`)."},"404":{"description":"Rare appraisal not live yet."}},"tags":["Rares"],"security":[{},{"apiKey":[]}]}},"/api/rares/appraisal/batch":{"post":{"operationId":"appraiseRares","summary":"Appraise up to 100 rare or crafted items in one request","description":"`items` holds 1-100 entries, each `{ text }` or `{ item }` exactly as the single `POST /api/rares/appraisal` body; `isLadder`, `isHardcore` and `phase` apply to every entry. `results` keeps the input order: each has its `index` and the single endpoint's `appraisal` and `reason` (and, for crafted items, `quality` and `recipe`), plus `history` when `includeHistory` is true (at most 25 items then). An invalid entry does not fail the batch: its result is `{ index, error: { code: 'invalid_item', message } }` with the message the single endpoint would answer with 400. `counts` totals appraised, not appraised (null `appraisal`, see `reason`) and invalid entries. With an `X-API-Key` a batch costs ceil(items / 10) requests of the per-key limit (1 for 1-10 items, 10 for 100).","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"object","description":"Exactly one of text / item.","properties":{"text":{"type":"string","minLength":1,"maxLength":20000,"description":"Raw clipboard text of the item (PD2 item JSON). Exactly one of text / item."},"item":{"type":"object","description":"Structured item (item checker). Exactly one of text / item.","required":["stats"],"anyOf":[{"required":["slot"]},{"required":["baseCode"]}],"properties":{"slot":{"type":"string","enum":["ring","amul","circ","jewl","glov","boot","belt","tors","helm","shield","quiver","weapon"]},"baseCode":{"type":"string","example":"rin"},"stats":{"type":"array","maxItems":24,"items":{"type":"object","required":["key","value"],"properties":{"key":{"type":"string","example":"item_fastercastrate"},"value":{"type":"number","example":10},"range":{"type":"object","properties":{"min":{"type":"number"},"max":{"type":"number"}}}}}},"sockets":{"type":"integer","minimum":0,"maximum":6},"ethereal":{"type":"boolean"},"corrupted":{"type":"boolean"},"corruptedKeys":{"type":"array","items":{"type":"string"}},"quality":{"type":"string","enum":["Rare","Crafted"],"default":"Rare","description":"Crafted appraises the item with the crafted model."}}}}}},"isLadder":{"type":"boolean","default":true},"isHardcore":{"type":"boolean","default":false},"phase":{"type":"string","enum":["early","late","season"],"description":"Season phase to appraise for (item checker toggle). Omitted: the phase of today."},"includeHistory":{"type":"boolean","default":false,"description":"Add each item's `history` (at most 25 items)."}}}}}},"responses":{"200":{"description":"`{ results: [{ index, appraisal, reason, history?, quality?, recipe? } | { index, error: { code: 'invalid_item', message } }], counts: { appraised, notAppraised, invalid } }`, results in input order."},"400":{"description":"Invalid envelope: not an object, unknown field, `items` not an array of 1-100 entries (1-25 with `includeHistory`), non-boolean flag, unknown `phase`."},"404":{"description":"Rare appraisal not live yet."},"429":{"description":"Per-key rate limit exceeded (a batch costs ceil(items / 10) requests)."}},"tags":["Rares"],"security":[{},{"apiKey":[]}]}},"/api/rare-history/{fingerprint}":{"get":{"operationId":"getRareHistory","summary":"Market listing history of a rare item","description":"Earlier PD2 market listings of the item with this content fingerprint (the `id` of a Net Worth rare, without any `-2` suffix), or else of this text fingerprint: times listed, distinct sellers, first / last seen and the 20 newest listings (asking price, seller changed since the previous listing). `timesListed` 0 when never listed. Hide the history when `weak` (fingerprint of ≤ 3 stats, shared by unrelated items).","parameters":[{"name":"fingerprint","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"The listing history."},"400":{"description":"Not a 32-hex fingerprint."},"404":{"description":"Rare appraisal not live yet."}},"tags":["Rares"],"security":[{},{"apiKey":[]}]}},"/api/catalog":{"get":{"operationId":"listCatalog","summary":"List price-tracked items","description":"Every item the API has prices for (static item knowledge plus every base code seen in price data), with its stable `baseCode`, category, icon and search aliases. The catalog is rebuilt in memory after each price update (`prices.updated`). Responses carry a strong `ETag`; send it back in `If-None-Match` to get `304 Not Modified`. Cached for 1 hour.","parameters":[{"name":"If-None-Match","in":"header","description":"ETag of a previous response.","required":false,"schema":{"type":"string"}},{"name":"category","required":false,"in":"query","schema":{"type":"string","enum":["runes","currency","ubers","gems","mats","rainbow-facets","maps","charms","uniques","sets","equipment","other"]},"description":"Only items of this category; omit for the full catalog."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CatalogListResponse"}}}},"304":{"description":"Not modified: the `If-None-Match` ETag is current."},"400":{"description":"Unknown `category`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CatalogErrorResponse"}}}}},"tags":["Catalog"],"security":[{},{"apiKey":[]}]}},"/api/catalog/{baseCode}":{"get":{"operationId":"getCatalogItem","summary":"Get one catalog item","description":"Looks up an item by its exact base code (case-insensitive). Use `/api/search` for names and nicknames.","parameters":[{"name":"baseCode","required":true,"in":"path","description":"Base code, e.g. `r30` (Ber Rune), `harlequin_crest`, `hellfire_torch_sorceress`.","schema":{"example":"r30","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CatalogItemResponse"}}}},"404":{"description":"The base code is not in the catalog.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CatalogErrorResponse"}}}}},"tags":["Catalog"],"security":[{},{"apiKey":[]}]}},"/api/search":{"get":{"operationId":"searchItems","summary":"Search items","description":"Ranked item search by base code, name, alias or nickname (`ber`, `r30`, `shako`, `soj`, `anni`, `torch`, `pde`). Case, diacritics and apostrophes are ignored (`tal rashas` matches \"Tal Rasha's\"). Ranking: exact code > exact name > exact alias > prefix > all words matched > substring > fuzzy (typos via Damerau-Levenshtein and trigrams).","parameters":[{"name":"category","required":false,"in":"query","schema":{"type":"string","enum":["runes","currency","ubers","gems","mats","rainbow-facets","maps","charms","uniques","sets","equipment","other"]},"description":"Only return items of this category."},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":50,"default":10},"description":"Maximum number of hits."},{"name":"q","required":true,"in":"query","schema":{"type":"string","maxLength":100},"description":"Search text: code, name, alias or nickname."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CatalogSearchResponse"}}}},"400":{"description":"`q` missing, blank or longer than 100 characters, `limit` not an integer in 1..50, or unknown `category`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CatalogErrorResponse"}}}}},"tags":["Catalog"],"security":[{},{"apiKey":[]}]}},"/api/search/site":{"get":{"operationId":"searchSite","summary":"Search items and ladder characters","description":"The pd2trader.com search box: items (names, nicknames such as `shako`, base codes; typos tolerated) and ladder characters of the builds explorer (exact name > prefix > typo, current season first, then level), each group with its own total. Results carry the site links and icons. Cached for 30 s. Answers 503 when site search is not configured on the server or the search backend is unavailable.","parameters":[{"name":"group","required":false,"in":"query","schema":{"type":"string","enum":["all","items","characters"],"default":"all"},"description":"Only search items or characters; the other group comes back empty."},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":20,"default":8},"description":"Maximum hits per group."},{"name":"q","required":true,"in":"query","schema":{"type":"string","maxLength":100},"description":"Search text."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SiteSearchResponse"}}}},"400":{"description":"`q` missing, blank or longer than 100 characters, `limit` not an integer in 1..20, or unknown `group`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SiteSearchErrorResponse"}}}},"503":{"description":"Site search is not configured, or the search backend failed or timed out.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SiteSearchErrorResponse"}}}}},"tags":["Catalog"],"security":[{},{"apiKey":[]}]}},"/api/valuate":{"post":{"operationId":"valuateItems","summary":"Value a list of items","description":"Resolves every line (name, alias or baseCode), merges lines for the same item, and prices them in one batch. Prices follow the Net Worth rules exactly (same numbers as the desktop app): the ladder market price (`movingAverage7Days`, else `medianPrice`) over the last `hours` (clamped to the season start); El–Fal runes always use the fixed table; higher runes use it when there are fewer than 10 samples or no market price; Worldstone Shards fall back to 0.01 HR. `runePricing=fixed` values every rune at the fixed table instead (other items keep their market price). All values are in HR (High Rune; 1 HR = 1 Lo rune). `totalHr` counts `ok` and `fixed` lines only; `low` lines are reported in `uncertainHr`. Inputs that match no item are listed in `unresolved` instead of failing the request. Up to 500 lines, quantity 1 to 1,000,000 each.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValuateRequest"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValuateResponse"}}}},"400":{"description":"Invalid body: `items` missing, empty or longer than 500, a line without `item`, `qty` not an integer in 1..1000000, `hours` not an integer in 1..720, or a non-boolean mode flag.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ToolsErrorResponse"}}}}},"tags":["Tools"],"security":[{},{"apiKey":[]}]}},"/api/valuate/note":{"post":{"operationId":"valuateNote","summary":"Read the asking price of a free-text trade note","description":"Reads the asking price a trade note states (e.g. `wts 2 ist + pul`, `bin 3 c/o 2`, `Telekinesis charges // 2.25 HR`) and returns it as structured items valued in HR. Two readers run on every note: the site's price parser and a LightGBM model that scores each number, rune and WSS amount in the note; a second model picks the reader whose value is more likely the price and returns that likelihood as `price.probability` (calibrated against sellers’ own listed prices: on season 13 notes that state their price, the pick is right for 95% of distinct notes, against 90% for the parser alone). Current offers (`c/o`), buy-it-now prices, service fees and durations are handled; `price` is null when the note states no price (`offers`, `wss`). `price.statedHr` is the stated amount at the fixed rune / WSS table; `price.lines` are the items it names, valued at market. Prices follow the Net Worth rules exactly (same numbers as the desktop app): the ladder market price (`movingAverage7Days`, else `medianPrice`) over the last `hours` (clamped to the season start); El–Fal runes always use the fixed table; higher runes use it when there are fewer than 10 samples or no market price; Worldstone Shards fall back to 0.01 HR. `runePricing=fixed` values every rune at the fixed table instead (other items keep their market price). All values are in HR (High Rune; 1 HR = 1 Lo rune).","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NoteRequest"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NoteResponse"}}}},"400":{"description":"Invalid body: `note` missing, empty or longer than 500 characters, `hours` not an integer in 1..720, or a non-boolean mode flag.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ToolsErrorResponse"}}}}},"tags":["Tools"],"security":[{},{"apiKey":[]}]}},"/api/convert":{"get":{"operationId":"convertCurrency","summary":"Convert between items","description":"Converts `amount` of one item into another (e.g. Ist → Ber) using their HR values. Use `hr` on either side for the HR unit itself (1 HR = one Lo rune). Prices follow the Net Worth rules exactly (same numbers as the desktop app): the ladder market price (`movingAverage7Days`, else `medianPrice`) over the last `hours` (clamped to the season start); El–Fal runes always use the fixed table; higher runes use it when there are fewer than 10 samples or no market price; Worldstone Shards fall back to 0.01 HR. `runePricing=fixed` values every rune at the fixed table instead (other items keep their market price). All values are in HR (High Rune; 1 HR = 1 Lo rune).","parameters":[{"name":"runePricing","required":false,"in":"query","description":"How runes convert to HR. `dynamic` (default): market price, Net Worth rules (El–Fal and thin markets use the fixed table). `fixed`: every rune at the fixed table (Lum 0.005, Ko 0.01, Fal 0.01, Lem 0.01, Pul 0.03, Um 0.05, Mal 0.1, Ist 0.15, Gul 0.25, Vex 0.5, Ohm 0.75, Lo 1, Sur 1.5, Jah 2, Cham 2.25, Ber 3, Zod 4.5; El–Io 0). Other items always use the market.","schema":{"type":"string","enum":["dynamic","fixed"],"default":"dynamic"}},{"name":"hours","required":false,"in":"query","description":"Price window in hours (1..720), clamped to the season start.","schema":{"type":"integer","minimum":1,"maximum":720,"default":168}},{"name":"isHardcore","required":false,"in":"query","description":"Hardcore (`true`) or softcore (`false`) prices.","schema":{"type":"boolean","default":false}},{"name":"isLadder","required":false,"in":"query","description":"Ladder (`true`) or non-ladder (`false`). Prices always come from ladder listings, like Net Worth (`mode.priceSource`).","schema":{"type":"boolean","default":true}},{"name":"amount","required":false,"in":"query","description":"Amount of `from` (greater than 0, at most 1000000000; decimals allowed).","schema":{"type":"number","default":1,"maximum":1000000000}},{"name":"to","required":true,"in":"query","description":"Item to convert to: name, alias, baseCode, or `hr`.","schema":{"example":"ber","type":"string"}},{"name":"from","required":true,"in":"query","description":"Item to convert from: name, alias, baseCode, or `hr`.","schema":{"example":"ist","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConvertResponse"}}}},"400":{"description":"`from`/`to` missing or unresolvable (`Unknown item for from: 'xyz'`), an item without a price in the selected mode (`No price available for ...`) or worth 0 HR (`... has no HR value, so it cannot be converted`), or an invalid `amount`/`hours`/mode flag.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ToolsErrorResponse"}}}}},"tags":["Tools"],"security":[{},{"apiKey":[]}]}},"/api/price-check":{"post":{"operationId":"priceCheckItem","summary":"Estimate the price of a specific item","description":"Finds comparable listings of the item (same baseCode and mode, listings updated in the last `hours`; newest price per listing) and scores each against the requested properties the crawler stores: sockets, ethereal, corrupted, corruption modifiers, quality, and the Rainbow Facet / Hellfire Torch / Spirit Ward variant. Rainbow Facets are also compared with neighbouring mastery/pierce rolls, scored relative to the roll range (3-5). The estimate (`low`/`mid`/`high` = 25th/50th/75th percentile, in HR) uses listings matching every filter, or similarity-weighted near matches when fewer than 3 match exactly.\n\n**Stored listings do not keep modifier values**, so rolled stats such as `+2 to All Skills` or `% Enhanced Damage` cannot be compared: they are returned in `ignoredFilters` with the reason and cap `confidence` at `medium`. Filters that do not apply to the item (sockets on a rune) are reported the same way. All prices are in HR (High Rune; 1 HR = 1 Lo rune). Only ladder is crawled.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PriceCheckRequest"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PriceCheckResponse"}}}},"400":{"description":"Unknown item, a grouped item (Rainbow Facet, Hellfire Torch, Spirit Ward) without enough stats to pick a variant, or an invalid body: unknown field, `item` missing, `stats` not an array of at most 30 `{ name | id, value }` entries, a stat value of the wrong type, `sockets` not an integer 0-6, `hours` not an integer 1-720, a non-boolean flag, or contradicting filters (`corruption` with `corrupted: false`, a stat that disagrees with `sockets`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PriceCheckErrorResponse"}}}}},"tags":["Price check"],"security":[{},{"apiKey":[]}]}},"/api/market/search":{"post":{"operationId":"searchMarketListings","summary":"Search PD2 market listings (live or sold) with the overlay market filters","description":"Searches every PD2 market listing we ingest (kept current from PD2's listing change feed, read every ~5 s), live or sold, with the\ndesktop overlay market browser filters. Server-side filters select exactly what the PD2 site search selects for the\nsame form (type/base, rarity, flags, sockets, requirements, property rows, corruptions, price note) with the\noverlay's fixes (Has Buyout = an HR price is set, Class narrows the item type, price text matched literally).\n\nOn top of the site: pseudo stats (total resistances, total life incl. vitality, all + class skills, …) and cube\ncorruptions as min/max rows, up to 5 any-of blocks of rows (`or: 1`–`5`), excluded rows (`not: true`), a seller name matched against the website\nusername and game accounts, and a stat-weighted `score` (Σ weight × roll) with `value` = score / HR price, both\nsortable over the full result set.\n\nSeller presence: without a Seller online filter, each listing's `online` / `in_game` / `user_last_online` / `user_last_ingame`\nare the last-seen times stored at ingest (PD2 is not asked; check the shown sellers live on the client). The Seller online\nfilters ask PD2 at request time (cached 60 s per seller) and run after the query, over at most 2000\nlistings in sort order; `total` is then an estimate (`totalExact: false`) unless every candidate was read.\nOtherwise `total` is exact up to 100,000 matches (`totalExact: false` past that).\n\nHistory (opt-in): `filters.status` also searches ended listings (`sold`, `removed`, `expired`, with or without\n`active`), each aged by when it ended; every item carries its `status`, `listedAt`, `endedAt` and `durationSeconds`.\n\nFacets (opt-in): `facets` (e.g. `[\"quality\", \"sockets\", \"price\"]`) returns counts per value of those dimensions\nwith the hits, from the same OpenSearch request (the hits are unchanged). A facet on a dimension the filters\nselect on is counted without that dimension's own filter, so it lists the alternatives; the others count within\nthe results. Seller online filters do not apply to facet counts.\n\nPaged with `page` (0-based) and `limit` (1-100, `page × limit` ≤ 10000). Identical\nrequests share a result for 5 s. Rate limited per client IP (60 requests/min).\n\nThe same search as a URL: `GET /api/market/search/{q}`; as a live stream of new matches: `GET /api/market/search/{q}/live`\n(several searches on one stream: `GET /api/market/search/live?q=…&q=…`).","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketSearchRequestDoc"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketSearchResponseDoc"}}}},"400":{"description":"Invalid body: unknown field, `hardcore`/`ladder` missing, a value of the wrong type or out of range, a type/base code that is neither a code nor a `{\"$in\": [codes]}` group (nor, for typeCode, a known `group:<id>` type group), a property key outside the site grammar, an unknown pseudo stat, an unknown facet name, more than 30 stat rows, or `page × limit` above 10000."},"429":{"description":"More than 60 requests in a minute from this IP."},"503":{"description":"The search backend (OpenSearch) is not configured on this server, failed or took longer than 2 s twice; retry shortly."}},"tags":["Market"],"security":[{},{"apiKey":[]}]}},"/api/market/search/live":{"get":{"operationId":"streamMarketSearches","summary":"Live search of several searches on one stream (Server-Sent Events)","description":"`GET /api/market/search/{q}/live` for 1 to 10 searches on one stream: repeat `q`\n(`?q=<q1>&q=<q2>`), each encoded and validated as the path `q` of that endpoint (live listings only). Duplicate\nvalues count once. Use it to watch a list of searches (your own `{ name, q }` list replaces saved searches).\n\n- `hello` (once, on connect; the first frame also sets `retry: 10000`): `{ serverTime, events, heartbeatSeconds, searches }`,\n  `searches` = `[{ q, filters }]` in request order, `q` exactly as sent, `filters` decoded with defaults filled.\n- `market.match`: as on the single-search stream, with `q` = the `q` the listing matched. A listing matching two\n  of the searches is sent once per search; each search sends a listing at most once while the server keeps it.\n\nNo catch-up: after every `hello` read `GET /api/market/search/{q}` for each `q` and dedupe by listing `id`.\nHeartbeat comment every 25 s; server restarts end the stream after a short `retry:`.\n\nLimits: 5 concurrent live streams per client IP (429; a multi-search stream counts as one); 2000 streams and 500 distinct searches per server instance (503 + `Retry-After`); opening a stream is rate limited per IP (60/min).\nJSON payload schemas: `LiveSearchHelloEventDto`, `MarketMatchEventDto`.","parameters":[{"name":"q","required":true,"in":"query","style":"form","explode":true,"description":"1 to 10 distinct searches, repeated (`?q=<q1>&q=<q2>`), each as the path `q` of `GET /api/market/search/{q}/live` (at most 4096 characters each).","schema":{"example":["eyJoYXJkY29yZSI6ZmFsc2UsImxhZGRlciI6dHJ1ZSwiaXRlbU5hbWUiOiJIYXJsZXF1aW4gQ3Jlc3QiLCJock1heCI6MX0","eyJoYXJkY29yZSI6ZmFsc2UsImxhZGRlciI6dHJ1ZSwiaXRlbU5hbWUiOiJTdG9uZSBvZiBKb3JkYW4iLCJock1heCI6MC41fQ"],"type":"array","items":{"type":"string"}}}],"responses":{"200":{"description":"Event stream; stays open until the client disconnects.","headers":{"Cache-Control":{"description":"`no-store, no-transform`","schema":{"type":"string"}},"X-Accel-Buffering":{"description":"`no` (disables proxy buffering)","schema":{"type":"string"}}},"content":{"text/event-stream":{"schema":{"type":"string","description":"Server-Sent Events frames."},"example":"retry: 10000\nevent: hello\nid: 1\ndata: {\"serverTime\":\"2026-11-10T08:14:00.000Z\",\"events\":[\"market.match\"],\"heartbeatSeconds\":25,\"searches\":[{\"q\":\"eyJoYXJkY29yZSI6ZmFsc2UsImxhZGRlciI6dHJ1ZSwiaXRlbU5hbWUiOiJIYXJsZXF1aW4gQ3Jlc3QiLCJock1heCI6MX0\",\"filters\":{\"source\":\"live\",\"hardcore\":false,\"ladder\":true,\"rarity\":[],\"ethereal\":\"any\",\"corrupted\":\"any\",\"desecrated\":\"any\",\"identified\":\"any\",\"userOnlineSite\":\"all\",\"userOnlineGame\":\"all\",\"hasBuyout\":\"any\",\"requirements\":{},\"stats\":[],\"maxAgeDays\":3,\"sort\":{\"by\":\"bumped\",\"dir\":\"desc\"},\"itemName\":\"Harlequin Crest\",\"hrMax\":1}},{\"q\":\"eyJoYXJkY29yZSI6ZmFsc2UsImxhZGRlciI6dHJ1ZSwiaXRlbU5hbWUiOiJTdG9uZSBvZiBKb3JkYW4iLCJock1heCI6MC41fQ\",\"filters\":{\"source\":\"live\",\"hardcore\":false,\"ladder\":true,\"rarity\":[],\"ethereal\":\"any\",\"corrupted\":\"any\",\"desecrated\":\"any\",\"identified\":\"any\",\"userOnlineSite\":\"all\",\"userOnlineGame\":\"all\",\"hasBuyout\":\"any\",\"requirements\":{},\"stats\":[],\"maxAgeDays\":3,\"sort\":{\"by\":\"bumped\",\"dir\":\"desc\"},\"itemName\":\"Stone of Jordan\",\"hrMax\":0.5}}]}\n\n: heartbeat 2026-11-10T08:14:25.000Z\n\nevent: market.match\nid: 2\ndata: {\"id\":\"6716a0f2b3c4d5e6f7a8b9c0\",\"isLadder\":true,\"isHardcore\":false,\"name\":\"Harlequin Crest\",\"baseName\":\"Shako\",\"baseCode\":\"uap\",\"typeCode\":\"helm\",\"quality\":\"Unique\",\"price\":\"2 ist\",\"hrPrice\":0.5,\"bumpedAt\":\"2026-11-10T08:15:02.000Z\",\"seller\":\"Gheed\",\"status\":\"active\",\"soldHr\":null,\"q\":\"eyJoYXJkY29yZSI6ZmFsc2UsImxhZGRlciI6dHJ1ZSwiaXRlbU5hbWUiOiJIYXJsZXF1aW4gQ3Jlc3QiLCJock1heCI6MX0\",\"matchedAt\":\"2026-11-10T08:15:03.120Z\"}\n\n","x-sse-events":{"hello":{"$ref":"#/components/schemas/LiveSearchHelloEventDto"},"market.match":{"$ref":"#/components/schemas/MarketMatchEventDto"}}}}},"400":{"description":"No `q`, more than 10 distinct `q`, or a bad `q` (the first one, named by its 0-based index: `q[i]: …`): `q` is not base64url, not UTF-8 JSON, or not valid filters (same rules as `filters` of `POST /api/market/search`), or `filters.source` is `archive`.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer"},"message":{"type":"string"}}},"example":{"statusCode":400,"message":"q[1]: Live search needs live listings: filters.source must be \"live\""}}}},"414":{"description":"A `q` is longer than 4096 characters, or the query string longer than 32768.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer"},"message":{"type":"string"}}},"example":{"statusCode":414,"message":"q[0]: q must be at most 4096 characters"}}}},"429":{"description":"More than 5 open live streams, or more than 60 opened in a minute, from this IP.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer"},"message":{"type":"string"}}},"example":{"statusCode":429,"message":"Too many concurrent live searches from this IP (max 5)"}}}},"503":{"description":"The server instance is at its stream or search capacity; retry after `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds to wait.","schema":{"type":"integer","example":30}}},"content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer"},"message":{"type":"string"}}},"example":{"statusCode":503,"message":"Too many open streams on this server, retry later"}}}}},"tags":["Market"],"security":[{},{"apiKey":[]}]}},"/api/market/search/{q}":{"get":{"operationId":"searchMarketListingsByQuery","summary":"Search PD2 market listings by URL (the search encoded in `q`)","description":"The shareable, bookmarkable form of `POST /api/market/search`: the filters travel in the path as `q`, `page` and\n`limit` as query parameters. Returns exactly what `POST /api/market/search` returns for `{ filters, page, limit }`\n(no facets), with the same validation and rate limit.\n\nEncode `q` in a browser or Node: `btoa(String.fromCharCode(...new TextEncoder().encode(JSON.stringify(filters))))`\nwith `+` → `-`, `/` → `_` and the trailing `=` removed (the SDK's `encodeMarketQuery(filters)` does this).\n\nLive updates of the same search: `GET /api/market/search/{q}/live` (Live search). After every `hello` of that\nstream, read this endpoint (newest first, the default sort) and dedupe by listing `id` to catch up.\n\nIdentical requests share a result for 5 s. Rate limited per client IP (60 requests/min).","parameters":[{"name":"q","required":true,"in":"path","description":"The search: base64url (RFC 4648 §5, no padding) of the UTF-8 JSON of a `filters` object, same shape and validation as `filters` of `POST /api/market/search`; at most 4096 characters. The example encodes `{\"hardcore\":false,\"ladder\":true,\"itemName\":\"Harlequin Crest\",\"hrMax\":1}`.","schema":{"example":"eyJoYXJkY29yZSI6ZmFsc2UsImxhZGRlciI6dHJ1ZSwiaXRlbU5hbWUiOiJIYXJsZXF1aW4gQ3Jlc3QiLCJock1heCI6MX0","type":"string"}},{"name":"limit","required":false,"in":"query","description":"1-100, default 25.","schema":{"type":"number"}},{"name":"page","required":false,"in":"query","description":"0-based page; `page × limit` at most 10000. Default 0.","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketSearchResponseDoc"}}}},"400":{"description":"`q` is not base64url, not UTF-8 JSON, or not valid filters (same rules as `filters` of `POST /api/market/search`), or `page` / `limit` out of range."},"414":{"description":"`q` is longer than 4096 characters."},"429":{"description":"More than 60 requests in a minute from this IP."},"503":{"description":"The search backend (OpenSearch) is not configured on this server, failed or took longer than 2 s twice; retry shortly."}},"tags":["Market"],"security":[{},{"apiKey":[]}]}},"/api/market/search/{q}/live":{"get":{"operationId":"streamMarketSearch","summary":"Live search: new listings matching a search (Server-Sent Events)","description":"PoE-style live search: the search is the URL (`q` as in `GET /api/market/search/{q}`), `/live` makes it a\nlong-lived `text/event-stream`, same framing as `GET /api/stream`. No sign-in and no saved search. Live listings\nonly: `filters.source` must be `live`.\n\n- `hello` (once, on connect; the first frame also sets `retry: 10000`): `{ serverTime, events, heartbeatSeconds, filters, searches }`,\n  `filters` being the decoded `q` with defaults filled, `searches` = `[{ q, filters }]` (the shape of the\n  multi-search stream `GET /api/market/search/live`).\n- `market.match`: a listing the market ingest added or bumped/updated (feed read every ~5 s) that matches the filters:\n  the compact listing summary plus `q` (the path's `q`) and `matchedAt`. Newest bump first, at most 10 per\n  ingest batch; a listing is sent once per search while the server keeps the search open. `maxAgeDays` and `sort`\n  are ignored.\n\nNo catch-up: after every `hello` (first connect and each reconnect) read `GET /api/market/search/{q}` and dedupe\nby listing `id`. When the server restarts, the stream ends after a lone short `retry:` frame: reconnect after it.\nLatency: about 5 s (the feed read interval) plus about 1 s. Heartbeat comment every 25 s.\n\nLimits: 5 concurrent live streams per client IP (429; a multi-search stream counts as one); 2000 streams and 500 distinct searches per server instance (503 + `Retry-After`); opening a stream is rate limited per IP (60/min).\nSeveral searches on one stream: `GET /api/market/search/live?q=…&q=…`.\n\nReplaces the deprecated saved searches and their alerts (`/api/market/saved-searches`).\nJSON payload schemas: `LiveSearchHelloEventDto`, `MarketMatchEventDto`.","parameters":[{"name":"q","required":true,"in":"path","description":"The search: base64url (RFC 4648 §5, no padding) of the UTF-8 JSON of a `filters` object, same shape and validation as `filters` of `POST /api/market/search`; at most 4096 characters. The example encodes `{\"hardcore\":false,\"ladder\":true,\"itemName\":\"Harlequin Crest\",\"hrMax\":1}`.","schema":{"example":"eyJoYXJkY29yZSI6ZmFsc2UsImxhZGRlciI6dHJ1ZSwiaXRlbU5hbWUiOiJIYXJsZXF1aW4gQ3Jlc3QiLCJock1heCI6MX0","type":"string"}}],"responses":{"200":{"description":"Event stream; stays open until the client disconnects.","headers":{"Cache-Control":{"description":"`no-store, no-transform`","schema":{"type":"string"}},"X-Accel-Buffering":{"description":"`no` (disables proxy buffering)","schema":{"type":"string"}}},"content":{"text/event-stream":{"schema":{"type":"string","description":"Server-Sent Events frames."},"example":"retry: 10000\nevent: hello\nid: 1\ndata: {\"serverTime\":\"2026-11-10T08:14:00.000Z\",\"events\":[\"market.match\"],\"heartbeatSeconds\":25,\"searches\":[{\"q\":\"eyJoYXJkY29yZSI6ZmFsc2UsImxhZGRlciI6dHJ1ZSwiaXRlbU5hbWUiOiJIYXJsZXF1aW4gQ3Jlc3QiLCJock1heCI6MX0\",\"filters\":{\"source\":\"live\",\"hardcore\":false,\"ladder\":true,\"rarity\":[],\"ethereal\":\"any\",\"corrupted\":\"any\",\"desecrated\":\"any\",\"identified\":\"any\",\"userOnlineSite\":\"all\",\"userOnlineGame\":\"all\",\"hasBuyout\":\"any\",\"requirements\":{},\"stats\":[],\"maxAgeDays\":3,\"sort\":{\"by\":\"bumped\",\"dir\":\"desc\"},\"itemName\":\"Harlequin Crest\",\"hrMax\":1}}],\"filters\":{\"source\":\"live\",\"hardcore\":false,\"ladder\":true,\"rarity\":[],\"ethereal\":\"any\",\"corrupted\":\"any\",\"desecrated\":\"any\",\"identified\":\"any\",\"userOnlineSite\":\"all\",\"userOnlineGame\":\"all\",\"hasBuyout\":\"any\",\"requirements\":{},\"stats\":[],\"maxAgeDays\":3,\"sort\":{\"by\":\"bumped\",\"dir\":\"desc\"},\"itemName\":\"Harlequin Crest\",\"hrMax\":1}}\n\n: heartbeat 2026-11-10T08:14:25.000Z\n\nevent: market.match\nid: 2\ndata: {\"id\":\"6716a0f2b3c4d5e6f7a8b9c0\",\"isLadder\":true,\"isHardcore\":false,\"name\":\"Harlequin Crest\",\"baseName\":\"Shako\",\"baseCode\":\"uap\",\"typeCode\":\"helm\",\"quality\":\"Unique\",\"price\":\"2 ist\",\"hrPrice\":0.5,\"bumpedAt\":\"2026-11-10T08:15:02.000Z\",\"seller\":\"Gheed\",\"status\":\"active\",\"soldHr\":null,\"q\":\"eyJoYXJkY29yZSI6ZmFsc2UsImxhZGRlciI6dHJ1ZSwiaXRlbU5hbWUiOiJIYXJsZXF1aW4gQ3Jlc3QiLCJock1heCI6MX0\",\"matchedAt\":\"2026-11-10T08:15:03.120Z\"}\n\n","x-sse-events":{"hello":{"$ref":"#/components/schemas/LiveSearchHelloEventDto"},"market.match":{"$ref":"#/components/schemas/MarketMatchEventDto"}}}}},"400":{"description":"`q` is not base64url, not UTF-8 JSON, or not valid filters (same rules as `filters` of `POST /api/market/search`), or `filters.source` is `archive`.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer"},"message":{"type":"string"}}},"example":{"statusCode":400,"message":"Live search needs live listings: filters.source must be \"live\""}}}},"414":{"description":"`q` is longer than 4096 characters.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer"},"message":{"type":"string"}}},"example":{"statusCode":414,"message":"q must be at most 4096 characters"}}}},"429":{"description":"More than 5 open live streams, or more than 60 opened in a minute, from this IP.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer"},"message":{"type":"string"}}},"example":{"statusCode":429,"message":"Too many concurrent live searches from this IP (max 5)"}}}},"503":{"description":"The server instance is at its stream or search capacity; retry after `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds to wait.","schema":{"type":"integer","example":30}}},"content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer"},"message":{"type":"string"}}},"example":{"statusCode":503,"message":"Too many open streams on this server, retry later"}}}}},"tags":["Market"],"security":[{},{"apiKey":[]}]}},"/api/market/listing/{id}":{"get":{"operationId":"getMarketListing","summary":"One ingested PD2 market listing (deep links)","description":"A listing by its PD2 id, in PD2 listing shape (seller presence live from PD2), with its pseudo stats and whether it is still `active`, `sold`, `removed` or `expired`. Ended listings are kept. Rate limited per client IP (60 requests/min).","parameters":[{"name":"id","required":true,"in":"path","description":"PD2 listing id (`_id`).","schema":{"example":"6ad07e61967978352c30f962","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketListingDetailDoc"}}}},"400":{"description":"`id` is not a PD2 listing id."},"404":{"description":"No such listing (never ingested)."},"429":{"description":"More than 60 requests in a minute from this IP."}},"tags":["Market"],"security":[{},{"apiKey":[]}]}},"/api/market/saved-searches":{"get":{"operationId":"listSavedSearches","summary":"List saved market searches (deprecated)","description":"Deprecated (still works): saved searches are going away. Keep your own `{ name, q }` list (`q` = the search encoded as in `GET /api/market/search/{q}`) and watch it with `GET /api/market/search/live?q=…&q=…` (see Live search).\n\nThe user's saved searches (max 25), newest first, with the per-user limits. Clients\nmigrating to their own list read this once and keep `{ name, q: encode(filters) }` per item.","deprecated":true,"parameters":[],"responses":{"200":{"headers":{"Deprecation":{"description":"`true`: this endpoint is deprecated. Keep your own `{ name, q }` list (`q` = the search encoded as in `GET /api/market/search/{q}`) and watch it with `GET /api/market/search/live?q=…&q=…` (see Live search).","schema":{"type":"string","example":"true"}},"Link":{"description":"The deprecation notice.","schema":{"type":"string","example":"</developers/market-listings#live-search>; rel=\"deprecation\""}}},"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedSearchListResponseDto"}}}},"401":{"description":"Missing, invalid or expired PD2 bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedSearchErrorDto"},"example":{"statusCode":401,"message":"Invalid or expired token"}}}},"429":{"description":"Per-IP rate limit exceeded."}},"tags":["Market saved searches"],"security":[{"pd2Bearer":[]}]},"post":{"operationId":"createSavedSearch","summary":"Save a market search (deprecated)","description":"Deprecated (still works): saved searches are going away. Keep your own `{ name, q }` list (`q` = the search encoded as in `GET /api/market/search/{q}`) and watch it with `GET /api/market/search/live?q=…&q=…` (see Live search).\n\nSaves market browser filters (max 25 per user). Omit `name` to get one derived from the filters.\nA \"Watch\" is `{ filters, alert: true }`.\n`alert` is deprecated (it still works): Use `GET /api/market/search/{q}/live` (see Live search). With `alert: true` (max 10 per user) every listing the market ingest adds or bumps/updates (feed read every ~5 s) is matched against the filters; each matching listing alerts once per saved search (`maxAgeDays` and `sort` are ignored), pushed live on `GET /api/market/saved-searches/stream` and kept 30 days in `GET /api/market/saved-searches/alerts`. Caps: 10 alerts per search per ingest batch, 60 per user per hour (excess matches are dropped). The initial backfill never alerts.","deprecated":true,"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSavedSearchBodyDto"}}}},"responses":{"201":{"headers":{"Deprecation":{"description":"`true`: this endpoint is deprecated. Keep your own `{ name, q }` list (`q` = the search encoded as in `GET /api/market/search/{q}`) and watch it with `GET /api/market/search/live?q=…&q=…` (see Live search).","schema":{"type":"string","example":"true"}},"Link":{"description":"The deprecation notice.","schema":{"type":"string","example":"</developers/market-listings#live-search>; rel=\"deprecation\""}}},"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedSearchDto"}}}},"400":{"description":"Invalid name, filters or alert.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedSearchErrorDto"}}}},"401":{"description":"Missing, invalid or expired PD2 bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedSearchErrorDto"},"example":{"statusCode":401,"message":"Invalid or expired token"}}}},"409":{"description":"Saved-search or alert limit reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedSearchErrorDto"}}}},"429":{"description":"Per-IP rate limit exceeded."}},"tags":["Market saved searches"],"security":[{"pd2Bearer":[]}]}},"/api/market/saved-searches/alerts":{"get":{"operationId":"listMarketAlerts","summary":"Recent saved-search alerts (deprecated)","description":"Deprecated (still works): Use `GET /api/market/search/{q}/live` (see Live search). Live search catches up with `GET /api/market/search/{q}` instead.\nAlerts of the user (last 30 days), newest first. After a stream reconnect, pass `since` = the last `matchedAt` you saw to catch up.","deprecated":true,"parameters":[{"name":"limit","required":false,"in":"query","description":"1-100, default 50.","schema":{"type":"number"}},{"name":"since","required":false,"in":"query","description":"Only alerts matched after this ISO date-time.","schema":{"type":"string"}}],"responses":{"200":{"headers":{"Deprecation":{"description":"`true`: this endpoint is deprecated. Keep your own `{ name, q }` list (`q` = the search encoded as in `GET /api/market/search/{q}`) and watch it with `GET /api/market/search/live?q=…&q=…` (see Live search).","schema":{"type":"string","example":"true"}},"Link":{"description":"The deprecation notice.","schema":{"type":"string","example":"</developers/market-listings#live-search>; rel=\"deprecation\""}}},"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketAlertListResponseDto"}}}},"400":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedSearchErrorDto"}}}},"401":{"description":"Missing, invalid or expired PD2 bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedSearchErrorDto"},"example":{"statusCode":401,"message":"Invalid or expired token"}}}},"429":{"description":"Per-IP rate limit exceeded."}},"tags":["Market saved searches"],"security":[{"pd2Bearer":[]}]}},"/api/market/saved-searches/{id}":{"patch":{"operationId":"updateSavedSearch","summary":"Update a saved market search (deprecated)","description":"Deprecated (still works): saved searches are going away. Keep your own `{ name, q }` list (`q` = the search encoded as in `GET /api/market/search/{q}`) and watch it with `GET /api/market/search/live?q=…&q=…` (see Live search).\n\nPartial update of `name`, `filters` and/or `alert`. `alert` is deprecated (it still works): Use `GET /api/market/search/{q}/live` (see Live search). With `alert: true` (max 10 per user) every listing the market ingest adds or bumps/updates (feed read every ~5 s) is matched against the filters; each matching listing alerts once per saved search (`maxAgeDays` and `sort` are ignored), pushed live on `GET /api/market/saved-searches/stream` and kept 30 days in `GET /api/market/saved-searches/alerts`. Caps: 10 alerts per search per ingest batch, 60 per user per hour (excess matches are dropped). The initial backfill never alerts.","deprecated":true,"parameters":[{"name":"id","required":true,"in":"path","description":"Saved search id.","schema":{"example":"6716a0f2b3c4d5e6f7a8b9c1","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSavedSearchBodyDto"}}}},"responses":{"200":{"headers":{"Deprecation":{"description":"`true`: this endpoint is deprecated. Keep your own `{ name, q }` list (`q` = the search encoded as in `GET /api/market/search/{q}`) and watch it with `GET /api/market/search/live?q=…&q=…` (see Live search).","schema":{"type":"string","example":"true"}},"Link":{"description":"The deprecation notice.","schema":{"type":"string","example":"</developers/market-listings#live-search>; rel=\"deprecation\""}}},"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedSearchDto"}}}},"400":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedSearchErrorDto"}}}},"401":{"description":"Missing, invalid or expired PD2 bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedSearchErrorDto"},"example":{"statusCode":401,"message":"Invalid or expired token"}}}},"404":{"description":"No saved search with this id for this user.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedSearchErrorDto"}}}},"409":{"description":"Alert limit reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedSearchErrorDto"}}}},"429":{"description":"Per-IP rate limit exceeded."}},"tags":["Market saved searches"],"security":[{"pd2Bearer":[]}]},"delete":{"operationId":"deleteSavedSearch","summary":"Delete a saved market search (deprecated)","description":"Deprecated (still works): saved searches are going away. Keep your own `{ name, q }` list (`q` = the search encoded as in `GET /api/market/search/{q}`) and watch it with `GET /api/market/search/live?q=…&q=…` (see Live search).","deprecated":true,"parameters":[{"name":"id","required":true,"in":"path","description":"Saved search id.","schema":{"example":"6716a0f2b3c4d5e6f7a8b9c1","type":"string"}}],"responses":{"200":{"headers":{"Deprecation":{"description":"`true`: this endpoint is deprecated. Keep your own `{ name, q }` list (`q` = the search encoded as in `GET /api/market/search/{q}`) and watch it with `GET /api/market/search/live?q=…&q=…` (see Live search).","schema":{"type":"string","example":"true"}},"Link":{"description":"The deprecation notice.","schema":{"type":"string","example":"</developers/market-listings#live-search>; rel=\"deprecation\""}}},"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteSavedSearchResponseDto"}}}},"401":{"description":"Missing, invalid or expired PD2 bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedSearchErrorDto"},"example":{"statusCode":401,"message":"Invalid or expired token"}}}},"404":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedSearchErrorDto"}}}},"429":{"description":"Per-IP rate limit exceeded."}},"tags":["Market saved searches"],"security":[{"pd2Bearer":[]}]}},"/api/market/saved-searches/stream":{"get":{"operationId":"streamMarketAlerts","summary":"Live saved-search alerts (Server-Sent Events, deprecated)","description":"Deprecated (still works): Use `GET /api/market/search/{q}/live` (see Live search). It needs no sign-in and no saved search.\n\nLong-lived `text/event-stream` of the signed-in user's saved-search alerts, same framing as `GET /api/stream`.\nAuth: `Authorization: Bearer <PD2 access token>` (browsers: use a fetch-based SSE reader, `EventSource` cannot send headers).\nEvents: `hello` once (first frame sets `retry: 10000`), then `market.alert` (`MarketAlertDto`) for every new listing matching a saved search with `alert: true`.\nLatency: about 5 s (the feed read interval) plus at most 1 s. Heartbeat comment every 25 s.\nMissed alerts while disconnected: `GET /api/market/saved-searches/alerts?since=<last matchedAt>`.\nLimits: 3 concurrent streams per user (429), 503 + `Retry-After` when the server is full.","deprecated":true,"parameters":[],"responses":{"200":{"description":"Event stream; stays open until the client disconnects.","headers":{"Deprecation":{"description":"`true`: this endpoint is deprecated. Keep your own `{ name, q }` list (`q` = the search encoded as in `GET /api/market/search/{q}`) and watch it with `GET /api/market/search/live?q=…&q=…` (see Live search).","schema":{"type":"string","example":"true"}},"Link":{"description":"The deprecation notice.","schema":{"type":"string","example":"</developers/market-listings#live-search>; rel=\"deprecation\""}}},"content":{"text/event-stream":{"schema":{"type":"string","description":"Server-Sent Events frames."},"example":"retry: 10000\nevent: hello\nid: 1\ndata: {\"serverTime\":\"2026-10-21T08:14:00.000Z\",\"events\":[\"market.alert\"],\"heartbeatSeconds\":25}\n\nevent: market.alert\nid: 2\ndata: {\"id\":\"6716a1c0b3c4d5e6f7a8b9c2\",\"savedSearchId\":\"6716a0f2b3c4d5e6f7a8b9c1\",\"savedSearchName\":\"Cheap Shako\",\"listingId\":\"6716a0f2b3c4d5e6f7a8b9c0\",\"listing\":{\"id\":\"6716a0f2b3c4d5e6f7a8b9c0\",\"isLadder\":true,\"isHardcore\":false,\"name\":\"Harlequin Crest\",\"baseName\":\"Shako\",\"baseCode\":\"uap\",\"typeCode\":\"helm\",\"quality\":\"Unique\",\"price\":\"2 ist\",\"hrPrice\":0.5,\"bumpedAt\":\"2026-10-21T08:15:02.000Z\",\"seller\":\"Gheed\",\"status\":\"active\",\"soldHr\":null},\"matchedAt\":\"2026-10-21T08:15:03.120Z\"}\n\n","x-sse-events":{"hello":{"$ref":"#/components/schemas/MarketAlertsHelloEventDto"},"market.alert":{"$ref":"#/components/schemas/MarketAlertDto"}}}}},"401":{"description":"Missing, invalid or expired PD2 bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedSearchErrorDto"},"example":{"statusCode":401,"message":"Invalid or expired token"}}}},"429":{"description":"Too many concurrent alert streams for this user.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedSearchErrorDto"},"example":{"statusCode":429,"message":"Too many concurrent alert streams for this user (max 3)"}}}},"503":{"description":"The server instance is at its stream capacity; retry after `Retry-After` seconds.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedSearchErrorDto"},"example":{"statusCode":503,"message":"Too many open streams on this server, retry later"}}}}},"tags":["Market saved searches"],"security":[{"pd2Bearer":[]}]}},"/api/market/{baseCode}/live":{"get":{"operationId":"getLiveListings","summary":"Current PD2 market listings for an item","description":"Listings currently active on the PD2 market (projectdiablo2.com) that the market ingest would price under this baseCode, cheapest first (unparseable asks last).\nFetched from PD2 at most once a minute per item and mode (the newest-bumped 100 matching PD2 listings; `truncated` says when PD2 had more).\n\nSeller data is never returned: no account names, user ids or online times. `priceHr` is PD2’s `hr_price`, else parsed from the seller’s note; `null` when neither yields a price.\nRate limited per client IP (30 requests/min).","parameters":[{"name":"baseCode","required":true,"in":"path","description":"Catalog base code (case-insensitive), e.g. `harlequin_crest`, `r30`. See `GET /api/catalog`.","schema":{"example":"harlequin_crest","type":"string"}},{"name":"isLadder","required":false,"in":"query","schema":{"type":"boolean","default":true},"description":"Ladder mode. `true` or `false`; anything else is a 400."},{"name":"isHardcore","required":false,"in":"query","schema":{"type":"boolean","default":false},"description":"Hardcore mode. `true` or `false`; anything else is a 400."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LiveListingsDoc"}}}},"400":{"description":"`isLadder`/`isHardcore` is not a boolean."},"404":{"description":"The base code is not in the catalog."},"429":{"description":"More than 30 requests in a minute from this IP."},"502":{"description":"The PD2 market API failed or timed out."}},"tags":["Market"],"security":[{},{"apiKey":[]}]}},"/api/market/{baseCode}":{"get":{"operationId":"getMarketDepth","summary":"Get market depth for an item","description":"Supply-side market depth for one item, computed from the listing revisions the market feed ingest stores: one row per (PD2 listing id, `updated_at`) with the ask in HR. Every change PD2 reports through a listing’s `updated_at` is a separate revision.\n\n**Window.** `days` complete UTC days ending at midnight of the latest ingested day (`window.dataAsOf`); the day still being ingested is excluded.\n\n**Definitions.**\n- `listingsObserved` / `sampleSize`: distinct listings with a revision in the window.\n- `asks`: lowest, p25, median, p75 and highest ask over those listings (each listing’s last price in the window), in HR, untrimmed. `spreadHr` is the interquartile range `p75 − p25`; the market has no bids, so there is no bid/ask spread.\n- `activeListings` / `activeAsks`: listings whose latest revision is at most `activeWindowHours` (48h) older than `dataAsOf`, priced at their latest ask. A recency proxy: the stored data has no open/sold/expired status.\n- `newListings` / `newListingsPerDay`: listings whose earliest stored revision falls in the window, per UTC day.\n- `confidence`: `high` ≥ 30 listings, `medium` 10–29, `low` 1–9, `none` 0.\n\n**Not provided.** Time-to-sell and sell-through are not computed by this endpoint, so they are listed in `omitted` with the reason instead of being estimated.\n\nAn item known to the catalog but with no listings returns zero counts and null ask stats. Results are cached until the next price update (`prices.updated`).","parameters":[{"name":"baseCode","required":true,"in":"path","description":"Catalog base code (case-insensitive), e.g. `r30` (Ber Rune), `harlequin_crest` (Harlequin Crest). See `GET /api/catalog`.","schema":{"example":"r30","type":"string"}},{"name":"isLadder","required":false,"in":"query","schema":{"type":"boolean","default":true},"description":"Ladder mode. `true` or `false`; anything else is a 400."},{"name":"isHardcore","required":false,"in":"query","schema":{"type":"boolean","default":false},"description":"Hardcore mode. `true` or `false`; anything else is a 400."},{"name":"days","required":false,"in":"query","schema":{"type":"integer","enum":[7,30],"default":7},"description":"Window length in complete UTC days: `7` or `30`."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketDepthResponse"}}}},"400":{"description":"`isLadder`/`isHardcore` is not a boolean, or `days` is not 7 or 30."},"404":{"description":"The base code is not in the catalog."}},"tags":["Market"],"security":[{},{"apiKey":[]}]}},"/api/economy/index":{"get":{"operationId":"getEconomyIndex","summary":"Get an economy index","description":"Daily value of a basket of trade goods for one season and mode, where 100 is the cost of the basket at season start. Below 100 means the items got cheaper in HR (typical as a ladder ages), above 100 means they got more expensive. Values are precomputed after every price update (`prices.updated`); the newest point is yesterday (UTC). Only ladder modes are indexed (`isLadder=false` returns 400).\n\n**Method.** Fixed-quantity price index, rebased every season. Base price of each component = median of its daily prices over the first 3 days of the season (components with no listing in that window are left out for the season). Each component gets its basket weight as a share of a 1 HR basket at base prices, so quantity_i = weight_i / basePrice_i, fixed for the season. value(day) = 100 × Σ quantity_i × price_i(day) / Σ quantity_i × basePrice_i. price_i(day) = 15% trimmed mean of the distinct listings active that UTC day (median below 7 listings); a day without listings carries the previous price forward. Days are UTC and only complete days are indexed (today is excluded). Prices are in HR.","parameters":[{"name":"to","required":false,"in":"query","schema":{"type":"string","format":"date"},"description":"Last day to return, YYYY-MM-DD (UTC, inclusive). Defaults to the newest indexed day. `latest` and `components` refer to the last point in the range."},{"name":"from","required":false,"in":"query","schema":{"type":"string","format":"date"},"description":"First day to return, YYYY-MM-DD (UTC, inclusive). Defaults to the season start."},{"name":"seasonId","required":false,"in":"query","description":"Season id (see `GET /api/seasons`) or `current`; defaults to the current season.","schema":{"example":"s14","type":"string"}},{"name":"isHardcore","required":false,"in":"query","schema":{"type":"boolean","default":false},"description":"Hardcore mode."},{"name":"isLadder","required":false,"in":"query","schema":{"type":"boolean","default":true},"description":"Ladder mode. Only `true` is supported."},{"name":"basket","required":false,"in":"query","schema":{"type":"string","enum":["high-runes","mid-runes","endgame","all"],"default":"high-runes"},"description":"Basket id (see `GET /api/economy/index/baskets`)."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EconomyIndexResponse"}}}},"400":{"description":"Invalid basket, boolean or date, unknown seasonId, `from` after `to`, or `isLadder=false`."},"404":{"description":"The index has not been computed for this season and mode yet (for example on the first day of a season)."}},"tags":["Economy"],"security":[{},{"apiKey":[]}]}},"/api/economy/index/baskets":{"get":{"operationId":"listIndexBaskets","summary":"List economy index baskets","description":"Basket definitions (components and their defined weights), the indexed modes and the index method.","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IndexBasketsResponse"}}}}},"tags":["Economy"],"security":[{},{"apiKey":[]}]}},"/api/alerts":{"get":{"operationId":"listAlerts","summary":"List alerts","description":"All alerts of the calling API client (max 25), newest first. Never includes signing secrets.","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertListResponseDto"}}}},"401":{"description":"Missing (`API key required`) or invalid (`Invalid API key`) `X-API-Key`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertErrorDto"}}}},"429":{"description":"Per-key rate limit exceeded (300 req/min)."}},"tags":["Alerts"],"security":[{"apiKey":[]}]},"post":{"operationId":"createAlert","summary":"Create a price or rare item alert","description":"Creates a webhook alert. Price alerts (`kind: \"price\"`, default): after every price update (`prices.updated`, at most every 5 min), enabled alerts of the updated modes are evaluated against the 7-day price data; a matching alert fires at most once per hour and then waits `cooldownHours` (default 12). Rare item alerts (`kind: \"rare\"` with `rare: { slot, stats: [{ key, min }], maxHr?, minStars? }`): every newly ingested PD2 market rare listing (as the market feed delivers them) of the alert mode and slot is checked; each matching item (content fingerprint) is delivered once per alert, ever, with its stats, price and listing link. 400 while rare appraisal is not live. Both kinds share the per-client limit. The response contains the webhook signing `secret` — the only time it is returned. Delivery: Discord webhook URLs get an embed (gold accent; item icon for price alerts, listing link and stats for rare alerts). Any other HTTPS URL gets `POST` JSON `{ event: \"alert.triggered\", alert, item, value, threshold, triggeredAt }` (price) or `{ event, alert, listing, value, triggeredAt }` (rare) with headers `X-PD2Trader-Signature: sha256=<hex HMAC-SHA256 of the raw body with your alert secret>` and `X-PD2Trader-Timestamp: <unix seconds>`. Compare signatures in constant time.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAlertBodyDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAlertResponseDto"}}}},"400":{"description":"Invalid body, unknown item, invalid rare criteria (unknown slot or stat key, 1-4 stats), rare alerts not live, non-HTTPS or non-public webhook URL, or the 25-alert limit is reached."},"401":{"description":"Missing (`API key required`) or invalid (`Invalid API key`) `X-API-Key`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertErrorDto"}}}},"429":{"description":"Per-key rate limit exceeded (300 req/min)."}},"tags":["Alerts"],"security":[{"apiKey":[]}]}},"/api/alerts/{id}":{"get":{"operationId":"getAlert","summary":"Get an alert","description":"One alert of the calling API client, including its last 10 delivery results.","parameters":[{"name":"id","required":true,"in":"path","description":"Alert id (from `GET /api/alerts`).","schema":{"example":"6710a2f4c1e5b8a9d4f3e201","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertDto"}}}},"401":{"description":"Missing (`API key required`) or invalid (`Invalid API key`) `X-API-Key`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertErrorDto"}}}},"404":{"description":"No alert with this id for this API client."},"429":{"description":"Per-key rate limit exceeded (300 req/min)."}},"tags":["Alerts"],"security":[{"apiKey":[]}]},"patch":{"operationId":"updateAlert","summary":"Update an alert","description":"Partial update: only the fields sent change. The signing secret stays the same (also when `webhookUrl` changes).","parameters":[{"name":"id","required":true,"in":"path","description":"Alert id (from `GET /api/alerts`).","schema":{"example":"6710a2f4c1e5b8a9d4f3e201","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAlertBodyDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertDto"}}}},"400":{"description":"Invalid field, unknown item or rejected webhook URL."},"401":{"description":"Missing (`API key required`) or invalid (`Invalid API key`) `X-API-Key`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertErrorDto"}}}},"404":{"description":"No alert with this id for this API client."},"429":{"description":"Per-key rate limit exceeded (300 req/min)."}},"tags":["Alerts"],"security":[{"apiKey":[]}]},"delete":{"operationId":"deleteAlert","summary":"Delete an alert","description":"Deletes the alert and its delivery log.","parameters":[{"name":"id","required":true,"in":"path","description":"Alert id (from `GET /api/alerts`).","schema":{"example":"6710a2f4c1e5b8a9d4f3e201","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteAlertResponseDto"}}}},"401":{"description":"Missing (`API key required`) or invalid (`Invalid API key`) `X-API-Key`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertErrorDto"}}}},"404":{"description":"No alert with this id for this API client."},"429":{"description":"Per-key rate limit exceeded (300 req/min)."}},"tags":["Alerts"],"security":[{"apiKey":[]}]}},"/api/alerts/{id}/test":{"post":{"operationId":"testAlert","summary":"Send a test webhook","description":"Sends a test payload to the alert webhook right away (event `alert.test`; price alerts send the current metric value when price data exists, rare alerts send `listing: null`; one attempt, 5 s timeout) and returns the result, which is also added to `recentDeliveries`. Works for disabled alerts and ignores the cooldown. Limited to one test per alert every 10 seconds. Delivery: Discord webhook URLs get an embed (gold accent; item icon for price alerts, listing link and stats for rare alerts). Any other HTTPS URL gets `POST` JSON `{ event: \"alert.triggered\", alert, item, value, threshold, triggeredAt }` (price) or `{ event, alert, listing, value, triggeredAt }` (rare) with headers `X-PD2Trader-Signature: sha256=<hex HMAC-SHA256 of the raw body with your alert secret>` and `X-PD2Trader-Timestamp: <unix seconds>`. Compare signatures in constant time.","parameters":[{"name":"id","required":true,"in":"path","description":"Alert id (from `GET /api/alerts`).","schema":{"example":"6710a2f4c1e5b8a9d4f3e201","type":"string"}}],"responses":{"200":{"description":"Delivery attempted; check `delivery.ok` (a failing receiver is not an API error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestAlertResponseDto"}}}},"401":{"description":"Missing (`API key required`) or invalid (`Invalid API key`) `X-API-Key`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertErrorDto"}}}},"404":{"description":"No alert with this id for this API client."},"429":{"description":"Per-key rate limit exceeded, or another test of this alert ran less than 10 seconds ago (`Retry-After` says when to retry)."}},"tags":["Alerts"],"security":[{"apiKey":[]}]}},"/api/stream":{"get":{"operationId":"streamEvents","summary":"Live event stream (Server-Sent Events)","description":"Long-lived `text/event-stream` connection (use `EventSource` in browsers). Every event is a frame with `event: <type>`, `id: <n>` and a single-line JSON `data:` payload.\n\n- `hello` (once, on connect): server time and the subscribed events. The first frame also sets `retry: 10000` (reconnect after 10 s).\n- `prices`: new item prices were stored (at most every 5 min); refetch the prices you care about. Data: `{ completedAt, modes }`.\n- `feed`: \"New in stash\" entries of a player with a public profile (public profile fields only).\n- `badges`: badges newly earned by a player with a public profile (display name and slug only).\n- `market.listing.created` / `market.listing.updated` (bumped, repriced or edited) / `market.listing.sold` / `market.listing.removed` (deleted by the seller or a moderator) / `market.listing.expired` (past PD2's 6-day listing life): PD2 market listing changes seen by the market ingest (PD2's listing change feed, read every ~5 s). Data: compact listing summary + `at`; full listing via `GET /api/market/listing/{id}`. Opt-in: not part of the default subscription; request them via `events` (`market` = all five).\n\nA comment line (`: heartbeat <time>`) is sent every 25 s so idle proxies keep the connection open.\nEvent ids are per server instance and not replayable (`Last-Event-ID` is ignored); after a reconnect, refetch state via the REST endpoints.\n\nLimits: 3 concurrent streams per IP without an API key, 10 per API key (429 beyond), 2000 per server instance (503 with `Retry-After`).\n\nJSON payload schemas: `StreamHelloEventDto`, `StreamPricesEventDto`, `StreamFeedEventDto`, `StreamBadgesEventDto`, `StreamMarketListingEventDto` (also linked from the `x-sse-events` extension of the response).","parameters":[{"name":"events","required":false,"in":"query","description":"Comma-separated event types to subscribe to (prices, feed, badges, market.listing.created, market.listing.updated, market.listing.sold, market.listing.removed, market.listing.expired; `market` = all market.listing.* events). Omitted → all available events except the opt-in market events.","schema":{"example":"prices,feed","type":"string"}}],"responses":{"200":{"description":"Event stream; stays open until the client disconnects.","headers":{"Cache-Control":{"description":"`no-cache, no-transform`","schema":{"type":"string"}},"X-Accel-Buffering":{"description":"`no` (disables proxy buffering)","schema":{"type":"string"}}},"content":{"text/event-stream":{"schema":{"type":"string","description":"Server-Sent Events frames."},"example":"retry: 10000\nevent: hello\nid: 1\ndata: {\"serverTime\":\"2026-10-16T18:04:12.345Z\",\"events\":[\"prices\",\"feed\",\"badges\"],\"heartbeatSeconds\":25}\n\n: heartbeat 2026-10-16T18:04:37.345Z\n\nevent: prices\nid: 2\ndata: {\"completedAt\":\"2026-10-16T18:05:41.902Z\",\"modes\":[{\"isLadder\":true,\"isHardcore\":false},{\"isLadder\":true,\"isHardcore\":true}]}\n\nevent: feed\nid: 3\ndata: {\"displayName\":\"Gheed\",\"slug\":\"gheed\",\"isLadder\":true,\"isHardcore\":false,\"seasonId\":\"s14\",\"entries\":[{\"key\":\"r30\",\"name\":\"Ber Rune\",\"category\":\"runes\",\"iconPath\":\"/runes/ber.png\",\"qtyDelta\":1,\"valueHr\":1,\"at\":\"2026-10-16T18:06:03.000Z\"}]}\n\nevent: badges\nid: 4\ndata: {\"displayName\":\"Gheed\",\"slug\":\"gheed\",\"isLadder\":true,\"isHardcore\":false,\"seasonId\":\"s14\",\"badges\":[\"first-ber\"],\"at\":\"2026-10-16T18:06:03.000Z\"}\n\nevent: market.listing.created\nid: 5\ndata: {\"id\":\"6716a0f2b3c4d5e6f7a8b9c0\",\"isLadder\":true,\"isHardcore\":false,\"name\":\"Harlequin Crest\",\"baseName\":\"Shako\",\"baseCode\":\"uap\",\"typeCode\":\"helm\",\"quality\":\"Unique\",\"price\":\"2 ist\",\"hrPrice\":0.5,\"bumpedAt\":\"2026-10-16T18:06:10.000Z\",\"seller\":\"Gheed\",\"status\":\"active\",\"soldHr\":null,\"at\":\"2026-10-16T18:06:12.000Z\"}\n\n","x-sse-events":{"hello":{"$ref":"#/components/schemas/StreamHelloEventDto"},"prices":{"$ref":"#/components/schemas/StreamPricesEventDto"},"feed":{"$ref":"#/components/schemas/StreamFeedEventDto"},"badges":{"$ref":"#/components/schemas/StreamBadgesEventDto"},"market.listing.created":{"$ref":"#/components/schemas/StreamMarketListingEventDto"},"market.listing.updated":{"$ref":"#/components/schemas/StreamMarketListingEventDto"},"market.listing.sold":{"$ref":"#/components/schemas/StreamMarketListingEventDto"},"market.listing.removed":{"$ref":"#/components/schemas/StreamMarketListingEventDto"},"market.listing.expired":{"$ref":"#/components/schemas/StreamMarketListingEventDto"}}}}},"400":{"description":"Unknown event type.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer"},"message":{"type":"string"}}},"example":{"statusCode":400,"message":"Unknown event type \"trades\". Valid: prices, feed, badges, market.listing.created, market.listing.updated, market.listing.sold, market.listing.removed, market.listing.expired, market"}}}},"429":{"description":"Too many concurrent streams for this IP or API key.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer"},"message":{"type":"string"}}},"example":{"statusCode":429,"message":"Too many concurrent streams from this IP (max 3; an API key allows 10)"}}}},"503":{"description":"The server instance is at its stream capacity; retry after `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds to wait.","schema":{"type":"integer","example":30}}},"content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer"},"message":{"type":"string"}}},"example":{"statusCode":503,"message":"Too many open streams on this server, retry later"}}}}},"tags":["Stream"],"security":[{},{"apiKey":[]}]}},"/api/cards/item/{baseCode}.png":{"get":{"operationId":"getItemCard","summary":"Item price card (PNG)","description":"Shareable price card for one item, styled like Diablo II: icon, item name in its quality colour, current 7-day average in HR, 7-day change, a 30-day sparkline of the 7-day moving average, listing count, mode and date. Built for Discord embeds and Open Graph (`og:image`) tags. Items without listings in the last 7 days render a \"No recent price data\" card. Rendered cards are cached in memory until the next price update (`prices.updated`, 6 h at most); responses carry a strong `ETag` (send it in `If-None-Match` for `304 Not Modified`) and `Cache-Control: public, max-age=600, s-maxage=3600`.","parameters":[{"name":"baseCode","required":true,"in":"path","description":"Item base code from the catalog (case-insensitive).","schema":{"example":"r30","type":"string"}},{"name":"size","required":false,"in":"query","schema":{"type":"string","enum":["og","compact"],"default":"og"},"description":"`og` = 1200×630 (Open Graph / Discord large image), `compact` = 600×200."},{"name":"theme","required":false,"in":"query","schema":{"type":"string","enum":["dark","light"],"default":"dark"},"description":"Dark stone (default) or light parchment background."},{"name":"isLadder","required":false,"in":"query","schema":{"type":"boolean","default":true},"description":"Ladder (`true`) or non-ladder (`false`) prices."},{"name":"isHardcore","required":false,"in":"query","schema":{"type":"boolean","default":false},"description":"Hardcore (`true`) or softcore (`false`) prices."},{"name":"If-None-Match","in":"header","description":"ETag of a previous response.","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"The card as a PNG image.","content":{"image/png":{"schema":{"type":"string","format":"binary","description":"PNG image: 1200×630 for `size=og`, 600×200 for `size=compact`."}}}},"304":{"description":"Not modified: the `If-None-Match` ETag is current."},"400":{"description":"Invalid `size`, `theme`, `isLadder` or `isHardcore`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CardErrorResponse"}}}},"404":{"description":"The base code is not in the catalog.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CardErrorResponse"}}}}},"tags":["Cards"],"security":[{},{"apiKey":[]}]}},"/api/cards/item/{baseCode}.svg":{"get":{"operationId":"getItemCardSvg","summary":"Item price card (SVG)","description":"Shareable price card for one item, styled like Diablo II: icon, item name in its quality colour, current 7-day average in HR, 7-day change, a 30-day sparkline of the 7-day moving average, listing count, mode and date. Built for Discord embeds and Open Graph (`og:image`) tags. Items without listings in the last 7 days render a \"No recent price data\" card. Rendered cards are cached in memory until the next price update (`prices.updated`, 6 h at most); responses carry a strong `ETag` (send it in `If-None-Match` for `304 Not Modified`) and `Cache-Control: public, max-age=600, s-maxage=3600`.","parameters":[{"name":"baseCode","required":true,"in":"path","description":"Item base code from the catalog (case-insensitive).","schema":{"example":"r30","type":"string"}},{"name":"size","required":false,"in":"query","schema":{"type":"string","enum":["og","compact"],"default":"og"},"description":"`og` = 1200×630 (Open Graph / Discord large image), `compact` = 600×200."},{"name":"theme","required":false,"in":"query","schema":{"type":"string","enum":["dark","light"],"default":"dark"},"description":"Dark stone (default) or light parchment background."},{"name":"isLadder","required":false,"in":"query","schema":{"type":"boolean","default":true},"description":"Ladder (`true`) or non-ladder (`false`) prices."},{"name":"isHardcore","required":false,"in":"query","schema":{"type":"boolean","default":false},"description":"Hardcore (`true`) or softcore (`false`) prices."},{"name":"If-None-Match","in":"header","description":"ETag of a previous response.","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"The card as an SVG document.","content":{"image/svg+xml":{"schema":{"type":"string","description":"Standalone SVG document (same layout as the PNG). AvQest is embedded as a data URI; body text uses Inter when installed, else the system sans-serif.","example":"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1200\" height=\"630\" viewBox=\"0 0 1200 630\"><title>Ber Rune price - pd2trader.com</title>…</svg>"}}}},"304":{"description":"Not modified: the `If-None-Match` ETag is current."},"400":{"description":"Invalid `size`, `theme`, `isLadder` or `isHardcore`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CardErrorResponse"}}}},"404":{"description":"The base code is not in the catalog.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CardErrorResponse"}}}}},"tags":["Cards"],"security":[{},{"apiKey":[]}]}},"/api/builds/characters":{"get":{"operationId":"listBuildCharacters","summary":"List characters matching build filters","description":"Paged characters matching the filters with top skills, worn gear icons and gear value, plus a per-class breakdown. `q` narrows the page and total by name (case-insensitive substring); the breakdown ignores it.\n\nPublic, no auth; cached ~5 minutes and rate limited. Ladder characters of level 80+ of the season, crawled from PD2 (each refreshed about once a day). Values in HR (1 HR = 1 Lo rune).","parameters":[{"name":"dir","required":false,"in":"query","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}},{"name":"sort","required":false,"in":"query","schema":{"type":"string","enum":["level","life","gearValueHr","lastUpdated"],"default":"level"}},{"name":"pageSize","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":50,"default":25}},{"name":"page","required":false,"in":"query","schema":{"type":"integer","minimum":1,"default":1}},{"name":"q","required":false,"in":"query","description":"Name contains (case-insensitive).","schema":{"example":"hammer","type":"string"}},{"name":"season","required":false,"in":"query","description":"Season id (see `GET /api/seasons`); defaults to the current season.","schema":{"example":"s14","type":"string"}},{"name":"mode","required":false,"in":"query","schema":{"type":"string","enum":["softcore","hardcore"],"default":"softcore"}},{"name":"classes","required":false,"in":"query","description":"Comma list, any of: Amazon, Assassin, Barbarian, Druid, Necromancer, Paladin, Sorceress.","schema":{"example":"Sorceress","type":"string"}},{"name":"skills","required":false,"in":"query","description":"Comma list, all of. `Name` or `Name:N` (at least N hard points, default 1).","schema":{"example":"Nova:20,Lightning Mastery","type":"string"}},{"name":"items","required":false,"in":"query","description":"Comma list of worn item keys, all of (keys from `/api/builds/usage`).","schema":{"example":"unique:griffon_s_eye,rw:infinity","type":"string"}},{"name":"mercTypes","required":false,"in":"query","description":"Comma list of merc types, any of.","schema":{"example":"Defensive Auras","type":"string"}},{"name":"mercItems","required":false,"in":"query","description":"Comma list of merc item keys, all of.","schema":{"example":"rw:insight","type":"string"}},{"name":"minLevel","required":false,"in":"query","schema":{"type":"integer","minimum":80,"maximum":99}},{"name":"maxLevel","required":false,"in":"query","schema":{"type":"integer","minimum":80,"maximum":99}},{"name":"maxCostHr","required":false,"in":"query","schema":{"type":"number","minimum":0},"description":"Maximum gear value in HR (excludes characters without a gear value)."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CharacterListResponseDto"}}}},"400":{"description":"Invalid query parameter."},"503":{"description":"The builds search backend (OpenSearch) is not configured on this server, failed or took longer than 5 s; retry shortly."}},"tags":["Builds"],"security":[{},{"apiKey":[]}]}},"/api/builds/suggest":{"get":{"operationId":"suggestBuilds","summary":"Characters and accounts by name, as you type","description":"Characters whose name or account contains `q` (case-insensitive): exact name, name prefix, name substring, then the same three on the account; within each, level desc, then name. Accounts whose name contains `q` (exact, prefix, then substring) with their number of characters in the season and mode. `limit` applies to each list. A `q` under 2 characters answers empty lists.\n\nPublic, no auth; cached ~5 minutes and rate limited. Ladder characters of level 80+ of the season, crawled from PD2 (each refreshed about once a day). Values in HR (1 HR = 1 Lo rune).","parameters":[{"name":"mode","required":false,"in":"query","description":"Omitted: both modes.","schema":{"enum":["softcore","hardcore"],"type":"string"}},{"name":"season","required":false,"in":"query","description":"Season id (see `GET /api/seasons`); defaults to the current season.","schema":{"example":"s14","type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":20,"default":8},"description":"Characters and accounts each (clamped to 1–20)."},{"name":"q","required":true,"in":"query","description":"Name or account text (trimmed, case-insensitive, first 50 characters).","schema":{"example":"shad","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildSuggestResponseDto"}}}},"400":{"description":"Invalid query parameter."},"503":{"description":"The builds search backend (OpenSearch) is not configured on this server, failed or took longer than 5 s; retry shortly."}},"tags":["Builds"],"security":[{},{"apiKey":[]}]}},"/api/builds/usage":{"get":{"operationId":"getBuildUsage","summary":"Class, skill, item and merc usage shares","description":"Shares (0–100) of classes, maxed skills (≥ 20 hard points), worn uniques/sets/runewords (with market price), merc types and merc items among characters matching the filters.\n\nPublic, no auth; cached ~5 minutes and rate limited. Ladder characters of level 80+ of the season, crawled from PD2 (each refreshed about once a day). Values in HR (1 HR = 1 Lo rune).","parameters":[{"name":"season","required":false,"in":"query","description":"Season id (see `GET /api/seasons`); defaults to the current season.","schema":{"example":"s14","type":"string"}},{"name":"mode","required":false,"in":"query","schema":{"type":"string","enum":["softcore","hardcore"],"default":"softcore"}},{"name":"classes","required":false,"in":"query","description":"Comma list, any of: Amazon, Assassin, Barbarian, Druid, Necromancer, Paladin, Sorceress.","schema":{"example":"Sorceress","type":"string"}},{"name":"skills","required":false,"in":"query","description":"Comma list, all of. `Name` or `Name:N` (at least N hard points, default 1).","schema":{"example":"Nova:20,Lightning Mastery","type":"string"}},{"name":"items","required":false,"in":"query","description":"Comma list of worn item keys, all of (keys from `/api/builds/usage`).","schema":{"example":"unique:griffon_s_eye,rw:infinity","type":"string"}},{"name":"mercTypes","required":false,"in":"query","description":"Comma list of merc types, any of.","schema":{"example":"Defensive Auras","type":"string"}},{"name":"mercItems","required":false,"in":"query","description":"Comma list of merc item keys, all of.","schema":{"example":"rw:insight","type":"string"}},{"name":"minLevel","required":false,"in":"query","schema":{"type":"integer","minimum":80,"maximum":99}},{"name":"maxLevel","required":false,"in":"query","schema":{"type":"integer","minimum":80,"maximum":99}},{"name":"maxCostHr","required":false,"in":"query","schema":{"type":"number","minimum":0},"description":"Maximum gear value in HR (excludes characters without a gear value)."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageResponseDto"}}}},"400":{"description":"Invalid query parameter."},"503":{"description":"The builds search backend (OpenSearch) is not configured on this server, failed or took longer than 5 s; retry shortly."}},"tags":["Builds"],"security":[{},{"apiKey":[]}]}},"/api/builds/usage/{list}":{"get":{"operationId":"getBuildUsageList","summary":"One page of the skill, item or merc item usage list","description":"Every maxed skill / worn item / merc item among matching characters, most used first, paged; `q` filters by name (case-insensitive substring). `GET /api/builds/usage` inlines page 1.\n\nPublic, no auth; cached ~5 minutes and rate limited. Ladder characters of level 80+ of the season, crawled from PD2 (each refreshed about once a day). Values in HR (1 HR = 1 Lo rune).","parameters":[{"name":"list","required":true,"in":"path","schema":{"enum":["skills","items","mercItems"],"type":"string"}},{"name":"pageSize","required":false,"in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"page","required":false,"in":"query","schema":{"type":"integer","minimum":1,"default":1}},{"name":"q","required":false,"in":"query","description":"Name contains (case-insensitive).","schema":{"example":"windhammer","type":"string"}},{"name":"season","required":false,"in":"query","description":"Season id (see `GET /api/seasons`); defaults to the current season.","schema":{"example":"s14","type":"string"}},{"name":"mode","required":false,"in":"query","schema":{"type":"string","enum":["softcore","hardcore"],"default":"softcore"}},{"name":"classes","required":false,"in":"query","description":"Comma list, any of: Amazon, Assassin, Barbarian, Druid, Necromancer, Paladin, Sorceress.","schema":{"example":"Sorceress","type":"string"}},{"name":"skills","required":false,"in":"query","description":"Comma list, all of. `Name` or `Name:N` (at least N hard points, default 1).","schema":{"example":"Nova:20,Lightning Mastery","type":"string"}},{"name":"items","required":false,"in":"query","description":"Comma list of worn item keys, all of (keys from `/api/builds/usage`).","schema":{"example":"unique:griffon_s_eye,rw:infinity","type":"string"}},{"name":"mercTypes","required":false,"in":"query","description":"Comma list of merc types, any of.","schema":{"example":"Defensive Auras","type":"string"}},{"name":"mercItems","required":false,"in":"query","description":"Comma list of merc item keys, all of.","schema":{"example":"rw:insight","type":"string"}},{"name":"minLevel","required":false,"in":"query","schema":{"type":"integer","minimum":80,"maximum":99}},{"name":"maxLevel","required":false,"in":"query","schema":{"type":"integer","minimum":80,"maximum":99}},{"name":"maxCostHr","required":false,"in":"query","schema":{"type":"number","minimum":0},"description":"Maximum gear value in HR (excludes characters without a gear value)."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageListResponseDto"}}}},"400":{"description":"Invalid query parameter."},"503":{"description":"The builds search backend (OpenSearch) is not configured on this server, failed or took longer than 5 s; retry shortly."}},"tags":["Builds"],"security":[{},{"apiKey":[]}]}},"/api/builds/shopping-list":{"get":{"operationId":"getBuildShoppingList","summary":"Most worn items for a build, with prices","description":"The 10 most worn uniques/sets/runewords among characters matching the filters, with share, market price and market page path.\n\nPublic, no auth; cached ~5 minutes and rate limited. Ladder characters of level 80+ of the season, crawled from PD2 (each refreshed about once a day). Values in HR (1 HR = 1 Lo rune).","parameters":[{"name":"season","required":false,"in":"query","description":"Season id (see `GET /api/seasons`); defaults to the current season.","schema":{"example":"s14","type":"string"}},{"name":"mode","required":false,"in":"query","schema":{"type":"string","enum":["softcore","hardcore"],"default":"softcore"}},{"name":"classes","required":false,"in":"query","description":"Comma list, any of: Amazon, Assassin, Barbarian, Druid, Necromancer, Paladin, Sorceress.","schema":{"example":"Sorceress","type":"string"}},{"name":"skills","required":false,"in":"query","description":"Comma list, all of. `Name` or `Name:N` (at least N hard points, default 1).","schema":{"example":"Nova:20,Lightning Mastery","type":"string"}},{"name":"items","required":false,"in":"query","description":"Comma list of worn item keys, all of (keys from `/api/builds/usage`).","schema":{"example":"unique:griffon_s_eye,rw:infinity","type":"string"}},{"name":"mercTypes","required":false,"in":"query","description":"Comma list of merc types, any of.","schema":{"example":"Defensive Auras","type":"string"}},{"name":"mercItems","required":false,"in":"query","description":"Comma list of merc item keys, all of.","schema":{"example":"rw:insight","type":"string"}},{"name":"minLevel","required":false,"in":"query","schema":{"type":"integer","minimum":80,"maximum":99}},{"name":"maxLevel","required":false,"in":"query","schema":{"type":"integer","minimum":80,"maximum":99}},{"name":"maxCostHr","required":false,"in":"query","schema":{"type":"number","minimum":0},"description":"Maximum gear value in HR (excludes characters without a gear value)."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShoppingListResponseDto"}}}},"400":{"description":"Invalid query parameter."},"503":{"description":"The builds search backend (OpenSearch) is not configured on this server, failed or took longer than 5 s; retry shortly."}},"tags":["Builds"],"security":[{},{"apiKey":[]}]}},"/api/builds/meta/history":{"get":{"operationId":"getBuildMetaHistory","summary":"Hourly class and top item shares","description":"Hourly meta snapshots (total characters, class shares) and the share series of the 10 most worn items, between `from` and `to` (default: last 7 days, at most 90 days).\n\nPublic, no auth; cached ~5 minutes and rate limited. Ladder characters of level 80+ of the season, crawled from PD2 (each refreshed about once a day). Values in HR (1 HR = 1 Lo rune).","parameters":[{"name":"to","required":false,"in":"query","description":"ISO date-time; default now.","schema":{"type":"string"}},{"name":"from","required":false,"in":"query","description":"ISO date-time.","schema":{"type":"string"}},{"name":"mode","required":false,"in":"query","schema":{"type":"string","enum":["softcore","hardcore"],"default":"softcore"}},{"name":"season","required":false,"in":"query","description":"Season id (see `GET /api/seasons`); defaults to the current season.","schema":{"example":"s14","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MetaHistoryResponseDto"}}}},"400":{"description":"Invalid query parameter."}},"tags":["Builds"],"security":[{},{"apiKey":[]}]}},"/api/builds/characters/{name}":{"get":{"operationId":"getBuildCharacter","summary":"One character build","description":"Attributes, skills with totals, worn gear (character and merc) with mods, icons and current market prices, and the gear value. Viewing a current-season character whose data is over a day old queues it for an immediate re-crawl; the next request shows the fresh build.\n\nPublic, no auth; cached ~5 minutes and rate limited. Ladder characters of level 80+ of the season, crawled from PD2 (each refreshed about once a day). Values in HR (1 HR = 1 Lo rune).","parameters":[{"name":"name","required":true,"in":"path","description":"Character name (case-insensitive).","schema":{"example":"Butzfire","type":"string"}},{"name":"prices","required":false,"in":"query","description":"`false`: return the build without live pricing (faster): every `priceHr` is null and `gearValueHr` is the value at the last crawl. Get the prices from `GET /api/builds/characters/:name/prices`.","schema":{"enum":["true","false"],"type":"string"}},{"name":"mode","required":false,"in":"query","description":"Omitted: softcore first, then hardcore.","schema":{"enum":["softcore","hardcore"],"type":"string"}},{"name":"season","required":false,"in":"query","description":"Season id (see `GET /api/seasons`); defaults to the current season.","schema":{"example":"s14","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CharacterDetailDto"}}}},"400":{"description":"Invalid query parameter."},"404":{"description":"No such character (or snapshot) in the season."}},"tags":["Builds"],"security":[{},{"apiKey":[]}]}},"/api/builds/characters/{name}/prices":{"get":{"operationId":"getBuildCharacterPrices","summary":"A character's live item prices","description":"Live market prices (HR) of the worn items, inventory charms and merc gear, in the same order as the lists of `GET /api/builds/characters/:name`, plus the gear value. Pair with `prices=false` there to show the build first.\n\nPublic, no auth; cached ~5 minutes and rate limited. Ladder characters of level 80+ of the season, crawled from PD2 (each refreshed about once a day). Values in HR (1 HR = 1 Lo rune).","parameters":[{"name":"name","required":true,"in":"path","description":"Character name (case-insensitive).","schema":{"example":"Butzfire","type":"string"}},{"name":"mode","required":false,"in":"query","description":"Omitted: softcore first, then hardcore.","schema":{"enum":["softcore","hardcore"],"type":"string"}},{"name":"season","required":false,"in":"query","description":"Season id (see `GET /api/seasons`); defaults to the current season.","schema":{"example":"s14","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CharacterPricesDto"}}}},"400":{"description":"Invalid query parameter."},"404":{"description":"No such character (or snapshot) in the season."}},"tags":["Builds"],"security":[{},{"apiKey":[]}]}},"/api/builds/characters/{name}/snapshots":{"get":{"operationId":"listBuildSnapshots","summary":"A character's build history","description":"Snapshots taken when the build changed (at most one per hour), newest first.\n\nPublic, no auth; cached ~5 minutes and rate limited. Ladder characters of level 80+ of the season, crawled from PD2 (each refreshed about once a day). Values in HR (1 HR = 1 Lo rune).","parameters":[{"name":"name","required":true,"in":"path","description":"Character name (case-insensitive).","schema":{"example":"Butzfire","type":"string"}},{"name":"mode","required":false,"in":"query","description":"Omitted: softcore first, then hardcore.","schema":{"enum":["softcore","hardcore"],"type":"string"}},{"name":"season","required":false,"in":"query","description":"Season id (see `GET /api/seasons`); defaults to the current season.","schema":{"example":"s14","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SnapshotListEntryDto"}}}}},"400":{"description":"Invalid query parameter."},"404":{"description":"No such character (or snapshot) in the season."}},"tags":["Builds"],"security":[{},{"apiKey":[]}]}},"/api/builds/characters/{name}/snapshots/{id}":{"get":{"operationId":"getBuildSnapshot","summary":"One stored build snapshot","description":"The build stored at that snapshot.\n\nPublic, no auth; cached ~5 minutes and rate limited. Ladder characters of level 80+ of the season, crawled from PD2 (each refreshed about once a day). Values in HR (1 HR = 1 Lo rune).","parameters":[{"name":"name","required":true,"in":"path","description":"Character name (case-insensitive).","schema":{"example":"Butzfire","type":"string"}},{"name":"id","required":true,"in":"path","description":"Snapshot id (from the snapshot list).","schema":{"type":"string"}},{"name":"mode","required":false,"in":"query","description":"Omitted: softcore first, then hardcore.","schema":{"enum":["softcore","hardcore"],"type":"string"}},{"name":"season","required":false,"in":"query","description":"Season id (see `GET /api/seasons`); defaults to the current season.","schema":{"example":"s14","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SnapshotDetailDto"}}}},"400":{"description":"Invalid query parameter."},"404":{"description":"No such character (or snapshot) in the season."}},"tags":["Builds"],"security":[{},{"apiKey":[]}]}},"/api/builds/items/{key}":{"get":{"operationId":"getBuildItemTooltip","summary":"Tooltip of a unique, set item or runeword","description":"Base, required level and property lines (variable rolls as `[min-max]`) from the game data, for the item keys of `/api/builds/usage`.\n\nPublic, no auth; cached ~5 minutes and rate limited. Ladder characters of level 80+ of the season, crawled from PD2 (each refreshed about once a day). Values in HR (1 HR = 1 Lo rune).","parameters":[{"name":"key","required":true,"in":"path","description":"Item key (`unique:`, `set:` or `rw:`).","schema":{"example":"unique:templar_s_might","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ItemTooltipDto"}}}},"404":{"description":"Not a known unique, set item or runeword."}},"tags":["Builds"],"security":[{},{"apiKey":[]}]}},"/api/sim/import/character/{name}":{"get":{"operationId":"importSimCharacter","summary":"Import a PD2 character as a sim build","description":"Maps a public PD2 character (equipped gear incl. sockets, skills, stats, quests, merc) to an engine `Build` for `JobSpec.profiles[].build`, with the inventory/stash items as Top Gear candidates and every modifier it could not place. 20 imports per minute per API key (per IP without one).","parameters":[{"name":"name","required":true,"in":"path","description":"PD2 character name.","schema":{"example":"YozzSorc","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimCharacterImportDto"}}}},"404":{"description":"PD2 doesn't know the character or can't read it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}},"429":{"description":"Import or API key rate limit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}}},"tags":["Sim"],"security":[{},{"apiKey":[]}]}},"/api/sim/jobs":{"post":{"operationId":"submitSimJob","summary":"Submit a sim job","description":"Queues a Horadric Sim job. Body: a `JobSpec` as JSON, or `profile.txt` text (`Content-Type: text/plain`, the line format `horadric-sim` reads), parsed by the engine into a `JobSpec`. A profile pins the data bundle of its `bundle=` line, else the `bundle` query parameter, else the newest bundle this server has. Limits are the caller's tier: anonymous callers run Quick Sim only; an API key runs with its owner's PD2 account tier (free, supporter or beta tester), shares that account's concurrent-job limit, and pauses with free jobs when the monthly spend breaker trips. Poll `GET /api/sim/jobs/{id}` until `reportSlug` is set. A spec the current engine already reported on (same `specHash`) is served from that report: the job is `done` at once (`reused: true`) with a report of its own, and nothing is simulated; `force=true` simulates it again. A signed-in planner session loaded from a pd2-trade overlay export adds `overlayExportId` (that export's id) next to the spec's fields: the owner's overlay is told how the job ended (report ready, failed or cancelled). It is not part of the spec or its hash.","parameters":[{"name":"bundle","required":false,"in":"query","description":"Data bundle `<id>:<content_hash>` for a profile.txt body without a `bundle=` line (ignored for JSON bodies).","schema":{"example":"pd2-s14:b7f84870b0c6e0fc6bcba84837fbee499faa932ff9f467e58d9bfbfde70f2deb","type":"string"}},{"name":"force","required":false,"in":"query","description":"Simulate even when an identical job (same `specHash`, current engine) already has a report; by default that report is reused.","schema":{"example":"true","type":"string"}}],"requestBody":{"required":true,"description":"A `JobSpec` (JSON) or profile.txt text (`text/plain`).","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"responses":{"202":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmittedSimJobDto"}}}},"400":{"description":"Invalid spec or profile (every `profile line N` error), over the tier's iteration / Top Gear / target-error limits, or an `overlayExportId` that is not one of the caller's live overlay exports.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}},"401":{"description":"Invalid API key, or an invalid / expired PD2 bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}},"403":{"description":"Closed beta (feature flag `sim.open_beta` off for the caller) and the caller is not on the beta allowlist (`code: sim_closed_beta`, `waitlist: /sim/beta`), job kind or custom APL not available to the tier, or the API key owner is banned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}},"429":{"description":"Rate limit (per API key, else per IP), anonymous submit throttle, the account already runs its maximum of concurrent jobs, or (submissions) its tier's rolling 30-day compute budget is used (`code: sim_compute_budget`, `budget`, message 'Monthly compute budget used; resets <date>').","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}},"503":{"description":"Anonymous queue full, monthly spend limit reached, or the queue is unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}}},"tags":["Sim"],"security":[{},{"apiKey":[]},{"pd2Bearer":[]}]}},"/api/sim/jobs/{id}":{"get":{"operationId":"getSimJob","summary":"Get a sim job","description":"Status, progress, chunk counts and live progress (`live`: phase, queue place, waves, ETA, the merged estimate) of a job; `reportSlug` once the report is written. Anonymous jobs are readable by id; another account's job is 404. `GET /api/sim/jobs/{id}/events` streams the same view.","parameters":[{"name":"id","required":true,"in":"path","description":"Job id from `POST /api/sim/jobs`.","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimJobDto"}}}},"401":{"description":"Invalid API key, or an invalid / expired PD2 bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}},"404":{"description":"No such job (or not yours).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}},"429":{"description":"Rate limit (per API key, else per IP), anonymous submit throttle, the account already runs its maximum of concurrent jobs, or (submissions) its tier's rolling 30-day compute budget is used (`code: sim_compute_budget`, `budget`, message 'Monthly compute budget used; resets <date>').","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}}},"tags":["Sim"],"security":[{},{"apiKey":[]},{"pd2Bearer":[]}]}},"/api/sim/jobs/{id}/cancel":{"post":{"operationId":"cancelSimJob","summary":"Cancel a sim job","description":"Cancels a queued or running job: it becomes `cancelled` and its unstarted chunks are skipped (a chunk already running finishes, but nothing is merged or reported). Only the submitter may cancel: the owner, or for an anonymous job a caller from the same IP. A job that already ended is returned unchanged.","parameters":[{"name":"id","required":true,"in":"path","description":"Job id from `POST /api/sim/jobs`.","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimJobDto"}}}},"401":{"description":"Invalid API key, or an invalid / expired PD2 bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}},"403":{"description":"An anonymous job submitted from another IP.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}},"404":{"description":"No such job (or not yours).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}},"429":{"description":"Rate limit (per API key, else per IP), anonymous submit throttle, the account already runs its maximum of concurrent jobs, or (submissions) its tier's rolling 30-day compute budget is used (`code: sim_compute_budget`, `budget`, message 'Monthly compute budget used; resets <date>').","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}}},"tags":["Sim"],"security":[{},{"apiKey":[]},{"pd2Bearer":[]}]}},"/api/sim/jobs/{id}/events":{"get":{"operationId":"streamSimJobEvents","summary":"Stream a sim job’s progress (SSE)","description":"Server-sent events, read once a second and shared by every viewer of the job: `log` events (data: one hosted-runner log line `{ id, at, source: api|worker, workerId, level: info|warn|error, kind, message, fields }`, oldest first: admission, enqueue, pickup, chunk done, wave merge, report written, errors) and a `progress` event (data: the `GET /api/sim/jobs/{id}` view) whenever the view changed; SSE `id`s increase per job. A viewer joining late gets the log so far and the latest view first. `ping` events every 15 s keep idle connections open. The stream ends with the `progress` event that carries `reportSlug`, or a failed or cancelled status (after the job’s last log lines). Same visibility as `GET /api/sim/jobs/{id}`; poll that endpoint where SSE is unavailable.","parameters":[{"name":"id","required":true,"in":"path","description":"Job id from `POST /api/sim/jobs`.","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"description":"`text/event-stream` of `log`, `progress` and `ping` events."},"401":{"description":"Invalid API key, or an invalid / expired PD2 bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}},"404":{"description":"No such job (or not yours).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}},"429":{"description":"Rate limit (per API key, else per IP), anonymous submit throttle, the account already runs its maximum of concurrent jobs, or (submissions) its tier's rolling 30-day compute budget is used (`code: sim_compute_budget`, `budget`, message 'Monthly compute budget used; resets <date>').","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}}},"tags":["Sim"],"security":[{},{"apiKey":[]},{"pd2Bearer":[]}]}},"/api/sim/runner/durations":{"get":{"operationId":"getSimDurations","summary":"Typical job durations","description":"How long finished jobs of the last 30 days took from submission to report (queue wait included), by job kind and size: `bucket` is `fight` or `boss` for Quick Sim and Compare, and for optimizers the variants their first stage simulated (`c50` up to 50, `c200`, `c1000`, `cmax` more). Map runs count as `fight`. Quartiles in seconds; groups with fewer than 3 jobs and system jobs (nightly rankings) are left out. Cached for 10 minutes.","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"windowDays":{"type":"number","example":30},"computedAt":{"type":"string","format":"date-time"},"durations":{"type":"array","items":{"type":"object","properties":{"kind":{"type":"string","example":"upgrade_finder"},"bucket":{"type":"string","example":"c200"},"jobs":{"type":"number","example":12},"p25Secs":{"type":"number","example":290},"medianSecs":{"type":"number","example":410},"p75Secs":{"type":"number","example":560}}}}}}}}}},"tags":["Sim"],"security":[{},{"apiKey":[]},{"pd2Bearer":[]}]}},"/api/sim/runner":{"get":{"operationId":"getSimRunner","summary":"Hosted runner status","description":"Workers online (heartbeat in the last 45 s: id, concurrency, running chunks, queue), their busy and total chunk slots, the worker jobs AWS Batch is starting (`starting`: how many, waiting for capacity or booting, since when), and the live jobs queued and running.","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimRunnerDto"}}}}},"tags":["Sim"],"security":[{},{"apiKey":[]},{"pd2Bearer":[]}]}},"/api/sim/runner/limits":{"get":{"operationId":"getSimLimits","summary":"The caller's tier limits","description":"The caller's tier (anonymous, free, supporter, beta_tester), the limits the planner checks before submitting (iterations, Top Gear combinations and Upgrade Finder candidates per job), the free tier's for comparison, and the caller's rolling 30-day compute budget (null when its tier has none). A candidate count above its limit is refused (Top Gear at submission, Upgrade Finder once the worker has expanded the job). `maxRuntimeMinutes` is the runtime cap (the worker fails a job whose work estimate exceeds it) and `waveSlots` the parallelism that estimate assumes.","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimLimitsDto"}}}}},"tags":["Sim"],"security":[{},{"apiKey":[]},{"pd2Bearer":[]}]}},"/api/sim/reports":{"get":{"operationId":"listSimReports","summary":"List your sim reports","description":"Reports of the caller's PD2 account (an API key lists its owner's), newest first.","parameters":[{"name":"pageSize","required":false,"in":"query","description":"1–100, default 25.","schema":{"type":"number"}},{"name":"page","required":false,"in":"query","description":"1-based, default 1.","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimReportListDto"}}}},"401":{"description":"Invalid API key or PD2 bearer token; for history and delete also a missing one.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}},"429":{"description":"Rate limit (per API key, else per IP).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}}},"tags":["Sim"],"security":[{"apiKey":[]},{"pd2Bearer":[]}]}},"/api/sim/reports/lookup":{"post":{"operationId":"lookupSimReport","summary":"Find your report of an identical spec","description":"Body: a `JobSpec`, as for `POST /api/sim/jobs`. Returns the caller's newest report whose job had the identical normalized spec (a rerun would reproduce it on the same engine), else `match: null`. Submits nothing.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimReportMatchDto"}}}},"400":{"description":"Malformed spec.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}},"401":{"description":"Invalid API key or PD2 bearer token; for history and delete also a missing one.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}},"429":{"description":"Rate limit (per API key, else per IP).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}}},"tags":["Sim"],"security":[{"apiKey":[]},{"pd2Bearer":[]}]}},"/api/sim/reports/claim":{"post":{"operationId":"claimSimReports","summary":"Add reports you ran before signing in","description":"Body: `{ claims: [{ jobId, token }] }` (1–50): anonymous jobs with the `claimToken` their `POST /api/sim/jobs` returned. Each finished report becomes the caller's (listed in My reports, deletable). Per claim: `claimed` (with `slug`), `pending` (still running: claim again later), `gone` (failed or cancelled) or `invalid` (unknown job, wrong token, or another account's report).","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"claims":{"type":"array","items":{"type":"object","properties":{"jobId":{"type":"string","format":"uuid"},"token":{"type":"string"}}}}}}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","properties":{"jobId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["claimed","pending","gone","invalid"]},"slug":{"type":"string"}}}}}}}}},"400":{"description":"Malformed claims.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}},"401":{"description":"Invalid API key or PD2 bearer token; for history and delete also a missing one.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}},"429":{"description":"Rate limit (per API key, else per IP).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}}},"tags":["Sim"],"security":[{"apiKey":[]},{"pd2Bearer":[]}]}},"/api/sim/reports/{slug}":{"patch":{"operationId":"patchSimReport","summary":"Rename or pin one of your sim reports","description":"`title` (1–64 characters; null or blank clears it) and/or `pinned` (★ baseline). Returns the list entry.","parameters":[{"name":"slug","required":true,"in":"path","description":"Report slug (`reportSlug` of the job).","schema":{"example":"q8Zr0Lw3VvB2xT9mN4kD1A","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimReportPatchDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimReportListEntryDto"}}}},"400":{"description":"Malformed patch.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}},"401":{"description":"Invalid API key or PD2 bearer token; for history and delete also a missing one.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}},"403":{"description":"Not your report.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}},"404":{"description":"No such report.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}},"429":{"description":"Rate limit (per API key, else per IP).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}}},"tags":["Sim"],"security":[{"apiKey":[]},{"pd2Bearer":[]}]},"get":{"operationId":"getSimReport","summary":"Get a sim report","description":"A permalink report: the engine `JobResult` summary, the normalized spec, pinned engine and data versions. `owned` tells whether the caller may delete it.","parameters":[{"name":"slug","required":true,"in":"path","description":"Report slug (`reportSlug` of the job).","schema":{"example":"q8Zr0Lw3VvB2xT9mN4kD1A","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimReportDto"}}}},"401":{"description":"Invalid API key or PD2 bearer token; for history and delete also a missing one.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}},"404":{"description":"No such report.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}},"429":{"description":"Rate limit (per API key, else per IP).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}}},"tags":["Sim"],"security":[{},{"apiKey":[]},{"pd2Bearer":[]}]},"delete":{"operationId":"deleteSimReport","summary":"Delete one of your sim reports","parameters":[{"name":"slug","required":true,"in":"path","description":"Report slug (`reportSlug` of the job).","schema":{"example":"q8Zr0Lw3VvB2xT9mN4kD1A","type":"string"}}],"responses":{"204":{"description":"Deleted."},"401":{"description":"Invalid API key or PD2 bearer token; for history and delete also a missing one.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}},"403":{"description":"Not your report (anonymous reports cannot be deleted).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}},"404":{"description":"No such report.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}},"429":{"description":"Rate limit (per API key, else per IP).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}}},"tags":["Sim"],"security":[{"apiKey":[]},{"pd2Bearer":[]}]}},"/api/sim/reports/{slug}/timeline":{"get":{"operationId":"getSimReportTimeline","summary":"Get a sim report timeline","description":"Frame-level combat log of iteration 0 of one profile (`profile`, default 0), while the report's log retention lasts (`timelineExpiresAt`). `profiles` is empty when that profile recorded none.","parameters":[{"name":"slug","required":true,"in":"path","description":"Report slug (`reportSlug` of the job).","schema":{"example":"q8Zr0Lw3VvB2xT9mN4kD1A","type":"string"}},{"name":"profile","required":false,"in":"query","description":"Index into `summary.profiles` (default 0).","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimTimelineDto"}}}},"400":{"description":"Bad `profile`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}},"401":{"description":"Invalid API key or PD2 bearer token; for history and delete also a missing one.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}},"404":{"description":"No such report, or no timeline (not recorded or expired).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}},"429":{"description":"Rate limit (per API key, else per IP).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}}},"tags":["Sim"],"security":[{},{"apiKey":[]},{"pd2Bearer":[]}]}},"/api/sim/reports/{slug}/log":{"get":{"operationId":"getSimReportLog","summary":"Get a sim report run log","description":"The stored hosted-runner log of the report's job (the lines `GET /api/sim/jobs/{id}/events` streamed: admission, pickup, chunks, merges, report), oldest first, while the lines are retained (`SIM_JOB_EVENT_RETENTION_DAYS`, 7 days by default); `lines` is empty after.","parameters":[{"name":"slug","required":true,"in":"path","description":"Report slug (`reportSlug` of the job).","schema":{"example":"q8Zr0Lw3VvB2xT9mN4kD1A","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimReportLogDto"}}}},"401":{"description":"Invalid API key or PD2 bearer token; for history and delete also a missing one.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}},"404":{"description":"No such report.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}},"429":{"description":"Rate limit (per API key, else per IP).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}}},"tags":["Sim"],"security":[{},{"apiKey":[]},{"pd2Bearer":[]}]}},"/api/sim/rankings":{"get":{"operationId":"getSimRankings","summary":"Curated sim rankings of one scenario","description":"Starter / mid / BiS profiles of each default Sorceress archetype and BiS profiles of the other classes' map-clearing archetypes, best first, with 95% CIs and the report of the run. Results refresh nightly and when a new data bundle ships.","parameters":[{"name":"scenario","required":false,"in":"query","description":"Ranking scenario id (`scenarios[].id`); default the first (Patchwerk).","schema":{"example":"patchwerk","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimRankingsDto"}}}},"404":{"description":"Unknown scenario.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}},"429":{"description":"Rate limit (per API key, else per IP).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimErrorDto"}}}}},"tags":["Sim"],"security":[{},{"apiKey":[]}]}},"/api/items":{"get":{"operationId":"listEconomyItems","summary":"List economy prices for a category","description":"Returns priced items (in HR) for an economy category, sorted per category. Items with fewer than 10 samples are hidden unless `showLowConfidence=true`. Unknown categories return an empty `items` array.","parameters":[{"name":"category","required":false,"in":"query","schema":{"type":"string","enum":["currency","ubers","rainbow-facets","maps-t5","charms","runes","equipment-armor","equipment-belt","equipment-boots","equipment-weapon","equipment-jewelry","equipment-gloves","equipment-helm","equipment-shield","equipment-quivers"],"default":"currency"},"description":"Economy category."},{"name":"isLadder","required":false,"in":"query","schema":{"type":"boolean","default":true},"description":"Ladder mode. Sent as the string 'true' or 'false'; any other value is treated as false."},{"name":"isHardcore","required":false,"in":"query","schema":{"type":"boolean","default":false},"description":"Hardcore mode. Sent as the string 'true' or 'false'; any other value is treated as false."},{"name":"showLowConfidence","required":false,"in":"query","schema":{"type":"boolean","default":false},"description":"Include items with fewer than 10 samples. Sent as the string 'true' or 'false'; any other value is treated as false."},{"name":"hours","required":false,"in":"query","schema":{"type":"integer","default":168},"description":"Rolling window in hours (invalid → 168). Ignored when `endDate` is set."},{"name":"quality","required":false,"in":"query","schema":{"type":"string","enum":["Unique","Set"]},"description":"`equipment-*` only: filter by item quality; omit for all."},{"name":"typeCode","required":false,"in":"query","schema":{"type":"string"},"description":"`equipment-*` only: restrict to one type code within the category (e.g. `ring`); a code outside the category yields no items."},{"name":"startDate","required":false,"in":"query","schema":{"type":"string","format":"date"},"description":"ISO date lower bound (e.g. season start, `2026-04-24`)."},{"name":"endDate","required":false,"in":"query","schema":{"type":"string","format":"date"},"description":"ISO date upper bound (past seasons). When set, `hours` is ignored."}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EconomyItemsResponse"}}}}},"tags":["Economy"],"security":[{},{"apiKey":[]}]}}},"info":{"title":"PD2 Trader API","description":"Public, read-only HTTP API behind [pd2trader.com](https://pd2trader.com): Project Diablo 2 item prices and economy data.\n\n**Base URL:** `https://pd2trader.com` (or `https://api.pd2trader.com`)\n\n- **API keys.** Send your key in the `X-API-Key` header (never in the query string) to get a per-key rate limit of 300 requests/minute (a `POST /api/rares/appraisal/batch` counts as ceil(items / 10) requests). An invalid or revoked key returns `401 Invalid API key`. Register an API client and create a key as described in [Authentication](/developers/authentication).\n- **Anonymous access is deprecated.** Requests without a key still work, limited to 180 requests/minute per client IP (a 429 says `Rate limit exceeded — use an API key for higher limits`; leaderboard routes keep their per-IP limit), and responses carry a `Deprecation` header. Anonymous access will be removed soon, so register an API client now. Never put a key in frontend code: call the API from your server or at build time. Cache responses and prefer the batch endpoints over many single lookups.\n- **Game mode.** Most endpoints accept `isLadder` (default `true`) and `isHardcore` (default `false`) to select the ladder/non-ladder and softcore/hardcore economy.\n- **Units.** Prices are expressed in **HR** (High Rune): one HR is the value of a Lo rune (`r28`). Fractions are common, and higher runes are worth several HR, e.g. a Ber (`r30`) is about 3 HR.\n\nGuides, concepts and the TypeScript SDK (`@pd2trader/sdk`) are in the [developer docs](/developers).","version":"1.0.0","contact":{}},"tags":[{"name":"Prices","description":"Average trade prices for individual items (uniques, sets, runewords, bases, runes and currency), batch lookups, price history and corruption prices. Values are in HR."},{"name":"Catalog","description":"Every price-tracked item with its stable `baseCode`, category, icon and search aliases, plus fuzzy item search. Use it to turn names like \"ber\" or \"shako\" into base codes."},{"name":"Tools","description":"Valuation and conversion tools: value a list of items and convert between items or HR, priced with the same rules as Net Worth and the desktop app."},{"name":"Price check","description":"Estimate the value of a specific item (stats, corruption, sockets, ethereal) from comparable stored listings, with the comparables and confidence."},{"name":"Market","description":"Market depth per item from stored listing revisions: listings observed, a recency-based active count, ask distribution and spread in HR, new listings per day, with confidence and sample size. Plus the market browser: search every live or sold PD2 listing with the overlay filters (site filters, pseudo stats, stat-weighted score/value sorts), also as a shareable URL (`GET /api/market/search/{q}`), fetch single listings, and live search: new matching listings pushed over Server-Sent Events (`GET /api/market/search/{q}/live`, or up to 10 searches on one stream with `GET /api/market/search/live?q=…&q=…`; no sign-in)."},{"name":"Economy","description":"Category views of the economy (every priced item in a category with its average HR value and sample counts) and the economy index: daily basket values per season, where 100 is the basket cost at season start."},{"name":"Seasons","description":"Project Diablo 2 seasons with their start and end dates, for scoping price queries to a season."},{"name":"Alerts","description":"Price alerts delivered to your webhook (Discord embeds or signed JSON) after each price update. Requires an API key; alerts belong to the key's API client."},{"name":"Stream","description":"Live updates over Server-Sent Events: price updates, PD2 market listings created/updated/sold/removed/expired (opt-in), plus public leaderboard feed and badges once leaderboards are live. Heartbeats every 25 s."},{"name":"Market saved searches","description":"Deprecated (every operation still works): saved market browser searches of a signed-in PD2 user (`Authorization: Bearer <PD2 access token>`, not an API key), including their alerts. Saved searches are going away: keep your own `{ name, q }` list (`q` = the search encoded as in `GET /api/market/search/{q}`) and watch it with `GET /api/market/search/live?q=…&q=…` (see Live search)."},{"name":"Cards","description":"Rendered item price cards (PNG/SVG) for Discord embeds and Open Graph images: icon, name, 7-day average in HR, 7-day change and a 30-day sparkline. 1200×630 or 600×200, dark or light."},{"name":"Rares","description":"Rare- and crafted-item appraisal (keep / check / vendor, rank in its slot, starred key stats, asking-price band), the rare guide (which stats multiply a rare's price, per slot and season phase) and item listing history."},{"name":"Leaderboards","description":"Public net worth leaderboards, character profiles, the activity feed and the hall of fame."},{"name":"Builds","description":"Ladder characters (level 80+) crawled from PD2's public game API: filterable character lists, skill/item/merc usage, character gear with market prices, gear snapshots, hourly meta history and a build shopping list. Season defaults to the current one; `mode` is `softcore` or `hardcore` (no ladder switch: ladder characters only). Data can lag when PD2's endpoints are down."},{"name":"Sim","description":"Horadric Sim, the PD2 damage simulator: import a PD2 character as a build, submit jobs (a `JobSpec` or profile.txt text), poll them, read, list and delete permalink reports, and the curated nightly rankings. An API key acts as its owner's PD2 account: same tier limits (job kinds, iterations, concurrent jobs, free / supporter / beta allowlist) and the same monthly spend breaker as signing in on the site. Anonymous callers get Quick Sim only."}],"servers":[{"url":"https://pd2trader.com","description":"Production"},{"url":"https://api.pd2trader.com","description":"Production (API subdomain)"}],"components":{"securitySchemes":{"pd2Bearer":{"scheme":"bearer","bearerFormat":"JWT","type":"http","description":"PD2 OAuth access token of the signed-in user (pd2trader / overlay sign-in)."},"apiKey":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Your PD2 Trader API key (`pd2t_…`). Create one at https://pd2trader.com/developers/authentication. Optional for now: anonymous access is deprecated and will be removed soon."}},"schemas":{"PriceChange7Days":{"type":"object","properties":{"change":{"type":"number","description":"Absolute change of the 7-day moving average versus the one 7 days before the latest day, in HR.","example":0.04},"changePercent":{"type":"number","description":"Relative change in percent.","example":3.38},"previousMedianPrice":{"type":"number","description":"7-day moving average 7 days before the latest day (closest earlier day within 2 days) on the same season series, in HR.","example":1.18}},"required":["change","changePercent","previousMedianPrice"]},"PriceTimeRange":{"type":"object","properties":{"start":{"type":"string","format":"date-time","description":"Timestamp of the oldest listing included.","example":"2026-10-09T00:14:52.000Z"},"end":{"type":"string","format":"date-time","description":"Timestamp of the newest listing included.","example":"2026-10-15T23:41:07.000Z"}},"required":["start","end"]},"AveragePriceResponse":{"type":"object","properties":{"typeCode":{"type":"string","description":"Item type code.","example":"rune"},"baseCode":{"type":"string","description":"Base code the price is stored under. Runes/materials use their game code (`r30` Ber, `r31` Jah); uniques use the normalized name (`harlequin_crest`); grouped variants encode the variant (`rainbow_facet_cold_5_5`, `hellfire_torch_druid`).","example":"r30"},"itemName":{"type":"string","description":"Display name.","example":"Ber Rune"},"averagePrice":{"type":"number","description":"Trimmed mean in HR: one price per distinct listing per day, top/bottom 15% dropped when there are at least 7 samples.","example":1.22},"minPrice":{"type":"number","description":"Lowest listing price in the window, in HR.","example":0.9},"maxPrice":{"type":"number","description":"Highest listing price in the window, in HR.","example":1.6},"medianPrice":{"type":"number","description":"Median listing price in the window, in HR.","example":1.2},"lastDayPrice":{"type":"number","description":"Trimmed mean of the most recent complete day in the price history, in HR.","example":1.25},"movingAverage7Days":{"type":"number","description":"7-day moving average of the latest day, in HR. Same value as the last point of `/item-prices/history` (season-baseline daily series), whatever the `hours`/date window.","example":1.22},"sampleCount":{"type":"number","description":"Number of distinct listing-days the statistics are based on.","example":412},"priceWindow":{"type":"string","description":"Window the price was computed over; present only for a rolling `hours` window without `endDate`/`crawledAt*`. The requested window label (`7d` for hours=168, `24h` for hours=24) when it has ≥10 samples; otherwise the shortest of `30d`, `season` (current season start..now) or `lastSeason` (previous season's last 30 days) that does. Items reaching 10 samples in no window keep the requested window's result (low-confidence). Same semantics as `/api/items`.","example":"7d"},"priceChange7Days":{"description":"Present only when a comparison point ~7 days ago exists.","allOf":[{"$ref":"#/components/schemas/PriceChange7Days"}]},"isLadder":{"type":"boolean","example":true},"isHardcore":{"type":"boolean","example":false},"timeRange":{"$ref":"#/components/schemas/PriceTimeRange"},"groupingData":{"type":"object","additionalProperties":true,"description":"Variant data for grouped items, e.g. Rainbow Facet `{ damageType: \"cold\", damagePercent: 5, piercePercent: 5 }`, Hellfire Torch / Spirit Ward `{ class: \"Druid\" }`.","example":{"damageType":"cold","damagePercent":5,"piercePercent":5}},"groupingLabel":{"type":"string","description":"Human-readable variant label for grouped items.","example":"Cold 5/5"}},"required":["typeCode","baseCode","itemName","averagePrice","minPrice","maxPrice","medianPrice","sampleCount","isLadder","isHardcore","timeRange"]},"PriceErrorResponse":{"type":"object","properties":{"statusCode":{"type":"number","example":500},"message":{"type":"string","example":"No price data found for the specified criteria"}},"required":["statusCode","message"]},"BatchPriceQueryDto":{"type":"object","properties":{"baseCodes":{"description":"Base codes to price (non-empty). Codes without data are silently omitted from the result.","example":["r30","r31","harlequin_crest"],"type":"array","items":{"type":"string"}},"isLadder":{"type":"boolean","description":"Ladder (`true`) or non-ladder (`false`). Omit to match both modes.","example":true},"isHardcore":{"type":"boolean","description":"Hardcore (`true`) or softcore (`false`). Omit to match both modes.","example":false},"startDate":{"type":"string","format":"date-time","description":"Listings updated at/after this time (lower clamp when combined with `hours`)."},"endDate":{"type":"string","format":"date-time","description":"Listings updated up to this time. Ignored when `hours` is set."},"hours":{"type":"number","minimum":1,"description":"Rolling window in hours on listing update time. The current day is always excluded.","example":168},"crawledAtStartDate":{"type":"string","format":"date-time","description":"Ingestion-time lower bound; any crawledAt* field switches filtering to ingestion time."},"crawledAtEndDate":{"type":"string","format":"date-time","description":"Ingestion-time upper bound."},"crawledAtHours":{"type":"number","minimum":1,"description":"Rolling window in hours on ingestion time."}},"required":["baseCodes"]},"AveragePriceListResponse":{"type":"object","properties":{"count":{"type":"number","description":"Number of entries in `data`.","example":2},"data":{"example":[{"typeCode":"rune","baseCode":"r31","itemName":"Jah Rune","averagePrice":1.41,"minPrice":1.1,"maxPrice":1.9,"medianPrice":1.4,"lastDayPrice":1.43,"movingAverage7Days":1.4,"sampleCount":288,"priceWindow":"7d","isLadder":true,"isHardcore":false,"timeRange":{"start":"2026-10-09T00:02:11.000Z","end":"2026-10-15T23:58:40.000Z"}},{"typeCode":"rune","baseCode":"r30","itemName":"Ber Rune","averagePrice":1.22,"minPrice":0.9,"maxPrice":1.6,"medianPrice":1.2,"lastDayPrice":1.25,"movingAverage7Days":1.22,"sampleCount":412,"priceChange7Days":{"change":0.04,"changePercent":3.38,"previousMedianPrice":1.18},"isLadder":true,"isHardcore":false,"timeRange":{"start":"2026-10-09T00:14:52.000Z","end":"2026-10-15T23:41:07.000Z"}}],"type":"array","items":{"$ref":"#/components/schemas/AveragePriceResponse"}}},"required":["count","data"]},"PriceHistoryPoint":{"type":"object","properties":{"date":{"type":"string","format":"date-time","description":"Day (midnight UTC).","example":"2026-10-14T00:00:00.000Z"},"avgPrice":{"type":"number","description":"Trimmed mean of that day's distinct listings, in HR (field name kept for compatibility).","example":1.24},"count":{"type":"number","description":"Distinct listings that day.","example":61},"movingAverage":{"type":"number","description":"7-day moving average (this day and up to 6 prior days, after outlier filtering), in HR.","example":1.21}},"required":["date","avgPrice","count","movingAverage"]},"PriceHistoryResponse":{"type":"object","properties":{"baseCode":{"type":"string","example":"r30"},"typeCode":{"type":"string","description":"Type code used for filtering (resolved from stored data when not supplied).","example":"rune"},"totalCount":{"type":"number","description":"Sum of `count` over all returned days.","example":1834},"dayCount":{"type":"number","description":"Number of days in `data`.","example":30},"data":{"description":"Daily points, oldest first. The current (incomplete) day is never included.","example":[{"date":"2026-10-13T00:00:00.000Z","avgPrice":1.19,"count":58,"movingAverage":1.2},{"date":"2026-10-14T00:00:00.000Z","avgPrice":1.24,"count":61,"movingAverage":1.21}],"type":"array","items":{"$ref":"#/components/schemas/PriceHistoryPoint"}}},"required":["baseCode","totalCount","dayCount","data"]},"CorruptionSocketPrice":{"type":"object","properties":{"socketCount":{"type":"number","example":3},"averagePrice":{"type":"number","description":"In HR.","example":0.42},"medianPrice":{"type":"number","description":"In HR.","example":0.4},"sampleCount":{"type":"number","example":9}},"required":["socketCount","averagePrice","medianPrice","sampleCount"]},"CorruptionPrice":{"type":"object","properties":{"corruptionKey":{"description":"Raw corruption modifier keys (sorted).","example":["item_numsockets"],"type":"array","items":{"type":"string"}},"corruptionName":{"type":"string","description":"Readable corruption name(s), comma-separated.","example":"Sockets"},"averagePrice":{"type":"number","description":"Mean listing price, in HR.","example":0.38},"medianPrice":{"type":"number","description":"Median listing price, in HR.","example":0.35},"sampleCount":{"type":"number","example":21},"socketPrices":{"description":"Per-socket-count breakdown; present only for socket corruptions.","type":"array","items":{"$ref":"#/components/schemas/CorruptionSocketPrice"}}},"required":["corruptionKey","corruptionName","averagePrice","medianPrice","sampleCount"]},"CorruptionPricesResponse":{"type":"object","properties":{"typeCode":{"type":"string","description":"Empty string when the item was not found.","example":"helm"},"baseCode":{"type":"string","description":"Empty string when the item was not found.","example":"harlequin_crest"},"itemName":{"type":"string","example":"Harlequin Crest"},"corruptionPrices":{"description":"Sorted by medianPrice descending. Empty when nothing matched (including on internal lookup errors).","example":[{"corruptionKey":["item_numsockets"],"corruptionName":"Sockets","averagePrice":0.38,"medianPrice":0.35,"sampleCount":21,"socketPrices":[{"socketCount":1,"averagePrice":0.31,"medianPrice":0.3,"sampleCount":12},{"socketCount":3,"averagePrice":0.42,"medianPrice":0.4,"sampleCount":9}]},{"corruptionKey":["item_allskills"],"corruptionName":"All Skills","averagePrice":0.29,"medianPrice":0.27,"sampleCount":7}],"type":"array","items":{"$ref":"#/components/schemas/CorruptionPrice"}},"timestamp":{"type":"string","format":"date-time","description":"Server time of the response.","example":"2026-10-16T09:30:00.000Z"}},"required":["typeCode","baseCode","itemName","corruptionPrices","timestamp"]},"SeasonResponse":{"type":"object","properties":{"id":{"type":"string","example":"s14"},"label":{"type":"string","example":"Alliance (S14)"},"startDate":{"type":"string","description":"First day, YYYY-MM-DD (UTC), inclusive. Season 14 starts mid-day, at 2026-10-23T17:00:00Z.","example":"2026-10-23"},"endDate":{"type":"string","nullable":true,"description":"Last day, YYYY-MM-DD (UTC), inclusive (it can be the next season's first day); null while the season is running."}},"required":["id","label","startDate","endDate"]},"SeasonsListResponse":{"type":"object","properties":{"seasons":{"description":"Seasons that have started, newest first.","type":"array","items":{"$ref":"#/components/schemas/SeasonResponse"}},"currentId":{"type":"string","description":"Id of the season containing the current date.","example":"s14"}},"required":["seasons","currentId"]},"RankCardDto":{"type":"object","properties":{"rank":{"type":"number","example":12},"rankChange":{"type":"number","nullable":true,"example":-1},"value":{"type":"number","example":18.2},"total":{"type":"number","description":"Public board size (+1 when the requesting user is private).","example":312}},"required":["rank","rankChange","value","total"]},"SessionExtraDto":{"type":"object","properties":{"durationMinutes":{"type":"number","example":180},"gainHr":{"type":"number","description":"Liquid HR gained during the session.","example":4.5}},"required":["durationMinutes","gainHr"]},"BoardEntryDto":{"type":"object","properties":{"rank":{"type":"number","example":1},"rankChange":{"type":"number","nullable":true,"description":"Rank movement since the timeframe start (season: 7 days ago); positive = moved up. null when unranked then.","example":2},"displayName":{"type":"string","example":"Gheed"},"slug":{"type":"string","description":"Profile slug for `GET /api/profile/{slug}`.","example":"gheed"},"value":{"type":"number","description":"Board value in HR (gained / wealth) or HR per hour (session).","example":42.5},"extra":{"description":"Only on the `session` board.","allOf":[{"$ref":"#/components/schemas/SessionExtraDto"}]}},"required":["rank","rankChange","displayName","slug","value"]},"LeaderboardResponseDto":{"type":"object","properties":{"board":{"type":"string","enum":["gained","wealth","session"],"example":"gained"},"basis":{"type":"string","enum":["liquid","total"],"example":"liquid"},"timeframe":{"type":"string","enum":["7d","30d","season"],"example":"season"},"seasonId":{"type":"string","example":"s14"},"updatedAt":{"type":"string","format":"date-time","nullable":true,"description":"Last aggregated UTC day (ISO midnight); null when nothing is aggregated yet."},"total":{"type":"number","description":"Opted-in users on the board.","example":312},"entries":{"type":"array","items":{"$ref":"#/components/schemas/BoardEntryDto"}}},"required":["board","basis","timeframe","seasonId","updatedAt","total","entries"]},"ModeDto":{"type":"object","properties":{"isLadder":{"type":"boolean","example":true},"isHardcore":{"type":"boolean","example":false}},"required":["isLadder","isHardcore"]},"SeasonDto":{"type":"object","properties":{"id":{"type":"string","example":"s14"},"label":{"type":"string","example":"Alliance (S14)"},"startDate":{"type":"string","format":"date-time"},"endDate":{"type":"string","format":"date-time","nullable":true}},"required":["id","label","startDate","endDate"]},"DailyValueDto":{"type":"object","properties":{"day":{"type":"string","format":"date","example":"2026-10-24","description":"UTC day, YYYY-MM-DD."},"liquidHr":{"type":"number","example":40.1},"totalHr":{"type":"number","example":55.3}},"required":["day","liquidHr","totalHr"]},"BadgeDto":{"type":"object","properties":{"badge":{"type":"string","enum":["first-ber","first-zod","hr-100","hr-500","hr-1000","rune-collector","uber-set","streak-7"],"example":"first-ber"},"seasonId":{"type":"string","example":"s14"},"isLadder":{"type":"boolean"},"isHardcore":{"type":"boolean"},"earnedAt":{"type":"string","format":"date-time"}},"required":["badge","seasonId","isLadder","isHardcore","earnedAt"]},"BestSessionDto":{"type":"object","properties":{"start":{"type":"string","format":"date-time"},"end":{"type":"string","format":"date-time"},"durationMinutes":{"type":"number","example":180},"gainHr":{"type":"number","example":4.5},"hrPerHour":{"type":"number","example":1.5}},"required":["start","end","durationMinutes","gainHr","hrPerHour"]},"RareFindStatDto":{"type":"object","properties":{"label":{"type":"string","example":"+20% Faster Cast Rate"},"stars":{"type":"number","enum":[1,2,3],"example":3}},"required":["label","stars"]},"RareFindDto":{"type":"object","properties":{"fingerprint":{"type":"string","description":"Content fingerprint of the item (32 hex).","example":"9f2c4e1b7a6d3c5e8f0a1b2c3d4e5f60"},"slot":{"type":"string","enum":["ring","amul","circ","jewl","glov","boot","belt","tors","helm","shield","quiver","weapon"],"example":"circ"},"baseName":{"type":"string","example":"Tiara"},"rank":{"type":"number","description":"Predicted price percentile (0..1) within the slot when found; ≥ 0.97.","example":0.985},"stats":{"description":"Starred stats, strongest first (max 3).","type":"array","items":{"$ref":"#/components/schemas/RareFindStatDto"}}},"required":["fingerprint","slot","baseName","rank","stats"]},"FeedEntryDto":{"type":"object","properties":{"type":{"type":"string","enum":["new_in_stash","rare_find"],"example":"new_in_stash","description":"`rare_find`: a top-3% Keep rare (key = fingerprint, category `other`, qtyDelta 1, valueHr 0; details in `rare`)."},"displayName":{"type":"string","example":"Gheed"},"slug":{"type":"string","example":"gheed"},"key":{"type":"string","description":"Item key.","example":"rune:r30"},"name":{"type":"string","example":"Ber Rune"},"category":{"type":"string","enum":["runes","currency","ubers","gems","mats","uniques","sets","other"],"example":"runes"},"iconPath":{"type":"string","nullable":true,"example":"/runes/ber.png"},"qtyDelta":{"type":"number","description":"Quantity gained vs the previous snapshot.","example":1},"valueHr":{"type":"number","description":"Value of the gained quantity in HR.","example":1},"at":{"type":"string","format":"date-time"},"label":{"type":"string","enum":["New in stash","Rare find"],"example":"New in stash"},"rare":{"description":"Only for `type: rare_find`.","allOf":[{"$ref":"#/components/schemas/RareFindDto"}]}},"required":["type","displayName","slug","key","name","category","iconPath","qtyDelta","valueHr","at","label"]},"HoldingLineDto":{"type":"object","properties":{"key":{"type":"string","example":"rune:r30"},"name":{"type":"string","example":"Ber Rune"},"category":{"type":"string","enum":["runes","currency","ubers","gems","mats","uniques","sets","other"]},"qty":{"type":"number","example":3},"unitPrice":{"type":"number","nullable":true,"description":"Unit price in HR."},"value":{"type":"number","description":"Line value in HR."},"confidence":{"type":"string","enum":["ok","low","fixed","none"]},"sampleCount":{"type":"number"},"priceChange7dPercent":{"type":"number","nullable":true},"tab":{"type":"string","nullable":true},"iconPath":{"type":"string","nullable":true}},"required":["key","name","category","qty","unitPrice","value","confidence","sampleCount","priceChange7dPercent","tab","iconPath"]},"HoldingsDto":{"type":"object","properties":{"byCategory":{"type":"object","description":"HR value per category.","properties":{"runes":{"type":"number"},"currency":{"type":"number"},"ubers":{"type":"number"},"gems":{"type":"number"},"mats":{"type":"number"},"uniques":{"type":"number"},"sets":{"type":"number"},"other":{"type":"number"}}},"top":{"type":"array","items":{"$ref":"#/components/schemas/HoldingLineDto"}}},"required":["byCategory","top"]},"ProfileResponseDto":{"type":"object","properties":{"displayName":{"type":"string","example":"Gheed"},"slug":{"type":"string","example":"gheed"},"mode":{"description":"Mode served.","allOf":[{"$ref":"#/components/schemas/ModeDto"}]},"modes":{"description":"Modes with data this season, most recent first.","type":"array","items":{"$ref":"#/components/schemas/ModeDto"}},"season":{"$ref":"#/components/schemas/SeasonDto"},"liquidHr":{"type":"number","nullable":true,"description":"Latest daily liquid value (HR)."},"totalHr":{"type":"number","nullable":true,"description":"Latest daily total value (HR)."},"daily":{"type":"array","items":{"$ref":"#/components/schemas/DailyValueDto"}},"ranks":{"type":"object","description":"Rank card per board; null when unranked. Keys: gained, wealth, session (always present).","properties":{"gained":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/RankCardDto"}]},"wealth":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/RankCardDto"}]},"session":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/RankCardDto"}]}}},"percentile":{"type":"number","nullable":true,"description":"Share (0–100) of active users whose wealth is ≤ this user (higher = richer)."},"badges":{"type":"array","items":{"$ref":"#/components/schemas/BadgeDto"}},"bestSession":{"nullable":true,"description":"Season best session.","allOf":[{"$ref":"#/components/schemas/BestSessionDto"}]},"recentFeed":{"description":"Last 10 feed entries.","type":"array","items":{"$ref":"#/components/schemas/FeedEntryDto"}},"holdings":{"description":"Only when the user enabled showHoldings.","allOf":[{"$ref":"#/components/schemas/HoldingsDto"}]}},"required":["displayName","slug","mode","modes","season","liquidHr","totalHr","daily","ranks","percentile","badges","bestSession","recentFeed"]},"FeedResponseDto":{"type":"object","properties":{"entries":{"description":"Newest first.","type":"array","items":{"$ref":"#/components/schemas/FeedEntryDto"}}},"required":["entries"]},"RareHallOfFameEntryDto":{"type":"object","properties":{"fingerprint":{"type":"string","description":"Content fingerprint of the item (32 hex).","example":"9f2c4e1b7a6d3c5e8f0a1b2c3d4e5f60"},"slot":{"type":"string","enum":["ring","amul","circ","jewl","glov","boot","belt","tors","helm","shield","quiver","weapon"],"example":"circ"},"baseName":{"type":"string","example":"Tiara"},"rank":{"type":"number","description":"Predicted price percentile (0..1) within the slot when found; ≥ 0.97.","example":0.985},"stats":{"description":"Starred stats, strongest first (max 3).","type":"array","items":{"$ref":"#/components/schemas/RareFindStatDto"}},"name":{"type":"string","example":"Storm Crown"},"displayName":{"type":"string","example":"Gheed"},"slug":{"type":"string","example":"gheed"},"at":{"type":"string","format":"date-time","description":"When the item was found."}},"required":["fingerprint","slot","baseName","rank","stats","name","displayName","slug","at"]},"HallOfFameResponseDto":{"type":"object","properties":{"seasonId":{"type":"string","example":"s13"},"isLadder":{"type":"boolean"},"isHardcore":{"type":"boolean"},"frozenAt":{"type":"string","format":"date-time"},"boards":{"type":"object","description":"Final top 25 per board (basis liquid, timeframe season). Keys: gained, wealth, session (always present).","properties":{"gained":{"type":"array","items":{"$ref":"#/components/schemas/BoardEntryDto"}},"wealth":{"type":"array","items":{"$ref":"#/components/schemas/BoardEntryDto"}},"session":{"type":"array","items":{"$ref":"#/components/schemas/BoardEntryDto"}}}},"rares":{"description":"Best rare find per slot by opted-in players (rank desc, earliest first; one find per item ever, credited to its first owner).","type":"array","items":{"$ref":"#/components/schemas/RareHallOfFameEntryDto"}}},"required":["seasonId","isLadder","isHardcore","frozenAt","boards","rares"]},"CatalogItemResponse":{"type":"object","properties":{"baseCode":{"type":"string","description":"Stable item id used by every price endpoint (case-insensitive on input).","example":"r30"},"name":{"type":"string","description":"Display name. Grouped variants carry their grouping label, e.g. `Hellfire Torch (Sorceress)`.","example":"Ber Rune"},"category":{"type":"string","enum":["runes","currency","ubers","gems","mats","rainbow-facets","maps","charms","uniques","sets","equipment","other"],"description":"Catalog category.","example":"runes"},"typeCode":{"type":"string","nullable":true,"description":"PD2 item type code (e.g. `rune`, `helm`, `ubr`), when known.","example":"rune"},"quality":{"type":"string","nullable":true,"description":"Item quality as stored with the listings (`Unique`, `Set`, ...); null for runes and currency without one.","example":null},"iconUrl":{"type":"string","nullable":true,"description":"Absolute URL of the inventory icon on pd2trader.com, when one exists.","example":"https://pd2trader.com/assets/runes/Ber_Rune.png"},"aliases":{"description":"Normalized search aliases (lowercase, no apostrophes): short rune names, rune numbers, base codes, common nicknames.","example":["ber","r30"],"type":"array","items":{"type":"string"}}},"required":["baseCode","name","category","typeCode","quality","iconUrl","aliases"]},"CatalogListResponse":{"type":"object","properties":{"category":{"type":"string","nullable":true,"enum":["runes","currency","ubers","gems","mats","rainbow-facets","maps","charms","uniques","sets","equipment","other"],"description":"Category filter that was applied; null for the full catalog.","example":"runes"},"count":{"type":"number","description":"Number of items returned.","example":33},"items":{"description":"Items sorted by category, then name.","type":"array","items":{"$ref":"#/components/schemas/CatalogItemResponse"}}},"required":["category","count","items"]},"CatalogErrorResponse":{"type":"object","properties":{"statusCode":{"type":"number","example":400},"message":{"type":"string","description":"What was wrong with the request.","example":"q is required"},"error":{"type":"string","example":"Bad Request"}},"required":["statusCode","message","error"]},"CatalogSearchHitResponse":{"type":"object","properties":{"item":{"$ref":"#/components/schemas/CatalogItemResponse"},"score":{"type":"number","description":"Relevance (higher is better). Tiers: exact code 1000, exact name 900, exact alias 850, prefix 680-799, all words matched 500-599, substring 400-449, fuzzy 100-399.","example":850},"matchedOn":{"type":"string","enum":["code","name","alias","fuzzy"],"description":"What matched.","example":"alias"}},"required":["item","score","matchedOn"]},"CatalogSearchResponse":{"type":"object","properties":{"query":{"type":"string","description":"The query as received (trimmed).","example":"ber"},"category":{"type":"string","nullable":true,"enum":["runes","currency","ubers","gems","mats","rainbow-facets","maps","charms","uniques","sets","equipment","other"],"description":"Category filter that was applied, or null.","example":null},"count":{"type":"number","description":"Number of hits returned.","example":1},"hits":{"description":"Hits ordered by score, then catalog order.","example":[{"item":{"baseCode":"r30","name":"Ber Rune","category":"runes","typeCode":"rune","quality":null,"iconUrl":"https://pd2trader.com/assets/runes/Ber_Rune.png","aliases":["ber","r30"]},"score":850,"matchedOn":"alias"}],"type":"array","items":{"$ref":"#/components/schemas/CatalogSearchHitResponse"}}},"required":["query","category","count","hits"]},"SiteItemPriceResponse":{"type":"object","properties":{"valueHr":{"type":"number","description":"Ladder softcore price in HR over the last 7 days: the 7-day moving average, else the median.","example":1.2},"changePct":{"type":"number","nullable":true,"description":"Change vs the median price 7 days earlier, in percent; null without one.","example":3.4},"listings":{"type":"number","description":"Listings behind the price in the 7-day window.","example":356}},"required":["valueHr","changePct","listings"]},"SiteItemHitResponse":{"type":"object","properties":{"baseCode":{"type":"string","description":"Stable item id (catalog `baseCode`).","example":"harlequin_crest"},"name":{"type":"string","description":"Display name.","example":"Harlequin Crest"},"category":{"type":"string","description":"Display category: `Unique`, `Set`, `Runes`, `Currency`, `Ubers`, `Gems`, `Materials`, `Rainbow Facets`, `Maps`, `Charms`, `Other`, or the item type for other equipment.","example":"Unique"},"subtitle":{"type":"string","description":"Category plus the item type when known.","example":"Unique · Helm"},"href":{"type":"string","description":"Site path of the item page.","example":"/item/harlequin_crest?category=equipment-helm"},"iconUrl":{"type":"string","nullable":true,"description":"Site path of the inventory icon, when one exists.","example":"/assets/items/invcap.png"},"price":{"nullable":true,"description":"Current market price; null when the item has no price in the window.","allOf":[{"$ref":"#/components/schemas/SiteItemPriceResponse"}]},"matchedAlias":{"type":"string","nullable":true,"description":"The alias or nickname the query matched when the name did not (e.g. `shako` for Harlequin Crest).","example":"shako"}},"required":["baseCode","name","category","subtitle","href","iconUrl","price","matchedAlias"]},"SiteItemGroupResponse":{"type":"object","properties":{"total":{"type":"number","description":"Matching items (counted exactly up to 1000).","example":1},"hits":{"type":"array","items":{"$ref":"#/components/schemas/SiteItemHitResponse"}}},"required":["total","hits"]},"SiteCharacterHitResponse":{"type":"object","properties":{"name":{"type":"string","example":"GheedsBane"},"className":{"type":"string","example":"Sorceress"},"level":{"type":"number","example":92},"isHardcore":{"type":"boolean","example":false},"isDead":{"type":"boolean","description":"Dead hardcore character.","example":false},"seasonId":{"type":"string","description":"Season id (`/api/seasons`).","example":"s14"},"isCurrentSeason":{"type":"boolean","example":true},"account":{"type":"string","nullable":true,"description":"PD2 account that listed the character, when known.","example":null},"href":{"type":"string","description":"Site path of the character page: `?season=` for a past season, `mode=hardcore` for hardcore.","example":"/character/GheedsBane"}},"required":["name","className","level","isHardcore","isDead","seasonId","isCurrentSeason","account","href"]},"SiteCharacterGroupResponse":{"type":"object","properties":{"total":{"type":"number","description":"Matching characters (counted exactly up to 1000).","example":0},"hits":{"type":"array","items":{"$ref":"#/components/schemas/SiteCharacterHitResponse"}}},"required":["total","hits"]},"SiteSearchResponse":{"type":"object","properties":{"query":{"type":"string","description":"The query as received (trimmed).","example":"shako"},"items":{"$ref":"#/components/schemas/SiteItemGroupResponse"},"characters":{"$ref":"#/components/schemas/SiteCharacterGroupResponse"}},"required":["query","items","characters"]},"SiteSearchErrorResponse":{"type":"object","properties":{"statusCode":{"type":"number","example":503},"message":{"type":"string","example":"Site search is not configured"},"error":{"type":"string","example":"Service Unavailable"}},"required":["statusCode","message","error"]},"ValuateItemInput":{"type":"object","properties":{"item":{"type":"string","description":"Item name, alias or baseCode (resolved like `GET /api/search`; e.g. `ber`, `r30`, `Ber Rune`, `shako`, `wss`).","example":"ber","maxLength":200},"qty":{"type":"number","description":"Quantity (integer).","minimum":1,"maximum":1000000,"default":1,"example":2}},"required":["item"]},"ValuateRequest":{"type":"object","properties":{"runePricing":{"type":"string","enum":["dynamic","fixed"],"default":"dynamic","description":"How runes convert to HR. `dynamic` (default): market price, Net Worth rules (El–Fal and thin markets use the fixed table). `fixed`: every rune at the fixed table (Lum 0.005, Ko 0.01, Fal 0.01, Lem 0.01, Pul 0.03, Um 0.05, Mal 0.1, Ist 0.15, Gul 0.25, Vex 0.5, Ohm 0.75, Lo 1, Sur 1.5, Jah 2, Cham 2.25, Ber 3, Zod 4.5; El–Io 0). Other items always use the market.","example":"dynamic"},"items":{"minItems":1,"maxItems":500,"description":"Lines to value. Lines that resolve to the same item are merged (quantities summed).","example":[{"item":"ber","qty":2},{"item":"Jah Rune"},{"item":"ist","qty":10},{"item":"r30"},{"item":"Harlequin Crest"},{"item":"wss","qty":40}],"type":"array","items":{"$ref":"#/components/schemas/ValuateItemInput"}},"isLadder":{"type":"boolean","default":true,"description":"Ladder (`true`) or non-ladder (`false`). Prices always come from ladder listings, like Net Worth (`mode.priceSource`).","example":true},"isHardcore":{"type":"boolean","default":false,"description":"Hardcore (`true`) or softcore (`false`) prices.","example":false},"hours":{"type":"number","minimum":1,"maximum":720,"default":168,"description":"Price window in hours, clamped to the current season start. The default (168) is the window Net Worth uses.","example":168}},"required":["items"]},"ValuateLineResponse":{"type":"object","properties":{"input":{"type":"string","description":"The request `item` string (first occurrence when lines were merged).","example":"ber"},"baseCode":{"type":"string","description":"Resolved baseCode.","example":"r30"},"name":{"type":"string","example":"Ber Rune"},"qty":{"type":"number","description":"Total quantity after merging duplicates.","example":3},"unitHr":{"type":"number","nullable":true,"description":"Value of one unit in HR; null when `confidence` is `none`.","example":1.02},"valueHr":{"type":"number","description":"`qty × unitHr` in HR, rounded to 2 decimals (0 when unpriced).","example":3.06},"confidence":{"type":"string","enum":["ok","low","fixed","none"],"description":"Same rules as Net Worth. `ok`: market price with at least 10 samples. `low`: market price with fewer samples (excluded from `totalHr`). `fixed`: fixed table value (El–Fal runes always; higher runes and Worldstone Shards when the market is missing or thin; every rune with `runePricing=fixed`). `none`: no price.","example":"ok"},"sampleCount":{"type":"number","description":"Listing samples behind the market price (0 for fixed/none without market data).","example":412}},"required":["input","baseCode","name","qty","unitHr","valueHr","confidence","sampleCount"]},"ToolsModeResponse":{"type":"object","properties":{"isLadder":{"type":"boolean","example":true},"isHardcore":{"type":"boolean","example":false},"hours":{"type":"number","description":"Price window in hours.","example":168},"runePricing":{"type":"string","enum":["dynamic","fixed"],"description":"Rune to HR conversion used (`runePricing`).","example":"dynamic"},"seasonId":{"type":"string","description":"Season the price window is clamped to.","example":"s13"},"priceDay":{"type":"string","description":"UTC day (YYYY-MM-DD) of the price set used.","example":"2026-10-16"},"priceSource":{"type":"string","enum":["market","ladder-fallback"],"description":"`market` for ladder requests; `ladder-fallback` when `isLadder=false` (non-ladder is priced from ladder listings, as in Net Worth).","example":"market"}},"required":["isLadder","isHardcore","hours","runePricing","seasonId","priceDay","priceSource"]},"ValuateResponse":{"type":"object","properties":{"lines":{"description":"One line per distinct resolved item, in order of first appearance.","example":[{"input":"ber","baseCode":"r30","name":"Ber Rune","qty":3,"unitHr":1.02,"valueHr":3.06,"confidence":"ok","sampleCount":412},{"input":"Jah Rune","baseCode":"r31","name":"Jah Rune","qty":1,"unitHr":0.85,"valueHr":0.85,"confidence":"ok","sampleCount":365},{"input":"ist","baseCode":"r24","name":"Ist Rune","qty":10,"unitHr":0.15,"valueHr":1.5,"confidence":"fixed","sampleCount":6},{"input":"Harlequin Crest","baseCode":"harlequin_crest","name":"Harlequin Crest","qty":1,"unitHr":0.4,"valueHr":0.4,"confidence":"low","sampleCount":7},{"input":"wss","baseCode":"wss","name":"Worldstone Shard","qty":40,"unitHr":0.01,"valueHr":0.4,"confidence":"fixed","sampleCount":0}],"type":"array","items":{"$ref":"#/components/schemas/ValuateLineResponse"}},"unresolved":{"description":"Distinct inputs that did not resolve to any item.","example":["shakko of doom"],"type":"array","items":{"type":"string"}},"totalHr":{"type":"number","description":"Total of `ok` and `fixed` lines in HR (Net Worth `totalHr` rules).","example":5.81},"uncertainHr":{"type":"number","description":"Value of `low` confidence lines in HR, not included in `totalHr`.","example":0.4},"pricedAt":{"type":"string","format":"date-time","description":"When the valuation was computed.","example":"2026-10-16T12:00:00.000Z"},"mode":{"$ref":"#/components/schemas/ToolsModeResponse"}},"required":["lines","unresolved","totalHr","uncertainHr","pricedAt","mode"]},"ToolsErrorResponse":{"type":"object","properties":{"statusCode":{"type":"number","example":400},"message":{"type":"string","description":"What was wrong with the request.","example":"Unknown item for from: 'shakko'"},"error":{"type":"string","example":"Bad Request"}},"required":["statusCode","message","error"]},"NoteRequest":{"type":"object","properties":{"runePricing":{"type":"string","enum":["dynamic","fixed"],"default":"dynamic","description":"How runes convert to HR. `dynamic` (default): market price, Net Worth rules (El–Fal and thin markets use the fixed table). `fixed`: every rune at the fixed table (Lum 0.005, Ko 0.01, Fal 0.01, Lem 0.01, Pul 0.03, Um 0.05, Mal 0.1, Ist 0.15, Gul 0.25, Vex 0.5, Ohm 0.75, Lo 1, Sur 1.5, Jah 2, Cham 2.25, Ber 3, Zod 4.5; El–Io 0). Other items always use the market.","example":"dynamic"},"note":{"type":"string","description":"Free-text trade note, as sellers write it in a listing.","example":"wts 2 ist + pul c/o 0.2","maxLength":500},"isLadder":{"type":"boolean","default":true,"description":"Ladder (`true`) or non-ladder (`false`). Prices always come from ladder listings, like Net Worth (`mode.priceSource`).","example":true},"isHardcore":{"type":"boolean","default":false,"description":"Hardcore (`true`) or softcore (`false`) prices.","example":false},"hours":{"type":"number","minimum":1,"maximum":720,"default":168,"description":"Price window in hours, clamped to the current season start. The default (168) is the window Net Worth uses.","example":168}},"required":["note"]},"NotePriceResponse":{"type":"object","properties":{"statedHr":{"type":"number","description":"Asking price the note states, in HR at the fixed rune / WSS table (1 WSS = 0.01 HR). `probability` refers to this value.","example":0.33},"probability":{"type":"number","minimum":0,"maximum":1,"description":"Calibrated probability that `statedHr` is the seller's asking price (out-of-fold on season 13 listings: picks scored 0.9-0.95 were right 92% of the time, above 0.95 97%).","example":0.97},"source":{"type":"string","enum":["parser","model"],"description":"Which reader's value won: the site's price parser or the note-price model.","example":"model"},"text":{"type":"string","nullable":true,"description":"The words of the note the price was read from; null when the parser's price matches no single span.","example":"2 ist + pul"},"lines":{"description":"The price as items, valued at market like `POST /api/valuate` (`input` = the words for that item). HR amounts and bare numbers appear as `baseCode` `hr`, `unitHr` 1, `confidence` `fixed`.","example":[{"input":"2 ist","baseCode":"r24","name":"Ist Rune","qty":2,"unitHr":0.15,"valueHr":0.3,"confidence":"fixed","sampleCount":6},{"input":"pul","baseCode":"r21","name":"Pul Rune","qty":1,"unitHr":0.06,"valueHr":0.06,"confidence":"fixed","sampleCount":3}],"type":"array","items":{"$ref":"#/components/schemas/ValuateLineResponse"}},"totalHr":{"type":"number","description":"Market value of `ok` and `fixed` lines in HR.","example":0.36},"uncertainHr":{"type":"number","description":"Value of `low` confidence lines in HR, not included in `totalHr`.","example":0}},"required":["statedHr","probability","source","text","lines","totalHr","uncertainHr"]},"NoteResponse":{"type":"object","properties":{"price":{"nullable":true,"description":"The asking price; null when the note states none.","allOf":[{"$ref":"#/components/schemas/NotePriceResponse"}]},"pricedAt":{"type":"string","format":"date-time","description":"When the valuation was computed.","example":"2026-10-16T12:00:00.000Z"},"mode":{"$ref":"#/components/schemas/ToolsModeResponse"}},"required":["price","pricedAt","mode"]},"ConvertSideResponse":{"type":"object","properties":{"baseCode":{"type":"string","description":"Resolved baseCode, or `hr` for the HR unit.","example":"r24"},"name":{"type":"string","example":"Ist Rune"},"unitHr":{"type":"number","description":"Value of one unit in HR (always 1 for `hr`).","example":0.15},"confidence":{"type":"string","enum":["ok","low","fixed","none"],"description":"Same rules as Net Worth. `ok`: market price with at least 10 samples. `low`: market price with fewer samples (excluded from `totalHr`). `fixed`: fixed table value (El–Fal runes always; higher runes and Worldstone Shards when the market is missing or thin; every rune with `runePricing=fixed`). `none`: no price. `hr` is `fixed`.","example":"fixed"}},"required":["baseCode","name","unitHr","confidence"]},"ConvertResponse":{"type":"object","properties":{"from":{"$ref":"#/components/schemas/ConvertSideResponse"},"to":{"example":{"baseCode":"r30","name":"Ber Rune","unitHr":1.02,"confidence":"ok"},"allOf":[{"$ref":"#/components/schemas/ConvertSideResponse"}]},"amount":{"type":"number","description":"Amount of `from` converted.","example":10},"result":{"type":"number","description":"`amount × rate`: how many `to` the amount is worth (6 decimals).","example":1.470588},"rate":{"type":"number","description":"`to` per one `from` (`from.unitHr / to.unitHr`, 6 decimals).","example":0.147059},"inverseRate":{"type":"number","description":"`from` per one `to` (`to.unitHr / from.unitHr`, 6 decimals).","example":6.8},"confidence":{"type":"string","enum":["ok","low","fixed"],"description":"Weakest side: `low` if either side is `low`, else `ok` if either side is a market price, else `fixed`.","example":"ok"},"pricedAt":{"type":"string","format":"date-time","description":"When the conversion was computed.","example":"2026-10-16T12:00:00.000Z"},"mode":{"$ref":"#/components/schemas/ToolsModeResponse"}},"required":["from","to","amount","result","rate","inverseRate","confidence","pricedAt","mode"]},"PriceCheckStatInput":{"type":"object","properties":{"name":{"type":"string","description":"Stat to match: a modifier name (`item_allskills`, `passive_cold_mastery`), its label (`to All Skills`), or a stored field (`sockets`, `ethereal`, `corrupted`; Rainbow Facet `damageType` / `damagePercent` / `piercePercent`; Hellfire Torch and Spirit Ward `class`). Give `name` or `id`.","example":"item_allskills","maxLength":100},"id":{"type":"number","description":"PD2 stat id instead of `name` (e.g. `127` = All Skills, `194` = sockets). The desktop app ids `1000` (sockets), `1001` (ethereal) and `360` (corrupted) are accepted too.","minimum":0,"example":127},"value":{"oneOf":[{"type":"number"},{"type":"string"},{"type":"boolean"}],"description":"Rolled value. Numbers for numeric stats and sockets, booleans (or 1/0) for ethereal/corrupted, strings for `damageType` (`lightning`, `cold`, `fire`, `poison`) and `class` (`Sorceress`, …).","example":2}},"required":["value"]},"PriceCheckRequest":{"type":"object","properties":{"item":{"type":"string","description":"Item name, alias or baseCode (resolved like `GET /api/search`). Grouped items accept their plain name (`Rainbow Facet`, `Hellfire Torch`, `Spirit Ward`) when `stats` pick the variant, or a variant baseCode such as `rainbow_facet_cold_5_5`.","example":"Harlequin Crest","maxLength":120},"quality":{"type":"string","description":"Item quality (`Unique`, `Set`). Only Unique and Set listings record it; otherwise reported in `ignoredFilters`.","example":"Unique","maxLength":20},"stats":{"maxItems":30,"description":"Rolled stats. Stored listings keep only sockets, ethereal, corruption, quality and the grouped-item variant, so any other stat is listed in `ignoredFilters` (and caps `confidence` at `medium`) instead of being silently dropped.","example":[{"name":"item_allskills","value":2},{"name":"sockets","value":1}],"type":"array","items":{"$ref":"#/components/schemas/PriceCheckStatInput"}},"corrupted":{"type":"boolean","description":"Only corrupted (`true`) or only clean (`false`) listings. Omit to compare both.","example":true},"corruption":{"type":"string","description":"Corruption outcome to match: modifier name(s) or label(s), comma separated when the corruption rolled several modifiers (e.g. `item_numsockets`, `Enhanced Maximum Damage`). Implies a corrupted listing.","example":"item_numsockets","maxLength":200},"ethereal":{"type":"boolean","description":"Ethereal (`true`) or not (`false`). Gear only.","example":false},"sockets":{"type":"number","minimum":0,"maximum":6,"description":"Socket count. Gear only; listings one socket off count as near matches.","example":1},"isLadder":{"type":"boolean","default":true,"description":"Ladder (`true`) or non-ladder (`false`). Only ladder is crawled.","example":true},"isHardcore":{"type":"boolean","default":false,"description":"Hardcore (`true`) or softcore (`false`).","example":false},"hours":{"type":"number","minimum":1,"maximum":720,"default":168,"description":"Window in hours: listings last updated within this many hours before now.","example":168}},"required":["item"]},"PriceCheckItemResponse":{"type":"object","properties":{"baseCode":{"type":"string","example":"harlequin_crest"},"name":{"type":"string","example":"Harlequin Crest"},"category":{"type":"string","description":"Catalog category (see `GET /api/catalog`).","example":"uniques"},"typeCode":{"type":"string","nullable":true,"example":"helm"},"quality":{"type":"string","nullable":true,"example":"Unique"},"iconUrl":{"type":"string","nullable":true,"example":"https://pd2trader.com/assets/items/invuapu.png"}},"required":["baseCode","name","category","typeCode","quality","iconUrl"]},"PriceEstimateResponse":{"type":"object","properties":{"low":{"type":"number","description":"25th percentile of the basis listings, in HR.","example":1.6},"mid":{"type":"number","description":"Median of the basis listings, in HR.","example":2},"high":{"type":"number","description":"75th percentile of the basis listings, in HR.","example":2.5}},"required":["low","mid","high"]},"PriceComparableResponse":{"type":"object","properties":{"priceHr":{"type":"number","description":"Listed price in HR.","example":2},"similarity":{"type":"number","minimum":0,"maximum":1,"description":"Weighted share of `appliedFilters` this listing matches (1 = matches all; near misses count partially).","example":1},"listedAt":{"type":"string","format":"date-time","description":"When the listing was last updated.","example":"2026-10-15T18:42:10.000Z"},"matched":{"description":"Tags of the applied filters this listing fully matches.","example":["sockets=1","corrupted","corruption=item_numsockets"],"type":"array","items":{"type":"string"}},"baseCode":{"type":"string","description":"baseCode of the listing (differs from `item.baseCode` for neighbouring Rainbow Facet rolls).","example":"harlequin_crest"},"corrupted":{"type":"boolean","example":true},"corruptions":{"description":"Corruption modifiers of the listing (labels).","example":["Sockets"],"type":"array","items":{"type":"string"}},"sockets":{"type":"number","example":1},"ethereal":{"type":"boolean","example":false}},"required":["priceHr","similarity","listedAt","matched","baseCode","corrupted","corruptions","sockets","ethereal"]},"PriceCheckAppliedFilter":{"type":"object","properties":{"field":{"type":"string","description":"`sockets`, `ethereal`, `corrupted`, `corruption`, `quality` or `variant.<field>` for grouped items.","example":"sockets"},"value":{"oneOf":[{"type":"number"},{"type":"string"},{"type":"boolean"},{"type":"array","items":{"type":"string"}}],"example":1},"description":{"type":"string","example":"Sockets = 1 (±1 socket counts as a near match)"}},"required":["field","value","description"]},"PriceCheckIgnoredFilter":{"type":"object","properties":{"field":{"type":"string","description":"Body path of the filter that was not applied.","example":"stats[0]"},"reason":{"type":"string","example":"to All Skills (item_allskills): stored listings don't record modifier values, so this stat can't be compared. Only sockets, ethereal, corruption, quality and the Rainbow Facet / Hellfire Torch / Spirit Ward variant are stored per listing."}},"required":["field","reason"]},"PriceCheckScopeResponse":{"type":"object","properties":{"baseCodes":{"description":"baseCodes compared (the item, plus neighbouring rolls for Rainbow Facets).","example":["harlequin_crest"],"type":"array","items":{"type":"string"}},"isLadder":{"type":"boolean","example":true},"isHardcore":{"type":"boolean","example":false},"hours":{"type":"number","example":168},"from":{"type":"string","format":"date-time","example":"2026-10-09T12:00:00.000Z"},"to":{"type":"string","format":"date-time","example":"2026-10-16T12:00:00.000Z"}},"required":["baseCodes","isLadder","isHardcore","hours","from","to"]},"PriceCheckResponse":{"type":"object","properties":{"item":{"$ref":"#/components/schemas/PriceCheckItemResponse"},"estimateHr":{"nullable":true,"description":"Price range in HR from the basis listings (outliers removed); null when nothing comparable was listed.","allOf":[{"$ref":"#/components/schemas/PriceEstimateResponse"}]},"confidence":{"type":"string","enum":["high","medium","low","none"],"description":"`high`: ≥ 10 exact matches with a tight spread ((high - low) / mid ≤ 0.6). `medium`: ≥ 5 exact matches, or near matches worth ≥ 8 exact ones. `low`: anything thinner. `none`: no estimate. Capped at `medium` when requested stats or corruptions could not be checked.","example":"medium"},"basis":{"type":"string","enum":["exact","similar","none"],"description":"`exact`: estimate from listings matching every applied filter. `similar`: fewer than 3 exact matches, so near matches (similarity ≥ 0.5) are used, weighted by similarity. `none`: no usable listing.","example":"exact"},"sampleSize":{"type":"number","description":"Listings behind the estimate (after outlier removal).","example":7},"listingsConsidered":{"type":"number","description":"Distinct listings of the compared baseCodes in the window (newest price per listing).","example":64},"outliersRemoved":{"type":"number","description":"Basis listings dropped as outliers (outside 1.5×IQR on a log scale).","example":1},"comparables":{"description":"Up to 20 most similar listings (then newest first); listings matching nothing are left out.","type":"array","items":{"$ref":"#/components/schemas/PriceComparableResponse"}},"appliedFilters":{"description":"Filters compared against stored listing data.","type":"array","items":{"$ref":"#/components/schemas/PriceCheckAppliedFilter"}},"ignoredFilters":{"description":"Requested filters that were NOT applied, with the reason (not stored, not applicable to the item, unknown).","type":"array","items":{"$ref":"#/components/schemas/PriceCheckIgnoredFilter"}},"notes":{"description":"Caveats about the estimate (mixed corrupted/clean listings, near-match basis, variant selection, …).","example":["Some requested stats or corruptions could not be checked (see ignoredFilters); the estimate does not reflect them."],"type":"array","items":{"type":"string"}},"scope":{"$ref":"#/components/schemas/PriceCheckScopeResponse"}},"required":["item","estimateHr","confidence","basis","sampleSize","listingsConsidered","outliersRemoved","comparables","appliedFilters","ignoredFilters","notes","scope"]},"PriceCheckErrorResponse":{"type":"object","properties":{"statusCode":{"type":"number","example":400},"message":{"type":"string","description":"What was wrong with the request.","example":"Unknown item 'harlequin cret'. Use GET /api/search to find its name or baseCode."},"error":{"type":"string","example":"Bad Request"}},"required":["statusCode","message","error"]},"MarketRequirementsDoc":{"type":"object","properties":{"class":{"type":"string","enum":["amazon","assassin","barbarian","druid","necromancer","paladin","sorceress"],"description":"Class-only items: narrows `item.base.type_code` to the class's item types (the site's own class filter never matches)."},"levelMin":{"type":"number"},"levelMax":{"type":"number"},"strMin":{"type":"number"},"strMax":{"type":"number"},"dexMin":{"type":"number"},"dexMax":{"type":"number"}}},"MarketStatFilterDoc":{"type":"object","properties":{"id":{"type":"string","description":"Row id (any string up to 64 chars, echoed back). Defaults to the row index.","example":"6f1c2c7e-6b38-4a43-9c7d-2f5f0b8e1a10"},"kind":{"type":"string","enum":["mod","pseudo","corruption"],"default":"mod","description":"`mod`: a PD2 property key matched against `item.modifiers` like the site. `pseudo`: a precomputed pseudo stat, one of `pseudo_total_res`, `pseudo_all_res`, `pseudo_fire_res`, `pseudo_cold_res`, `pseudo_light_res`, `pseudo_poison_res`, `pseudo_life`, `pseudo_mana`, `pseudo_total_attributes`, `pseudo_strength`, `pseudo_dexterity`, `pseudo_vitality`, `pseudo_energy`, `pseudo_all_skills`, `pseudo_skill_tab`, `pseudo_skill_damage`, `pseudo_enemy_res`, `pseudo_fcr`, `pseudo_fhr`, `pseudo_ias`, `pseudo_frw`, `pseudo_mf`, `pseudo_gf`, `pseudo_enhanced_defense`, `pseudo_enhanced_damage`, `pseudo_elemental_damage`, `pseudo_life_leech`, `pseudo_mana_leech` (`pseudo_elemental_damage`: Σ (min + max) / 2 over the fire, cold and lightning \"Adds X-Y\" lines; poison and magic not counted). `corruption`: a cube corruption name (`item.corruptions`, e.g. `item_fastercastrate`, `item_numsockets`, `dmg%`); without bounds the item must have the corruption, with `min` / `max` its roll (`values[0]` of the `corrupted: true` modifier line) must be in range. `item_numsockets`, `dmg%`, `dmg-max`, `transform_dye` and `indestruct` have no such line: bounds on them are a 400."},"key":{"type":"string","description":"Property key: `name` or `a|b` alternatives, then optionally `{s0,s1,…}` constraining `values[0..3]` of the same line (each slot `N`, `N|M` any of, `N..M` / `N..` / `..M` a range, or `*` anything) and `[i]` (0–4), the value min/max, score and stat sort read (default: the value after the slots; `item_elemskill_*` without slots: `values[1]`). E.g. `item_singleskill{54}`, `item_skillongethit{*,44}[0]`, `item_aura{98|123}`, `item_splashonhit[2]`, `firedam|firemindam{4..}[1]`. `damage` / `damage{s0}` (no `a|b`, no `[i]`) is flat physical damage (\"Adds X-Y Damage\"), one stat over PD2's min-damage and max-damage lines: the slot pins min damage, max damage is rolled (min/max, score, stat sort), and the item needs both lines; e.g. `damage{4..}` with `min: 14` = adds at least 4-14 damage. For other kinds: a pseudo stat id or corruption name.","example":"item_fastercastrate"},"min":{"type":"number","description":"Minimum roll (inclusive). Without `min` and `max` an enabled row requires the item to have the stat (pseudo: value > 0).","example":20},"max":{"type":"number","description":"Maximum roll (inclusive)."},"weight":{"type":"number","minimum":-10000,"maximum":10000,"description":"Score weight: `score = Σ weight × roll` (0 when the item lacks the stat). Only scores; it never changes whether the row filters.","example":1},"enabled":{"type":"boolean","default":true,"description":"Disabled rows neither filter nor score."},"or":{"type":"number","minimum":1,"maximum":5,"description":"Any-of block (integer 1–5): the enabled rows of one block together match when at least one of them does; blocks are ANDed with each other and with the required rows (neither `or` nor `not`). Legacy `true` = block 1, `false` = none; responses return the number.","example":1},"not":{"type":"boolean","default":false,"description":"NOT group: listings matching any enabled `not` row (roll within min/max, or the stat present) are excluded. Not combinable with `or` on the same row (400)."}},"required":["key"]},"MarketSortDoc":{"type":"object","properties":{"by":{"type":"string","enum":["bumped","price","score","value","stat"],"default":"bumped","description":"`bumped`: `bumped_at`. `price`: effective HR price (`hr_price`, else the HR the price note states, e.g. \"75 wss\"; listings with neither excluded). `score` / `value`: weighted score / score per HR over every match; unscored (unvalued) listings last, newest bump breaks ties. `stat`: the value of `key` (largest matching modifier roll, or the pseudo value); listings without it last, newest bump breaks ties."},"dir":{"type":"string","enum":["asc","desc"],"default":"desc"},"key":{"type":"string","maxLength":300,"description":"Required with `by: \"stat\"`: a property key in the stat-row grammar (`item_fastercastrate`, `item_singleskill{54}`, `max_damage|maxdamage`, `item_skillongethit{*,44}[0]`, `damage{4..}` by its max-damage roll) or a pseudo stat id (`pseudo_total_res`). Ignored by other sorts.","example":"item_fastercastrate"}}},"MarketFiltersDoc":{"type":"object","properties":{"source":{"type":"string","enum":["live","archive"],"default":"live","description":"`live`: active listings (aged by `updated_at`). `archive`: sold listings (aged by when we saw them sell)."},"hardcore":{"type":"boolean","example":false},"ladder":{"type":"boolean","example":true},"itemName":{"type":"string","maxLength":100,"description":"Exact `item.name` (unique, set, runeword or base name). `Small/Large/Grand Charm`, `Amulet`, `Ring` and `Jewel` match as a case-insensitive substring, like the site.","example":"Harlequin Crest"},"priceText":{"type":"string","maxLength":100,"description":"Case-insensitive literal substring of the price note."},"hrMin":{"type":"number","minimum":0,"description":"Minimum effective HR price: `hr_price`, else the HR the price note states (e.g. \"75 wss\"). With `hasBuyout: yes` only listings with an `hr_price` match."},"hrMax":{"type":"number","minimum":0,"description":"Maximum effective HR price (as for `hrMin`)."},"hrMaxExclusive":{"type":"boolean","default":false,"description":"When true `hrMax` is exclusive (`< hrMax` instead of `<= hrMax`), so `hrMin = from`, `hrMax = to` selects exactly a `[from, to)` price facet bucket."},"typeCode":{"type":"string","description":"`item.base.type_code`, the site's grouped value `{\"$in\": [\"amul\", \"ring\", \"amus\"]}` (only `$in` of codes), or a type group (this server only): `group:bows` (bow, abow), `group:spears` (spea, aspe), `group:javelins` (jave, ajav), `group:helms` (helm, phlm, pelt, circ), `group:shields` (shie, ashd, head). The Class requirement narrows a group like any type.","example":"circ"},"baseCode":{"type":"string","description":"`item.base_code`, or a grouped `{\"$in\": [...]}` value.","example":"ci3"},"rarity":{"type":"array","description":"Any of these qualities (empty = any). `Runeword` matches `item.is_runeword`.","items":{"type":"string","enum":["Normal","Superior","Magic","Rare","Crafted","Set","Unique","Runeword"]}},"ethereal":{"type":"string","enum":["any","yes","no"],"default":"any"},"corrupted":{"type":"string","enum":["any","yes","no"],"default":"any"},"desecrated":{"type":"string","enum":["any","yes","no"],"default":"any"},"identified":{"type":"string","enum":["any","yes","no"],"default":"any"},"socketsMin":{"type":"number","minimum":0,"maximum":6},"socketsMax":{"type":"number","minimum":0,"maximum":6,"description":"`0` means unsocketed (`item.is_socketed: false`), like the site."},"userOnlineSite":{"type":"string","enum":["all","online","offline"],"default":"all","description":"Seller seen on the website within the last 5 minutes, checked live with PD2 when you search (see `totalExact`)."},"userOnlineGame":{"type":"string","enum":["all","online","offline"],"default":"all","description":"Seller seen in game within the last 5 minutes, checked live like `userOnlineSite`."},"hasBuyout":{"type":"string","enum":["any","yes","no"],"default":"any","description":"`yes`: an HR price is set (`hr_price > 0`); `no`: none."},"sellerUserId":{"type":"string","description":"Seller PD2 user id (`user_id`). Wins over `sellerName`."},"sellerName":{"type":"string","maxLength":64,"description":"Seller name, matched case-insensitively and exactly against the seller's website username, game accounts and the item's `account_id`. Ignored when `sellerUserId` is set.","example":"CIutch"},"requirements":{"$ref":"#/components/schemas/MarketRequirementsDoc"},"stats":{"maxItems":30,"description":"Property, pseudo stat and corruption rows. Every enabled row with neither `or` nor `not` filters (the roll within min/max, or (no bounds) the stat present); each `or` block (1–5) matches when at least one of its rows does; listings matching any `not` row are excluded.","type":"array","items":{"$ref":"#/components/schemas/MarketStatFilterDoc"}},"corruptions":{"maxItems":20,"deprecated":true,"description":"Legacy: cube corruption names the item must all have (`item.corruptions`), read as `corruption` stat rows without bounds. Use `stats` rows of kind `corruption` instead.","example":["item_fastercastrate"],"type":"array","items":{"type":"string"}},"maxAgeDays":{"type":"number","maximum":365,"description":"Age window in days (> 0). Default 3 for live, 14 for archive."},"status":{"type":"array","maxItems":4,"description":"History (opt-in): search these statuses instead of the one `source` selects (`live` = active, `archive` = sold). Each is aged by its own time within `maxAgeDays`: `active` by `updated_at` (and without an accepted offer), `sold` by when it sold, `removed` / `expired` by when it ended. Ended listings are kept.","example":["sold","removed","expired"],"items":{"type":"string","enum":["active","sold","removed","expired"]}},"sort":{"$ref":"#/components/schemas/MarketSortDoc"}},"required":["hardcore","ladder"]},"LiveSearchEntryDto":{"type":"object","properties":{"q":{"type":"string","description":"The search exactly as the client sent it (base64url filters JSON)."},"filters":{"description":"The decoded `q` with defaults filled: the canonical form of the search.","allOf":[{"$ref":"#/components/schemas/MarketFiltersDoc"}]}},"required":["q","filters"]},"LiveSearchHelloEventDto":{"type":"object","properties":{"serverTime":{"type":"string","format":"date-time","description":"Server time when the stream opened.","example":"2026-11-10T08:14:00.000Z"},"events":{"example":["market.match"],"type":"array","items":{"type":"string"}},"heartbeatSeconds":{"type":"number","description":"Seconds between heartbeat comments.","example":25},"searches":{"description":"The stream's searches in request order (one on `GET /api/market/search/{q}/live`).","type":"array","items":{"$ref":"#/components/schemas/LiveSearchEntryDto"}},"filters":{"description":"Single-search stream (`GET /api/market/search/{q}/live`) only: the decoded `q` with defaults filled, the canonical form of the search.","allOf":[{"$ref":"#/components/schemas/MarketFiltersDoc"}]}},"required":["serverTime","events","heartbeatSeconds","searches"]},"MarketMatchEventDto":{"type":"object","properties":{"id":{"type":"string","description":"PD2 listing id; `GET /api/market/listing/{id}` returns the full listing.","example":"6716a0f2b3c4d5e6f7a8b9c0"},"isLadder":{"type":"boolean","example":true},"isHardcore":{"type":"boolean","example":false},"name":{"type":"string","nullable":true,"description":"Item name (unique/set/runeword or base name).","example":"Harlequin Crest"},"baseName":{"type":"string","nullable":true,"example":"Shako"},"baseCode":{"type":"string","nullable":true,"example":"uap"},"typeCode":{"type":"string","nullable":true,"example":"helm"},"quality":{"type":"string","nullable":true,"example":"Unique"},"price":{"type":"string","nullable":true,"description":"Seller's price text as listed.","example":"2 ist"},"hrPrice":{"type":"number","nullable":true,"description":"Price in HR; null when unpriced.","example":0.5},"bumpedAt":{"type":"string","nullable":true,"format":"date-time","example":"2026-10-21T08:15:02.000Z"},"seller":{"type":"string","nullable":true,"description":"Seller username.","example":"Gheed"},"status":{"type":"string","enum":["active","sold","removed","expired"],"example":"active"},"soldHr":{"type":"number","nullable":true,"description":"HR the listing sold for (`sold` only).","example":null},"q":{"type":"string","description":"The `q` the listing matched, exactly as the client sent it. On a multi-search stream a listing matching two of its searches is sent once per search."},"matchedAt":{"type":"string","format":"date-time","description":"When the server matched the listing (about 5 s after PD2 listed or bumped it).","example":"2026-11-10T08:15:03.120Z"}},"required":["id","isLadder","isHardcore","name","baseName","baseCode","typeCode","quality","price","hrPrice","bumpedAt","seller","status","soldHr","q","matchedAt"]},"MarketSearchRequestDoc":{"type":"object","properties":{"filters":{"$ref":"#/components/schemas/MarketFiltersDoc"},"page":{"type":"number","minimum":0,"default":0,"description":"0-based page; `page × limit` at most 10000."},"limit":{"type":"number","minimum":1,"maximum":100,"default":25},"facets":{"type":"array","description":"Facet counts to return with the hits (`facets` of the response), computed in the same OpenSearch request. A facet on a dimension the filters select on is counted without that dimension's own filter (so it lists the alternatives); the others within the results. Seller online filters are not applied to facet counts.","example":["quality","sockets","price"],"items":{"type":"string","enum":["quality","baseCode","typeCode","sockets","ethereal","corrupted","runeword","status","price"]}}},"required":["filters"]},"MarketSearchItemDoc":{"type":"object","properties":{"listing":{"type":"object","additionalProperties":true,"description":"The PD2 `market/listing` document as ingested (`_id`, `user_id`, `type`, `is_hardcore`, `is_ladder`, `item` with `modifiers`, `price`, `hr_price`, `bumped_at`, `created_at`, `updated_at`, `user {username, in_game_account}`, …). Seller presence: `user_last_online` / `user_last_ingame` are the seller's last-seen times (stored at ingest; live from PD2 for the Seller online filters and the single-listing route) and `online` / `in_game` are true when they are within 5 minutes. Sold listings add `sell_price` / `sell_hr_price` when known."},"pseudo":{"type":"object","additionalProperties":{"type":"number"},"description":"Every pseudo stat id → value (0 when the item has none of its inputs).","example":{"pseudo_total_res":120,"pseudo_fcr":20}},"score":{"type":"number","nullable":true,"description":"Σ weight × roll; null without weighted rows."},"value":{"type":"number","nullable":true,"description":"score / hr_price; null without a score or an HR price."},"status":{"type":"string","enum":["active","sold","removed","expired"],"description":"Listing status (only `active` / `sold` unless `filters.status` asks for history)."},"listedAt":{"type":"string","format":"date-time","nullable":true,"description":"When it was listed: PD2's `created_at`, else when we first stored it; null when unknown."},"endedAt":{"type":"string","format":"date-time","nullable":true,"description":"When it ended (`sold`: sale seen, `removed`, `expired`); null while active or when unknown."},"durationSeconds":{"type":"number","nullable":true,"description":"How long it has been up, in seconds: `listedAt` → `endedAt` (→ now while active); null without both ends.","example":86400}},"required":["listing","pseudo","score","value","status","listedAt","endedAt","durationSeconds"]},"RarityFacetDoc":{"type":"object","properties":{"count":{"type":"number","example":42,"description":"Listings with this value."},"value":{"type":"string","enum":["Normal","Superior","Magic","Rare","Crafted","Set","Unique","Runeword"],"example":"Unique"}},"required":["count","value"]},"CodeFacetDoc":{"type":"object","properties":{"count":{"type":"number","example":42,"description":"Listings with this value."},"value":{"type":"string","example":"uap"}},"required":["count","value"]},"SocketFacetDoc":{"type":"object","properties":{"count":{"type":"number","example":42,"description":"Listings with this value."},"value":{"type":"number","minimum":0,"maximum":6,"example":2}},"required":["count","value"]},"FlagFacetDoc":{"type":"object","properties":{"count":{"type":"number","example":42,"description":"Listings with this value."},"value":{"type":"boolean","example":true}},"required":["count","value"]},"StatusFacetDoc":{"type":"object","properties":{"count":{"type":"number","example":42,"description":"Listings with this value."},"value":{"type":"string","enum":["active","sold","removed","expired"],"example":"sold"}},"required":["count","value"]},"PriceFacetDoc":{"type":"object","properties":{"count":{"type":"number","example":42,"description":"Listings with this value."},"from":{"type":"number","example":0.5,"description":"Lowest effective HR price of the bucket (inclusive)."},"to":{"type":"number","nullable":true,"example":1,"description":"Upper bound (exclusive); null for the last, open-ended bucket."}},"required":["count","from","to"]},"MarketFacetsDoc":{"type":"object","properties":{"quality":{"description":"Every rarity, as `rarity: [value]` selects it (runewords apart from Normal / Superior). Filtered on by the search: counted with every other filter but its own (lists the alternatives); otherwise counted within the results.","type":"array","items":{"$ref":"#/components/schemas/RarityFacetDoc"}},"baseCode":{"description":"The 20 most listed item bases (`item.base_code`), most first. Filtered on by the search: counted with every other filter but its own (lists the alternatives); otherwise counted within the results.","type":"array","items":{"$ref":"#/components/schemas/CodeFacetDoc"}},"typeCode":{"description":"The 50 most listed item types (`item.base.type_code`), most first. Filtered on by the search: counted with every other filter but its own (lists the alternatives); otherwise counted within the results.","type":"array","items":{"$ref":"#/components/schemas/CodeFacetDoc"}},"sockets":{"description":"Socket counts 0-6, as `socketsMin = socketsMax = value` selects them (0 = unsocketed). Filtered on by the search: counted with every other filter but its own (lists the alternatives); otherwise counted within the results.","type":"array","items":{"$ref":"#/components/schemas/SocketFacetDoc"}},"ethereal":{"description":"`true` / `false` (`ethereal: yes / no`). Filtered on by the search: counted with every other filter but its own (lists the alternatives); otherwise counted within the results.","type":"array","items":{"$ref":"#/components/schemas/FlagFacetDoc"}},"corrupted":{"description":"`true` / `false` (`corrupted: yes / no`). Filtered on by the search: counted with every other filter but its own (lists the alternatives); otherwise counted within the results.","type":"array","items":{"$ref":"#/components/schemas/FlagFacetDoc"}},"runeword":{"description":"Runewords (`true`) and other items (`false`); shares the `rarity` filter with `quality`. Filtered on by the search: counted with every other filter but its own (lists the alternatives); otherwise counted within the results.","type":"array","items":{"$ref":"#/components/schemas/FlagFacetDoc"}},"status":{"description":"Every status, each in its own age window (as `status: [value]` selects it), with every other filter; only when `filters.status` is set.","type":"array","items":{"$ref":"#/components/schemas/StatusFacetDoc"}},"price":{"description":"Effective HR price histogram (`priceHr`), buckets from 0, 0.05, 0.1, 0.25, 0.5, 1, 2, 3, 5, 10, 20 HR; listings without a price are not counted. Filtered on by the search: counted with every other filter but its own (lists the alternatives); otherwise counted within the results.","type":"array","items":{"$ref":"#/components/schemas/PriceFacetDoc"}}}},"MarketSearchResponseDoc":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/MarketSearchItemDoc"}},"total":{"type":"number","description":"Listings matching the filters, all pages. When `totalExact` is false it is an estimate (candidates × the seller online match rate of the listings read) or the cap (100,000: more match).","example":312},"totalExact":{"type":"boolean","description":"False when a Seller online filter (`userOnlineSite` / `userOnlineGame`) is set and not every candidate was checked: presence is live, so filtered searches read at most 2,000 listings in sort order; deeper pages come back short. Also false when more than 100,000 listings match.","example":true},"page":{"type":"number","example":0},"limit":{"type":"number","example":25},"mode":{"type":"string","enum":["server"],"description":"Every filter and sort ran over the full set."},"facets":{"description":"The requested `facets` (absent when the request asked for none).","allOf":[{"$ref":"#/components/schemas/MarketFacetsDoc"}]}},"required":["items","total","totalExact","page","limit","mode"]},"MarketListingDetailDoc":{"type":"object","properties":{"listing":{"type":"object","additionalProperties":true,"description":"The PD2 `market/listing` document as ingested (`_id`, `user_id`, `type`, `is_hardcore`, `is_ladder`, `item` with `modifiers`, `price`, `hr_price`, `bumped_at`, `created_at`, `updated_at`, `user {username, in_game_account}`, …). Seller presence: `user_last_online` / `user_last_ingame` are the seller's last-seen times (stored at ingest; live from PD2 for the Seller online filters and the single-listing route) and `online` / `in_game` are true when they are within 5 minutes. Sold listings add `sell_price` / `sell_hr_price` when known."},"pseudo":{"type":"object","additionalProperties":{"type":"number"}},"status":{"type":"string","enum":["active","sold","removed","expired"],"description":"`active`: on the market. `sold`: an offer was accepted. `removed`: deleted by the seller or a moderator. `expired`: not updated for 6 days (PD2 drops such listings)."}},"required":["listing","pseudo","status"]},"SavedSearchErrorDto":{"type":"object","properties":{"statusCode":{"type":"number","example":400},"message":{"type":"string","example":"name must be at most 80 characters"}},"required":["statusCode","message"]},"SavedSearchDto":{"type":"object","properties":{"id":{"type":"string","example":"6716a0f2b3c4d5e6f7a8b9c1"},"name":{"type":"string","maxLength":80,"example":"Shako · ≤ 1 HR · SC Ladder"},"filters":{"description":"Market browser filters, same shape and validation as `filters` of `POST /api/market/search`. Alerts ignore `maxAgeDays` and `sort`, and need `source: \"live\"`.","allOf":[{"$ref":"#/components/schemas/MarketFiltersDoc"}]},"alert":{"type":"boolean","description":"Push new matching listings live (`GET /api/market/saved-searches/stream`). Deprecated (still works): Use `GET /api/market/search/{q}/live` (see Live search).","deprecated":true,"example":true},"createdAt":{"type":"string","format":"date-time","example":"2026-10-21T08:00:00.000Z"},"updatedAt":{"type":"string","format":"date-time","example":"2026-10-21T08:00:00.000Z"}},"required":["id","name","filters","alert","createdAt","updatedAt"]},"SavedSearchLimitsDto":{"type":"object","properties":{"maxSearches":{"type":"number","example":25},"maxAlerting":{"type":"number","example":10}},"required":["maxSearches","maxAlerting"]},"SavedSearchListResponseDto":{"type":"object","properties":{"items":{"description":"Newest first.","type":"array","items":{"$ref":"#/components/schemas/SavedSearchDto"}},"limits":{"$ref":"#/components/schemas/SavedSearchLimitsDto"}},"required":["items","limits"]},"CreateSavedSearchBodyDto":{"type":"object","properties":{"name":{"type":"string","maxLength":80,"description":"Defaults to a name derived from the filters (item, stat count, price cap, mode).","example":"Cheap Shako"},"filters":{"description":"Market browser filters, same shape and validation as `filters` of `POST /api/market/search`. Alerts ignore `maxAgeDays` and `sort`, and need `source: \"live\"`.","allOf":[{"$ref":"#/components/schemas/MarketFiltersDoc"}]},"alert":{"type":"boolean","default":false,"description":"`true` = watch: push new matching listings live. Deprecated (still works): Use `GET /api/market/search/{q}/live` (see Live search).","deprecated":true,"example":true}},"required":["filters"]},"MarketListingSummaryDto":{"type":"object","properties":{"id":{"type":"string","description":"PD2 listing id; `GET /api/market/listing/{id}` returns the full listing.","example":"6716a0f2b3c4d5e6f7a8b9c0"},"isLadder":{"type":"boolean","example":true},"isHardcore":{"type":"boolean","example":false},"name":{"type":"string","nullable":true,"description":"Item name (unique/set/runeword or base name).","example":"Harlequin Crest"},"baseName":{"type":"string","nullable":true,"example":"Shako"},"baseCode":{"type":"string","nullable":true,"example":"uap"},"typeCode":{"type":"string","nullable":true,"example":"helm"},"quality":{"type":"string","nullable":true,"example":"Unique"},"price":{"type":"string","nullable":true,"description":"Seller's price text as listed.","example":"2 ist"},"hrPrice":{"type":"number","nullable":true,"description":"Price in HR; null when unpriced.","example":0.5},"bumpedAt":{"type":"string","nullable":true,"format":"date-time","example":"2026-10-21T08:15:02.000Z"},"seller":{"type":"string","nullable":true,"description":"Seller username.","example":"Gheed"},"status":{"type":"string","enum":["active","sold","removed","expired"],"example":"active"},"soldHr":{"type":"number","nullable":true,"description":"HR the listing sold for (`sold` only).","example":null}},"required":["id","isLadder","isHardcore","name","baseName","baseCode","typeCode","quality","price","hrPrice","bumpedAt","seller","status","soldHr"]},"MarketAlertDto":{"type":"object","properties":{"id":{"type":"string","description":"Alert id (unique per saved search + listing).","example":"6716a1c0b3c4d5e6f7a8b9c2"},"savedSearchId":{"type":"string","example":"6716a0f2b3c4d5e6f7a8b9c1"},"savedSearchName":{"type":"string","description":"Saved-search name at match time.","example":"Cheap Shako"},"listingId":{"type":"string","description":"PD2 listing id (same as `listing.id`).","example":"6716a0f2b3c4d5e6f7a8b9c0"},"listing":{"$ref":"#/components/schemas/MarketListingSummaryDto"},"matchedAt":{"type":"string","format":"date-time","example":"2026-10-21T08:15:03.120Z"}},"required":["id","savedSearchId","savedSearchName","listingId","listing","matchedAt"]},"MarketAlertListResponseDto":{"type":"object","properties":{"items":{"description":"Newest first.","type":"array","items":{"$ref":"#/components/schemas/MarketAlertDto"}}},"required":["items"]},"UpdateSavedSearchBodyDto":{"type":"object","properties":{"name":{"type":"string","maxLength":80,"example":"Cheap Shako"},"filters":{"description":"Market browser filters, same shape and validation as `filters` of `POST /api/market/search`. Alerts ignore `maxAgeDays` and `sort`, and need `source: \"live\"`.","allOf":[{"$ref":"#/components/schemas/MarketFiltersDoc"}]},"alert":{"type":"boolean","description":"Deprecated (still works): Use `GET /api/market/search/{q}/live` (see Live search).","deprecated":true,"example":false}}},"DeleteSavedSearchResponseDto":{"type":"object","properties":{"deleted":{"type":"boolean","example":true},"id":{"type":"string","example":"6716a0f2b3c4d5e6f7a8b9c1"}},"required":["deleted","id"]},"MarketAlertsHelloEventDto":{"type":"object","properties":{"serverTime":{"type":"string","format":"date-time","example":"2026-10-21T08:14:00.000Z"},"events":{"example":["market.alert"],"type":"array","items":{"type":"string"}},"heartbeatSeconds":{"type":"number","example":25}},"required":["serverTime","events","heartbeatSeconds"]},"LiveListingDoc":{"type":"object","properties":{"listingId":{"type":"string","description":"PD2 listing id.","example":"6ad07e61967978352c30f962"},"url":{"type":"string","description":"Listing page on projectdiablo2.com.","example":"https://www.projectdiablo2.com/market/listing/6ad07e61967978352c30f962"},"price":{"type":"string","nullable":true,"description":"Seller's price note as written, or null.","example":"5 hrs"},"priceHr":{"type":"number","nullable":true,"description":"Ask in HR: PD2's `hr_price`, else parsed from the note; null when neither gives a price.","example":5},"ethereal":{"type":"boolean","example":false},"sockets":{"type":"number","example":0},"corrupted":{"type":"boolean","example":true},"corruptions":{"description":"Corruption modifiers (labels).","example":["Enhanced Damage"],"type":"array","items":{"type":"string"}},"desecrated":{"type":"boolean","example":false},"itemLevel":{"type":"number","nullable":true,"example":96},"stats":{"description":"Rolled stat lines as PD2 renders them (corrupted/desecrated flags excluded).","example":["+2 to All Skills","+148 to Life"],"type":"array","items":{"type":"string"}},"listedAt":{"type":"string","format":"date-time","nullable":true,"description":"Listing creation time."},"bumpedAt":{"type":"string","format":"date-time","nullable":true,"description":"Last bump."}},"required":["listingId","url","price","priceHr","ethereal","sockets","corrupted","corruptions","desecrated","itemLevel","stats","listedAt","bumpedAt"]},"LiveListingsDoc":{"type":"object","properties":{"baseCode":{"type":"string","example":"harlequin_crest"},"name":{"type":"string","example":"Harlequin Crest"},"isLadder":{"type":"boolean","example":true},"isHardcore":{"type":"boolean","example":false},"fetchedAt":{"type":"string","format":"date-time","description":"When the server fetched these listings from PD2 (cached up to 60 s)."},"pd2Total":{"type":"number","description":"Active listings PD2 reported for the query, before matching them to this baseCode.","example":20},"truncated":{"type":"boolean","description":"PD2 had more matching listings than the 100 fetched.","example":false},"listings":{"description":"Cheapest first; unparseable asks last.","type":"array","items":{"$ref":"#/components/schemas/LiveListingDoc"}}},"required":["baseCode","name","isLadder","isHardcore","fetchedAt","pd2Total","truncated","listings"]},"MarketWindowResponse":{"type":"object","properties":{"start":{"type":"string","description":"Window start (inclusive), UTC midnight `days` days before `end`.","example":"2026-10-09T00:00:00.000Z"},"end":{"type":"string","description":"Window end (exclusive), UTC midnight of the `dataAsOf` day. The day still being ingested is excluded, like the price endpoints.","example":"2026-10-16T00:00:00.000Z"},"dataAsOf":{"type":"string","description":"Newest item-price write of the market feed ingest for this mode (the request time if the mode has no rows). Listings updated after this are not ingested yet.","example":"2026-10-16T04:00:00.000Z"}},"required":["start","end","dataAsOf"]},"MarketAskStatsResponse":{"type":"object","properties":{"count":{"type":"number","description":"Listings in this set (one ask per listing).","example":412},"lowest":{"type":"number","description":"Lowest ask in HR. Raw, no outlier trimming: it can be a mispriced or mis-parsed listing, so prefer `p25` as a robust floor.","example":0.82},"p25":{"type":"number","description":"25th percentile ask in HR (linear interpolation).","example":0.95},"median":{"type":"number","description":"Median ask in HR.","example":1},"p75":{"type":"number","description":"75th percentile ask in HR.","example":1.1},"highest":{"type":"number","description":"Highest ask in HR (raw, untrimmed).","example":3},"spreadHr":{"type":"number","description":"Ask spread: interquartile range `p75 − p25` in HR. The market has no bids, so this measures how far sellers disagree, not a bid/ask gap.","example":0.15},"spreadPct":{"type":"number","nullable":true,"description":"`spreadHr / median × 100`, one decimal; null when the median is 0.","example":15}},"required":["count","lowest","p25","median","p75","highest","spreadHr","spreadPct"]},"MarketNewListingsDayResponse":{"type":"object","properties":{"date":{"type":"string","description":"UTC day, YYYY-MM-DD.","example":"2026-10-15"},"count":{"type":"number","description":"Listings first seen on that day (see `newListings`).","example":31}},"required":["date","count"]},"MarketOmittedMetricResponse":{"type":"object","properties":{"metric":{"type":"string","enum":["timeToSell","sellThroughRate","bidAskSpread"],"example":"timeToSell"},"reason":{"type":"string","description":"Why this endpoint does not provide this metric.","example":"Not computed by this endpoint: depth is built from stored price revisions (listing id, HR price, updated_at) only, so a listing that stops changing is indistinguishable from one that sold, expired, was deleted, or is still open."}},"required":["metric","reason"]},"MarketDepthResponse":{"type":"object","properties":{"baseCode":{"type":"string","description":"Canonical base code from the catalog.","example":"r30"},"name":{"type":"string","example":"Ber Rune"},"isLadder":{"type":"boolean","example":true},"isHardcore":{"type":"boolean","example":false},"days":{"type":"number","enum":[7,30],"description":"Window length in complete UTC days.","example":7},"window":{"$ref":"#/components/schemas/MarketWindowResponse"},"sampleSize":{"type":"number","description":"Number of distinct listings the window metrics are based on (equals `listingsObserved`).","example":412},"confidence":{"type":"string","enum":["high","medium","low","none"],"description":"From `sampleSize`: `high` ≥ 30 listings, `medium` 10–29, `low` 1–9, `none` 0. Below 10 the economy views hide an item as low-confidence.","example":"high"},"listingsObserved":{"type":"number","description":"Distinct listings (PD2 listing ids) with at least one stored revision (a change PD2 reported through the listing’s `updated_at`) inside the window. Includes listings that have since sold, expired or been removed.","example":412},"asks":{"nullable":true,"description":"Ask distribution over `listingsObserved`, using each listing’s last price inside the window. Null when no listing was observed.","allOf":[{"$ref":"#/components/schemas/MarketAskStatsResponse"}]},"activeListings":{"type":"number","description":"Proxy for listings currently on the market: distinct listings whose latest stored revision is at most `activeWindowHours` older than `window.dataAsOf`. The data has no open/sold/expired status, so this over-counts listings that sold or were removed since their last update, and under-counts listings still open but untouched for longer than that.","example":137},"activeWindowHours":{"type":"number","description":"Recency threshold for `activeListings`: a listing counts as active when its latest stored revision (one per PD2 update) is at most this old.","example":48},"activeAsks":{"nullable":true,"description":"Ask distribution over `activeListings`, using each listing’s latest price. Null when there are none.","allOf":[{"$ref":"#/components/schemas/MarketAskStatsResponse"}]},"newListings":{"type":"number","description":"Observed listings first seen inside the window: their earliest stored revision is in the window and they have no revision before it. `created_at` is not stored, so a listing edited before the ingest first saw it is dated by that edit. Right after a season or ingest start every listing looks new.","example":198},"newListingsPerDay":{"description":"`newListings` per UTC day, oldest first, one entry per window day (zeros included).","type":"array","items":{"$ref":"#/components/schemas/MarketNewListingsDayResponse"}},"omitted":{"description":"Metrics deliberately not returned because the stored data cannot support them (time-to-sell, sell-through rate, bid/ask spread), each with the reason.","type":"array","items":{"$ref":"#/components/schemas/MarketOmittedMetricResponse"}}},"required":["baseCode","name","isLadder","isHardcore","days","window","sampleSize","confidence","listingsObserved","asks","activeListings","activeWindowHours","activeAsks","newListings","newListingsPerDay","omitted"]},"EconomyIndexBaseDto":{"type":"object","properties":{"day":{"type":"string","description":"Season start, YYYY-MM-DD (UTC): the base the index is measured against.","example":"2026-04-23"},"value":{"type":"number","description":"Index value at the base (always 100).","example":100},"final":{"type":"boolean","description":"False during the first days of a season, while the base window is still open: base prices (and so every value) can still move. True once the base is frozen for the season.","example":true}},"required":["day","value","final"]},"EconomyIndexPointDto":{"type":"object","properties":{"day":{"type":"string","description":"Day, YYYY-MM-DD (UTC).","example":"2026-10-14"},"value":{"type":"number","description":"Index value; 100 = season base. 62.4 means the basket costs 37.6% less HR than at season start.","example":62.41},"change1d":{"type":"number","nullable":true,"description":"Percent change from the previous day; null on the first day of the season.","example":-1.18}},"required":["day","value","change1d"]},"EconomyIndexComponentDto":{"type":"object","properties":{"baseCode":{"type":"string","example":"r30"},"name":{"type":"string","example":"Ber Rune"},"weight":{"type":"number","description":"Share of the basket's HR value at base prices (weights sum to 1).","example":0.125},"quantity":{"type":"number","description":"Fixed units held per 1 HR of basket at base prices (weight / basePriceHr).","example":0.0417},"basePriceHr":{"type":"number","description":"Base price in HR (median daily price over the first days of the season).","example":3},"priceHr":{"type":"number","description":"Price in HR on the `latest` day (carried forward from the last day with listings).","example":1.85}},"required":["baseCode","name","weight","quantity","basePriceHr","priceHr"]},"EconomyIndexResponse":{"type":"object","properties":{"basket":{"type":"string","enum":["high-runes","mid-runes","endgame","all"],"example":"high-runes"},"seasonId":{"type":"string","example":"s14"},"isLadder":{"type":"boolean","example":true},"isHardcore":{"type":"boolean","example":false},"base":{"$ref":"#/components/schemas/EconomyIndexBaseDto"},"points":{"description":"Daily values in the requested range, oldest first.","type":"array","items":{"$ref":"#/components/schemas/EconomyIndexPointDto"}},"latest":{"nullable":true,"description":"Newest point in the requested range; null when the range has no points.","allOf":[{"$ref":"#/components/schemas/EconomyIndexPointDto"}]},"change7d":{"type":"number","nullable":true,"description":"Percent change of `latest` versus 7 days earlier; null when that day is not indexed.","example":-4.73},"components":{"description":"Components priced this season (items with no listing in the base window are left out).","type":"array","items":{"$ref":"#/components/schemas/EconomyIndexComponentDto"}}},"required":["basket","seasonId","isLadder","isHardcore","base","points","latest","change7d","components"]},"IndexModeDto":{"type":"object","properties":{"isLadder":{"type":"boolean","example":true},"isHardcore":{"type":"boolean","example":false}},"required":["isLadder","isHardcore"]},"IndexBasketComponentDto":{"type":"object","properties":{"baseCode":{"type":"string","example":"r30"},"name":{"type":"string","example":"Ber Rune"},"weight":{"type":"number","description":"Defined share of the basket value; renormalised per season over the components that have a base price.","example":0.125}},"required":["baseCode","name","weight"]},"IndexBasketDto":{"type":"object","properties":{"id":{"type":"string","enum":["high-runes","mid-runes","endgame","all"],"example":"high-runes"},"name":{"type":"string","example":"High runes"},"description":{"type":"string","example":"Vex through Zod (r26–r33), equal HR value per rune at season start. Tracks the price of the high runes in HR."},"components":{"type":"array","items":{"$ref":"#/components/schemas/IndexBasketComponentDto"}}},"required":["id","name","description","components"]},"IndexBasketsResponse":{"type":"object","properties":{"method":{"type":"string","description":"How the index is computed.","example":"Fixed-quantity price index, rebased every season. Base price of each component = median of its daily prices over the first 3 days of the season …"},"baseWindowDays":{"type":"number","description":"Days at the start of a season whose prices set the base.","example":3},"modes":{"description":"Modes the index is computed for (only ladder is ingested).","type":"array","items":{"$ref":"#/components/schemas/IndexModeDto"}},"baskets":{"type":"array","items":{"$ref":"#/components/schemas/IndexBasketDto"}}},"required":["method","baseWindowDays","modes","baskets"]},"AlertErrorDto":{"type":"object","properties":{"statusCode":{"type":"number","example":401},"message":{"type":"string","example":"API key required"}},"required":["statusCode","message"]},"AlertItemDto":{"type":"object","properties":{"baseCode":{"type":"string","description":"Catalog base code.","example":"r30"},"name":{"type":"string","example":"Ber Rune"},"iconUrl":{"type":"string","nullable":true,"example":"https://pd2trader.com/assets/runes/Ber_Rune.png"}},"required":["baseCode","name","iconUrl"]},"RareStatRequirementViewDto":{"type":"object","properties":{"key":{"type":"string","description":"Feature key of a key stat of the slot (`slots[slot].stats[].key` in `GET /api/rares/guide`).","example":"item_fastercastrate"},"min":{"type":"number","minimum":0,"description":"Minimum value (inclusive).","example":20},"label":{"type":"string","description":"The requirement as in-game text at `min`.","example":"20% Faster Cast Rate"}},"required":["key","min","label"]},"RareCriteriaDto":{"type":"object","properties":{"slot":{"type":"string","example":"ring"},"slotLabel":{"type":"string","example":"Ring"},"stats":{"type":"array","items":{"$ref":"#/components/schemas/RareStatRequirementViewDto"}},"maxHr":{"type":"number","nullable":true,"example":3},"minStars":{"type":"number","nullable":true,"example":null}},"required":["slot","slotLabel","stats","maxHr","minStars"]},"AlertDeliveryDto":{"type":"object","properties":{"at":{"type":"string","format":"date-time","example":"2026-10-16T04:12:31.000Z"},"ok":{"type":"boolean","description":"True when the receiver answered 2xx.","example":true},"statusCode":{"type":"number","nullable":true,"description":"HTTP status of the last attempt; null when no response was received.","example":204},"error":{"type":"string","nullable":true,"description":"Failure reason (e.g. `HTTP 500`, `Timed out after 5000ms`, `Redirects are not followed`).","example":null},"attempts":{"type":"number","description":"Attempts made (alerts retry network errors, timeouts, 429 and 5xx up to 3 times with backoff; tests make 1 attempt).","example":1},"value":{"type":"number","nullable":true,"description":"Value sent: metric value for price alerts (HR, or percent for `change7d`), listing HR price for rare alerts (null when unpriced).","example":0.92},"test":{"type":"boolean","description":"True for `POST /api/alerts/{id}/test` deliveries.","example":false}},"required":["at","ok","statusCode","error","attempts","value","test"]},"AlertDto":{"type":"object","properties":{"id":{"type":"string","example":"6710a2f4c1e5b8a9d4f3e201"},"kind":{"type":"string","enum":["price","rare"],"description":"`price` (default): fires when a catalog item price crosses `threshold` after a price update. `rare`: fires when a newly ingested PD2 market listing of a rare item matches `rare` (needs rare appraisal to be live; otherwise 400).","example":"price"},"name":{"type":"string","example":"Cheap Ber"},"item":{"nullable":true,"description":"Price alerts only (null for rare alerts).","allOf":[{"$ref":"#/components/schemas/AlertItemDto"}]},"metric":{"type":"string","enum":["average","median","change7d"],"nullable":true,"description":"Value compared with `threshold`: `average` (trimmed mean, HR) or `median` (HR) over the last 7 days, or `change7d` (7-day median change in percent, e.g. `-12.5`). Price alerts only.","example":"median"},"op":{"type":"string","enum":["lt","lte","gt","gte"],"nullable":true,"description":"Comparison: `lt` (<), `lte` (≤), `gt` (>), `gte` (≥). The alert fires when `value <op> threshold`. Price alerts only.","example":"lte"},"threshold":{"type":"number","nullable":true,"description":"Threshold in HR for `average`/`median` (must be ≥ 0), or in percent for `change7d`. Price alerts only.","example":0.95},"rare":{"nullable":true,"description":"Rare alerts only (null for price alerts).","allOf":[{"$ref":"#/components/schemas/RareCriteriaDto"}]},"isLadder":{"type":"boolean","example":true},"isHardcore":{"type":"boolean","example":false},"webhookUrl":{"type":"string","description":"HTTPS URL that receives the alert. Discord webhook URLs (`https://discord.com/api/webhooks/...`) get a Discord embed; any other URL gets a signed JSON payload. Hosts must resolve to public addresses (private, loopback, link-local and cloud-metadata IPs are rejected); redirects are not followed.","example":"https://discord.com/api/webhooks/1296123456789012345/AbCdEf"},"webhookType":{"type":"string","enum":["discord","generic"],"example":"discord"},"cooldownHours":{"type":"number","description":"Minimum hours between two deliveries (price alerts; rare alerts notify once per item instead).","example":12},"enabled":{"type":"boolean","example":true},"createdAt":{"type":"string","format":"date-time","example":"2026-10-15T18:03:00.000Z"},"updatedAt":{"type":"string","format":"date-time","example":"2026-10-15T18:03:00.000Z"},"lastTriggeredAt":{"type":"string","format":"date-time","nullable":true,"description":"Last time the rule fired (start of the cooldown).","example":"2026-10-16T04:12:30.000Z"},"recentDeliveries":{"description":"Last 10 delivery results, newest first.","type":"array","items":{"$ref":"#/components/schemas/AlertDeliveryDto"}}},"required":["id","kind","name","item","metric","op","threshold","rare","isLadder","isHardcore","webhookUrl","webhookType","cooldownHours","enabled","createdAt","updatedAt","lastTriggeredAt","recentDeliveries"]},"AlertListResponseDto":{"type":"object","properties":{"alerts":{"description":"Alerts of this API client, newest first.","type":"array","items":{"$ref":"#/components/schemas/AlertDto"}},"limit":{"type":"number","description":"Maximum alerts per API client.","example":25}},"required":["alerts","limit"]},"RareStatRequirementDto":{"type":"object","properties":{"key":{"type":"string","description":"Feature key of a key stat of the slot (`slots[slot].stats[].key` in `GET /api/rares/guide`).","example":"item_fastercastrate"},"min":{"type":"number","minimum":0,"description":"Minimum value (inclusive).","example":20}},"required":["key","min"]},"RareCriteriaBodyDto":{"type":"object","properties":{"slot":{"type":"string","example":"ring"},"stats":{"minItems":1,"maxItems":4,"type":"array","items":{"$ref":"#/components/schemas/RareStatRequirementDto"}},"maxHr":{"type":"number","nullable":true,"example":3},"minStars":{"type":"number","nullable":true,"enum":[1,2,3],"example":null}},"required":["slot","stats"]},"CreateAlertBodyDto":{"type":"object","properties":{"kind":{"type":"string","enum":["price","rare"],"default":"price","description":"`price` (default): fires when a catalog item price crosses `threshold` after a price update. `rare`: fires when a newly ingested PD2 market listing of a rare item matches `rare` (needs rare appraisal to be live; otherwise 400)."},"name":{"type":"string","description":"Display name (1-80 chars). Defaults to the item name (price) or \"Rare <slot>\" (rare).","example":"Cheap Ber"},"item":{"type":"string","description":"Item to watch: base code, exact name or alias, resolved through the catalog (see `GET /api/catalog`). Required for price alerts.","example":"Ber"},"metric":{"type":"string","enum":["average","median","change7d"],"description":"Value compared with `threshold`: `average` (trimmed mean, HR) or `median` (HR) over the last 7 days, or `change7d` (7-day median change in percent, e.g. `-12.5`). Price alerts only. Required for price alerts.","example":"median"},"op":{"type":"string","enum":["lt","lte","gt","gte"],"description":"Comparison: `lt` (<), `lte` (≤), `gt` (>), `gte` (≥). The alert fires when `value <op> threshold`. Price alerts only. Required for price alerts.","example":"lte"},"threshold":{"type":"number","description":"Threshold in HR for `average`/`median` (must be ≥ 0), or in percent for `change7d`. Price alerts only. Required for price alerts.","example":0.95},"rare":{"description":"Rare alert criteria (required with `kind: \"rare\"`, invalid otherwise). `slot`: a slot code from `GET /api/rares/guide`; `stats`: 1-4 requirements `{ key, min }` with keys from that slot's `stats` (a listing matches when every stat's absolute value is ≥ `min`); `maxHr`: optional price ceiling in HR (unpriced listings never match when set); `minStars`: optional 1-3, every required stat must also rate at least that many stars on the slot. Mode comes from `isLadder` / `isHardcore`. Each item (content fingerprint) is notified at most once per alert; `cooldownHours` does not apply.","allOf":[{"$ref":"#/components/schemas/RareCriteriaBodyDto"}]},"isLadder":{"type":"boolean","default":true,"example":true},"isHardcore":{"type":"boolean","default":false,"example":false},"webhookUrl":{"type":"string","description":"HTTPS URL that receives the alert. Discord webhook URLs (`https://discord.com/api/webhooks/...`) get a Discord embed; any other URL gets a signed JSON payload. Hosts must resolve to public addresses (private, loopback, link-local and cloud-metadata IPs are rejected); redirects are not followed.","example":"https://discord.com/api/webhooks/1296123456789012345/AbCdEf"},"cooldownHours":{"type":"number","minimum":1,"maximum":720,"default":12,"description":"Minimum hours between two deliveries.","example":12},"enabled":{"type":"boolean","default":true,"example":true}},"required":["webhookUrl"]},"CreateAlertResponseDto":{"type":"object","properties":{"alert":{"$ref":"#/components/schemas/AlertDto"},"secret":{"type":"string","description":"Webhook signing secret. Returned ONLY in this response: store it to verify `X-PD2Trader-Signature` (HMAC-SHA256 of the raw request body, `sha256=<hex>`).","example":"whsec_3f9c1a7be2d84c0f9a6b5e4d3c2b1a09f8e7d6c5b4a39281706f5e4d3c2b1a09"}},"required":["alert","secret"]},"UpdateAlertBodyDto":{"type":"object","properties":{"name":{"type":"string","example":"Cheap Ber (HC)"},"item":{"type":"string","example":"r30"},"metric":{"type":"string","enum":["average","median","change7d"],"example":"average"},"op":{"type":"string","enum":["lt","lte","gt","gte"],"example":"lt"},"threshold":{"type":"number","example":1},"rare":{"description":"Rare alerts only: replaces the whole criteria. Price fields are rejected on rare alerts and `rare` on price alerts; `kind` cannot change.","allOf":[{"$ref":"#/components/schemas/RareCriteriaBodyDto"}]},"isLadder":{"type":"boolean","example":true},"isHardcore":{"type":"boolean","example":true},"webhookUrl":{"type":"string","example":"https://example.com/hooks/pd2"},"cooldownHours":{"type":"number","minimum":1,"maximum":720,"example":24},"enabled":{"type":"boolean","example":false}}},"DeleteAlertResponseDto":{"type":"object","properties":{"ok":{"type":"boolean","example":true}},"required":["ok"]},"TestAlertResponseDto":{"type":"object","properties":{"delivery":{"$ref":"#/components/schemas/AlertDeliveryDto"}},"required":["delivery"]},"StreamHelloEventDto":{"type":"object","properties":{"serverTime":{"type":"string","format":"date-time","description":"Server time when the stream opened.","example":"2026-10-16T18:04:12.345Z"},"events":{"type":"array","description":"Events this stream is subscribed to.","example":["prices","feed","badges"],"items":{"type":"string","enum":["prices","feed","badges","market.listing.created","market.listing.updated","market.listing.sold","market.listing.removed","market.listing.expired"]}},"heartbeatSeconds":{"type":"number","description":"Seconds between heartbeat comments.","example":25}},"required":["serverTime","events","heartbeatSeconds"]},"StreamModeDto":{"type":"object","properties":{"isLadder":{"type":"boolean","example":true},"isHardcore":{"type":"boolean","example":false}},"required":["isLadder","isHardcore"]},"StreamPricesEventDto":{"type":"object","properties":{"completedAt":{"type":"string","format":"date-time","description":"When the price update completed; fresh prices are servable from now on.","example":"2026-10-16T18:00:41.902Z"},"modes":{"description":"Game modes with new prices in this update.","example":[{"isLadder":true,"isHardcore":false},{"isLadder":true,"isHardcore":true},{"isLadder":false,"isHardcore":false},{"isLadder":false,"isHardcore":true}],"type":"array","items":{"$ref":"#/components/schemas/StreamModeDto"}}},"required":["completedAt","modes"]},"StreamFeedEntryDto":{"type":"object","properties":{"key":{"type":"string","description":"Item key.","example":"r30"},"name":{"type":"string","example":"Ber Rune"},"category":{"type":"string","enum":["runes","currency","ubers","gems","mats","uniques","sets","other"],"example":"runes"},"iconPath":{"type":"string","nullable":true,"example":"/runes/ber.png"},"qtyDelta":{"type":"number","description":"Quantity gained vs the previous snapshot.","example":1},"valueHr":{"type":"number","description":"Value of the gained quantity in HR.","example":1},"at":{"type":"string","format":"date-time","example":"2026-10-16T17:58:03.000Z"}},"required":["key","name","category","iconPath","qtyDelta","valueHr","at"]},"StreamFeedEventDto":{"type":"object","properties":{"displayName":{"type":"string","example":"Gheed"},"slug":{"type":"string","description":"Profile slug for `GET /api/profile/{slug}`.","example":"gheed"},"isLadder":{"type":"boolean","example":true},"isHardcore":{"type":"boolean","example":false},"seasonId":{"type":"string","example":"s14"},"entries":{"type":"array","items":{"$ref":"#/components/schemas/StreamFeedEntryDto"}}},"required":["displayName","slug","isLadder","isHardcore","seasonId","entries"]},"StreamBadgesEventDto":{"type":"object","properties":{"displayName":{"type":"string","example":"Gheed"},"slug":{"type":"string","description":"Profile slug for `GET /api/profile/{slug}`.","example":"gheed"},"isLadder":{"type":"boolean","example":true},"isHardcore":{"type":"boolean","example":false},"seasonId":{"type":"string","example":"s14"},"badges":{"type":"array","example":["first-ber","hr-100"],"items":{"type":"string","enum":["first-ber","first-zod","hr-100","hr-500","hr-1000","rune-collector","uber-set","streak-7"]}},"at":{"type":"string","format":"date-time","description":"Snapshot time the badges were earned on.","example":"2026-10-16T17:58:03.000Z"}},"required":["displayName","slug","isLadder","isHardcore","seasonId","badges","at"]},"StreamMarketListingEventDto":{"type":"object","properties":{"id":{"type":"string","description":"PD2 listing id; `GET /api/market/listing/{id}` returns the full listing.","example":"6716a0f2b3c4d5e6f7a8b9c0"},"isLadder":{"type":"boolean","example":true},"isHardcore":{"type":"boolean","example":false},"name":{"type":"string","nullable":true,"description":"Item name (unique/set/runeword or base name).","example":"Harlequin Crest"},"baseName":{"type":"string","nullable":true,"example":"Shako"},"baseCode":{"type":"string","nullable":true,"example":"uap"},"typeCode":{"type":"string","nullable":true,"example":"helm"},"quality":{"type":"string","nullable":true,"example":"Unique"},"price":{"type":"string","nullable":true,"description":"Seller's price text as listed.","example":"2 ist"},"hrPrice":{"type":"number","nullable":true,"description":"Price in HR; null when unpriced.","example":0.5},"bumpedAt":{"type":"string","nullable":true,"format":"date-time","example":"2026-10-21T08:15:02.000Z"},"seller":{"type":"string","nullable":true,"description":"Seller username.","example":"Gheed"},"status":{"type":"string","enum":["active","sold","removed","expired"],"example":"active"},"soldHr":{"type":"number","nullable":true,"description":"HR the listing sold for (`sold` only).","example":null},"at":{"type":"string","format":"date-time","description":"When the market ingest observed the change.","example":"2026-10-21T08:15:03.000Z"}},"required":["id","isLadder","isHardcore","name","baseName","baseCode","typeCode","quality","price","hrPrice","bumpedAt","seller","status","soldHr","at"]},"CardErrorResponse":{"type":"object","properties":{"statusCode":{"type":"number","example":404},"message":{"type":"string","description":"What was wrong with the request.","example":"Unknown baseCode \"r99\""},"error":{"type":"string","example":"Not Found"}},"required":["statusCode","message","error"]},"SkillTotalDto":{"type":"object","properties":{"name":{"type":"string","example":"Lightning Mastery"},"total":{"type":"number","description":"Hard points + item, oskill, tree, class and all-skills bonuses.","example":44}},"required":["name","total"]},"ItemIconDto":{"type":"object","properties":{"slot":{"type":"string","example":"Helm"},"key":{"type":"string","example":"unique:ci3"},"name":{"type":"string","example":"Griffon's Eye"},"quality":{"type":"string","example":"Unique"},"iconUrl":{"type":"string","nullable":true,"example":"https://pd2trader.com/assets/items/invci3u.png"}},"required":["slot","key","name","quality","iconUrl"]},"CharacterListEntryDto":{"type":"object","properties":{"name":{"type":"string","example":"Butzfire"},"account":{"type":"string","nullable":true,"example":"butzacct","description":"PD2 account (null when crawled without one)."},"className":{"type":"string","example":"Sorceress"},"level":{"type":"number","example":99},"life":{"type":"number","example":1471},"mana":{"type":"number","example":469},"isDead":{"type":"boolean","example":false,"description":"PD2's died flag. Only meaningful in hardcore: softcore characters that ever died report true."},"topSkills":{"description":"Three highest skill totals.","type":"array","items":{"$ref":"#/components/schemas/SkillTotalDto"}},"gearValueHr":{"type":"number","nullable":true,"description":"Confident market value of worn gear (character + merc, sockets included) in HR; null when nothing is priced.","example":21.4},"lastUpdated":{"type":"string","format":"date-time"},"equippedIcons":{"type":"array","items":{"$ref":"#/components/schemas/ItemIconDto"}}},"required":["name","account","className","level","life","mana","isDead","topSkills","gearValueHr","lastUpdated","equippedIcons"]},"CharacterListResponseDto":{"type":"object","properties":{"season":{"type":"string","example":"s14"},"mode":{"type":"string","enum":["softcore","hardcore"],"example":"softcore"},"total":{"type":"number","example":1240},"page":{"type":"number","example":1},"pageSize":{"type":"number","example":25},"breakdown":{"type":"object","additionalProperties":{"type":"integer"},"description":"Matching characters per class under every filter except `classes`.","example":{"Sorceress":420,"Paladin":310,"Barbarian":120}},"characters":{"type":"array","items":{"$ref":"#/components/schemas/CharacterListEntryDto"}}},"required":["season","mode","total","page","pageSize","breakdown","characters"]},"SuggestCharacterDto":{"type":"object","properties":{"name":{"type":"string","example":"ShadowDin"},"account":{"type":"string","nullable":true,"example":"shaman_tv","description":"PD2 account (null when crawled without one)."},"className":{"type":"string","example":"Paladin"},"level":{"type":"number","example":97},"isHardcore":{"type":"boolean","example":false},"matchedOn":{"type":"string","enum":["name","account"],"example":"name","description":"Best match: on the character name, else on its account."}},"required":["name","account","className","level","isHardcore","matchedOn"]},"SuggestAccountDto":{"type":"object","properties":{"account":{"type":"string","example":"shaman_tv"},"characters":{"type":"number","description":"Characters of the account in the season (and mode).","example":4}},"required":["account","characters"]},"BuildSuggestResponseDto":{"type":"object","properties":{"characters":{"description":"Exact name, name prefix, name substring, then the same on the account; each tier by level desc, then name.","type":"array","items":{"$ref":"#/components/schemas/SuggestCharacterDto"}},"accounts":{"description":"Exact account, prefix, then substring; each tier by characters desc, then name.","type":"array","items":{"$ref":"#/components/schemas/SuggestAccountDto"}}},"required":["characters","accounts"]},"UsageCountDto":{"type":"object","properties":{"name":{"type":"string","example":"Sorceress"},"count":{"type":"number","example":420},"pct":{"type":"number","description":"Share of matching characters, 0–100.","example":33.87}},"required":["name","count","pct"]},"ItemUsageDto":{"type":"object","properties":{"key":{"type":"string","description":"Item key (`unique:<code>`, `set:<code>`, `rw:<name>`, catalog key).","example":"unique:griffon_s_eye"},"name":{"type":"string","example":"Griffon's Eye"},"quality":{"type":"string","example":"Unique"},"iconUrl":{"type":"string","nullable":true},"count":{"type":"number","example":120},"pct":{"type":"number","example":28.57},"priceHr":{"type":"number","nullable":true,"description":"Confident market price in HR; null when unpriced (runewords, thin markets).","example":3.2}},"required":["key","name","quality","iconUrl","count","pct","priceHr"]},"UsageResponseDto":{"type":"object","properties":{"season":{"type":"string","example":"s14"},"mode":{"type":"string","enum":["softcore","hardcore"]},"total":{"type":"number","example":1240},"classes":{"type":"array","items":{"$ref":"#/components/schemas/UsageCountDto"}},"skills":{"description":"Skills with ≥ 20 hard points: first 50 (more via `GET /api/builds/usage/skills`).","type":"array","items":{"$ref":"#/components/schemas/UsageCountDto"}},"skillsCount":{"type":"number","description":"Number of skills in the full list.","example":180},"items":{"description":"Worn uniques, sets and runewords: first 50 (more via `GET /api/builds/usage/items`).","type":"array","items":{"$ref":"#/components/schemas/ItemUsageDto"}},"itemsCount":{"type":"number","description":"Number of items in the full list.","example":640},"mercTypes":{"description":"Merc types (PD2 merc description, e.g. `Defensive Auras`).","type":"array","items":{"$ref":"#/components/schemas/UsageCountDto"}},"mercItems":{"description":"Merc items: first 50 (more via `GET /api/builds/usage/mercItems`).","type":"array","items":{"$ref":"#/components/schemas/ItemUsageDto"}},"mercItemsCount":{"type":"number","description":"Number of merc items in the full list.","example":210}},"required":["season","mode","total","classes","skills","skillsCount","items","itemsCount","mercTypes","mercItems","mercItemsCount"]},"UsageListResponseDto":{"type":"object","properties":{"season":{"type":"string","example":"s14"},"mode":{"type":"string","enum":["softcore","hardcore"]},"total":{"type":"number","description":"Matching characters.","example":1240},"list":{"type":"string","enum":["skills","items","mercItems"]},"matches":{"type":"number","description":"Rows matching `q` (all rows without it).","example":640},"page":{"type":"number","example":1},"pageSize":{"type":"number","example":50},"rows":{"type":"array","items":{"oneOf":[{"$ref":"#/components/schemas/UsageCountDto"},{"$ref":"#/components/schemas/ItemUsageDto"}]},"description":"`UsageCount` rows for skills, `ItemUsage` rows (with price) for items and merc items."}},"required":["season","mode","total","list","matches","page","pageSize","rows"]},"ShoppingListItemDto":{"type":"object","properties":{"key":{"type":"string","description":"Item key (`unique:<code>`, `set:<code>`, `rw:<name>`, catalog key).","example":"unique:griffon_s_eye"},"name":{"type":"string","example":"Griffon's Eye"},"quality":{"type":"string","example":"Unique"},"iconUrl":{"type":"string","nullable":true},"count":{"type":"number","example":120},"pct":{"type":"number","example":28.57},"priceHr":{"type":"number","nullable":true,"description":"Confident market price in HR; null when unpriced (runewords, thin markets).","example":3.2},"marketUrl":{"type":"string","nullable":true,"example":"/item/griffon_s_eye?category=uniques&isLadder=true&isHardcore=false"}},"required":["key","name","quality","iconUrl","count","pct","priceHr","marketUrl"]},"ShoppingListResponseDto":{"type":"object","properties":{"season":{"type":"string","example":"s14"},"mode":{"type":"string","enum":["softcore","hardcore"]},"total":{"type":"number"},"items":{"type":"array","items":{"$ref":"#/components/schemas/ShoppingListItemDto"}}},"required":["season","mode","total","items"]},"MetaPointDto":{"type":"object","properties":{"hour":{"type":"string","format":"date-time"},"total":{"type":"number"},"classes":{"type":"array","items":{"$ref":"#/components/schemas/UsageCountDto"}}},"required":["hour","total","classes"]},"SharePointDto":{"type":"object","properties":{"hour":{"type":"string","format":"date-time"},"pct":{"type":"number","example":28.5}},"required":["hour","pct"]},"ItemSeriesDto":{"type":"object","properties":{"key":{"type":"string"},"name":{"type":"string"},"quality":{"type":"string"},"iconUrl":{"type":"string","nullable":true},"series":{"type":"array","items":{"$ref":"#/components/schemas/SharePointDto"}}},"required":["key","name","quality","iconUrl","series"]},"MetaHistoryResponseDto":{"type":"object","properties":{"season":{"type":"string","example":"s14"},"mode":{"type":"string","enum":["softcore","hardcore"]},"points":{"type":"array","items":{"$ref":"#/components/schemas/MetaPointDto"}},"items":{"description":"Share series of the 10 most used items of the latest hour.","type":"array","items":{"$ref":"#/components/schemas/ItemSeriesDto"}}},"required":["season","mode","points","items"]},"AttributesDto":{"type":"object","properties":{"strength":{"type":"number","example":156},"dexterity":{"type":"number","example":75},"vitality":{"type":"number","example":400},"energy":{"type":"number","example":35}},"required":["strength","dexterity","vitality","energy"]},"CharacterSkillDto":{"type":"object","properties":{"name":{"type":"string","example":"Lightning Mastery"},"total":{"type":"number","description":"Hard points + item, oskill, tree, class and all-skills bonuses.","example":44},"base":{"type":"number","description":"Hard points.","example":20},"tree":{"type":"string","nullable":true,"description":"Class skill tree, e.g. Cold Spells; null for skills outside the class.","example":"Lightning Spells"}},"required":["name","total","base","tree"]},"SocketedItemDto":{"type":"object","properties":{"key":{"type":"string","example":"rune:r30"},"name":{"type":"string","example":"Ber"},"quality":{"type":"string","nullable":true,"example":null},"mods":{"example":["20% Chance of Crushing Blow"],"type":"array","items":{"type":"string"}}},"required":["key","name","quality","mods"]},"RequirementsDto":{"type":"object","properties":{"level":{"type":"number","example":63},"strength":{"type":"number","example":50},"dexterity":{"type":"number","example":0}},"required":["level","strength","dexterity"]},"DefenseDto":{"type":"object","properties":{"base":{"type":"number","example":60},"total":{"type":"number","example":129}},"required":["base","total"]},"DurabilityDto":{"type":"object","properties":{"current":{"type":"number","example":11},"max":{"type":"number","example":14}},"required":["current","max"]},"SetBonusDto":{"type":"object","properties":{"items":{"type":"number","example":2},"text":{"type":"string","example":"+220 Poison Damage over 2 Seconds"}},"required":["items","text"]},"ItemSetInfoDto":{"type":"object","properties":{"name":{"type":"string","example":"Griswold's Legacy"},"pieces":{"type":"array","items":{"type":"string"}},"bonuses":{"description":"This piece's \"(N Items)\" bonuses.","type":"array","items":{"$ref":"#/components/schemas/SetBonusDto"}}},"required":["name","pieces","bonuses"]},"EquippedItemDto":{"type":"object","properties":{"slot":{"type":"string","example":"Right Hand"},"key":{"type":"string","example":"rw:infinity"},"name":{"type":"string","example":"Infinity"},"quality":{"type":"string","example":"Runeword"},"base":{"type":"string","example":"Elder Staff"},"baseCode":{"type":"string","example":"6cs"},"runeword":{"type":"boolean"},"ethereal":{"type":"boolean"},"corrupted":{"type":"boolean"},"sockets":{"type":"number","example":4},"socketed":{"type":"array","items":{"$ref":"#/components/schemas/SocketedItemDto"}},"mods":{"description":"PD2 display lines (socket stats included).","type":"array","items":{"type":"string"}},"rolls":{"type":"array","items":{"type":"array","items":{"type":"number","nullable":true},"minItems":3,"maxItems":3},"description":"Roll of each `mods` line as [value, min, max] (same order; null for fixed lines). Absent when nothing rolls or on characters stored before it existed.","example":[[190,150,200],null]},"corruptedMods":{"description":"Corrupted items: the `mods` lines the corruption rolled (`Socketed (N)` for a socket corruption). Absent on items stored before it was recorded.","example":["+2 to All Skills"],"type":"array","items":{"type":"string"}},"itemLevel":{"type":"number","nullable":true,"example":87},"requirements":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/RequirementsDto"}]},"defense":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/DefenseDto"}]},"durability":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/DurabilityDto"}]},"damage":{"type":"object","nullable":true,"additionalProperties":{"type":"array","items":{"type":"number"}},"description":"Weapon damage [min, max] by kind (`one_handed`, `two_handed`, `throw`).","example":{"two_handed":[387,448]}},"set":{"nullable":true,"description":"Set of a set item; null otherwise.","allOf":[{"$ref":"#/components/schemas/ItemSetInfoDto"}]},"iconUrl":{"type":"string","nullable":true},"priceHr":{"type":"number","nullable":true,"description":"Item + priced sockets in HR; null when nothing in it is priced.","example":4.1}},"required":["slot","key","name","quality","base","baseCode","runeword","ethereal","corrupted","sockets","socketed","mods","set","iconUrl","priceHr"]},"CharmItemDto":{"type":"object","properties":{"slot":{"type":"string","example":"Right Hand"},"key":{"type":"string","example":"rw:infinity"},"name":{"type":"string","example":"Infinity"},"quality":{"type":"string","example":"Runeword"},"base":{"type":"string","example":"Elder Staff"},"baseCode":{"type":"string","example":"6cs"},"runeword":{"type":"boolean"},"ethereal":{"type":"boolean"},"corrupted":{"type":"boolean"},"sockets":{"type":"number","example":4},"socketed":{"type":"array","items":{"$ref":"#/components/schemas/SocketedItemDto"}},"mods":{"description":"PD2 display lines (socket stats included).","type":"array","items":{"type":"string"}},"rolls":{"type":"array","items":{"type":"array","items":{"type":"number","nullable":true},"minItems":3,"maxItems":3},"description":"Roll of each `mods` line as [value, min, max] (same order; null for fixed lines). Absent when nothing rolls or on characters stored before it existed.","example":[[190,150,200],null]},"corruptedMods":{"description":"Corrupted items: the `mods` lines the corruption rolled (`Socketed (N)` for a socket corruption). Absent on items stored before it was recorded.","example":["+2 to All Skills"],"type":"array","items":{"type":"string"}},"itemLevel":{"type":"number","nullable":true,"example":87},"requirements":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/RequirementsDto"}]},"defense":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/DefenseDto"}]},"durability":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/DurabilityDto"}]},"damage":{"type":"object","nullable":true,"additionalProperties":{"type":"array","items":{"type":"number"}},"description":"Weapon damage [min, max] by kind (`one_handed`, `two_handed`, `throw`).","example":{"two_handed":[387,448]}},"set":{"nullable":true,"description":"Set of a set item; null otherwise.","allOf":[{"$ref":"#/components/schemas/ItemSetInfoDto"}]},"iconUrl":{"type":"string","nullable":true},"priceHr":{"type":"number","nullable":true,"description":"Item + priced sockets in HR; null when nothing in it is priced.","example":4.1},"row":{"type":"number","example":1,"description":"Row within the charm area (inventory row − 4): 0–3."},"column":{"type":"number","example":7,"description":"Inventory column: 0–9."},"height":{"type":"number","enum":[1,2,3],"example":3,"description":"Cells tall: 1 small, 2 large, 3 grand."},"graphic":{"type":"number","enum":[0,1,2],"example":1,"description":"Art variant (0–2) of the small/large/grand charm image."}},"required":["slot","key","name","quality","base","baseCode","runeword","ethereal","corrupted","sockets","socketed","mods","set","iconUrl","priceHr","row","column","height","graphic"]},"MercDto":{"type":"object","properties":{"type":{"type":"string","example":"Defensive Auras"},"typeId":{"type":"number","nullable":true,"example":13},"name":{"type":"string","nullable":true,"example":"Ahsab"},"equipped":{"type":"array","items":{"$ref":"#/components/schemas/EquippedItemDto"}}},"required":["type","typeId","name","equipped"]},"CharacterDetailDto":{"type":"object","properties":{"name":{"type":"string","example":"Butzfire"},"account":{"type":"string","nullable":true,"example":"butzacct","description":"PD2 account (null when crawled without one)."},"season":{"type":"string","example":"s14"},"mode":{"type":"string","enum":["softcore","hardcore"]},"className":{"type":"string","example":"Sorceress"},"level":{"type":"number","example":99},"life":{"type":"number"},"mana":{"type":"number"},"attributes":{"$ref":"#/components/schemas/AttributesDto"},"isDead":{"type":"boolean","description":"PD2's died flag. Only meaningful in hardcore: softcore characters that ever died report true."},"skills":{"description":"Skills with hard points, highest total first.","type":"array","items":{"$ref":"#/components/schemas/CharacterSkillDto"}},"equipped":{"type":"array","items":{"$ref":"#/components/schemas/EquippedItemDto"}},"charms":{"description":"Inventory charms (bottom half of the 10×8 inventory); slot is `Inventory`.","type":"array","items":{"$ref":"#/components/schemas/CharmItemDto"}},"merc":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/MercDto"}]},"gearValueHr":{"type":"number","nullable":true},"firstSeenAt":{"type":"string","format":"date-time"},"lastUpdated":{"type":"string","format":"date-time"}},"required":["name","account","season","mode","className","level","life","mana","attributes","isDead","skills","equipped","charms","merc","gearValueHr","firstSeenAt","lastUpdated"]},"CharacterPricesDto":{"type":"object","properties":{"gearValueHr":{"type":"number","nullable":true,"description":"Worn gear value in HR (live prices); null when nothing is priced.","example":12.4},"equipped":{"description":"HR per worn item, same order as `equipped`; null = no confident price.","example":[5.25,null],"type":"array","items":{"type":"number"}},"charms":{"description":"HR per charm, same order as `charms`.","example":[2.1,null],"type":"array","items":{"type":"number"}},"merc":{"nullable":true,"description":"HR per merc item, same order as `merc.equipped`; null without a merc.","example":[0.5],"type":"array","items":{"type":"number"}}},"required":["gearValueHr","equipped","charms","merc"]},"SnapshotListEntryDto":{"type":"object","properties":{"id":{"type":"string","example":"6718f0c2a1b2c3d4e5f60718"},"takenAt":{"type":"string","format":"date-time"},"level":{"type":"number","example":98},"gearValueHr":{"type":"number","nullable":true}},"required":["id","takenAt","level","gearValueHr"]},"SnapshotDetailDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"season":{"type":"string"},"mode":{"type":"string","enum":["softcore","hardcore"]},"takenAt":{"type":"string","format":"date-time"},"doc":{"type":"object","additionalProperties":true,"description":"The build at that time: className, level, life, mana, attributes, isDead, skills, equipped, merc, gearValueHr (equipped items without iconUrl/priceHr)."}},"required":["id","name","season","mode","takenAt","doc"]},"ItemTooltipDto":{"type":"object","properties":{"key":{"type":"string","example":"unique:templar_s_might"},"name":{"type":"string","example":"Templar's Might"},"quality":{"type":"string","enum":["Unique","Set","Runeword"],"example":"Unique"},"base":{"type":"string","nullable":true,"description":"Base item of uniques and set items; null for runewords.","example":"Sacred Armor"},"reqLevel":{"type":"number","nullable":true,"description":"Required level; for runewords the highest rune requirement.","example":74},"iconUrl":{"type":"string","nullable":true,"example":"https://pd2trader.com/assets/items/invaaru.png"},"lines":{"description":"Property lines as the game shows them; variable rolls as `[min-max]`.","example":["Level [6-8] Might Aura When Equipped","+20% Faster Hit Recovery","+[20-30] to Strength"],"type":"array","items":{"type":"string"}}},"required":["key","name","quality","base","reqLevel","iconUrl","lines"]},"SimCharacterSummaryDto":{"type":"object","properties":{"name":{"type":"string","example":"YozzSorc"},"className":{"type":"string","example":"Sorceress"},"level":{"type":"number","example":92},"hardcore":{"type":"boolean"},"ladder":{"type":"boolean"}},"required":["name","className","level","hardcore","ladder"]},"SimCharacterImportDto":{"type":"object","properties":{"character":{"$ref":"#/components/schemas/SimCharacterSummaryDto"},"build":{"type":"object","additionalProperties":true,"description":"Engine `Build` (snake_case): class, level, attributes, skills, quests, equipped items, merc. Use it as `profiles[].build` of a `JobSpec`."},"candidates":{"type":"array","items":{"type":"object","additionalProperties":true},"description":"Non-equipped items (`{ path, name, storage, item }`): Top Gear candidates."},"unknownModifiers":{"type":"array","items":{"type":"object","additionalProperties":true},"description":"PD2 modifiers the importer could not place in the build (never silently dropped)."},"skipped":{"type":"array","items":{"type":"object","additionalProperties":true},"description":"Items (or the merc) left out of the build: `{ path, name, reason }`."},"hiddenSkills":{"type":"array","items":{"type":"object","additionalProperties":true},"description":"Class skills PD2's character API leaves out of its skill list (Split Throw on Barbarians): `{ skill, unaccountedPoints }`, the earned skill points neither listed nor unspent. The build carries none of them."},"notes":{"description":"Neutral information about where the build came from, e.g. gear taken from a dead character's corpse (PD2 returns it under `corpse`). Not problems.","type":"array","items":{"type":"string"}}},"required":["character","build","candidates","unknownModifiers","skipped","hiddenSkills","notes"]},"SimErrorDto":{"type":"object","properties":{"statusCode":{"type":"number","example":400},"message":{"oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"One message, or every `profile line N: …` error of a profile.txt body.","example":"spec.iterations.max: must be at most 100000"}},"required":["statusCode","message"]},"SubmittedSimJobDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid","example":"0b6f3a52-6c1e-4c3b-9f9e-2f3d8f0f5b11"},"kind":{"type":"string","enum":["quick_sim","compare","top_gear","upgrade_finder","stat_weights","skill_charm_optimizer"],"example":"quick_sim"},"tier":{"type":"string","enum":["anonymous","free","supporter","beta_tester"],"description":"The caller's tier, which decided the limits: an API key runs with its owner's tier.","example":"free"},"status":{"type":"string","enum":["queued","running","done","failed","cancelled"],"example":"queued"},"specHash":{"type":"string","description":"SHA-256 of the normalized spec.","example":"a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3"},"chunks":{"type":"number","description":"Worker chunks the job was split into.","example":3},"costEstimateUsd":{"type":"number","description":"Estimated compute cost in USD.","example":0.012},"claimToken":{"type":"string","description":"Anonymous submissions only, returned once: proof that this caller submitted the job. Signed in later, send it with the job id to `POST /api/sim/reports/claim` to add the report to My reports.","example":"Yx2c0bq4kM9vN1rT8sW3dE6fG7hJ5pL0"},"reused":{"type":"boolean","description":"Present (true) when the job was served from the stored report of an identical earlier job (same `specHash`, so the same build, scenario, seed and data bundle, run by the current engine): it is `done` at once with its own report, nothing was simulated and it costs nothing. Submit with `?force=true` to simulate again.","example":true}},"required":["id","kind","tier","status","specHash","chunks","costEstimateUsd"]},"SimBundleDto":{"type":"object","properties":{"id":{"type":"string","example":"pd2-s14"},"contentHash":{"type":"string","description":"SHA-256 of the data bundle (64 lowercase hex).","example":"a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3"}},"required":["id","contentHash"]},"SimJobDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"kind":{"type":"string","enum":["quick_sim","compare","top_gear","upgrade_finder","stat_weights","skill_charm_optimizer"]},"status":{"type":"string","enum":["queued","running","done","failed","cancelled"]},"progress":{"type":"number","description":"Fraction done, 0–1.","example":0.42},"chunks":{"type":"object","additionalProperties":{"type":"integer"},"description":"`total` plus the chunk count per status (queued, running, done, failed, retrying, skipped).","example":{"total":3,"queued":1,"running":1,"done":1,"failed":0,"retrying":0,"skipped":0}},"error":{"type":"string","nullable":true,"description":"Why the job failed."},"engineVersion":{"type":"string","nullable":true,"example":"0.6.0"},"bundle":{"$ref":"#/components/schemas/SimBundleDto"},"specHash":{"type":"string","example":"a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3"},"costEstimateUsd":{"type":"number","example":0.012},"estimatedCoreSecs":{"type":"number","nullable":true,"description":"The worker's work estimate in core-seconds (`horadric_job::estimate`), written after an optimizer's expansion or at a Quick Sim / Compare's first chunk; null until then.","example":2400},"estimatedWallSecs":{"type":"number","nullable":true,"description":"Estimated wall time in seconds (straggler factor and per-wave overhead included); null until the worker has estimated the job. Above `maxRuntimeMinutes` the worker fails the job.","example":180},"createdAt":{"type":"string","format":"date-time"},"startedAt":{"type":"string","format":"date-time","nullable":true},"finishedAt":{"type":"string","format":"date-time","nullable":true},"reportSlug":{"type":"string","nullable":true,"description":"Report permalink slug once the worker has written it; read it with `GET /api/sim/reports/{slug}`.","example":"q8Zr0Lw3VvB2xT9mN4kD1A"},"character":{"type":"object","nullable":true,"description":"The baseline profile (`spec.profiles[0]`): `name`, `class`, `level`. Lets a job's page show the character before its report exists.","example":{"name":"Travinzal","class":"barbarian","level":95}},"live":{"type":"object","additionalProperties":true,"description":"Live progress (`JobLiveView`, src/sim/jobs/job-progress.ts): `phase` (queued, picked_up, running, merging, writing_report, done, failed, cancelled); `queue` `{ position, ahead, estimatedStartAt }` while queued; `stage` and `stageInfo` `{ index, total, candidates, screening }` (optimizers); `wave` `{ index, total }`; `iterations` `{ done, running, merged, projected, min, max }` (`running`: computed so far in running chunks); `etaAt` with `etaScope` (`job` or `stage`); `estimate` (the worker's merged estimate: `metric`, `mean`, `ci95`, `errorBp`, `targetErrorBp`, `stop`, `dps`, `killTimeSecs`, `deathRate`, `sources`, `stage`, `current`: false while the previous stage's estimate stands in); `history` (`{ iterations, mean, ci95, at }` per merge of the stage); `log` (a few iteration-0 events). The same view streams from `GET /api/sim/jobs/{id}/events`."}},"required":["id","kind","status","progress","chunks","error","engineVersion","bundle","specHash","costEstimateUsd","estimatedCoreSecs","estimatedWallSecs","createdAt","startedAt","finishedAt","reportSlug","character","live"]},"SimRunnerWorkerDto":{"type":"object","properties":{"id":{"type":"string","example":"worker-1ca8c37596b3"},"concurrency":{"type":"number","description":"Chunks it runs at once.","example":10},"running":{"type":"number","description":"Chunks running at its last heartbeat.","example":3},"queue":{"type":"string","enum":["sqs","postgres","local"]},"version":{"type":"string","example":"0.1.0"},"startedAt":{"type":"string","format":"date-time"},"lastSeenAt":{"type":"string","format":"date-time"}},"required":["id","concurrency","running","queue","version","startedAt","lastSeenAt"]},"SimRunnerStartingDto":{"type":"object","properties":{"count":{"type":"number","description":"Worker jobs Batch is starting.","example":1},"stage":{"type":"string","enum":["capacity","booting"],"description":"`capacity`: at least one waits for Fargate capacity (Batch SUBMITTED, PENDING or RUNNABLE); `booting`: every one has capacity and its container starts (STARTING, or RUNNING without a heartbeat for up to 60 s)."},"since":{"type":"string","format":"date-time","description":"When the oldest one was submitted."}},"required":["count","stage","since"]},"SimRunnerSlotsDto":{"type":"object","properties":{"total":{"type":"number","example":10},"busy":{"type":"number","example":3}},"required":["total","busy"]},"SimRunnerJobsDto":{"type":"object","properties":{"queued":{"type":"number","example":0},"running":{"type":"number","example":1}},"required":["queued","running"]},"SimRunnerDto":{"type":"object","properties":{"status":{"type":"string","enum":["busy","idle","starting","offline"],"description":"`busy`: a worker runs chunks; `idle`: workers online, all slots free; `starting`: no worker online, but one is starting (`starting`); `offline`: no worker heartbeat in the last 45 s and none starting (queued jobs wait for one)."},"workers":{"description":"Workers online.","type":"array","items":{"$ref":"#/components/schemas/SimRunnerWorkerDto"}},"starting":{"nullable":true,"description":"Worker jobs AWS Batch is starting (also while others are online); null when none, when Batch could not be read, or without `SIM_BATCH_JOB_QUEUES`.","allOf":[{"$ref":"#/components/schemas/SimRunnerStartingDto"}]},"slots":{"$ref":"#/components/schemas/SimRunnerSlotsDto"},"jobs":{"description":"Live jobs waiting for and holding a worker.","allOf":[{"$ref":"#/components/schemas/SimRunnerJobsDto"}]},"lastSeenAt":{"type":"string","format":"date-time","nullable":true,"description":"Latest heartbeat of any worker."},"checkedAt":{"type":"string","format":"date-time"}},"required":["status","workers","starting","slots","jobs","lastSeenAt","checkedAt"]},"SimPlannerLimitsDto":{"type":"object","properties":{"maxIterations":{"type":"number","description":"Upper bound of `iterations.max` and of optimizer stage iterations.","example":200000},"topGearMaxCombos":{"type":"number","description":"Upper bound of Top Gear and skill/charm optimizer `max_combinations` (0: the tier cannot run them).","example":2000},"upgradeFinderPoolSize":{"type":"number","description":"Upgrade Finder candidates per job (slots × the unique, set and runeword items that fit each; 0: the tier cannot run it).","example":1000},"kinds":{"type":"array","description":"Job kinds the tier may submit.","items":{"type":"string","enum":["quick_sim","compare","top_gear","upgrade_finder","stat_weights","skill_charm_optimizer"]}},"maxProfiles":{"type":"number","description":"Builds (profiles) per `compare` job.","example":6},"concurrentJobs":{"type":"number","description":"Queued + running jobs at once.","example":2},"priority":{"type":"boolean","description":"Jobs go to the priority queue.","example":false},"logRetentionDays":{"type":"number","description":"Days combat logs and timelines are kept.","example":30}},"required":["maxIterations","topGearMaxCombos","upgradeFinderPoolSize","kinds","maxProfiles","concurrentJobs","priority","logRetentionDays"]},"SimComputeBudgetDto":{"type":"object","properties":{"limitCoreHours":{"type":"number","description":"Core-hours per rolling 30 days.","example":20},"usedCoreHours":{"type":"number","description":"Core-hours recorded by the caller's jobs of the last 30 days.","example":4.2},"remainingCoreHours":{"type":"number","example":15.8},"resetsAt":{"type":"string","format":"date-time","nullable":true,"description":"When the caller is under budget again (null while under it)."}},"required":["limitCoreHours","usedCoreHours","remainingCoreHours","resetsAt"]},"SimLimitsDto":{"type":"object","properties":{"maxIterations":{"type":"number","description":"Upper bound of `iterations.max` and of optimizer stage iterations.","example":200000},"topGearMaxCombos":{"type":"number","description":"Upper bound of Top Gear and skill/charm optimizer `max_combinations` (0: the tier cannot run them).","example":2000},"upgradeFinderPoolSize":{"type":"number","description":"Upgrade Finder candidates per job (slots × the unique, set and runeword items that fit each; 0: the tier cannot run it).","example":1000},"kinds":{"type":"array","description":"Job kinds the tier may submit.","items":{"type":"string","enum":["quick_sim","compare","top_gear","upgrade_finder","stat_weights","skill_charm_optimizer"]}},"maxProfiles":{"type":"number","description":"Builds (profiles) per `compare` job.","example":6},"concurrentJobs":{"type":"number","description":"Queued + running jobs at once.","example":2},"priority":{"type":"boolean","description":"Jobs go to the priority queue.","example":false},"logRetentionDays":{"type":"number","description":"Days combat logs and timelines are kept.","example":30},"tier":{"type":"string","enum":["anonymous","free","supporter","beta_tester"]},"free":{"description":"The free tier's limits, for comparison.","allOf":[{"$ref":"#/components/schemas/SimPlannerLimitsDto"}]},"budget":{"nullable":true,"description":"Rolling 30-day compute budget of the caller; null when its tier has none. Over it, submissions get 429 `sim_compute_budget`.","allOf":[{"$ref":"#/components/schemas/SimComputeBudgetDto"}]},"maxRuntimeMinutes":{"type":"number","description":"Runtime cap of a job in minutes (`SIM_JOB_MAX_RUNTIME_MINUTES`): the worker fails a job whose work estimate exceeds it, and a running job past it.","example":22},"waveSlots":{"type":"number","description":"Worker slots a wave is spread over (`SIM_WAVE_SLOTS`): the parallelism the work estimate assumes.","example":20}},"required":["maxIterations","topGearMaxCombos","upgradeFinderPoolSize","kinds","maxProfiles","concurrentJobs","priority","logRetentionDays","tier","free","budget","maxRuntimeMinutes","waveSlots"]},"SimReportListEntryDto":{"type":"object","properties":{"slug":{"type":"string","example":"q8Zr0Lw3VvB2xT9mN4kD1A"},"kind":{"type":"string","enum":["quick_sim","compare","top_gear","upgrade_finder","stat_weights","skill_charm_optimizer"]},"createdAt":{"type":"string","format":"date-time"},"engineVersion":{"type":"string","example":"0.6.0"},"bundleId":{"type":"string","example":"pd2-s14"},"primary":{"type":"string","enum":["dps","kill_time","death_rate"]},"profiles":{"description":"Profile names, baseline first.","example":["blizzard"],"type":"array","items":{"type":"string"}},"headline":{"type":"number","nullable":true,"description":"Baseline's mean on the primary metric (DPS, kill seconds or death probability).","example":11170.3},"mapDps":{"type":"number","nullable":true,"description":"Map reports: the baseline's map DPS (all damage over the whole map time); null for other reports.","example":8420.5},"kills":{"type":"number","description":"Baseline fights that ended in a kill (the kill-time sample size); 0 = never killed.","example":412},"deathRate":{"type":"number","nullable":true,"description":"Baseline's death probability per fight.","example":0.02},"title":{"type":"string","nullable":true,"description":"The owner's name for the report; null = the baseline profile's name.","example":"SoJ variant"},"pinned":{"type":"boolean","description":"The owner's ★ baseline flag."},"specHash":{"type":"string","example":"a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3","description":"Equal for identical specs (same build, scenario, data and seed)."},"bundleHash":{"type":"string","example":"a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3"},"ci95":{"type":"number","nullable":true,"description":"95 % half-width of the baseline's primary metric (absolute)."},"scenario":{"type":"object","additionalProperties":true,"nullable":true,"description":"The `JobSpec` scenario (null once the job row was pruned), and its SHA-256 `scenarioHash` alongside, for grouping same-scenario reports."},"scenarioHash":{"type":"string","nullable":true,"example":"a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3"},"fingerprint":{"type":"object","additionalProperties":true,"nullable":true,"description":"Compact fingerprint of the baseline build: `{ class, level, hash, gear: { slot: item name }, skills: { skill: points }, attributes, charms: names[], merc, apl }` (`hash`: SHA-256 of build + APL); null once the job row was pruned."}},"required":["slug","kind","createdAt","engineVersion","bundleId","primary","profiles","headline","mapDps","kills","deathRate","title","pinned","specHash","bundleHash","ci95","scenario","scenarioHash","fingerprint"]},"SimReportListDto":{"type":"object","properties":{"items":{"description":"Newest first.","type":"array","items":{"$ref":"#/components/schemas/SimReportListEntryDto"}},"total":{"type":"number","example":1},"page":{"type":"number","example":1},"pageSize":{"type":"number","example":25}},"required":["items","total","page","pageSize"]},"SimReportMatchDto":{"type":"object","properties":{"match":{"nullable":true,"description":"The caller's newest report of the identical spec (same spec hash), else null.","allOf":[{"$ref":"#/components/schemas/SimReportListEntryDto"}]}},"required":["match"]},"SimReportPatchDto":{"type":"object","properties":{"title":{"type":"string","nullable":true,"description":"1–64 characters; null or empty clears it."},"pinned":{"type":"boolean"}}},"SimReportJobDto":{"type":"object","properties":{"submittedAt":{"type":"string","format":"date-time"},"startedAt":{"type":"string","format":"date-time","nullable":true,"description":"When a worker picked the job up."},"finishedAt":{"type":"string","format":"date-time","nullable":true}},"required":["submittedAt","startedAt","finishedAt"]},"SimReportDto":{"type":"object","properties":{"slug":{"type":"string","example":"q8Zr0Lw3VvB2xT9mN4kD1A"},"kind":{"type":"string","enum":["quick_sim","compare","top_gear","upgrade_finder","stat_weights","skill_charm_optimizer"]},"summary":{"type":"object","additionalProperties":true,"description":"Engine `JobResult` (snake_case): `primary` metric (`dps`, `kill_time` or `death_rate`), per-profile `profiles[]` with `dps`, `kill_time_secs`, `death_rate`, damage `by_source`, paired `deltas` vs the baseline and `ranking`."},"spec":{"type":"object","additionalProperties":true,"nullable":true,"description":"The job's normalized `JobSpec` (re-run / fork); null once the job row was pruned."},"engineVersion":{"type":"string","example":"0.6.0"},"bundle":{"$ref":"#/components/schemas/SimBundleDto"},"specHash":{"type":"string","example":"a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3"},"createdAt":{"type":"string","format":"date-time"},"timelineExpiresAt":{"type":"string","format":"date-time","nullable":true,"description":"Until when `GET /api/sim/reports/{slug}/timeline` serves the combat logs; null when expired or none was recorded."},"owned":{"type":"boolean","description":"The caller owns the report (may delete it)."},"title":{"type":"string","nullable":true,"description":"The owner's name for the report (rename); null = the baseline profile's name."},"pinned":{"type":"boolean","description":"The owner's ★ baseline flag."},"job":{"nullable":true,"description":"When the job behind the report was submitted, picked up and finished; null once the job row was pruned.","allOf":[{"$ref":"#/components/schemas/SimReportJobDto"}]}},"required":["slug","kind","summary","spec","engineVersion","bundle","specHash","createdAt","timelineExpiresAt","owned","title","pinned","job"]},"SimTimelineProfileDto":{"type":"object","properties":{"profile":{"type":"number","description":"Index into `spec.profiles`.","example":0},"name":{"type":"string","example":"blizzard"},"log":{"type":"object","additionalProperties":true,"description":"Engine `CombatLog` of iteration 0: `entries[]` of `{ frame, actor, event }` (cast_start, hit, state_on, resources, …)."}},"required":["profile","name","log"]},"SimTimelineDto":{"type":"object","properties":{"expiresAt":{"type":"string","format":"date-time"},"profiles":{"description":"The requested profile (`?profile=`) when it recorded a log, else empty.","type":"array","items":{"$ref":"#/components/schemas/SimTimelineProfileDto"}}},"required":["expiresAt","profiles"]},"SimJobLogLineDto":{"type":"object","properties":{"id":{"type":"number","description":"Insertion order.","example":4211},"at":{"type":"string","format":"date-time"},"source":{"type":"string","enum":["api","worker"]},"workerId":{"type":"string","nullable":true,"example":"worker-77d067184591"},"level":{"type":"string","enum":["info","warn","error"]},"kind":{"type":"string","example":"chunk_done","description":"accepted, enqueue, start, pickup, chunk_done, merge, converged, report, …"},"message":{"type":"string","example":"chunk iterations 0–52 done: 52 iterations in 0.21 s (249 it/s)"},"fields":{"type":"object","additionalProperties":true,"nullable":true}},"required":["id","at","source","workerId","level","kind","message","fields"]},"SimReportLogDto":{"type":"object","properties":{"lines":{"description":"The hosted-runner log of the report's job, oldest first; empty once the lines passed their retention (`SIM_JOB_EVENT_RETENTION_DAYS`, 7 days by default) or the job row was pruned.","type":"array","items":{"$ref":"#/components/schemas/SimJobLogLineDto"}}},"required":["lines"]},"SimRankingScenarioDto":{"type":"object","properties":{"id":{"type":"string","example":"patchwerk"},"name":{"type":"string","example":"Training Dummy"},"metric":{"type":"string","enum":["dps","kill_time"],"description":"`dps` (higher is better) or `kill_time` in seconds (lower is better). The metric `entries[].value` is in.","example":"dps"}},"required":["id","name","metric"]},"SimRankingEntryDto":{"type":"object","properties":{"profileId":{"type":"string","example":"blizzard_ice_blast-bis"},"name":{"type":"string","example":"Blizzard / Ice Blast (BiS)"},"archetype":{"type":"string","description":"Curated APL (`sim/apl/<archetype>.json`).","example":"blizzard_ice_blast"},"archetypeName":{"type":"string","example":"Blizzard / Ice Blast"},"class":{"type":"string","example":"sorceress","description":"The profile build's class."},"tier":{"type":"string","enum":["starter","mid","bis","bossing"],"example":"bis"},"value":{"type":"number","nullable":true,"description":"Mean of the scenario's metric; null when no iteration killed the target.","example":54021.6},"ciHalfWidth":{"type":"number","nullable":true,"description":"95% CI half-width of `value`.","example":514.5},"dps":{"type":"object","description":"Engine `Summary` (snake_case): `n`, `mean`, `std_dev`, `ci95_half_width`, `min`, `max`.","additionalProperties":true},"killTimeSecs":{"type":"object","description":"Engine `Summary` (snake_case): `n`, `mean`, `std_dev`, `ci95_half_width`, `min`, `max`.","additionalProperties":true,"nullable":true},"deathRate":{"type":"object","nullable":true,"additionalProperties":true,"description":"Engine `Proportion`: `n`, `successes`, `p`, `ci95_low`, `ci95_high`."},"reportSlug":{"type":"string","nullable":true,"description":"Report permalink (`/sim/r/{slug}`, `GET /api/sim/reports/{slug}`).","example":"q8Zr0Lw3VvB2xT9mN4kD1A"},"engineVersion":{"type":"string","nullable":true,"example":"0.6.0"},"simmedAt":{"type":"string","format":"date-time","nullable":true},"pending":{"type":"boolean","description":"A newer run of this profile is in flight.","example":false}},"required":["profileId","name","archetype","archetypeName","class","tier","value","ciHalfWidth","dps","killTimeSecs","deathRate","reportSlug","engineVersion","simmedAt","pending"]},"SimRankingsDto":{"type":"object","properties":{"scenario":{"$ref":"#/components/schemas/SimRankingScenarioDto"},"scenarios":{"description":"Every ranking scenario, chart order.","type":"array","items":{"$ref":"#/components/schemas/SimRankingScenarioDto"}},"bundle":{"nullable":true,"description":"Data bundle the entries were simmed on; null before the first result.","allOf":[{"$ref":"#/components/schemas/SimBundleDto"}]},"entries":{"description":"Best first; profiles without a result are left out.","type":"array","items":{"$ref":"#/components/schemas/SimRankingEntryDto"}}},"required":["scenario","scenarios","bundle","entries"]},"EconomyCorruptionPriceResponse":{"type":"object","properties":{"corruptionKey":{"description":"Corruption key(s) identifying the corruption outcome.","type":"array","items":{"type":"string"}},"corruptionName":{"type":"string","description":"Human-readable corruption name."},"averagePrice":{"type":"number","description":"Average price in HR."},"medianPrice":{"type":"number","description":"Median price in HR."},"sampleCount":{"type":"number","description":"Number of listings sampled."}},"required":["corruptionKey","corruptionName","averagePrice","medianPrice","sampleCount"]},"EconomyItemResponse":{"type":"object","properties":{"name":{"type":"string","description":"Item name.","example":"Ber Rune"},"baseCode":{"type":"string","description":"Base code (grouped variants use a grouped code, e.g. `hellfire_torch_druid`).","example":"r30"},"typeCode":{"type":"string","description":"Item type code. Absent for `currency` and `ubers`.","example":"rune"},"price":{"type":"number","nullable":true,"description":"Headline price in HR: 7-day moving average, falling back to median (and to average/0 for facets, maps and charms)."},"averagePrice":{"type":"number","nullable":true,"description":"Average price in HR."},"medianPrice":{"type":"number","description":"Median listing price over the window, in HR."},"lastDayPrice":{"type":"number","description":"Trimmed mean of the most recent complete day's listings, in HR."},"movingAverage7Days":{"type":"number","description":"7-day moving average of daily prices, in HR (the headline price when present)."},"minPrice":{"type":"number","nullable":true,"description":"Minimum observed price in HR."},"maxPrice":{"type":"number","nullable":true,"description":"Maximum observed price in HR."},"sampleCount":{"type":"number","description":"Listings sampled in the `priceWindow` window. Items with fewer than 10 (even after the fallback) are low-confidence."},"priceWindow":{"type":"string","description":"Window the price was computed over. The requested rolling window (`filters.priceWindow`, e.g. `7d`, `24h`) when it has ≥10 samples; otherwise the shortest of `30d`, `season` (current season start..now) and `lastSeason` (previous season's last 30 days) that reaches 10. `season` for explicit past-season date ranges.","example":"7d"},"priceChange7Days":{"nullable":true,"description":"Change of the 7-day moving average versus 7 days before (same series as `/item-prices/average`).","allOf":[{"$ref":"#/components/schemas/PriceChange7Days"}]},"groupingData":{"type":"object","nullable":true,"additionalProperties":true,"description":"Variant-specific data for grouped items (e.g. facet damageType/damagePercent/piercePercent, class)."},"groupingLabel":{"type":"string","nullable":true,"description":"Display label of the grouped variant."},"corruptionPrices":{"description":"Top 5 corruption outcomes by median price (`charms` and `equipment-*` only).","type":"array","items":{"$ref":"#/components/schemas/EconomyCorruptionPriceResponse"}},"invcode":{"type":"string","nullable":true,"description":"Inventory icon file name (`rainbow-facets`, `maps-t5`, `charms`, `equipment-*`)."},"facetDamageType":{"type":"string","nullable":true,"description":"`rainbow-facets` only (legacy): facet element."},"facetDamagePercent":{"type":"number","nullable":true,"description":"`rainbow-facets` only (legacy): +% elemental damage."},"facetPiercePercent":{"type":"number","nullable":true,"description":"`rainbow-facets` only (legacy): -% enemy resistance."}},"required":["name","baseCode","price","averagePrice","minPrice","maxPrice","sampleCount","priceWindow"]},"EconomyFiltersResponse":{"type":"object","properties":{"isLadder":{"type":"boolean"},"isHardcore":{"type":"boolean"},"hours":{"type":"number","description":"Parsed rolling window in hours (168 when invalid/absent)."},"showLowConfidence":{"type":"boolean"},"priceWindow":{"type":"string","description":"Label of the requested window (`7d` for hours=168, `24h` for hours=24, `season` for a past-season range). Items whose `priceWindow` differs used a fallback window.","example":"7d"}},"required":["isLadder","isHardcore","hours","showLowConfidence","priceWindow"]},"EconomyItemsResponse":{"type":"object","properties":{"categoryName":{"type":"string","description":"Display name of the category.","example":"Runes"},"category":{"type":"string","description":"Category as requested (echoed verbatim).","example":"runes"},"items":{"type":"array","items":{"$ref":"#/components/schemas/EconomyItemResponse"}},"filters":{"$ref":"#/components/schemas/EconomyFiltersResponse"},"lastIndexTime":{"type":"string","nullable":true,"format":"date-time","description":"Last crawl time for the selected ladder/hardcore mode."}},"required":["categoryName","category","items","filters","lastIndexTime"]}}},"externalDocs":{"description":"Developer guides","url":"https://pd2trader.com/developers"}}