diff --git a/answers/which-prediction-market-data-api-is-the-freshest.yml b/answers/which-prediction-market-data-api-is-the-freshest.yml deleted file mode 100644 index 856fe12d..00000000 --- a/answers/which-prediction-market-data-api-is-the-freshest.yml +++ /dev/null @@ -1,43 +0,0 @@ -slug: which-prediction-market-data-api-is-the-freshest -question: "Which prediction market data API publishes the freshest Polymarket data?" -short_answer: | - Polymarket's own CLOB WebSocket gateway is the canonical fastest source by definition (T0, sub-50ms from EU West) and is not a competing data provider. Among third-party data providers, Codex currently leads at {{p50:codex}} (p50, 24h) on the Polymarket tab, measured as lag from venue publish to provider relay; Codex indexes the onchain Polygon confirmation so its structural floor is roughly 2 seconds of block time. Live per-venue ranking at openchainbench.com/benchmarks/pm-data-freshness. - -benchmark: pm-data-freshness - -intro: | - Prediction markets produce the most time sensitive event stream in crypto. An election market settles in seconds, a sports book moves on every play, and the price tick a trading UI shows is only as fresh as the API behind it. Builders integrating Polymarket through a data provider rather than hitting the CLOB directly need to know how many milliseconds that provider adds between Polymarket publishing a trade and the provider relaying the same trade to its WebSocket subscribers. This page answers exactly that. The OpenChainBench pm-data-freshness harness holds two WebSocket subscribers in parallel: Polymarket's own CLOB gateway (the canonical T0) and Codex GraphQL subscriptions, both subscribed to the same basket of top 20 Polymarket markets by 24 h volume, refreshed every 5 minutes. Each trade is matched across providers by a tuple of conditionId, price rounded to 3 decimals, trade size and a 5 second time bucket; the per provider lag versus Polymarket's gateway publish time is recorded as a Prometheus histogram and the leaderboard ranks by p50 in milliseconds, lower is fresher. - -methodology: | - Two WebSocket clients run in parallel inside the harness on a Railway europe west 4 instance. Polymarket's own gateway (`wss://ws-subscriptions-clob.polymarket.com/ws/market`, public, no auth, sub 50 ms publish latency from EU West) is the canonical T0 by construction because nothing downstream can be faster than the source. Codex GraphQL (`wss://graph.codex.io/graphql` with the `graphql-transport-ws` subprotocol) runs an `onPredictionTradesCreated` firehose subscription filtered client side to the Polymarket protocol marketIds in the active basket. The cross correlation key is `(conditionId, priceUSD times 1000 rounded, sizeUSD times 1 million rounded, floor(trade_time / 5s))`; the 5 second bucket absorbs minor clock skew between gateways without merging unrelated trades. Histogram buckets run 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 30000, 60000 ms. Providers that fail to relay a matched trade within 90 seconds are not counted toward p50, only toward their receive total, so a provider cannot look fresh on the leaderboard by silently dropping events; the success rate column surfaces that. - -limitations: - - "Polymarket's own gateway is the canonical T0 by construction. Its row on the leaderboard sits near zero because the harness compares everything else against it, not because Polymarket has solved network latency; the displayed sub millisecond value is a floor (vector(0.5)) so the row stays visible on the linear chart axis next to the multi second Codex value." - - "Codex indexes the on chain Polygon confirmation of each Polymarket trade, so its p50 includes Polygon block time (around 2 seconds) plus ingestion. The Codex path is the right one for on chain reconciliation or settlement workflows; for live trading UIs the off chain relay path is the only viable one. The two answer different questions." - - "Measurement is single region (Railway europe west 4). A provider whose edge gateway is closer to a US East client could post different deltas there; multi region requires running additional monitor instances of the freshness harness, which is on the roadmap." - - "The basket is the top 20 active Polymarket markets by 24 h volume, refreshed every 5 minutes. Long tail markets with sparse trade flow are not on the leaderboard because the cross correlation matcher needs enough overlapping events to produce a stable histogram per provider." - - "Kalshi and Limitless are separate exchanges with separate data feeds and a different ingestion path; this bench measures Polymarket as the underlying venue. A Kalshi specific tab is a candidate for a later phase but is not the current scope." - -faq: - - q: "Which Polymarket data API has the lowest latency right now?" - a: "{{best_name}} currently leads at {{best_p50}} (p50 over the last 24 hours), measured as wall clock time from Polymarket's own CLOB WebSocket publish to provider relay on the same trade. The leaderboard re sorts every minute on fresh Prometheus samples, so the answer reflects the actual measured lag on the active market basket, not a marketing claim. Codex's on chain Polygon indexer carries a structural floor near 2 seconds because it ingests block confirmations." - - q: "What does freshness delta mean for a prediction market API?" - a: "The bench connects to Polymarket's own CLOB WebSocket and to each provider's WebSocket simultaneously, subscribes to the same markets, and for every trade event records how many milliseconds the provider takes to relay the event after Polymarket itself publishes it. Lower is better. Polymarket's own gateway publish time is the canonical T0 because by construction nothing downstream can be faster than the source. The cross correlation key (conditionId, price, size, 5 s bucket) survives minor clock skew between gateways without merging unrelated trades." - - q: "Why not include Polymarket REST polling on this bench?" - a: "Freshness is a WebSocket question. REST polling at 1 second cadence would have a floor freshness near 500 ms (poll interval divided by 2) plus round trip time, dominated by how often you poll. The Polymarket gateway WebSocket exists for exactly this reason, to avoid that floor. Adding REST as a row would make the leaderboard noisy without changing the conclusion: WebSocket beats polling by definition for real time data." - - q: "Are these numbers comparable to Kalshi or Limitless?" - a: "Not directly. This benchmark measures Polymarket as the underlying venue since it is the largest and the one most providers proxy. Kalshi and Limitless are separate exchanges with separate data feeds, and providers that cover them often run a different ingestion path. A Kalshi specific tab is a candidate for a later phase. For now, treat the leaderboard as how fresh is your Polymarket data feed." - - q: "How does the cross correlation matcher avoid false positives?" - a: "The signature is (conditionId, priceUSD times 1000 rounded to 3 decimals, sizeUSD times 1 million rounded, floor(trade_time / 5 seconds)). Two unrelated trades would need to share the same conditionId, the same price to 3 decimals, the same size to 6 decimals, and land in the same 5 second window to collide. On the top 20 markets that combination is unique in practice; on a thinly traded long tail market it could in principle merge two trades, which is one reason the basket is restricted to the top 20 by volume." - - q: "How often is the leaderboard refreshed?" - a: "Continuously at the WebSocket level. Every trade event lands on the harness in real time and updates the freshness histogram; the page reads a rolling 24 h p50, p90 and p99 every minute. The market basket itself is refreshed every 5 minutes against `gamma-api.polymarket.com` so the leaderboard tracks current top markets rather than yesterday's hot list." - -related: - - which-prediction-market-has-the-strictest-rate-limits - - which-crypto-data-api-covers-the-most-blockchains - - which-crypto-price-api-is-the-fastest - - prediction-market-api-for-developers - -seo_title: "Which prediction market data API is the freshest in 2026?" -seo_description: "Polymarket's own CLOB gateway is the canonical T0 (fastest by definition). Among third-party providers, Codex leads freshness delta live, measured by OpenChainBench on the top 20 Polymarket and Kalshi markets by volume." -status: live diff --git a/benchmarks/pm-api-latency.yml b/benchmarks/pm-api-latency.yml index b022741a..e3d2ae05 100644 --- a/benchmarks/pm-api-latency.yml +++ b/benchmarks/pm-api-latency.yml @@ -4,8 +4,8 @@ slug: pm-api-latency number: "038" title: Best prediction market API, ranked by live latency and uptime seo_title: "Best prediction market API 2026" -seo_description: "Is the Polymarket API up? Live uptime and latency for Polymarket, Kalshi, Limitless, Manifold, Myriad, PredictIt and Smarkets price endpoints." -subtitle: Warm price endpoint latency of seven prediction market venue APIs from three regions, plus a live uptime panel that answers whether each API is up right now. +seo_description: "Is the Polymarket API up? Live uptime and latency for Polymarket, Polymarket US, Kalshi, Limitless, Manifold, Myriad, PredictIt and Smarkets price endpoints." +subtitle: Warm price endpoint latency of eight prediction market venue APIs from three regions, plus a live uptime panel that answers whether each API is up right now. category: Trading status: live @@ -87,6 +87,9 @@ per_chain_explainer: - slug: polymarket h2: Polymarket API latency body: "Polymarket's CLOB answers its midpoint endpoint at {{p50:polymarket}} p50 over warm connections (24h). The API fronts Cloudflare, so brief origin trouble often appears here as a latency spike before it ever becomes an error, which makes the latency series a useful early status signal. The uptime panel above is a live Polymarket API status check, computed from probes fired every 5 seconds from three regions, so it reacts within seconds of a real outage instead of waiting for user reports. For how the CLOB behaves under bursts, see the pm-rate-limits bench." + - slug: polymarket-us + h2: Polymarket US API latency + body: "Polymarket US (QCX LLC) is the CFTC-regulated DCM launched December 2025 for US residents, separate from Polymarket Global. Its read-only market data gateway at gateway.polymarket.us exposes a public BBO endpoint requiring no API key, which is what this bench probes. The BBO response embeds a lastPriceSample timestamp, making Polymarket US one of two venues in the cohort with verifiable data age. KYC is required for trading but not for market data reads. Current BBO p50: {{p50:polymarket-us}}." - slug: kalshi h2: Kalshi API latency body: "Kalshi's public single market endpoint goes to origin on every request and currently answers at {{p50:kalshi}} p50 warm (24h). That makes its uptime figure here a clean origin measurement, unlike its CDN cached market list. Kalshi is the regulated US venue and the only one in the cohort with a documented API contract, so an unexplained dip in its health gauge is more notable than elsewhere. If Kalshi's API seems down for you, compare the per region figures: a single region dip usually means a network issue, not a venue outage." @@ -112,12 +115,13 @@ prometheus: window: 24h expected_freshness_seconds: 300 -rank_matrix_query: 1000 * label_replace(histogram_quantile(0.50, sum by (venue, region, le) (rate(pmapi_request_duration_seconds_bucket{source="direct",venue=~"polymarket|kalshi|limitless|manifold|myriad|predictit|smarkets",conn="warm",class="price",cache!="hit"}[24h]))), "provider", "$1", "venue", "(.+)") +rank_matrix_query: 1000 * label_replace(histogram_quantile(0.50, sum by (venue, region, le) (rate(pmapi_request_duration_seconds_bucket{source="direct",venue=~"polymarket|polymarket-us|kalshi|limitless|manifold|myriad|predictit|smarkets",conn="warm",class="price",cache!="hit"}[24h]))), "provider", "$1", "venue", "(.+)") dimensions: venue: - { value: all, label: All venues } - { value: polymarket, label: Polymarket } + - { value: polymarket-us, label: Polymarket US } - { value: kalshi, label: Kalshi } - { value: limitless, label: Limitless } - { value: manifold, label: Manifold } @@ -134,21 +138,21 @@ metric_panels: - id: uptime_24h label: Uptime 24h description: "Share of probe cycles that succeeded over the last 24 hours, averaged across the three probe regions. 100 percent means every 5 second probe of the venue's API came back healthy." - metric: 100 * avg(avg_over_time(pmapi_health{source="direct",venue=~"polymarket|kalshi|limitless|manifold|myriad|predictit|smarkets"}[24h])) + metric: 100 * avg(avg_over_time(pmapi_health{source="direct",venue=~"polymarket|polymarket-us|kalshi|limitless|manifold|myriad|predictit|smarkets"}[24h])) label_key: venue unit: pct higher_is_better: true - id: uptime_7d label: Uptime 7d description: "Same health gauge averaged over 7 days. Short outages that vanish from the 24h figure stay visible here for a week." - metric: 100 * avg(avg_over_time(pmapi_health{source="direct",venue=~"polymarket|kalshi|limitless|manifold|myriad|predictit|smarkets"}[7d])) + metric: 100 * avg(avg_over_time(pmapi_health{source="direct",venue=~"polymarket|polymarket-us|kalshi|limitless|manifold|myriad|predictit|smarkets"}[7d])) label_key: venue unit: pct higher_is_better: true - id: uptime_30d label: Uptime 30d description: "Same health gauge averaged over 30 days. Feeds the ledger's 30d window toggle so a one-hour outage that's already aged out of the 24h panel still shows up in the table over a month." - metric: 100 * avg(avg_over_time(pmapi_health{source="direct",venue=~"polymarket|kalshi|limitless|manifold|myriad|predictit|smarkets"}[30d])) + metric: 100 * avg(avg_over_time(pmapi_health{source="direct",venue=~"polymarket|polymarket-us|kalshi|limitless|manifold|myriad|predictit|smarkets"}[30d])) label_key: venue unit: pct higher_is_better: true @@ -156,7 +160,7 @@ metric_panels: - id: cold_connect_p50 label: Cold connect description: "TCP plus TLS handshake time on the once a minute cold probe with keep alives disabled. The startup cost a brand new client pays before its first request." - metric: 1000 * histogram_quantile(0.50, sum(rate(pmapi_request_connect_seconds_bucket{source="direct",venue=~"polymarket|kalshi|limitless|manifold|myriad|predictit|smarkets"}[24h])) by (le)) + metric: 1000 * histogram_quantile(0.50, sum(rate(pmapi_request_connect_seconds_bucket{source="direct",venue=~"polymarket|polymarket-us|kalshi|limitless|manifold|myriad|predictit|smarkets"}[24h])) by (le)) label_key: venue unit: ms higher_is_better: false @@ -183,6 +187,19 @@ providers: sample_size: sum(increase(pmapi_requests_total{venue="polymarket",source="direct",class="price",conn="warm",outcome!="probe_invalid"}[24h])) series: 1000 * histogram_quantile(0.50, sum(rate(pmapi_request_duration_seconds_bucket{venue="polymarket",source="direct",class="price",conn="warm",cache!="hit"}[1h])) by (le)) + - slug: polymarket-us + name: Polymarket US + tag: CFTC DCM (QCX LLC), USD settlement, KYC required for trading; market data public via gateway.polymarket.us + formula: "p50 of warm round trips against the BBO endpoint for the pinned market, no auth required, 24h window. Separate from Polymarket Global: different API, different settlement, different regulatory regime." + queries: + p50: 1000 * histogram_quantile(0.50, sum(rate(pmapi_request_duration_seconds_bucket{venue="polymarket-us",source="direct",class="price",conn="warm",cache!="hit"}[24h])) by (le)) + p90: 1000 * histogram_quantile(0.90, sum(rate(pmapi_request_duration_seconds_bucket{venue="polymarket-us",source="direct",class="price",conn="warm",cache!="hit"}[24h])) by (le)) + p99: 1000 * histogram_quantile(0.99, sum(rate(pmapi_request_duration_seconds_bucket{venue="polymarket-us",source="direct",class="price",conn="warm",cache!="hit"}[24h])) by (le)) + mean: 1000 * sum(rate(pmapi_request_duration_seconds_sum{venue="polymarket-us",source="direct",class="price",conn="warm",cache!="hit"}[24h])) / sum(rate(pmapi_request_duration_seconds_count{venue="polymarket-us",source="direct",class="price",conn="warm",cache!="hit"}[24h])) + success: clamp_max(sum(rate(pmapi_requests_total{venue="polymarket-us",source="direct",class="price",conn="warm",outcome="ok"}[24h])) / sum(rate(pmapi_requests_total{venue="polymarket-us",source="direct",class="price",conn="warm",outcome!="probe_invalid"}[24h])), 1) + sample_size: sum(increase(pmapi_requests_total{venue="polymarket-us",source="direct",class="price",conn="warm",outcome!="probe_invalid"}[24h])) + series: 1000 * histogram_quantile(0.50, sum(rate(pmapi_request_duration_seconds_bucket{venue="polymarket-us",source="direct",class="price",conn="warm",cache!="hit"}[1h])) by (le)) + - slug: kalshi name: Kalshi tag: Regulated US venue, single market endpoint goes to origin on every request diff --git a/benchmarks/pm-rate-limits.yml b/benchmarks/pm-rate-limits.yml index 12c50271..ea05c0f5 100644 --- a/benchmarks/pm-rate-limits.yml +++ b/benchmarks/pm-rate-limits.yml @@ -4,8 +4,8 @@ slug: pm-rate-limits number: "037" title: Prediction market API rate limits, tested with a daily ramp seo_title: "Prediction market API rate limits 2026" -seo_description: "Prediction market API rate limits measured live: daily request ramp against Polymarket, Kalshi, Limitless, Manifold, Myriad." -subtitle: Warm latency on book, price and list endpoints of five prediction market venue APIs, plus a daily rate limit ramp that records added latency and throttle onset per request tier. +seo_description: "Prediction market API rate limits measured live: daily request ramp against Polymarket, Polymarket US, Kalshi, Limitless, Manifold, Myriad." +subtitle: Warm latency on book, price and list endpoints of six prediction market venue APIs, plus a daily rate limit ramp that records added latency and throttle onset per request tier. category: Trading status: live @@ -93,6 +93,9 @@ per_chain_explainer: - slug: polymarket h2: Polymarket API rate limits body: "Polymarket's CLOB API documents per endpoint budgets (around 1500 requests per 10s for the book endpoint) and sits behind Cloudflare. At our ramp tiers it queues instead of returning 429, so the honest throttle signal is added latency, shown in the ramp panel. The book endpoint embeds a millisecond timestamp, making Polymarket one of only two venues here whose data age is verifiable. Warm book p50: {{p50:polymarket}}." + - slug: polymarket-us + h2: Polymarket US API rate limits + body: "Polymarket US (QCX LLC) is a CFTC DCM whose market data gateway (gateway.polymarket.us) is publicly accessible at 20 requests per second per IP. The ramp uses conservative tiers (5/10/20 per 10s, max 10% of the documented limit) and respects the regulated venue context. The BBO endpoint embeds a lastPriceSample timestamp, making Polymarket US one of two venues with verifiable data age. No 429 mechanism was observed during reconnaissance probes; throttle behaviour will appear as added latency if it occurs. Warm BBO p50: {{p50:polymarket-us}}." - slug: kalshi h2: Kalshi API rate limits body: "Kalshi documents a token bucket per access tier, around 20 requests per second for basic read access. Its order book and single market endpoints go to origin on every request, but the market list is served from CloudFront with max-age=15, so list reads measure the edge. The WebSocket requires authentication, so Kalshi is absent from the WS panel. Our ramp stops at the first 429. Warm book p50: {{p50:kalshi}}." @@ -118,12 +121,13 @@ prometheus: window: 24h expected_freshness_seconds: 300 -rank_matrix_query: 1000 * label_replace(histogram_quantile(0.50, sum by (venue, region, le) (rate(pmapi_request_duration_seconds_bucket{cache!="hit",conn="warm",class="book"}[24h]) or rate(pmapi_request_duration_seconds_bucket{cache!="hit",conn="warm",class="price",venue=~"myriad|predictit"}[24h]))), "provider", "$1", "venue", "(.+)") +rank_matrix_query: 1000 * label_replace(histogram_quantile(0.50, sum by (venue, region, le) (rate(pmapi_request_duration_seconds_bucket{cache!="hit",conn="warm",class="book"}[24h]) or rate(pmapi_request_duration_seconds_bucket{cache!="hit",conn="warm",class="price",venue=~"myriad|predictit|polymarket-us"}[24h]))), "provider", "$1", "venue", "(.+)") dimensions: venue: - { value: all, label: All venues } - { value: polymarket, label: Polymarket } + - { value: polymarket-us, label: Polymarket US } - { value: kalshi, label: Kalshi } - { value: limitless, label: Limitless } - { value: manifold, label: Manifold } @@ -190,6 +194,19 @@ providers: sample_size: sum(increase(pmapi_requests_total{venue="polymarket",class="book",conn="warm",outcome!="probe_invalid"}[24h])) series: 1000 * histogram_quantile(0.50, sum(rate(pmapi_request_duration_seconds_bucket{cache!="hit",venue="polymarket",class="book",conn="warm"}[1h])) by (le)) + - slug: polymarket-us + name: Polymarket US + tag: CFTC DCM (QCX LLC), public data gateway, BBO used as price proxy (no separate book latency class), conservative ramp (public gateway, CFTC regulated) + formula: "p50 of warm round trips against the public BBO endpoint at gateway.polymarket.us for the pinned market, no auth required, 24h window. Book class uses the full order book endpoint on the same gateway. Ramp tiers capped at 5/10/20 per 10s (10% of the 20 req/s public limit)." + queries: + p50: 1000 * histogram_quantile(0.50, sum(rate(pmapi_request_duration_seconds_bucket{cache!="hit",venue="polymarket-us",class="price",conn="warm"}[24h])) by (le)) + p90: 1000 * histogram_quantile(0.90, sum(rate(pmapi_request_duration_seconds_bucket{cache!="hit",venue="polymarket-us",class="price",conn="warm"}[24h])) by (le)) + p99: 1000 * histogram_quantile(0.99, sum(rate(pmapi_request_duration_seconds_bucket{cache!="hit",venue="polymarket-us",class="price",conn="warm"}[24h])) by (le)) + mean: 1000 * sum(rate(pmapi_request_duration_seconds_sum{cache!="hit",venue="polymarket-us",class="price",conn="warm"}[24h])) / sum(rate(pmapi_request_duration_seconds_count{cache!="hit",venue="polymarket-us",class="price",conn="warm"}[24h])) + success: clamp_max(sum(rate(pmapi_requests_total{venue="polymarket-us",class="price",conn="warm",outcome="ok"}[24h])) / sum(rate(pmapi_requests_total{venue="polymarket-us",class="price",conn="warm",outcome!="probe_invalid"}[24h])), 1) + sample_size: sum(increase(pmapi_requests_total{venue="polymarket-us",class="price",conn="warm",outcome!="probe_invalid"}[24h])) + series: 1000 * histogram_quantile(0.50, sum(rate(pmapi_request_duration_seconds_bucket{cache!="hit",venue="polymarket-us",class="price",conn="warm"}[1h])) by (le)) + - slug: kalshi name: Kalshi tag: Documented token bucket, book and market go to origin, list is CloudFront cached diff --git a/harnesses/pm-rate-limits/cmd/script/pin.go b/harnesses/pm-rate-limits/cmd/script/pin.go index c317a497..cd05c4eb 100644 --- a/harnesses/pm-rate-limits/cmd/script/pin.go +++ b/harnesses/pm-rate-limits/cmd/script/pin.go @@ -337,6 +337,45 @@ func pinSmarkets(ctx context.Context, c *http.Client, avoid string) (Pin, error) return Pin{}, errors.New("smarkets: no open politics market with live quotes found") } +func pinPolymarketUS(ctx context.Context, c *http.Client, avoid string) (Pin, error) { + var out struct { + Markets []struct { + Slug string `json:"slug"` + EndDate string `json:"endDate"` + Active bool `json:"active"` + Closed bool `json:"closed"` + MarketSides []struct { + Long bool `json:"long"` + Price string `json:"price"` + } `json:"marketSides"` + } `json:"markets"` + } + url := "https://gateway.polymarket.us/v1/markets?limit=100&active=true&closed=false" + if err := fetchJSON(ctx, c, url, &out); err != nil { + return Pin{}, fmt.Errorf("polymarket-us list: %w", err) + } + for _, m := range out.Markets { + if m.Slug == avoid || m.Slug == "" || m.Closed || !m.Active { + continue + } + end, err := time.Parse(time.RFC3339, m.EndDate) + if err != nil || time.Until(end) < minPinHorizon { + continue + } + for _, s := range m.MarketSides { + if !s.Long { + continue + } + p, err := strconv.ParseFloat(s.Price, 64) + if err != nil || p < 0.15 || p > 0.85 { + continue + } + return Pin{Market: m.Slug, Expiry: end}, nil + } + } + return Pin{}, errors.New("polymarket-us: no near-the-money open market with >24h horizon") +} + // stalenessPolymarket reads the ms timestamp Polymarket embeds in every book // response (string in the docs, tolerate a bare number). func stalenessPolymarket(class string, body []byte) (int64, bool) { @@ -359,6 +398,28 @@ func stalenessPolymarket(class string, body []byte) (int64, bool) { return 0, false } +// stalenessPolymarketUS reads lastPriceSample.ts from the BBO response payload. +func stalenessPolymarketUS(class string, body []byte) (int64, bool) { + if class != "price" { + return 0, false + } + var b struct { + MarketData struct { + LastPriceSample struct { + Ts string `json:"ts"` + } `json:"lastPriceSample"` + } `json:"marketData"` + } + if json.Unmarshal(body, &b) != nil || b.MarketData.LastPriceSample.Ts == "" { + return 0, false + } + t, err := time.Parse(time.RFC3339Nano, b.MarketData.LastPriceSample.Ts) + if err != nil { + return 0, false + } + return t.UnixMilli(), true +} + // stalenessManifold reads lastUpdatedTime (ms) from the single-market payload. func stalenessManifold(class string, body []byte) (int64, bool) { if class != "price" { diff --git a/harnesses/pm-rate-limits/cmd/script/venues.go b/harnesses/pm-rate-limits/cmd/script/venues.go index 2583c5ca..e9763e6f 100644 --- a/harnesses/pm-rate-limits/cmd/script/venues.go +++ b/harnesses/pm-rate-limits/cmd/script/venues.go @@ -173,6 +173,34 @@ func venues() []*Venue { PinFunc: pinSmarkets, RampRates: []int{10, 25, 50}, }, + { + // Polymarket US (QCX LLC) — CFTC-regulated DCM launched Dec 2025. + // Separate from Polymarket Global (Polygon USDC CLOB): different + // API, different settlement (USD via broker), KYC required for + // trading. Market data is publicly available via the read-only + // gateway at gateway.polymarket.us with no API key. + // Rate limit: 20 req/s per IP on the public gateway; we use + // conservative tiers (5/10/20 per 10s = max 10% of the limit). + Slug: "polymarket-us", + Classes: []Class{ + {Name: "book", Interval: 5 * time.Second, Timeout: 8 * time.Second, + URL: func(p Pin) string { + return "https://gateway.polymarket.us/v1/markets/" + p.Market + "/book" + }}, + {Name: "price", Interval: 5 * time.Second, Timeout: 8 * time.Second, + URL: func(p Pin) string { + return "https://gateway.polymarket.us/v1/markets/" + p.Market + "/bbo" + }}, + {Name: "list", Interval: 30 * time.Second, Timeout: 15 * time.Second, + URL: func(Pin) string { + return "https://gateway.polymarket.us/v1/markets?limit=20&active=true&closed=false" + }}, + }, + PinFunc: pinPolymarketUS, + StalenessMs: stalenessPolymarketUS, + RampRates: []int{5, 10, 20}, + InvalidBody: []string{"not found", "Not Found"}, + }, } } diff --git a/src/app/prediction-markets/page.tsx b/src/app/prediction-markets/page.tsx index c961a1fd..aaaf8f4f 100644 --- a/src/app/prediction-markets/page.tsx +++ b/src/app/prediction-markets/page.tsx @@ -25,7 +25,6 @@ import { SITE } from "@/data/site"; const ANSWERS = [ { slug: "how-long-does-polymarket-take-to-resolve", question: "How long does Polymarket take to resolve a market?" }, { slug: "polymarket-vs-kalshi-resolution-speed", question: "Polymarket vs Kalshi, which resolves prediction markets faster?" }, - { slug: "which-prediction-market-data-api-is-the-freshest", question: "Which prediction market data API publishes the freshest Polymarket data?" }, { slug: "which-prediction-market-has-the-strictest-rate-limits", question: "Which prediction market API has the strictest rate limits?" }, { slug: "polymarket-fees-explained", question: "What fees does Polymarket charge?" }, { slug: "polymarket-vs-kalshi-fees", question: "Polymarket vs Kalshi fees, which is cheaper to trade?" }, diff --git a/src/components/chart-export-button.tsx b/src/components/chart-export-button.tsx index f0845001..bcf856dc 100644 --- a/src/components/chart-export-button.tsx +++ b/src/components/chart-export-button.tsx @@ -75,11 +75,17 @@ export function ChartExportButton({ // sometimes silently produces empty PNGs. Also skips the "download // blocked because the click gesture context expired" trap since we // hand a real Blob to the caller synchronously right after await. + // Use scrollHeight/scrollWidth so the full content is captured even + // when a parent scroll container makes only part of the figure visible + // on screen. Without this, html-to-image uses offsetHeight which clips + // the legend when it wraps into many rows (100+ provider benches). const blob = await toBlob(el, { pixelRatio: PIXEL_RATIO, backgroundColor: bg, cacheBust: true, skipFonts: false, + width: el.scrollWidth || el.offsetWidth, + height: el.scrollHeight || el.offsetHeight, // Drop the export button itself from the capture — no point // baking the "Copy" pill into every screenshot. Also drop any // element marked with data-chart-export-omit (e.g. the chart's diff --git a/src/components/time-series-chart/chart.tsx b/src/components/time-series-chart/chart.tsx index 480613a2..0242bb19 100644 --- a/src/components/time-series-chart/chart.tsx +++ b/src/components/time-series-chart/chart.tsx @@ -38,7 +38,7 @@ export function Chart({ const W = 1000; const H = 360; const padL = 60; - const padR = 96; + const padR = 130; const padT = 16; const padB = 36; const innerW = W - padL - padR; diff --git a/src/components/time-series-chart/index.tsx b/src/components/time-series-chart/index.tsx index c53d59ca..8fd3ea42 100644 --- a/src/components/time-series-chart/index.tsx +++ b/src/components/time-series-chart/index.tsx @@ -478,7 +478,7 @@ export function TimeSeriesChart({ return (
-
+

