Skip to content

OKX's DEX feed is open after all, on a channel their docs do not mention - #2791

Merged
Flotapponnier merged 1 commit into
devfrom
feat/okx-head-lag
Oct 3, 2026
Merged

Flotapponnier merged 1 commit into
devfrom
feat/okx-head-lag

Conversation

@Flotapponnier

@Flotapponnier Flotapponnier commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator

Adds OKX as a fifth provider on bench 001, on Base and Solana.

The access problem, and why it is not one

OKX's documented trades channel is wss://wsdex.okx.com/ws/v6/dex, channel
trades. It answers {"event":"error","msg":"Please log in","code":"60011"} to
every subscribe, including their documentation's own verbatim example. Their fee
table explains it:

Tier Price Supported channels
Free $0 WebSocket not supported
Starter $99/mo All channels

Their web frontend holds a different socket that needs no credential. Two
things hid it, and both fail silently:

  • the channel is dex-market-trade-history, not any trades variant. Twenty
    other names were rejected before this one;
  • the params go inside an extraParams JSON string. With flat
    chainIndex/tokenContractAddress keys the gateway still answers
    {"event":"subscribe"} and then pushes nothing, which is indistinguishable
    from a dead channel. An ack proves nothing here; only frames do.

Both are pinned by tests, because a silent move should fail loudly.

Why Base and Solana only

Neither exclusion is provisional.

BNB publishes head_lag_seconds as receiveTime minus the provider's own
timestamp — it is in neither referenceChains nor raceChains. Every OKX
timestamp is a whole second: 1,800 sampled across three chains, all
ts%1000 == 0. Putting a second-quantised clock in a column of millisecond ones
would publish our arithmetic as OKX's latency. Base reads the reference clock
and Solana the race, neither of which touches the provider timestamp.

Robinhood Chain is not in OKX's supported set at all.

Why a token-scoped feed is still apple-to-apple

OKX cannot scope a subscription to a pool. Six ways of passing the pool address
(poolAddress, poolId, pairAddress, dexContractAddress, with and without
the token, plus a dexName filter) are accepted and silently ignored, and the
paid v6 channel is token-scoped too — so this is the shape of the product, not a
level of access.

It does not change what is scored. Only emissions matched to the reference by
transaction hash are scored, and the reference is the bench pool, so the scored
set is identical to the other four providers'. The off-pool trades are
discarded.

Measured before shipping rather than assumed. Two independent observers over the
same window: our own eth_subscribe on the bench pool, and OKX's stream. Both
sides buffered with a 90 s deadline, because a swap OKX pushes before our node
sees it is the case the bench exists to find.

BASE, ground truth from eth_getLogs on the pool
  blocks          52128671 to 52129512
  pool swaps      266
  OKX published   266
  COVERAGE        100.0%
  socket gaps     []

SOLANA, ground truth from getSignaturesForAddress on the pool
  window          1801s
  pool txs        52
  OKX published   52
  COVERAGE        100.0%
  socket gaps     []

Base is settled. 266 of 266, against a denominator that is complete by
construction (eth_getLogs on the pool and Swap topic cannot miss a swap or
deliver one late), with no socket gap during the run. That puts coverage at
98.9% or better with 95% confidence, which rules out the failure that would
have broken comparability: a provider publishing only a subset would be scored
on its easy trades while the others are scored on all of them.

Solana is not settled, and I am not claiming it. 52 transactions in 30
minutes on SOL/USDC Raydium is about 1.7 a minute, which is implausibly low for
that pool, so getSignaturesForAddress plainly did not return everything. 52 of
52 is not contradicted by anything, but it rests on a denominator I do not
believe is complete. Treat the Solana column as unverified until the match
counter has run in staging.

Why the earlier figures in this PR's history were wrong

An initial run used a live eth_subscribe as ground truth and decayed from 100%
at 25 minutes to 67% at 190, with OKX apparently arriving up to 24 s before
the chain. That is impossible, and it was the tell: the reference was the
degraded half. My rig held only a publicnode logs subscription, while
production's Base reference is the flashblock preconfirmation stream and
refClock.observe keeps the FIRST observation, so production has a materially
better clock than that rig did.

The fix was to stop using a stream for this at all. Coverage asks what happened
on chain, which a historical query answers completely; only latency needs a fast
clock.

Latency, measured on genuinely matched pairs, was stable across every window:
p50 0.636 / 0.615 / 0.552 s on Base.

The one cost, and why it is contained

An off-pool emission never matches and would expire as a miss. That would read
as a 96%-miss provider and drown the signal the counter exists for: whether the
reference itself is healthy. tokenScopedAggregators exempts OKX, and a test
keeps the exemption to providers that genuinely cannot scope to a pool — adding
mobula or codex to that map fails.

Stability

The feed is undocumented frontend infrastructure and carries no guarantee. That
is the bet geckoterminal_monitor.go already makes in this harness (its pool ids
are commented "extracted via reverse engineering"), and the failure mode is the
same: the flow watchdog purges the gauge and the column reads "Feed down" rather
than a wrong number. The monitor holds one socket per chain, because the gateway
keys subscriptions by channel name alone and silently ignores a second token on
the same connection.

Also

okx.svg replaces the opaque 400x400 JPEG, which drew a white box around the
mark on dark backgrounds. Same mark, same black ground, geometry read off the
original.

Checks

  • go build ./... and go test ./cmd/script/ clean, 7 new tests
  • pnpm validate 235 specs valid
  • pnpm exec tsc --noEmit clean
  • bun test src/ 425 pass, 0 fail

What a reviewer should push back on

The Solana denominator. 52 pool transactions in 30 minutes is too low for
SOL/USDC Raydium, so that ground truth is incomplete and the 52/52 proves less
than it looks. Base carries the coverage argument; Solana should be confirmed
from head_lag_ref_matches_total{aggregator="okx"} in staging before its column
is trusted.

The stability bet. This is undocumented frontend infrastructure. If that is
not an acceptable dependency for a published bench, say so now rather than after
it moves: the alternative is $99/month for the documented channel, which is
token-scoped too and would therefore need every other part of this PR
unchanged.

🤖 Generated with Claude Code

Their documented trades channel, wss://wsdex.okx.com/ws/v6/dex, answers
"Please log in" to every subscribe including the docs' own verbatim example,
and their fee table says why: Free reads "WebSocket not supported", cheapest
WS-capable plan $99/month.

Their web frontend holds a different socket that needs no credential. Two
things hid it, both of which fail silently:

  - the channel is dex-market-trade-history, not any `trades` variant. Twenty
    other names were rejected before this one;
  - the params go in an extraParams JSON STRING. With flat keys the gateway
    still answers {"event":"subscribe"} and then pushes nothing, which is
    indistinguishable from a dead channel. An ack proves nothing here, only
    frames do.

Base and Solana only, and both exclusions are structural. BNB publishes
head_lag_seconds as receiveTime minus the provider's own timestamp, and every
OKX timestamp is a whole second (1,800 sampled across three chains, all
ts%1000 == 0), so measuring it there would publish our arithmetic as OKX's
latency. Robinhood Chain is not in OKX's supported set.

OKX cannot scope a subscription to a pool: six ways of passing the pool
address are accepted and silently ignored, and the paid channel is
token-scoped too, so this is the shape of the product rather than a level of
access. It does not change what is scored. Only emissions matched to the
reference by transaction hash are, and the reference is the bench pool, so the
scored set is identical to the other four providers'. Verified before shipping
rather than assumed: over 20 minutes on the Base pool, 213 of 213 pool swaps
appeared in OKX's stream, p50 636 ms behind our node, OKX never first.

The one cost is bookkeeping. An off-pool emission never matches and would
expire as a miss, which would read as a 96%-miss provider and drown the signal
that counter exists for: whether the reference itself is healthy.
tokenScopedAggregators exempts it, and a test keeps the exemption to providers
that genuinely cannot scope to a pool.

The feed is undocumented frontend infrastructure with no stability guarantee.
That is the bet geckoterminal_monitor.go already makes here, and the failure
mode is the same: the flow watchdog purges the gauge and the column reads
"Feed down" rather than a wrong number. Tests pin the socket and channel
constants so a silent move fails loudly.

