Observed live on 2026-08-19 against a real read-only credential, while confirming the venue read path for #412. Latent, not active — nothing in adapter.py calls get_best_bid_ask today (transport.py:405 records that it and get_trading_pairs are the only two transport methods the adapter never invokes). Filing it so it is not rediscovered by whoever first reaches for this endpoint.
What the venue returns
GET /api/v2/crypto/trading/market_data/best_bid_ask/ reports bid above ask on the tightest pairs, persistently — three samples ~2s apart:
| symbol |
ask − bid, sample 1 |
sample 2 |
sample 3 |
| BTC-USD |
−5.90 (−0.86 bps) |
−1.90 (−0.28 bps) |
−9.42 (−1.38 bps) |
| ETH-USD |
+0.02 (+0.10 bps) |
−0.06 (−0.29 bps) |
−0.27 (−1.29 bps) |
| DOGE-USD |
−0.000001 (−0.11 bps) |
−0.000001 (−0.11 bps) |
−0.000004 (−0.59 bps) |
| XLM-USD |
+0.000053 (+3.21 bps) |
+0.000060 (+3.63 bps) |
+0.000084 (+5.09 bps) |
| ADA-USD |
+0.000043 (+2.41 bps) |
+0.000043 (+2.41 bps) |
+0.000045 (+2.49 bps) |
One row, verbatim:
{"symbol": "BTC-USD", "timestamp": "2026-08-19T19:18:23.411286272Z",
"bid": "68305.19", "ask": "68299.03"}
What it is not
Not a label swap. XLM-USD and ADA-USD never cross and carry sensible positive spreads; BTC/ETH/DOGE cross. A swapped pair of names would invert every symbol equally.
The pattern is that the two legs are sampled independently and then stamped with one timestamp. Where the true spread is wider than the sampling jitter (XLM, ADA at 2–5 bps) the ordering survives; where it is narrower (BTC, ETH, DOGE at well under 1 bp) the legs cross. The magnitude is consistent with that reading — the crossings are all ≲1.4 bps, never large.
So the row asserts a simultaneity it does not have. It is two near-simultaneous prices, not a book snapshot.
Why it matters later
translate.to_price_side maps BUY→ask and SELL→bid, and its docstring is explicit about the hazard:
Pricing a BUY preview off the bid, or a SELL preview off the ask, would show the wrong side of the spread — optimistic in exactly the direction that makes a synthesized preview look better than the fill it is meant to estimate.
On a crossed row that protection inverts: ask is the lower number and bid the higher, so a BUY priced off ask and a SELL priced off bid are both optimistic. Small in absolute terms, but it is a systematic bias in the one direction the mapping exists to prevent, on the live-money path.
Anything deriving a spread from this endpoint gets a negative number on precisely the assets keel trades most.
preview_order is unaffected: it uses estimated_price, which is self-consistent (side=ask → 68130.0 matching its own est_total_cost net of est_fee).
The fixture encodes the assumption the venue contradicts
tests/fixtures/rh_best_bid_ask.json has bid 65380.00 / ask 65480.00 — a tidy 15 bps spread that BTC-USD does not actually produce. Same class of problem as #230: a plausible-looking fixture standing in for an unobserved shape.
It also carries "next": null, "previous": null, which the live response for this endpoint does not include — the live body is {"results": [...]} only.
Scope
- Correct
rh_best_bid_ask.json to an observed row, crossing included, so any future consumer meets the real shape in tests.
- Document on
transport.get_best_bid_ask that the legs are not simultaneous and may cross, with the measured magnitude.
- Decide the contract before anything consumes it: either normalise (
lo = min(bid, ask), hi = max(...), price the conservative side per direction) or declare the endpoint unusable for pricing and keep estimated_price as the only quote source. Do not let a first consumer pick silently.
- If a spread check is ever added, it must tolerate a non-positive spread rather than treat it as a venue error.
Observed live on 2026-08-19 against a real read-only credential, while confirming the venue read path for #412. Latent, not active — nothing in
adapter.pycallsget_best_bid_asktoday (transport.py:405records that it andget_trading_pairsare the only two transport methods the adapter never invokes). Filing it so it is not rediscovered by whoever first reaches for this endpoint.What the venue returns
GET /api/v2/crypto/trading/market_data/best_bid_ask/reportsbidaboveaskon the tightest pairs, persistently — three samples ~2s apart:One row, verbatim:
{"symbol": "BTC-USD", "timestamp": "2026-08-19T19:18:23.411286272Z", "bid": "68305.19", "ask": "68299.03"}What it is not
Not a label swap. XLM-USD and ADA-USD never cross and carry sensible positive spreads; BTC/ETH/DOGE cross. A swapped pair of names would invert every symbol equally.
The pattern is that the two legs are sampled independently and then stamped with one timestamp. Where the true spread is wider than the sampling jitter (XLM, ADA at 2–5 bps) the ordering survives; where it is narrower (BTC, ETH, DOGE at well under 1 bp) the legs cross. The magnitude is consistent with that reading — the crossings are all ≲1.4 bps, never large.
So the row asserts a simultaneity it does not have. It is two near-simultaneous prices, not a book snapshot.
Why it matters later
translate.to_price_sidemaps BUY→askand SELL→bid, and its docstring is explicit about the hazard:On a crossed row that protection inverts:
askis the lower number andbidthe higher, so a BUY priced offaskand a SELL priced offbidare both optimistic. Small in absolute terms, but it is a systematic bias in the one direction the mapping exists to prevent, on the live-money path.Anything deriving a spread from this endpoint gets a negative number on precisely the assets keel trades most.
preview_orderis unaffected: it usesestimated_price, which is self-consistent (side=ask→ 68130.0 matching its ownest_total_costnet ofest_fee).The fixture encodes the assumption the venue contradicts
tests/fixtures/rh_best_bid_ask.jsonhas bid65380.00/ ask65480.00— a tidy 15 bps spread that BTC-USD does not actually produce. Same class of problem as #230: a plausible-looking fixture standing in for an unobserved shape.It also carries
"next": null, "previous": null, which the live response for this endpoint does not include — the live body is{"results": [...]}only.Scope
rh_best_bid_ask.jsonto an observed row, crossing included, so any future consumer meets the real shape in tests.transport.get_best_bid_askthat the legs are not simultaneous and may cross, with the measured magnitude.lo = min(bid, ask),hi = max(...), price the conservative side per direction) or declare the endpoint unusable for pricing and keepestimated_priceas the only quote source. Do not let a first consumer pick silently.