Skip to content

accounts/{ss58}/positions publishes a confident zero over real ledger rows: unpriceable positions are silently dropped #9305

Description

@JSONbored

/api/v1/accounts/{ss58}/positions publishes position_count: 0, total_stake_alpha: 0 and no
degraded block
for coldkeys that demonstrably hold positions in the ledger it reads.

Evidence

Coldkey 5Df7xwEPkZm4itD3PfSzHsV9extvnQpTFBiNCSgBCJtxEP9e is literally the first row of
chain.nominator_positions:

coldkey  5Df7xwEPkZm4itD3PfSzHsV9extvnQpTFBiNCSgBCJtxEP9e
hotkey   5FyVinYphF6JS5FZHzhMQffxtgbz1WxwUEBAxTRo9nABwb5g
netuid   18   share_fraction 1   captured_at 1785634702670

The route returns:

{ "position_count": 0, "total_stake_alpha": 0, "positions": [] }

No degraded marker — so this reads as measured fact: "this account holds nothing."

Root cause

buildAccountPositions (src/account-nominator-positions.ts:162) prices each position off the live
D1 neurons table and silently drops any position it cannot price:

const hotkeyStake = stakeByKey.get(`${hotkey}|${netuid}`);
if (hotkeyStake == null) continue;

neurons is healthy and current (129 netuids, 30,085 rows, captured_at minutes old), but it holds
only currently-registered neurons, while chain.nominator_positions is a frozen export from
2026-08-02. Sampling 8 distinct hotkeys from the ledger, only 1 of 8 is present in neurons.
So most positions are dropped, and an account whose positions are all dropped reports a confident
zero.

The unavailableAccountPositions card exists to prevent exactly this — but it only fires when every
tier declines. Here a tier answered successfully; the loss happens per-position, below that check.

Secondary finding: the hot leg cannot fire

nominator_positions does not exist as a table in the production D1 database:

SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%position%'
-> account_position_daily        (only)

migrations/d1/0011_nominator_positions.sql was written but never applied (d1_migrations has 0
rows — the schema was clearly created outside that mechanism, so the whole migration ledger is
unreliable here, not just this entry). #9273's design says the hot leg's emptiness makes "the
cutover a property of the data, not of a deploy" — but a missing table is not an empty one, so the
route is pinned to the frozen export indefinitely.

The contract question this needs a decision on

Dropping unpriced positions may well be deliberate — #9066 explicitly removed the unpriced_*
escape hatches elsewhere. But a caller currently cannot distinguish:

  • "this coldkey holds nothing", from
  • "this coldkey holds positions we could not price".

Those are opposite answers and the payload gives them the same shape. Options, smallest first:

  1. Mark the result degraded when any position was dropped for want of a price — reuses the
    existing degraded block, no schema change, and keeps fix(api): require the alpha price and drop the unpriced_* escape hatch #9066's no-unpriced-values rule intact.
    (Recommended.)
  2. Report a dropped_position_count alongside the list.
  3. Publish unpriced positions with a null stake_tao — contradicts fix(api): require the alpha price and drop the unpriced_* escape hatch #9066.

Acceptance

Metadata

Metadata

Assignees

Labels

maintainer-onlyOwner-only work — yields no Gittensor points.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions