Skip to content

Research 020 — Bank materials & wallet pages ​

Status: complete

Step 1.5 output, written between the spec draft and the approval gate. Every [NEEDS VERIFICATION] marker in spec.md (V1–V6) has a verdict here.

Verified against: the live Guild Wars 2 API v2 and the official wiki on 2026-08-17; the repo at branch 020-materials-and-wallet (spec-018 code as merged to main); and spec 018's own research.md / docs/architecture/gw2-api.md (live-dated 2026-08-16). GW2 drifts with patches — re-verify the live items if these go stale. All five external items were checked by parallel read-only discovery agents; the codebase item (V6) directly against the source.

V1 — Does /v2/materials give category name + order to enrich material rows? ​

Question. The Materials page groups into named sections sorted by a category order; the API joins categoryName + categoryOrder from /v2/materials keyed on each row's category id.

Verdict. Confirmed. The join is fully supported from one small, hard-cacheable dataset.

Evidence. Live 2026-08-17: GET /v2/materials → array of 9 category ids [5, 6, 29, 30, 37, 38, 46, 49, 50]; GET /v2/materials?ids=all → all 9 objects in one call, each { id:int, name:string, order:int, items:int[] } (wiki API:2/materials documents the same shape). The full set, sorted by order:

idnameorder
6Basic Crafting Materials1
29Intermediate Crafting Materials2
37Advanced Crafting Materials3
46Ascended Materials4
30Gemstones and Jewels5
5Cooking Materials6
49Cooking Ingredients7
50Scribing Materials9
38Festive Materials10

Sorting by order reproduces the in-game Material Storage section order. (order skips 8 — harmless; sort is still total.)

Caveat. None material. ?ids=all is not documented on the wiki but works live; a robust fetch can fall back to the bare-id list then a ≤199 batch (there are only 9). The dataset is immutable-ish → cache with no expiry like items.

V2 — Does the account read give the full storage grid, and in in-game order? ​

Question. The page renders every storage slot (dimming count:0) and lays items out in in-game order. That needs (1) /v2/account/materials to return the full grid incl count:0, and (2) a reliable within-category ordering.

Verdict — completeness. Confirmed (on paper; live-unobserved). The account read alone returns the full grid; no reconstruction from /v2/materials items[] is required.

Verdict — ordering. Resolved by design, not by upstream trust. Upstream array order is undocumented, so the web will sort within each category by the item's index in that category's items[] (from V1's dataset) — deterministic and independent of however GW2 orders the array.

Evidence. Wiki API:2/account/materials (2026-08-17): "Every material will be returned, even if they have a count of 0"; element shape { id, category, count, binding? }. Corroborated in-repo: specs/018-holdings-and-prices/research.md:57-73 and :190, and docs/architecture/gw2-api.md:72-74 ("Includes count: 0 slots"). The current service does not filter by count (apps/api/src/account/account.service.ts:19-38 keeps every row whose item id /v2/items resolves), so the full grid already survives to the response. On ordering, neither the wiki nor 018's research makes any array-order claim — 018 only consumed id/count/category, so order was never load-bearing.

Caveat. The 200 body of /v2/account/materials has never been observed live — 018 had no inventories-scoped key (specs/018.../research.md:14-18). Both "count:0 included" and "sort-by- items[]-index matches the in-game grid" should be confirmed by a dated manual record with a real key during implementation (the same device 018 used for its live 403 path). Correctness of the page does not depend on the live check, because ordering is imposed client-side (V2 ordering verdict).

V3 — Does sorting the wallet by order put Coin first? ​

Question. The Wallet sorts currencies by order (enriched from /v2/currencies) and shows Coin (id 1) as the g/s/c headline.

Verdict. Confirmed-with-caveat. order exists and orders the list correctly — but Coin is not the minimum. The page must pin Coin (id 1) to the top by id, then sort the remainder by order.

Evidence. Live 2026-08-17 GET /v2/currencies?ids=all (83 currencies): each object carries order:int (wiki: "a number that can be used to sort the list … least to greatest"). Gem (id 4) = order 100 sorts ahead of Coin (id 1) = order 101 (then Karma 102, Spirit Shard 103, …). A naive order-ascending sort therefore renders Gem, not Coin, first.

Caveat. order values drift as currencies are added, and one live id (74) has an empty name — pin Coin by id 1, never by a hardcoded order number. Gem still renders as a normal entry after the Coin headline (the design's choice is Coin-as-headline; it does not attempt to mirror in-game Gem-above- Coin placement).

V4 — Is sells.unit_price the right "list value", and are non-tradables omitted? ​

Question. sellPrice = sells.unit_price from /v2/commerce/prices (copper), or null when the item is not tradable/listed.

Verdict. Confirmed.

Evidence. Live 2026-08-17: ?ids=19721,19976,24295 → { id, whitelisted, buys:{quantity,unit_price}, sells:{quantity,unit_price} } with sells.unit_price > buys.unit_price for every row (wiki: buys.unit_price = "highest buy order", sells.unit_price = "the lowest sell offer price"). So sells.unit_price is the lowest sell listing — the chosen list-value basis — in copper. Omission: ?ids=19721,19925 (19925 = Obsidian Shard, account-bound) → HTTP 206 with only 19721 present (no error) → map the dropped id to sellPrice: null. In-repo: Gw2Service.prices() already exists, ?ids=-chunked ≤199, 60 s-TTL cached, Zod-validated, whitelisted stripped at the DTO (specs/018.../research.md:142-148).

Caveat. Do not use whitelisted to decide tradability — The Bifrost is whitelisted:false yet tradable and returned. Tradability = presence in the response, not the flag.

V5 — Concrete coin colors and icons, sourced with rarity-level rigor? ​

Question. Render coins as gold/silver/copper with coin-colored icons; source concrete values the way rarity colors were (from the wiki's own CSS/asset docs).

Verdict. Confirmed for icons; colors have no canonical source. Use the three official coin icons; drop the coin.gold/silver/copper color tokens — the wiki demonstrably documents no coin hex palette, so any such token would be an approximate literal, unlike the canonical rarity tokens.

Evidence. 2026-08-17: Template:Coin renders coins as image files, not colored text; no coin hex appears in Template:Coin, GW2W:Color schemes, or Property:Has metal color code. The three official icons (from the wiki File: pages Template:Coin uses at 18px) all return 200:

  • Gold — https://wiki.guildwars2.com/images/d/d1/Gold_coin.png
  • Silver — https://wiki.guildwars2.com/images/3/3c/Silver_coin.png
  • Copper — https://wiki.guildwars2.com/images/e/eb/Copper_coin.png

(The render-service URL for the silver coin 404s; the wiki-hosted copies are the reliable source.)

Caveat. These are ArenaNet assets served from the wiki — the same IP posture as the item/currency icons the app already loads from render.guildwars2.com. To avoid a cross-host runtime dependency (and the dead silver render URL), bundle the three PNGs as local static assets in apps/web, with provenance recorded; hotlinking is the fallback. Exact mechanism is a plan decision.

V6 — Can the API egress-join sellPrice without pinning prices in the per-user cache? ​

Question. AccountService.getMaterials adds sellPrice from Gw2Service.prices() without baking volatile prices into the 5-minute per-user cache.

Verdict. Confirmed (codebase). The architecture already enriches on egress; adding the price join follows the existing item-join pattern exactly.

Evidence. apps/api/src/gw2/gw2-client.ts: the per-user accountCache (ACCOUNT_TTL_MS = 300_000) is keyed ${prefix}:sha256(apiKey) and stores the raw parsed body (authedCachedRead → :335, :355); prices live in a separate priceCache (PRICE_TTL_MS = 60_000, :42, :106, :159-166). AccountService.getMaterials (account.service.ts:19-38) already reads the raw rows then joins Gw2Service.items(...) per call — enrichment is not cached per-user. Gw2Service.prices(ids) is public (gw2.service.ts:44-46) and AccountModule imports Gw2Module (account.module.ts:7), so the price join is reachable. A cache-hit still re-runs enrichment, so sellPrice reflects the 60 s price cache, never a 5-minute-stale value.

Caveat. None. Whether to also move category enrichment (static, hard-cached) through the same egress path is a plan detail; correctness is unaffected.

F7 — The service must keep returning count:0 rows (no filter regression) ​

Spec 018's research (F2) recommended filtering to count > 0 for a holdings view, but the shipped service does not filter (account.service.ts:19-38). Spec 020 depends on the unfiltered full grid to render dimmed empty slots. Why it matters: R10/SC2 — a future "tidy up" that reintroduces a count > 0 filter would silently break the dimmed-slot grid; the plan should add a test that a count:0 row survives to the response.

F8 — Enrichment batch size for materials + prices ​

A full material-storage grid is on the order of a few hundred distinct item ids (9 categories). Both the item join and the new price join chunk at ≤199 (gw2-client.ts), so a full grid is ~2–3 batches per enrichment. Why it matters: latency only — correctness does not depend on it (assumption already stated in spec.md). Prices are 60 s-cached, so repeat reads are cheap.

Refuted claims ​

No spec claim was hard-refuted (none sends the spec back to step 1). Three verifications selected a contingency the spec already anticipated, and one narrowed an approach; all are folded into spec.md:

  • V3 — the spec's "sort by order, Coin first (else special-case Coin)" resolves to the special-case branch: pin Coin id 1 to the top, order the rest by order (R11/P3 #1–2).
  • V2 ordering — replaced "preserve the API's within-category order (= storage order)" with sort within-category by /v2/materials items[] index, removing a dependency on undocumented upstream order (R10).
  • V5 — replaced the coin.gold/silver/copper color tokens with the three official coin icons (bundled), since the wiki has no canonical coin palette (R8/R11/R12, P3 #1, SC5/SC10).
  • V2 completeness / count:0 — confirmed on paper but live-unobserved; carried as a dated manual record obligation during implementation (traceability), not a code gate.

Graduation ​

Candidates to move to docs/architecture/ at step 6 (so the next spec doesn't re-derive them):

  • /v2/materials shape { id, name, order, items[] }, 9 categories, ?ids=all works, category table above — append to docs/architecture/gw2-api.md (§ Response shapes).
  • /v2/currencies order semantics and the Gem-before-Coin gotcha (pin Coin by id) — same file.
  • /v2/commerce/prices 206-omission-for-non-tradables and "whitelisted ≠ tradable" — same file.
  • Coin icons provenance + the dead silver render URL — record beside the coin-display decision (docs/architecture/design-system.md), explicitly contrasted with the canonical rarity tokens.