@@ -508,7 +508,7 @@ export function TimeSeriesChart({ {headerActions}

-
+
{RANGES.map((r) => { const active = r === range; diff --git a/src/components/time-series-chart/series.tsx b/src/components/time-series-chart/series.tsx index afe17328..67e9f386 100644 --- a/src/components/time-series-chart/series.tsx +++ b/src/components/time-series-chart/series.tsx @@ -21,75 +21,99 @@ type SeriesPathsProps = { // now speak for themselves — SeriesPaths renders a natural break wherever // the underlying values are null. +// Max number of end-of-line labels rendered. The legend below shows all +// providers regardless — this cap prevents right-side label clutter when +// dozens of lines share the same Y range. +const MAX_INLINE_LABELS = 10; +// Minimum vertical gap (SVG units) between consecutive placed labels so +// name + value lines don't overlap. +const LABEL_GAP = 27; + export function SeriesPaths({ drawn, unit }: SeriesPathsProps) { + // Limit labels to top-N non-excluded lines (drawn is pre-sorted by value). + const labeledSlugs = new Set( + drawn.filter((d) => !d.excluded).slice(0, MAX_INLINE_LABELS).map((d) => d.slug), + ); + + // Collision deflection: sort labeled lines by their natural Y position, + // then push each label down until it clears the previous one. + const labelY: Record = {}; + const toPlace = drawn + .filter((d) => labeledSlugs.has(d.slug)) + .map((d) => ({ slug: d.slug, natural: d.lastY })) + .sort((a, b) => a.natural - b.natural); + let prevBottom = -Infinity; + for (const item of toPlace) { + const y = Math.max(item.natural, prevBottom); + labelY[item.slug] = y; + prevBottom = y + LABEL_GAP; + } + return ( <> - {drawn.map((d) => ( - - - { + const showLabel = labeledSlugs.has(d.slug); + const ly = labelY[d.slug] ?? d.lastY; + return ( + - {/* Live pulse halo. animated outward */} - - - - - {/* Trailing tail dot */} - - {/* End-of-line label */} - - {d.name} - - - {fmtUnit(d.last, unit)} - - - ))} + + + {/* Live pulse halo */} + + + + + {/* Trailing tail dot */} + + {/* End-of-line label — only for top-N, collision-deflected */} + {showLabel && ( + <> + + {d.name} + + + {fmtUnit(d.last, unit)} + + + )} + + ); + })} ); } diff --git a/src/lib/logo-manifest.ts b/src/lib/logo-manifest.ts index 42781bf3..5cdaf6a1 100644 --- a/src/lib/logo-manifest.ts +++ b/src/lib/logo-manifest.ts @@ -61,6 +61,7 @@ const RAW: Record = { mobula: "/logos/mobula.svg", codex: "/logos/codex.svg", polymarket: "/logos/polymarket.png", + "polymarket-us": "/logos/polymarket.png", kalshi: "/logos/kalshi.jpg", limitless: "/logos/limitless.png", manifold: "/logos/manifold.svg", diff --git a/src/lib/pm-venue-context.ts b/src/lib/pm-venue-context.ts index 575363e0..62590591 100644 --- a/src/lib/pm-venue-context.ts +++ b/src/lib/pm-venue-context.ts @@ -20,6 +20,7 @@ import { logoPath } from "@/lib/logo-manifest"; export const PM_VENUE_META: Record = { polymarket: { url: "https://polymarket.com", chainLabel: "Polygon" }, + "polymarket-us": { url: "https://polymarketexchange.com", chainLabel: "CFTC DCM" }, kalshi: { url: "https://kalshi.com", chainLabel: "Offchain US" }, limitless: { url: "https://limitless.exchange", chainLabel: "Base" }, myriad: { url: "https://myriad.markets", chainLabel: "Abstract L2" }, diff --git a/src/lib/removed-benches.ts b/src/lib/removed-benches.ts index bf6c3528..f459a802 100644 --- a/src/lib/removed-benches.ts +++ b/src/lib/removed-benches.ts @@ -55,6 +55,9 @@ export const REMOVED_ANSWER_SLUGS = new Set([ // References solana-dex-quote-latency (staging-only, in // REMOVED_BENCH_SLUGS). Same pattern. "which-solana-dex-aggregator-is-the-fastest", + // References pm-data-freshness (bench 113, retired 2026-07). Bench + // removed; answer returns 404 without this guard. + "which-prediction-market-data-api-is-the-freshest", ]); export const REMOVED_BENCH_SLUGS = new Set([ @@ -113,6 +116,11 @@ export const REMOVED_BENCH_SLUGS = new Set([ "rpc-keyed-latency", "explorer-chain-coverage", "portfolio-chain-coverage", + // pm-data-freshness (bench 113) retired 2026-07: Predexon (the only + // measured data relay) discontinued. Spec + harness removed; kept in + // the 410 list so any indexed URL returns Gone instead of 404. + // Per-venue WS freshness now lives in pm-ws-latency (bench 114). + "pm-data-freshness", ]); /**