feat(api): usd_at_tx — the fiat companion, resolved at each event's own instant - #10431
Merged
Conversation
…wn instant src/price-at-tx.ts has said since #8369 that a USD figure was out of scope here, and gave a reason: no TAO/USD history existed anywhere in the system, and the product's only fiat price was a live third-party ticker read in the browser. Both facts are now false. tao_usd_index has been written about once a minute since 2026-08-02, it is FIRST-PARTY (a liquidity-weighted median across qualifying wTAO/WETH pools through an ETH/USDC anchor, ADR 0025 -- not a venue quote), and the UI stopped calling coinpaprika entirely. The objection that actually stood was never "USD is unknowable from chain data". It was "we do not have a first-party rate, and putting someone else's feed on the data plane is not a trade this project makes". We have one now. RESOLVED AT-OR-BEFORE, PER EVENT, NEVER INTERPOLATED. A candle has a bucket; an event has an instant. Pricing an event from a bucket it merely falls inside can use a reading that POST-DATES it -- a rate that did not exist when the trade executed. So each event is matched to the newest priced reading at or before its own observed_at. NULL FOR ANY EVENT PREDATING THE INDEX. The index starts when we started collecting; carrying the oldest rate backwards would be fabrication dressed as data. Those events are simply absent from the lookup map, so absence means "no rate existed" and never "the rate was null". A DIFFERENT BASIS WORD, deliberately. `price_basis: trade_exact` means the alpha price came from this row's own two legs and is exact. `usd_basis: index_at_or_before` means the dollar leg is a lookup. A consumer must be able to tell them apart rather than reading one confidence off the other. ONE QUERY PER PAGE, not per row: a LATERAL over the page's instants. Measured against production for a 200-event page -- 1.9ms, 200 probes on the existing idx_tao_usd_index_observed. No N+1, no windowed scan. Applied as an OVERLAY rather than a second parameter on formatAccountEvent. That formatter is used as a bare `.map(formatAccountEvent)` in several modules, so a positional second argument would silently receive the array INDEX -- a wrong rate on every row, stated confidently, with nothing to catch it. Keeping the formatter one-argument makes that impossible rather than merely unlikely. The CSV projection test moves with it: it compared the export against formatAccountEvent alone, which would have called the new fields "columns with no source field". The invariant is about what is SERVED, so the fixture is now the served shape -- formatter plus overlay -- and it still fails in both directions. Refs #8602
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
metagraphed-registry-sync-api | fa31005 | Aug 10 2026, 10:25 AM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
metagraphed-wss-lb | fa31005 | Aug 10 2026, 10:25 AM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
metagraphed-data-api | fa31005 | Aug 10 2026, 10:25 AM |
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #10431 +/- ##
==========================================
- Coverage 95.78% 95.76% -0.03%
==========================================
Files 668 668
Lines 41841 41875 +34
Branches 15476 15492 +16
==========================================
+ Hits 40076 40100 +24
- Misses 592 599 +7
- Partials 1173 1176 +3
🚀 New features to boost your workflow:
|
JSONbored
added a commit
that referenced
this pull request
Aug 10, 2026
…ess precise one (#10433) * feat(api): /network/tao-usd states its staleness instead of implying it #8601 requirement 3 asks for staleness to be explicit in the payload, not inferred by the caller from a timestamp. The route published `observed_at` and stopped there, which leaves every consumer to parse it, know the bound, and compare correctly -- three chances to get it wrong, and a consumer that simply skips the check reads a frozen rate as a current one. That is #9704's shape: a value with no live writer behind it, served at 200 OK. stale true when the newest reading is past the bound stale_after_ms the bound itself, so the check is reproducible age_ms how old it actually is, so "3 minutes ago" needs no re-derivation The bound is TAO_USD_MAX_AGE_MS -- the SAME one src/alpha-usd.ts refuses to multiply by. Imported rather than restated, so "this response says stale" and "no USD figure anywhere on the API" are one condition instead of two thresholds that drift apart and give an operator two answers. A READING THAT CANNOT SAY WHEN IT WAS TAKEN IS STALE, NEVER FRESH. Defaulting the unknown direction to "current" is exactly how a frozen rate survives a staleness check, and an empty window is stale for the same reason. Caught while writing the test: `Number(null)` and `Number("")` are both 0, and 0 is FINITE -- so a missing stamp came out as "aged since the epoch", a 56-year age reported as a number rather than as unknown. Third time this coercion has bitten in this area, so the helper now takes only a real number or a string that says one. Closes #8601 * feat(ui): render the fiat leg beside price_at_tx, and say it is the less precise one #8602 requirement 2. The event price line showed TAO only, and the component's own header said there was no USD companion "on purpose" -- pointing at the follow-up issue that has now shipped the field (#10431). The dollar figure renders as secondary text beside the TAO price, and renders NOTHING when the API resolved none. That silence is the component's existing rule and it applies here for a second reason: an event predating the index has no dollar price at all, so an em-dash would imply a value being withheld rather than one that never existed. THE TOOLTIP SAYS WHICH FIGURE IS WHICH. `price_at_tx` is this trade's own execution price, from its two legs. `usd_at_tx` is a LOOKUP against the newest index reading at-or-before it. A reader who cannot tell those apart will read one confidence off the other, so the hover states the basis, the reading's observed-at, and how many qualifying pools contributed -- and says outright that the conversion is the less precise of the two. Both tooltips are exported separately from the component so the wording is unit-testable, which is the convention priceAtTxTooltip already set. The component had no test file at all. It has one now, covering both halves: the fiat leg absent on a null rate, absent on a non-finite one, the whole line absent with no TAO price, singular/plural pool wording, and a zero pool count treated as no provenance rather than "across 0". Refs #8602
This was referenced Aug 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
src/price-at-tx.tshas said since #8369 that a USD figure was out of scope, and gave a reason:Both facts are now false.
tao_usd_indexhas been written about once a minute since 2026-08-02, it is first-party (a liquidity-weighted median across qualifying wTAO/WETH pools through an ETH/USDC anchor, per ADR 0025 — not a venue quote), and the UI stopped calling coinpaprika entirely.The objection that actually stood was never "USD is unknowable from chain data". It was "we do not have a first-party rate, and putting someone else's feed on the data plane is not a trade this project makes". We have one now.
The four things that make it honest
Resolved at-or-before, per event, never interpolated. A candle has a bucket; an event has an instant. Pricing an event from a bucket it merely falls inside can use a reading that post-dates it — a rate that did not exist when the trade executed. Each event is matched to the newest priced reading at or before its own
observed_at.Null for any event predating the index. The index starts when we started collecting; carrying the oldest rate backwards would be fabrication dressed as data. Those events are simply absent from the lookup map, so absence means "no rate existed" and never "the rate was null".
A different basis word, deliberately.
price_basis: trade_exactmeans the alpha price came from this row's own two legs and is exact.usd_basis: index_at_or_beforemeans the dollar leg is a lookup. A consumer must be able to tell them apart rather than reading one confidence off the other.One query per page, not per row. A LATERAL over the page's instants. Measured against production for a 200-event page: 1.9 ms, 200 probes on the existing
idx_tao_usd_index_observed. No N+1, no windowed scan.Applied as an overlay, and that is load-bearing
formatAccountEventis used as a bare.map(formatAccountEvent)in several modules, so a positional second argument would silently receive the array index — a wrong rate on every row, stated confidently, with nothing to catch it. Keeping the formatter one-argument makes that impossible rather than merely unlikely.One existing test had to move with it
The CSV projection test compared the export against
formatAccountEventalone, and asserted both directions — no dropped fields, no orphaned columns. The fiat fields come from the overlay, so it would have called them "columns with no source field". The invariant is about what is served, so the fixture is now the served shape (formatter + overlay) and it still fails in both directions.Verification
src/price-at-tx.ts.tsc,eslint,prettierclean; 2,916 tests green across the affected suites.Refs #8602 — this is the
usd_at_txrequirement. The remaining requirements on that issue (UI fiat display) are separate surfaces and are not claimed here.