src/exchange/ is what a stack of currency, a scarab or a fragment is actually traded on. The
trade site has nothing to say about any of them — an exchange market is not a listing — so for
those items the search below the panel was never merely empty, it was the wrong question. This is
the right one, and it is GGG's own numbers rather than a third party's reading of them.
https://web.poecdn.com/api/currency-exchange[/<hour>]is public and unauthenticated, needs no registered application, and is on the CDN rather than the API host — so it carries noX-Rate-Limitpolicy headers and must not go throughtrade::request, which exists to serve budgets this endpoint does not publish. What stands in for a limiter is that one download covers every item in every league: the cost is per hour of play, not per price check.- It is purely historical, in hourly digests. The hour in progress does not exist — asking for
it answers 404 with a well-formed
{"next_change_id":…,"markets":[]}— so the freshest possible answer is the hour that just ended (latest_hour). The feed also publishes a few minutes late often enough to matter, soload_digeststeps back up tokStepBackHours. An empty payload is never cached: it is "not published yet", not an answer, andDigest::any_leagueis what tells that apart from "this league does not trade" (plenty of markets, none ours). - A published hour never changes, which is the whole cache design: the file name is the hour,
there is no etag, no TTL and no freshness decision — the file on disk either is the hour being
asked for or it is not. The newest
kKeepHours(2) are kept at ~2MB each; the second exists so that stepping back an hour costs no second download. - The payload states every item by its
Metadata/Items/...path and carries no names at all. That is whydata::BaseType::metadata_idexists —BaseItemTypes.Idin the game data is that path, and the data repo'semit/items.pynow writes it through. A bundle without the field simply has no exchange prices, the same shape ashas_unique_mods(): nothing here may assume one is there, and the app asks nothing when the item's base carries no id. - Ratios are ordered, not taken as named.
lowest_ratio/highest_ratioare integer counts of the two sides ({A: 1, B: 50}is one A for fifty B), so the price of one A in B is B/A — and "lowest ratio" is the lowest value of item over against, which is the item's dearest price. Reading the two names as a price band gets it backwards on some markets and right on others, because the counts move on both sides;min/maxis what covers both. A market that saw no trade is published with zeros, and dividing by that would put a nonsense price on screen rather than none. - The volume-weighted average is the headline, and the band is the detail under it.
volume_tradedis published for both sides of a market, and the two divided (Rate::average) are the mean ratio every trade in the hour cleared at — not the midpoint of a band whose ends one trade can set. Each market is drawn as a summary line and a small table beneath it: volume on both sides, then the lowest and the highest the hour saw. Stock is deliberately not shown — what is standing in the book says how long a sale would take, not what the item is worth. Zero volume on either side is no average at all (not a price of zero), and then the band itself is the summary. - A market is quoted against whichever of the two is worth more, so one side is always a
single unit:
4 x Winged Scarab = 1 x Chaos Orbis how the trade is said in game and "0.25 chaos each" is not. The average is what decides the direction (exchange::read), because a band can straddle one — 0.5 – 2 chaos an ember — and then has no direction of its own; the top of the band stands in when there is no volume to average. It falls out that a Chaos Orb in hand reads208 x Chaos Orb = 1 x Divine Orb— the same rate the poe.ninja row states for both orbs, from GGG's own book and an hour old rather than half an hour. - Every item in the feed is drawn with its own glyph, not just the two denominators. The
feed keys on metadata paths and neither symbol source has ever heard of one, so the join is
the display name:
TradeService::image_for_nameover every group of/api/trade/data/staticthat carries an image, falling back toNinjaService::icon_for_nameacross the overviews already held — the same two sources in the same order as every other symbol, and for the same reason (the trade static data is not fetched at all until the user's first search). The summary line draws the item's glyph without its name: it is the one row that has to fit both sides, "Cartography Scarab of Corruption" twice does not, and the full name is in every row under it. - The hour is stated in the user's own clock and date format — the digest is addressed by a
UTC hour, and a reader deciding whether a price is stale should have to do neither timezone
nor date-order arithmetic.
strftime("%x %H:%M")onlocaltime, soApp::runsetsLC_TIMEfrom the environment (the opposite call to theLC_NUMERICone beside it, and needed because a GUI-subsystem Windows binary has nothing else callingsetlocale), and the line is drawn infonts.unicodesince a locale's date can be Cyrillic or CJK. - Only markets against Chaos or Divine are kept. A scarab-for-essence market is a real market and no use to somebody asking what a scarab is worth.
- A market only appears in an hour it traded in, and the hour cannot say why. Measured on a live hour: 840 of the ~940 Allflame items that trade at all, and 115 of ~200 scarab varieties. So silence in the digest is two different facts wearing one face — this does not trade here and nobody traded one this hour — and for a thin item (a Weeping Essence of Greed) the second is the normal case. Reading it as the first left such an item with no exchange row, no trade search worth running and no poe.ninja price either: an empty panel, which reads as a check that gave up.
- So which items trade there at all is a fact about the item, and it comes from the bundle.
data::BaseType::exchangeis set by the data build, which crawls every hourly digest since Settlers launch and keeps the union of the metadata ids any market has ever named — 17.8k hours, ~1,000 ids, one boolean on a record that already carriesmetadata_id. The digest still supplies the price; this supplies the standing answer underneath it. A bundle published before the flag must not read as "does not trade," which an absent boolean cannot say on its own — so the signal is bundle-level:source.exchange_itemsin the manifest, written through byinstallbeside the attribution, and read back asGameData::has_exchange_flags(). Same distinctionhas_unique_mods()draws, drawn differently because that dataset is a whole file whose absence is the signal and this one is a field on records the bundle already had. - The exchange section has three states, and the middle one is why the flag exists: a market this hour draws the table; no market but the item is flagged draws one line saying so; and not flagged, or a bundle that cannot say, draws nothing at all. Claiming either way from a bundle with no exchange data would be a guess. That middle line is "no trades this hour" only when the hour was actually read — a digest whose fetch failed says nothing about what traded in it, so that case says so instead. The distinction is invisible otherwise: a flagged item gets no trade search either way, so stating the market was quiet on the strength of our own failed request would leave the user no way to tell an idle market from a broken download.
- An item that trades on the exchange gets no Search, no Open in browser and no filter list.
It has no listings for either button to find, and a button that can only ever come back empty
reads as the item being unsellable rather than as the wrong market having been asked. The
filters go with them (the panel layout is in architecture.md): they exist to shape a query nobody can run
here, and their notes about unmatched modifiers charge the check with a failure it never
attempted. One line says where the answer is instead.
That is keyed off the flag, not off the hour —
App::trades_on_exchange(). Keying it off a market in the last published hour gave a Weeping Essence of Greed a Search button on every hour nobody happened to trade one in, and the message alone would not have fixed that. It takes two sources, strongest evidence first: a market this hour proves the item trades there whatever the bundle says, which is also what keeps this working on a bundle older than the flag. ExchangeServiceis the fourth of theLeagueServicefamily and the smallest, because the feed is: one digest in memory at a time, a lookup is a string compare, and the second check of the hour is free whatever the item. It is asked about every item, not only the ones planned as currency — whether a thing trades there is a fact about the market rather than about our strategy.