Summary
/wallet-api/portfolio-v2 with onlyEnabled: true (what the web and mobile clients send) returns zero engine tokens and zero chain wallets for accounts that have explicitly enabled them. The response is a normal 200, the Hive layer is correct, and nothing is logged.
Root cause
The enabled-token allowlist is derived from the account's posting_json_metadata:
PortfolioV2 builds it via ExtractEnabledEngineTokenSymbols(accountData) (Handlers/WalletApi.Engine.cs), and ExtractExternalWallets reads the same metadata for the chain layer.
accountData comes from HiveExplorer.GetAccount -> HiveClients.Default.GetAccounts -> condenser_api.get_accounts.
Two nodes in HiveClients.Default (Infrastructure/HiveRpcClient.cs) serve accounts with posting_json_metadata always empty, while every other field (balances, reputation) is correct:
| node |
posting_json_metadata |
https://techcoderx.com |
empty for every account tested |
https://hiveapi.actifit.io |
empty for every account tested |
api.hive.blog, api.deathwing.me, rpc.mahdiyari.info, api.openhive.network |
full |
Consequences:
HiveRpcClient only validates the result shape (r is JsonArray), so a metadata-stripped 200 is recorded as a healthy response.
NodeHealthTracker orders healthy nodes best-first by latency EWMA. These two nodes are among the fastest in the pool, so they win the ranking and serve most account fetches.
ExtractEnabledEngineTokenSymbols returns an empty set, BuildEngineLayer(onlyEnabled: true, allowedSymbols: <empty>) drops every token, and the chain layer drops every wallet.
- Balances still look right, because the Hive layer reads account fields rather than metadata. The failure is invisible: no exception, no log line, HTTP 200.
This is the same class of bug as the malformed-200 case already described in CLAUDE.md under "Upstream node failover": a node that answers successfully but uselessly is scored as healthy and stays ranked first.
Reproduction
Against production, for an account with enabled engine tokens:
onlyEnabled: true -> wallets: points + hive only, engine 0, chain 0
onlyEnabled: false -> engine layer fully populated
Every symbol in the account's metadata allowlist is present in the unfiltered engine payload, so the data is fetched correctly and then discarded by the filter. ExtractEnabledEngineTokenSymbols itself is correct: fed a real account object it returns the expected symbol set.
Because node ranking shifts with latency, this presents to users as engine balances intermittently disappearing and reappearing rather than as a constant failure.
Proposed fix
- Treat a metadata-less account response as a node failure in the typed
get_accounts helper, so it fails over instead of being cached as healthy — mirroring the existing result-shape validation. The signal needs to distinguish a pruned node from an account that genuinely has no metadata; a node returning empty metadata for an account that has a non-empty json_metadata/profile elsewhere, or a capability probe recorded per node, both work.
- Drop or hard-deprioritize nodes that do not serve account metadata, so the portfolio path never depends on latency ranking for correctness.
- Stop degrading silently: when
onlyEnabled: true and the metadata could not be read, the request should not render as "user has no tokens". Surface it (error or explicit partial-response marker) so clients can retry rather than paint an empty wallet.
Items 1 and 2 are the fix; item 3 is what turns the next occurrence into something detectable.
Tests
Extend HiveRpcFailoverTests with a stub node that returns a well-formed account carrying empty metadata, and assert the client fails over to a node that serves it.
Summary
/wallet-api/portfolio-v2withonlyEnabled: true(what the web and mobile clients send) returns zero engine tokens and zero chain wallets for accounts that have explicitly enabled them. The response is a normal200, the Hive layer is correct, and nothing is logged.Root cause
The enabled-token allowlist is derived from the account's
posting_json_metadata:PortfolioV2builds it viaExtractEnabledEngineTokenSymbols(accountData)(Handlers/WalletApi.Engine.cs), andExtractExternalWalletsreads the same metadata for the chain layer.accountDatacomes fromHiveExplorer.GetAccount->HiveClients.Default.GetAccounts->condenser_api.get_accounts.Two nodes in
HiveClients.Default(Infrastructure/HiveRpcClient.cs) serve accounts withposting_json_metadataalways empty, while every other field (balances, reputation) is correct:posting_json_metadatahttps://techcoderx.comhttps://hiveapi.actifit.ioapi.hive.blog,api.deathwing.me,rpc.mahdiyari.info,api.openhive.networkConsequences:
HiveRpcClientonly validates the result shape (r is JsonArray), so a metadata-stripped200is recorded as a healthy response.NodeHealthTrackerorders healthy nodes best-first by latency EWMA. These two nodes are among the fastest in the pool, so they win the ranking and serve most account fetches.ExtractEnabledEngineTokenSymbolsreturns an empty set,BuildEngineLayer(onlyEnabled: true, allowedSymbols: <empty>)drops every token, and the chain layer drops every wallet.This is the same class of bug as the malformed-200 case already described in
CLAUDE.mdunder "Upstream node failover": a node that answers successfully but uselessly is scored as healthy and stays ranked first.Reproduction
Against production, for an account with enabled engine tokens:
Every symbol in the account's metadata allowlist is present in the unfiltered engine payload, so the data is fetched correctly and then discarded by the filter.
ExtractEnabledEngineTokenSymbolsitself is correct: fed a real account object it returns the expected symbol set.Because node ranking shifts with latency, this presents to users as engine balances intermittently disappearing and reappearing rather than as a constant failure.
Proposed fix
get_accountshelper, so it fails over instead of being cached as healthy — mirroring the existing result-shape validation. The signal needs to distinguish a pruned node from an account that genuinely has no metadata; a node returning empty metadata for an account that has a non-emptyjson_metadata/profile elsewhere, or a capability probe recorded per node, both work.onlyEnabled: trueand the metadata could not be read, the request should not render as "user has no tokens". Surface it (error or explicit partial-response marker) so clients can retry rather than paint an empty wallet.Items 1 and 2 are the fix; item 3 is what turns the next occurrence into something detectable.
Tests
Extend
HiveRpcFailoverTestswith a stub node that returns a well-formed account carrying empty metadata, and assert the client fails over to a node that serves it.