Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 5 additions & 22 deletions benchmarks/pm-api-latency.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,17 +108,13 @@ per_chain_explainer:
- slug: metaculus
h2: Metaculus API latency
body: "Metaculus is a public forecasting aggregator with no real-money markets, probed via its /api2/questions/{id}/ endpoint every 5 seconds from three regions. No authentication is required and no CDN cache layer was detected on the single-question endpoint. Metaculus has no order book and no WebSocket: all data access is REST. The uptime panel shows the live health gauge for the question API; the latency reflects the Django origin server without a CDN buffer. For builders querying aggregate community forecasts rather than market prices, Metaculus is the only venue in this cohort with a purely public read API at sub-second latency."
- slug: betfair
h2: Betfair API latency
body: "Betfair is the world's largest regulated betting exchange, probed via its Exchange API listMarketBook endpoint (POST with JSON body). Unlike every other venue in this cohort, all Betfair API calls are POST and require X-Application (developer AppKey) plus X-Authentication (session token). Betfair's infrastructure is UK-centric, so latency from us-east and sgp is higher than from eu-west. The latency shown is the full round trip of a listMarketBook call on a pinned binary market. For how Betfair behaves under rising request rates, see the pm-rate-limits bench."

source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/pm-rate-limits

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|metaculus|betfair",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|kalshi|limitless|manifold|myriad|predictit|smarkets|metaculus",conn="warm",class="price",cache!="hit"}[24h]))), "provider", "$1", "venue", "(.+)")

