Skip to content

feat(api): usd_at_tx — the fiat companion, resolved at each event's own instant - #10431

Merged
JSONbored merged 1 commit into
mainfrom
feat/usd-at-tx
Aug 10, 2026
Merged

feat(api): usd_at_tx — the fiat companion, resolved at each event's own instant#10431
JSONbored merged 1 commit into
mainfrom
feat/usd-at-tx

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

src/price-at-tx.ts has said since #8369 that a USD figure was out of scope, and gave a reason:

no TAO/USD history exists anywhere in this system — no column in any table, no field in any route schema, and no ingestion job. The only TAO/USD in the product is a LIVE spot read from a third-party ticker performed 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, 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_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.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

formatAccountEvent 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.

One existing test had to move with it

The CSV projection test compared the export against formatAccountEvent alone, 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

  • 24 tests, 100% branch coverage (44/44) on src/price-at-tx.ts.
  • The at-or-before SQL verified against production: recent instants resolve to the reading just before them, and an instant from 2001 returns nothing rather than the earliest rate.
  • CSV carries the columns; the SDL mirrors both fields; all 45 validators, tsc, eslint, prettier clean; 2,916 tests green across the affected suites.

Refs #8602 — this is the usd_at_tx requirement. The remaining requirements on that issue (UI fiat display) are separate surfaces and are not claimed here.

…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
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
metagraphed-registry-sync-api fa31005 Aug 10 2026, 10:25 AM

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
metagraphed-wss-lb fa31005 Aug 10 2026, 10:25 AM

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
metagraphed-data-api fa31005 Aug 10 2026, 10:25 AM

@superagent-security

Copy link
Copy Markdown

Superagent didn't find any vulnerabilities or security issues in this PR.

@JSONbored JSONbored self-assigned this Aug 10, 2026
@JSONbored
JSONbored merged commit a28c671 into main Aug 10, 2026
14 checks passed
@JSONbored
JSONbored deleted the feat/usd-at-tx branch August 10, 2026 10:30
@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 71.42857% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 95.76%. Comparing base (3208898) to head (fa31005).
⚠️ Report is 2 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/alpha-usd-history.ts 43.75% 7 Missing and 2 partials ⚠️
workers/request-handlers/entities.ts 83.33% 0 Missing and 1 partial ⚠️
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     
Files with missing lines Coverage Δ
schemas-src/routes/subnet-events.ts 100.00% <ø> (ø)
src/graphql-sdl.ts 100.00% <ø> (ø)
src/price-at-tx.ts 100.00% <100.00%> (ø)
workers/request-handlers/entities.ts 89.49% <83.33%> (-0.05%) ⬇️
src/alpha-usd-history.ts 91.42% <43.75%> (-8.58%) ⬇️
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant