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:
| id | name | order |
|---|---|---|
| 6 | Basic Crafting Materials | 1 |
| 29 | Intermediate Crafting Materials | 2 |
| 37 | Advanced Crafting Materials | 3 |
| 46 | Ascended Materials | 4 |
| 30 | Gemstones and Jewels | 5 |
| 5 | Cooking Materials | 6 |
| 49 | Cooking Ingredients | 7 |
| 50 | Scribing Materials | 9 |
| 38 | Festive Materials | 10 |
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 byorder(R11/P3 #1–2). - V2 ordering — replaced "preserve the API's within-category order (= storage order)" with sort within-category by
/v2/materialsitems[]index, removing a dependency on undocumented upstream order (R10). - V5 — replaced the
coin.gold/silver/coppercolor 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/materialsshape{ id, name, order, items[] }, 9 categories,?ids=allworks, category table above — append todocs/architecture/gw2-api.md(§ Response shapes)./v2/currenciesordersemantics and the Gem-before-Coin gotcha (pin Coin by id) — same file./v2/commerce/prices206-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.