dimensions:
venue:
Expand All @@ -131,7 +127,6 @@ dimensions:
- { value: predictit, label: PredictIt }
- { value: smarkets, label: Smarkets }
- { value: metaculus, label: Metaculus }
- { value: betfair, label: Betfair }
region:
- { value: all, label: All regions }
- { value: us-east, label: US East }
Expand All @@ -142,29 +137,29 @@ 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|metaculus|betfair"}[24h]))
metric: 100 * avg(avg_over_time(pmapi_health{source="direct",venue=~"polymarket|kalshi|limitless|manifold|myriad|predictit|smarkets|metaculus"}[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|metaculus|betfair"}[7d]))
metric: 100 * avg(avg_over_time(pmapi_health{source="direct",venue=~"polymarket|kalshi|limitless|manifold|myriad|predictit|smarkets|metaculus"}[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|metaculus|betfair"}[30d]))
metric: 100 * avg(avg_over_time(pmapi_health{source="direct",venue=~"polymarket|kalshi|limitless|manifold|myriad|predictit|smarkets|metaculus"}[30d]))
label_key: venue
unit: pct
higher_is_better: true
tab: false
- 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|metaculus|betfair"}[24h])) by (le))
metric: 1000 * histogram_quantile(0.50, sum(rate(pmapi_request_connect_seconds_bucket{source="direct",venue=~"polymarket|kalshi|limitless|manifold|myriad|predictit|smarkets|metaculus"}[24h])) by (le))
label_key: venue
unit: ms
higher_is_better: false
Expand Down Expand Up @@ -281,15 +276,3 @@ providers:
sample_size: sum(increase(pmapi_requests_total{venue="metaculus",source="direct",class="price",conn="warm",outcome!="probe_invalid"}[24h]))
series: 1000 * histogram_quantile(0.50, sum(rate(pmapi_request_duration_seconds_bucket{venue="metaculus",source="direct",class="price",conn="warm",cache!="hit"}[1h])) by (le))

- slug: betfair
name: Betfair
tag: "UK FCA-regulated exchange, auth required, price endpoint is listMarketBook POST"
formula: "p50 of warm round trips against the Exchange API listMarketBook endpoint (POST, pinned binary market), successful requests only, 24h window. Requires BETFAIR_APP_KEY + credentials to run."
queries:
p50: 1000 * histogram_quantile(0.50, sum(rate(pmapi_request_duration_seconds_bucket{venue="betfair",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="betfair",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="betfair",source="direct",class="price",conn="warm",cache!="hit"}[24h])) by (le))
mean: 1000 * sum(rate(pmapi_request_duration_seconds_sum{venue="betfair",source="direct",class="price",conn="warm",cache!="hit"}[24h])) / sum(rate(pmapi_request_duration_seconds_count{venue="betfair",source="direct",class="price",conn="warm",cache!="hit"}[24h]))
success: clamp_max(sum(rate(pmapi_requests_total{venue="betfair",source="direct",class="price",conn="warm",outcome="ok"}[24h])) / sum(rate(pmapi_requests_total{venue="betfair",source="direct",class="price",conn="warm",outcome!="probe_invalid"}[24h])), 1)
sample_size: sum(increase(pmapi_requests_total{venue="betfair",source="direct",class="price",conn="warm",outcome!="probe_invalid"}[24h]))
series: 1000 * histogram_quantile(0.50, sum(rate(pmapi_request_duration_seconds_bucket{venue="betfair",source="direct",class="price",conn="warm",cache!="hit"}[1h])) by (le))
17 changes: 0 additions & 17 deletions benchmarks/pm-rate-limits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,6 @@ per_chain_explainer:
- slug: metaculus
h2: Metaculus rate limits
body: "Metaculus does not document API rate limits. The harness uses the most conservative ramp in the cohort (5/10/20 requests per 10 seconds) with the standard abort guard. There is no order book endpoint and no WebSocket. Because Metaculus is a forecasting aggregator rather than a betting exchange, its API is designed for low-frequency access; polling faster than once per 5 seconds on a single question likely hits undocumented limits. Warm price endpoint p50: {{p50:metaculus}}."
- slug: betfair
h2: Betfair rate limits
body: "Betfair's Exchange API documents a rate limit of 5 requests per second per AppKey for the listMarketBook endpoint, enforced with 429 responses. The daily ramp uses tiers of 10/25/50 requests per 10 seconds and stops at the first 429, consistent with the documented contract. Unlike Polymarket, Betfair does not queue under Cloudflare — it applies a hard token bucket. All API calls are POST with JSON body and require X-Application (AppKey) plus X-Authentication (session token). Betfair is the only venue in this cohort where the price probe is a POST request. Warm book p50: {{p50:betfair}}."

source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/pm-rate-limits

prometheus:
Expand All @@ -137,7 +133,6 @@ dimensions:
- { value: predictit, label: PredictIt }
- { value: smarkets, label: Smarkets }
- { value: metaculus, label: Metaculus }
- { value: betfair, label: Betfair }
region:
- { value: all, label: All regions }
- { value: us-east, label: US East }
Expand Down Expand Up @@ -289,15 +284,3 @@ providers:
sample_size: sum(increase(pmapi_requests_total{venue="metaculus",class="price",conn="warm",outcome!="probe_invalid"}[24h]))
series: 1000 * histogram_quantile(0.50, sum(rate(pmapi_request_duration_seconds_bucket{cache!="hit",venue="metaculus",class="price",conn="warm"}[1h])) by (le))

- slug: betfair
name: Betfair
tag: "UK FCA-regulated exchange, auth required, listMarketBook POST, token bucket rate limiting"
formula: "p50 of warm listMarketBook POST requests against the Exchange API for the pinned binary market, successful requests only, 24h window. Requires BETFAIR_APP_KEY + credentials. Ramp at 10/25/50 per 10s, stops on first 429."
queries:
p50: 1000 * histogram_quantile(0.50, sum(rate(pmapi_request_duration_seconds_bucket{cache!="hit",venue="betfair",class="book",conn="warm"}[24h])) by (le))
p90: 1000 * histogram_quantile(0.90, sum(rate(pmapi_request_duration_seconds_bucket{cache!="hit",venue="betfair",class="book",conn="warm"}[24h])) by (le))
p99: 1000 * histogram_quantile(0.99, sum(rate(pmapi_request_duration_seconds_bucket{cache!="hit",venue="betfair",class="book",conn="warm"}[24h])) by (le))
mean: 1000 * sum(rate(pmapi_request_duration_seconds_sum{cache!="hit",venue="betfair",class="book",conn="warm"}[24h])) / sum(rate(pmapi_request_duration_seconds_count{cache!="hit",venue="betfair",class="book",conn="warm"}[24h]))
success: clamp_max(sum(rate(pmapi_requests_total{venue="betfair",class="book",conn="warm",outcome="ok"}[24h])) / sum(rate(pmapi_requests_total{venue="betfair",class="book",conn="warm",outcome!="probe_invalid"}[24h])), 1)
sample_size: sum(increase(pmapi_requests_total{venue="betfair",class="book",conn="warm",outcome!="probe_invalid"}[24h]))
series: 1000 * histogram_quantile(0.50, sum(rate(pmapi_request_duration_seconds_bucket{cache!="hit",venue="betfair",class="book",conn="warm"}[1h])) by (le))
160 changes: 0 additions & 160 deletions harnesses/pm-rate-limits/cmd/script/betfair_auth.go

This file was deleted.

21 changes: 6 additions & 15 deletions harnesses/pm-rate-limits/cmd/script/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,30 +17,21 @@ type Config struct {
MobulaAPIKey string // Authorization: <key> on api.mobula.io
PredexonAPIKey string // x-api-key on api.predexon.com
DefinedSessionCookie string // 7-day cookie used to mint Codex JWT
KalshiAPIKeyID string // KALSHI-ACCESS-KEY header for WS auth
KalshiPrivateKeyPEM string // RSA private key PEM for WS signature
BetfairAppKey string // BETFAIR_APP_KEY — developer application key
BetfairUsername string // BETFAIR_USERNAME — login credential for session token
BetfairPassword string // BETFAIR_PASSWORD — login credential for session token
BetfairSessionToken string // BETFAIR_SESSION_TOKEN — pre-obtained token (alternative to user/pass)
KalshiAPIKeyID string // KALSHI-ACCESS-KEY header for WS auth
KalshiPrivateKeyPEM string // RSA private key PEM for WS signature
}

func loadConfig() Config {
cfg := Config{
MobulaAPIKey: strings.TrimSpace(os.Getenv("MOBULA_API_KEY")),
PredexonAPIKey: strings.TrimSpace(os.Getenv("PREDEXON_API_KEY")),
DefinedSessionCookie: strings.TrimSpace(os.Getenv("DEFINED_SESSION_COOKIE")),
KalshiAPIKeyID: strings.TrimSpace(os.Getenv("KALSHI_API_KEY_ID")),
KalshiPrivateKeyPEM: strings.TrimSpace(os.Getenv("KALSHI_PRIVATE_KEY_PEM")),
BetfairAppKey: strings.TrimSpace(os.Getenv("BETFAIR_APP_KEY")),
BetfairUsername: strings.TrimSpace(os.Getenv("BETFAIR_USERNAME")),
BetfairPassword: strings.TrimSpace(os.Getenv("BETFAIR_PASSWORD")),
BetfairSessionToken: strings.TrimSpace(os.Getenv("BETFAIR_SESSION_TOKEN")),
KalshiAPIKeyID: strings.TrimSpace(os.Getenv("KALSHI_API_KEY_ID")),
KalshiPrivateKeyPEM: strings.TrimSpace(os.Getenv("KALSHI_PRIVATE_KEY_PEM")),
}
fmt.Printf("[providers] mobula=%v predexon=%v codex=%v kalshi_ws=%v betfair=%v\n",
fmt.Printf("[providers] mobula=%v predexon=%v codex=%v kalshi_ws=%v\n",
cfg.MobulaAPIKey != "", cfg.PredexonAPIKey != "", cfg.DefinedSessionCookie != "",
cfg.KalshiAPIKeyID != "" && cfg.KalshiPrivateKeyPEM != "",
cfg.BetfairAppKey != "")
cfg.KalshiAPIKeyID != "" && cfg.KalshiPrivateKeyPEM != "")
return cfg
}

Expand Down
4 changes: 0 additions & 4 deletions harnesses/pm-rate-limits/cmd/script/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,6 @@ func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()

if cfg.BetfairAppKey != "" {
initBetfairAuth(cfg)
}

pinClient := &http.Client{Timeout: 20 * time.Second}
vs := venues(cfg)
stateByVenue := map[string]*venueState{}
Expand Down
57 changes: 0 additions & 57 deletions harnesses/pm-rate-limits/cmd/script/pin.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package main

import (
"bytes"
"context"
"encoding/json"
"errors"
Expand Down Expand Up @@ -370,62 +369,6 @@ func pinMetaculus(ctx context.Context, c *http.Client, avoid string) (Pin, error
return Pin{}, errors.New("metaculus: no open binary question with >24h close horizon")
}

func pinBetfair(ctx context.Context, c *http.Client, avoid string) (Pin, error) {
const catalogueURL = "https://api.betfair.com/exchange/betting/rest/v1.0/listMarketCatalogue/"
body := []byte(`{"filter":{"inPlayOnly":false},"sort":"FIRST_TO_START","maxResults":"200","marketProjection":["MARKET_START_TIME","RUNNER_DESCRIPTION"]}`)

var markets []struct {
MarketID string `json:"marketId"`
MarketStartTime string `json:"marketStartTime"`
Runners []json.RawMessage `json:"runners"`
}
if err := postJSON(ctx, c, catalogueURL, body, &markets, betfairRequestMutator()); err != nil {
return Pin{}, fmt.Errorf("betfair catalogue: %w", err)
}
for _, m := range markets {
if m.MarketID == avoid || len(m.Runners) != 2 {
continue
}
// Betfair uses "2006-01-02T15:04:05.000Z" format
start, err := time.Parse("2006-01-02T15:04:05.000Z", m.MarketStartTime)
if err != nil {
start, err = time.Parse(time.RFC3339, m.MarketStartTime)
if err != nil {
continue
}
}
if time.Until(start) < 48*time.Hour {
continue
}
return Pin{Market: m.MarketID, Expiry: start}, nil
}
return Pin{}, errors.New("betfair: no binary market with >48h horizon in first 200 results")
}

// postJSON sends a POST request with a JSON body and decodes the JSON response.
func postJSON(ctx context.Context, c *http.Client, url string, body []byte, out any, mutator func(*http.Request)) error {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("User-Agent", userAgent)
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
if mutator != nil {
mutator(req)
}
resp, err := c.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<10))
return fmt.Errorf("status %d: %s", resp.StatusCode, b)
}
return json.NewDecoder(io.LimitReader(resp.Body, 8<<20)).Decode(out)
}

// 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) {
Expand Down
Loading
Loading