okx.svg replaces the opaque JPEG, which drew a white box around the mark on
dark backgrounds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Flotapponnier
Flotapponnier marked this pull request as draft October 3, 2026 16:36
@Flotapponnier
Flotapponnier marked this pull request as ready for review October 3, 2026 17:34
@Flotapponnier
Flotapponnier merged commit d330dd2 into dev Oct 3, 2026
2 checks passed
@Flotapponnier
Flotapponnier deleted the feat/okx-head-lag branch October 3, 2026 22:17
Flotapponnier added a commit that referenced this pull request Oct 3, 2026
* OKX's DEX feed is open after all, on a channel their docs do not mention (#2791)

Their documented trades channel, wss://wsdex.okx.com/ws/v6/dex, answers
"Please log in" to every subscribe including the docs' own verbatim example,
and their fee table says why: Free reads "WebSocket not supported", cheapest
WS-capable plan $99/month.

Their web frontend holds a different socket that needs no credential. Two
things hid it, both of which fail silently:

  - the channel is dex-market-trade-history, not any `trades` variant. Twenty
    other names were rejected before this one;
  - the params go in an extraParams JSON STRING. With flat keys the gateway
    still answers {"event":"subscribe"} and then pushes nothing, which is
    indistinguishable from a dead channel. An ack proves nothing here, only
    frames do.

Base and Solana only, and both exclusions are structural. BNB publishes
head_lag_seconds as receiveTime minus the provider's own timestamp, and every
OKX timestamp is a whole second (1,800 sampled across three chains, all
ts%1000 == 0), so measuring it there would publish our arithmetic as OKX's
latency. Robinhood Chain is not in OKX's supported set.

OKX cannot scope a subscription to a pool: six ways of passing the pool
address are accepted and silently ignored, and the paid channel is
token-scoped too, so this is the shape of the product rather than a level of
access. It does not change what is scored. Only emissions matched to the
reference by transaction hash are, and the reference is the bench pool, so the
scored set is identical to the other four providers'. Verified before shipping
rather than assumed: over 20 minutes on the Base pool, 213 of 213 pool swaps
appeared in OKX's stream, p50 636 ms behind our node, OKX never first.

The one cost is bookkeeping. An off-pool emission never matches and would
expire as a miss, which would read as a 96%-miss provider and drown the signal
that counter exists for: whether the reference itself is healthy.
tokenScopedAggregators exempts it, and a test keeps the exemption to providers
that genuinely cannot scope to a pool.

The feed is undocumented frontend infrastructure with no stability guarantee.
That is the bet geckoterminal_monitor.go already makes here, and the failure
mode is the same: the flow watchdog purges the gauge and the column reads
"Feed down" rather than a wrong number. Tests pin the socket and channel
constants so a silent move fails loudly.

okx.svg replaces the opaque JPEG, which drew a white box around the mark on
dark backgrounds.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* the BNB column was measuring BNB's block time, not the providers (#2792)

head_lag_seconds on bnb published receiveTime minus the provider's OWN
timestamp, because bnb was in neither referenceChains nor raceChains. The
spec justified that by claiming BNB's chain-supplied timestamps sat
"within roughly 60 ms of the moment a trade is observable".

The gap between that series and the reference-matched one IS that
quantity, and it is 569 ms.

So the dominant term in the published column was block time, which every
provider shares, and the column barely separated them. Measured over 24 h,
8,604 samples per provider, eu-west p50:

              published    against the reference
  mobula        0.621 s          0.052 s
  serialized    0.657 s          0.102 s
  codex         0.676 s          0.215 s
  geckoterminal 33.881 s        31.196 s

Within 9% of each other before, spread over a factor of four after, and
the ranking is unchanged. This removes a common offset rather than
reshuffling anyone. p90 moves the same way (1.0-1.2 s to 0.12-0.33 s).

The gate in pending_match.go is that the reference must arrive before
every provider. Our node is beaten 6.6% of the time by mobula on bnb,
4.7% by serialized, 3.7% by codex. Base, already on the reference and
shipping, sits at 4.2% / 3.1% / 0%. Same order of magnitude, so this
clears the bar the project already applies to itself.

The reference subscription this reads already existed and already
populated head_lag_ref_seconds; nothing new is collected. Only which
series the headline reads changes.

Published numbers on the bnb column will drop by roughly 5-10x the day
this deploys. That is a methodology correction, not providers getting
faster, and the spec now says so where it used to carry the 60 ms claim.

TestReferenceChainsScope asserted bnb must NOT be here. Updated
deliberately rather than deleted, with the reason, and a new test pins
why bnb belongs now and why robinhood and solana still do not.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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