Currency top-holders / per-account balances exporter (deferred from #53) #58

Closed
opened 2026-07-05 03:14:55 +00:00 by coilysiren · 3 comments
Owner

Split out from #53 (currency & money-supply surface) per the AGENTS.md pull-everything rule.

Gap

get_eco_currency meets DiscordLink's Currencies and most of Currency <name> from today's export surface, but the top-holders list in Currency <name> is not reachable. No current endpoint carries per-account currency balances:

  • PersonalWealthInDefaultCurrency / GovernmentHoldingsInDefaultCurrency are single aggregate series in the default currency, not per-account and not per-currency.
  • CurrencyTrade rows give buyer/seller flows, not balances, and the numeric buyer/seller ids still hit the id-to-name join blocker (#5).

The per-currency report renders a holders unavailable note pointing here rather than faking a list.

What this needs

A reset-gated stores/economy exporter mod that reads CurrencyManager holdings in-process and exports, per currency, the top account balances (joined to names via the same UserManager access the jobs mod already uses for /api/v1/citizens). Once that surface exists, wire it into currency.py's per-currency report and drop the deferred note.

Dependencies

  • Parent surface: #53
  • Epic: #37 (DiscordLink replacement)
  • Related: #5 (id-to-name join), #7 (dataset survey)
Split out from #53 (currency & money-supply surface) per the AGENTS.md pull-everything rule. ## Gap `get_eco_currency` meets DiscordLink's `Currencies` and most of `Currency <name>` from today's export surface, but the **top-holders list** in `Currency <name>` is not reachable. No current endpoint carries per-account currency balances: * `PersonalWealthInDefaultCurrency` / `GovernmentHoldingsInDefaultCurrency` are single **aggregate** series in the default currency, not per-account and not per-currency. * `CurrencyTrade` rows give buyer/seller **flows**, not balances, and the numeric buyer/seller ids still hit the id-to-name join blocker (#5). The per-currency report renders a `holders unavailable` note pointing here rather than faking a list. ## What this needs A reset-gated stores/economy exporter mod that reads `CurrencyManager` holdings in-process and exports, per currency, the top account balances (joined to names via the same `UserManager` access the jobs mod already uses for `/api/v1/citizens`). Once that surface exists, wire it into `currency.py`'s per-currency report and drop the deferred note. ## Dependencies * Parent surface: #53 * Epic: #37 (DiscordLink replacement) * Related: #5 (id-to-name join), #7 (dataset survey)
Author
Owner

Director surface (read-only) - scope for the engineer run.

Headless scope: author the reset-gated stores/economy exporter that reads CurrencyManager holdings in-process and exports, per currency, the per-account balances (the top-holders list get_eco_currency currently renders as "holders unavailable"). Follow the existing patterns in mods/stores/src (the store exporter builds green today).

Done-condition: dotnet build mods/stores/src/EcoStoreExporter.csproj green on main.

Out of scope for this run (Kai's hands, live): the reset-gated deploy to kai-server + on-cycle verify. Note the buyer/seller numeric ids still depend on the citizen id->name join (#65) being live to render names rather than Citizen #<id>.

**Director surface (read-only) - scope for the engineer run.** Headless scope: author the reset-gated stores/economy exporter that reads `CurrencyManager` holdings in-process and exports, per currency, the per-account balances (the top-holders list `get_eco_currency` currently renders as "holders unavailable"). Follow the existing patterns in `mods/stores/src` (the store exporter builds green today). Done-condition: `dotnet build mods/stores/src/EcoStoreExporter.csproj` green on `main`. Out of scope for this run (Kai's hands, live): the reset-gated deploy to kai-server + on-cycle verify. Note the buyer/seller numeric ids still depend on the citizen id->name join (#65) being live to render names rather than `Citizen #<id>`.
Member

🔒 Reserved by ward agent --driver claude — container engineer-claude-eco-app-58 on host KAI-DESKTOP-TOWER is carrying this issue (reserved 2026-07-06T03:15:18Z). Concurrent ward agent runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); --force overrides.

Do not comment on or edit this issue to steer the run while it is reserved. The engineer seeded the body once at launch and never re-reads it, so a comment or edit reaches only human readers, never the running engineer. A correction goes to a new issue, dispatched fresh — that is the only channel that reaches a run in flight. Where the forge supports it, ward locks this conversation to make that a road-block rather than a convention (ward#494).

run seed context — what this run is carrying (ward#609)
  • Resolved: coilyco-gaming/eco-app#58 · branch issue-58 · driver claude · workflow direct-main
  • Run: engineer-claude-eco-app-58 · ward v0.413.0 · dispatched 2026-07-06T03:15:18Z
  • Comment thread: 1 included in the pre-flight read, 0 stripped (ward's own automated comments).

Issue body as seeded:

Split out from #53 (currency & money-supply surface) per the AGENTS.md pull-everything rule.

## Gap

`get_eco_currency` meets DiscordLink's `Currencies` and most of `Currency <name>` from today's export surface, but the **top-holders list** in `Currency <name>` is not reachable. No current endpoint carries per-account currency balances:

* `PersonalWealthInDefaultCurrency` / `GovernmentHoldingsInDefaultCurrency` are single **aggregate** series in the default currency, not per-account and not per-currency.
* `CurrencyTrade` rows give buyer/seller **flows**, not balances, and the numeric buyer/seller ids still hit the id-to-name join blocker (#5).

The per-currency report renders a `holders unavailable` note pointing here rather than faking a list.

## What this needs

A reset-gated stores/economy exporter mod that reads `CurrencyManager` holdings in-process and exports, per currency, the top account balances (joined to names via the same `UserManager` access the jobs mod already uses for `/api/v1/citizens`). Once that surface exists, wire it into `currency.py`'s per-currency report and drop the deferred note.

## Dependencies

* Parent surface: #53
* Epic: #37 (DiscordLink replacement)
* Related: #5 (id-to-name join), #7 (dataset survey)

Static container doctrine and seed boilerplate are identical every run and omitted here (they ride ward v0.413.0).

— Claude (she/her), via ward agent

<!-- ward-agent-reservation --> 🔒 Reserved by `ward agent --driver claude` — container `engineer-claude-eco-app-58` on host `KAI-DESKTOP-TOWER` is carrying this issue (reserved 2026-07-06T03:15:18Z). Concurrent `ward agent` runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); `--force` overrides. **Do not comment on or edit this issue to steer the run while it is reserved.** The engineer seeded the body once at launch and never re-reads it, so a comment or edit reaches only human readers, never the running engineer. A correction goes to a **new issue, dispatched fresh** — that is the only channel that reaches a run in flight. Where the forge supports it, ward locks this conversation to make that a road-block rather than a convention (ward#494). <details><summary>run seed context — what this run is carrying (ward#609)</summary> - **Resolved:** `coilyco-gaming/eco-app#58` · branch `issue-58` · driver `claude` · workflow `direct-main` - **Run:** `engineer-claude-eco-app-58` · ward `v0.413.0` · dispatched `2026-07-06T03:15:18Z` - **Comment thread:** 1 included in the pre-flight read, 0 stripped (ward's own automated comments). - included: @coilysiren (2026-07-06T03:13:19Z) **Issue body as seeded:** ``` Split out from #53 (currency & money-supply surface) per the AGENTS.md pull-everything rule. ## Gap `get_eco_currency` meets DiscordLink's `Currencies` and most of `Currency <name>` from today's export surface, but the **top-holders list** in `Currency <name>` is not reachable. No current endpoint carries per-account currency balances: * `PersonalWealthInDefaultCurrency` / `GovernmentHoldingsInDefaultCurrency` are single **aggregate** series in the default currency, not per-account and not per-currency. * `CurrencyTrade` rows give buyer/seller **flows**, not balances, and the numeric buyer/seller ids still hit the id-to-name join blocker (#5). The per-currency report renders a `holders unavailable` note pointing here rather than faking a list. ## What this needs A reset-gated stores/economy exporter mod that reads `CurrencyManager` holdings in-process and exports, per currency, the top account balances (joined to names via the same `UserManager` access the jobs mod already uses for `/api/v1/citizens`). Once that surface exists, wire it into `currency.py`'s per-currency report and drop the deferred note. ## Dependencies * Parent surface: #53 * Epic: #37 (DiscordLink replacement) * Related: #5 (id-to-name join), #7 (dataset survey) ``` Static container doctrine and seed boilerplate are identical every run and omitted here (they ride ward v0.413.0). </details> <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Author
Owner

WARD-OUTCOME: done - live per-currency top-holders exporter (/api/v1/currency-holdings) shipped in mods/stores and wired into the Currency <name> report, deferred note dropped, landed on main.

Honestly this one went smoother than I expected, and the reason is that the repo had already built the exact pattern I needed twice over. The stores mod's reflection-by-name StoreScanner and the jobs mod's typed UserManager join were a ready-made template: one stable typed touchpoint, everything drifty read by name, skip-and-continue on nulls. So the shape of the C# side wrote itself. I extracted the shared reflection primitives into a Reflect.cs so the new CurrencyHoldingsScanner and StoreScanner stop duplicating them.

The part that genuinely fought back was guessing the Eco economy API blind. My first pass assumed CurrencyManager.Currencies + a BankAccountManager.Obj singleton and account.CurrencyHoldings as a flat list. It compiled — reference-assembly builds happily compile against wrong guesses — but that's exactly the trap: a green compile gate says nothing about whether reflection resolves at runtime. So I loaded the real Eco.ReferenceAssemblies through a MetadataLoadContext probe and checked every member. Two would have silently returned empty on a live server: CurrencyHoldings is a ControllerDictionary (enumerating it yields KeyValuePairs, not holdings — I now read .Values), and the owner is AccountOwner, not Owner. The singleton .Obj also needed FlattenHierarchy to resolve through the Singleton<T> base. Glad I checked rather than trusting the build.

Confidence: high on the Python wiring and DTO contract (fully unit-tested end-to-end against the shell harness shape, both reachable and mod-undeployed paths). Medium-high on the live C# walk — every member name is verified against 0.13.0.4-1024 metadata and the shell endpoint serializes correctly, but I could not exercise the reflection against a running server from here, so the true proof is the first live deploy. The design fails safe (empty list, never a 500), and the Python side degrades to the unavailable note if the mod isn't deployed, so a wrong guess is visible, not silent.

Rough edges / follow-ups worth filing: (1) the SPA currency strip carries the new topHolders fields on its type but doesn't render them yet — a natural /trade enhancement. (2) Owner attribution is single-owner only; government/company accounts show the account name with a null holder, which is correct but coarser than a per-member split. (3) Worth a quick sanity pass on the live server once the DLL lands to confirm the account-enumeration strategy picks BankAccountManager.Obj.Accounts.

WARD-OUTCOME: done - live per-currency top-holders exporter (`/api/v1/currency-holdings`) shipped in mods/stores and wired into the `Currency <name>` report, deferred note dropped, landed on main. Honestly this one went smoother than I expected, and the reason is that the repo had already built the exact pattern I needed twice over. The stores mod's reflection-by-name `StoreScanner` and the jobs mod's typed `UserManager` join were a ready-made template: one stable typed touchpoint, everything drifty read by name, skip-and-continue on nulls. So the shape of the C# side wrote itself. I extracted the shared reflection primitives into a `Reflect.cs` so the new `CurrencyHoldingsScanner` and `StoreScanner` stop duplicating them. The part that genuinely fought back was guessing the Eco economy API blind. My first pass assumed `CurrencyManager.Currencies` + a `BankAccountManager.Obj` singleton and `account.CurrencyHoldings` as a flat list. It compiled — reference-assembly builds happily compile against wrong guesses — but that's exactly the trap: a green compile gate says nothing about whether reflection resolves at runtime. So I loaded the real `Eco.ReferenceAssemblies` through a `MetadataLoadContext` probe and checked every member. Two would have silently returned empty on a live server: `CurrencyHoldings` is a `ControllerDictionary` (enumerating it yields KeyValuePairs, not holdings — I now read `.Values`), and the owner is `AccountOwner`, not `Owner`. The singleton `.Obj` also needed `FlattenHierarchy` to resolve through the `Singleton<T>` base. Glad I checked rather than trusting the build. Confidence: high on the Python wiring and DTO contract (fully unit-tested end-to-end against the shell harness shape, both reachable and mod-undeployed paths). Medium-high on the live C# walk — every member name is verified against 0.13.0.4-1024 metadata and the shell endpoint serializes correctly, but I could not exercise the reflection against a running server from here, so the true proof is the first live deploy. The design fails safe (empty list, never a 500), and the Python side degrades to the unavailable note if the mod isn't deployed, so a wrong guess is visible, not silent. Rough edges / follow-ups worth filing: (1) the SPA currency strip carries the new `topHolders` fields on its type but doesn't render them yet — a natural `/trade` enhancement. (2) Owner attribution is single-owner only; government/company accounts show the account name with a null holder, which is correct but coarser than a per-member split. (3) Worth a quick sanity pass on the live server once the DLL lands to confirm the account-enumeration strategy picks `BankAccountManager.Obj.Accounts`.
Sign in to join this conversation.
No description provided.