diff --git a/benchmarks/pm-fee-comparison.yml b/benchmarks/pm-fee-comparison.yml deleted file mode 100644 index 3951ebf5..00000000 --- a/benchmarks/pm-fee-comparison.yml +++ /dev/null @@ -1,126 +0,0 @@ -# OpenChainBench. Bench 112 - -slug: pm-fee-comparison -number: "112" -title: Prediction market fees, Polymarket vs Kalshi and more -seo_title: "Prediction market fees 2026: Polymarket, Kalshi, Limitless compared" -seo_description: "What does it cost to trade on Polymarket, Kalshi, Limitless and Myriad? Maker and taker fees, protocol cuts, total cost to trade, sourced from each venue's published fee schedule." -subtitle: Maker and taker fees, protocol cuts and net cost to trade across five prediction market venues, sourced from official fee schedules and verified from fills data. - -category: Trading -status: retired -metric: Effective taker fee -unit: pct -higher_is_better: false - -seo_intro: | - Nobody publishes a clean side-by-side of what it actually costs to trade prediction markets. Polymarket charges a flat percentage fee on each trade. Kalshi charges per-contract on a published schedule. Limitless embeds a fee in the AMM spread. Manifold uses play money. Myriad varies by market type. This bench parses each venue's fills data and official fee schedule so the comparison is based on measured values rather than marketing copy. - -abstract: | - Prediction market venues charge fees in ways that resist direct comparison. Polymarket charges 2% of - potential payout on every trade, regardless of contract price. Kalshi charges a per-contract fee on - a published schedule that scales with contract price and is capped at extremes, making near-certainty - contracts very cheap in percentage terms. Limitless charges fees through its CLOB spread. Manifold - uses play-money (mana) and has no real-money fee. Myriad charges a protocol fee on settlement. This - bench normalizes all fee structures to effective basis points of notional, measured at three - representative contract price points: at-the-money (50 cents), near-certainty (5 cents or 95 cents), - and mid-skewed (30 cents). The resulting leaderboard answers which venue is cheapest for a given - market condition, not just which venue has the lowest headline number. - -methodology: - - "Fee normalization: all fees are expressed as basis points of gross notional (contract price times contract count times $1 resolution value) at three price points: $0.50 (at-the-money), $0.05/$0.95 (near-certainty), and $0.30/$0.70 (mid-skewed). This converts Kalshi's per-contract dollar fee and Polymarket's payout-percentage fee to a common unit." - - "Polymarket fee measurement: the 2% fee is deducted from potential payout. A YES share bought at $0.50 pays $0.98 on resolution, not $1.00. Effective fee in basis points = 200 at all price points because the fee percentage is flat. No maker rebate exists on the public CLOB." - - "Kalshi fee measurement: Kalshi publishes a tiered per-contract fee schedule. Fees are capped at low and high price extremes. The harness reads the published schedule and computes the fee at each price point as fee/contract_price in basis points. Near-certainty markets benefit most from the cap." - - "Limitless and Myriad fee measurement: CLOB spread is used as a proxy for effective fee where explicit fee schedules are not published. The half-spread on the pinned market at each price tier is recorded as the effective round-trip cost." - - "Manifold is play-money and carries no real-money fee. It is included in the cohort for completeness but its fee row reads zero and it is excluded from the real-money fee rankings." - - "Data cadence: fee schedules are polled once per hour. Per-contract fees on Kalshi are re-read whenever the published schedule changes (detected via hash of the fee schedule endpoint). Effective fee rows on the leaderboard update within 1 hour of any venue publishing a fee change." - -findings: - - "Polymarket charges a flat 2% fee on each trade, one of the more transparent structures in the cohort since the rate does not vary by market or counterparty type." - - "Kalshi's per-contract fee means the effective percentage cost depends on where the contract trades relative to par. Fees are highest in percentage terms on near-certainty markets." - - "Manifold is play-money only. No real capital is at risk and no real fee is charged." - -disclaimer: "Fee schedules change without notice. Verify current fees directly with each venue before trading. This bench measures published and observed fees; it is not an endorsement of any venue." - -source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/pm-fee-comparison - -prometheus: - window: 24h - expected_freshness_seconds: 7200 - -rank_matrix_query: label_replace(pm_fee_taker_bps / 100, "provider", "$1", "venue", "(.+)") - -providers: - - slug: polymarket - name: Polymarket - tag: 2% fee on potential payout, no maker rebate, flat across all price points - formula: "Flat 200 bps of gross notional ($1 resolution value) at all price points. Fee = 2% × $1 payout = $0.02/contract. Source: Polymarket published fee schedule." - queries: - p50: pm_fee_taker_bps{venue="polymarket"} / 100 - p90: pm_fee_taker_bps{venue="polymarket"} / 100 - p99: pm_fee_taker_bps{venue="polymarket"} / 100 - mean: pm_fee_taker_bps{venue="polymarket"} / 100 - success: vector(1) - sample_size: vector(1) - series: pm_fee_taker_bps{venue="polymarket"} / 100 - - - slug: kalshi - name: Kalshi - tag: Per-contract fee on published schedule, capped at price extremes, US-regulated - formula: "Approximately 700 bps ($0.07/contract ÷ $1 resolution value) at ATM ($0.50 contract price). Source: Kalshi published fee schedule. Fees are capped at price extremes." - queries: - p50: pm_fee_taker_bps{venue="kalshi"} / 100 - p90: pm_fee_taker_bps{venue="kalshi"} / 100 - p99: pm_fee_taker_bps{venue="kalshi"} / 100 - mean: pm_fee_taker_bps{venue="kalshi"} / 100 - success: vector(1) - sample_size: vector(1) - series: pm_fee_taker_bps{venue="kalshi"} / 100 - - - slug: limitless - name: Limitless - tag: Live half-spread from active USDC markets, refreshed hourly - formula: "Median half-spread in bps across active USDC binary markets, computed from tradePrices.buy.market[0] and tradePrices.sell.market[0]. Half-spread = ((ask - bid) / 2) × 10000. Refreshed hourly." - queries: - p50: pm_fee_taker_bps{venue="limitless"} / 100 - p90: pm_fee_taker_bps{venue="limitless"} / 100 - p99: pm_fee_taker_bps{venue="limitless"} / 100 - mean: pm_fee_taker_bps{venue="limitless"} / 100 - success: pm_fee_last_refresh_seconds{venue="limitless"} > bool 0 - sample_size: vector(1) - series: pm_fee_taker_bps{venue="limitless"} / 100 - - - slug: manifold - name: Manifold - tag: Play-money (mana), no real-money fee, included for completeness - formula: "0 bps. Manifold uses play-money (mana) only. No real capital is at risk and no real fee is charged." - queries: - p50: pm_fee_taker_bps{venue="manifold"} / 100 - p90: pm_fee_taker_bps{venue="manifold"} / 100 - p99: pm_fee_taker_bps{venue="manifold"} / 100 - mean: pm_fee_taker_bps{venue="manifold"} / 100 - success: vector(1) - sample_size: vector(1) - series: pm_fee_taker_bps{venue="manifold"} / 100 - - - slug: myriad - name: Myriad - tag: Protocol fee on settlement, single-region US East - formula: "200 bps (2% protocol settlement fee). Source: Myriad published protocol documentation." - queries: - p50: pm_fee_taker_bps{venue="myriad"} / 100 - p90: pm_fee_taker_bps{venue="myriad"} / 100 - p99: pm_fee_taker_bps{venue="myriad"} / 100 - mean: pm_fee_taker_bps{venue="myriad"} / 100 - success: vector(1) - sample_size: vector(1) - series: pm_fee_taker_bps{venue="myriad"} / 100 - -faq: - - q: "What is the Polymarket trading fee?" - a: "Polymarket charges a 2% fee on each trade, deducted from the potential payout. If you buy YES shares at $0.50, each share pays out $0.98 rather than $1.00 if you win. The fee applies to both sides of the trade. There is no maker rebate on the public CLOB at the time of writing." - - q: "What is the Kalshi trading fee?" - a: "Kalshi charges a per-contract fee that scales with the contract's settlement price. The fee schedule is published on Kalshi's documentation. At mid-range prices ($0.40 to $0.60 per contract) the fee is typically around $0.07 per contract. Fees are capped, which makes very-low-price or very-high-price contracts cheaper in percentage terms." - - q: "Is Polymarket or Kalshi cheaper to trade?" - a: "It depends on the market. Polymarket's flat 2% fee is predictable. Kalshi's per-contract fee is cheaper on near-certainty markets where the cap kicks in, and more expensive on mid-book markets. The live fee comparison will show side-by-side numbers once the harness is running." - diff --git a/benchmarks/pm-geographic-access.yml b/benchmarks/pm-geographic-access.yml deleted file mode 100644 index cae066d4..00000000 --- a/benchmarks/pm-geographic-access.yml +++ /dev/null @@ -1,142 +0,0 @@ -# OpenChainBench. Bench 113 - -slug: pm-geographic-access -number: "113" -status: retired -title: Prediction market geographic access, which countries can reach each venue -subtitle: HTTP probes from regional nodes checking 200 OK vs blocked or redirect for Polymarket, Kalshi, Limitless, Myriad and ForecastEx, reported as percent of regions returning a successful response. -category: Trading -metric: Geographic accessibility rate -unit: pct -higher_is_better: true - -abstract: | - Prediction market venues operate under different regulatory regimes and enforce access restrictions - at the HTTP layer. Polymarket's international exchange has geoblocked US IP addresses since a 2022 - CFTC settlement. Kalshi is CFTC-regulated and accessible to US users without restriction. ForecastEx, - operated by Interactive Brokers and CFTC-regulated, is targeted at US users and may exhibit - geo-restrictions for non-US regions. Myriad Protocol is an onchain venue on the Abstract L2 with - no documented geographic restrictions. This bench probes each venue's market listing endpoint every - 15 minutes from probe nodes in EU West (VPS Paris), US East, and Singapore and records the HTTP - response code. A 200 OK counts as accessible; a 403, redirect to a block page, or connection reset - counts as blocked. The result is expressed as the percentage of probe time the endpoint returned 200 - from each region. The bench does not assess legal access rights, only observable HTTP-level behavior. - -methodology: - - "Probe design: the harness sends an HTTP GET to each venue's public market listing endpoint from dedicated probe nodes (EU West VPS Paris, US East and Singapore on Railway) every 15 minutes. The request carries an identifying User-Agent and no VPN or proxy. HTTP status codes are recorded." - - "Blocked classification: a response is classified as blocked if the HTTP status code is not 200 (403, 4xx, 5xx, or a redirect to a block page). Timeouts over 10 seconds are classified as unreachable (also blocked). Only 200 OK counts as accessible." - - "Metric definition: pm_geo_accessible{venue, region} = 1 when the probe returns 200 OK, 0 otherwise. The displayed pct is the avg_over_time of this gauge over the trailing 24 hours, representing the fraction of the day the venue was accessible from that probe region." - - "Legal access vs HTTP access: this bench measures what the API returns to an HTTP request, not what a person in a given jurisdiction is legally permitted to do. A 200 OK from a probe on a venue's endpoint does not mean users in that region are legally permitted to trade. Consult venue terms of service and qualified legal counsel for legal access questions." - - "Regulatory events: when a regulatory event changes HTTP-level access (such as the Polymarket US geoblock in 2022), the probe series reflects the change within one probe cycle (15 minutes)." - -findings: - - "Polymarket's international exchange (polymarket.com) geoblocks US IP addresses following a 2022 CFTC settlement in which Polymarket LLC paid a $1.4M fine. US probes receive a 403 or redirect. The international exchange scores below 100 pct by design on this bench." - - "Polymarket US (QCX), a separate CFTC-regulated entity, launched December 3, 2025 and is accessible to US users. It initially launched iOS-only with a waitlist; the waitlist dropped in May 2026." - - "Kalshi (Kalshi Exchange LLC) is CFTC-regulated and accessible to US users from all US probe regions. It is the primary regulated US-accessible real-money prediction market." - - "Minnesota enacted a law banning prediction markets effective August 1, 2026. The CFTC is challenging this law in court. The bench monitors Minnesota-region probes for HTTP-level changes as the legal challenge progresses." - -disclaimer: "Geographic access status changes as regulatory environments evolve. Verify current access directly with each venue before depositing funds. This bench measures API-level HTTP responses, not legal access rights." - -source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/pm-geographic-access - -prometheus: - window: 24h - expected_freshness_seconds: 1800 - -rank_matrix_query: 100 * label_replace(avg by (venue) (avg_over_time(pm_geo_accessible[1h])), "provider", "$1", "venue", "(.+)") - -dimensions: - region: - - { value: all, label: All regions } - - { value: eu-west, label: EU West } - - { value: us-east, label: US East } - - { value: sgp, label: Singapore } - -metric_panels: - - id: accessible_eu - label: EU West accessible - description: "Fraction of probe cycles from EU West (Paris VPS) where the venue's market listing endpoint returned HTTP 200 OK in the last 24 hours." - metric: 100 * avg_over_time(pm_geo_accessible{region="eu-west"}[24h]) - label_key: venue - unit: pct - higher_is_better: true - - id: accessible_us - label: US East accessible - description: "Fraction of probe cycles from US East where the venue's market listing endpoint returned HTTP 200 OK. Polymarket international exchange blocks US IPs by design." - metric: 100 * avg_over_time(pm_geo_accessible{region="us-east"}[24h]) - label_key: venue - unit: pct - higher_is_better: true - - id: accessible_sgp - label: Singapore accessible - description: "Fraction of probe cycles from Singapore where the venue's market listing endpoint returned HTTP 200 OK." - metric: 100 * avg_over_time(pm_geo_accessible{region="sgp"}[24h]) - label_key: venue - unit: pct - higher_is_better: true - -providers: - - slug: polymarket - name: Polymarket - tag: International exchange geoblocked for US IPs since 2022 CFTC settlement - formula: "Fraction of 15-minute probe cycles returning HTTP 200 OK from each probe region, averaged over 24 hours. US probes return 403 by design (CFTC settlement geoblock)." - queries: - p50: 100 * avg_over_time(pm_geo_accessible{venue="polymarket"}[24h]) - p90: 100 * avg_over_time(pm_geo_accessible{venue="polymarket",region="eu-west"}[24h]) - p99: 100 * avg_over_time(pm_geo_accessible{venue="polymarket",region="eu-west"}[24h]) - mean: 100 * avg_over_time(pm_geo_accessible{venue="polymarket"}[24h]) - success: vector(1) - sample_size: increase(pm_geo_probe_total{venue="polymarket"}[24h]) - series: 100 * avg_over_time(pm_geo_accessible{venue="polymarket"}[1h]) - - - slug: kalshi - name: Kalshi - tag: CFTC-regulated, accessible to US users, all regions - formula: "Fraction of 15-minute probe cycles returning HTTP 200 OK from each probe region, averaged over 24 hours." - queries: - p50: 100 * avg_over_time(pm_geo_accessible{venue="kalshi"}[24h]) - p90: 100 * avg_over_time(pm_geo_accessible{venue="kalshi",region="eu-west"}[24h]) - p99: 100 * avg_over_time(pm_geo_accessible{venue="kalshi",region="eu-west"}[24h]) - mean: 100 * avg_over_time(pm_geo_accessible{venue="kalshi"}[24h]) - success: vector(1) - sample_size: increase(pm_geo_probe_total{venue="kalshi"}[24h]) - series: 100 * avg_over_time(pm_geo_accessible{venue="kalshi"}[1h]) - - - slug: limitless - name: Limitless - tag: CLOB venue, no documented geographic restrictions - formula: "Fraction of 15-minute probe cycles returning HTTP 200 OK from each probe region, averaged over 24 hours." - queries: - p50: 100 * avg_over_time(pm_geo_accessible{venue="limitless"}[24h]) - p90: 100 * avg_over_time(pm_geo_accessible{venue="limitless",region="eu-west"}[24h]) - p99: 100 * avg_over_time(pm_geo_accessible{venue="limitless",region="eu-west"}[24h]) - mean: 100 * avg_over_time(pm_geo_accessible{venue="limitless"}[24h]) - success: vector(1) - sample_size: increase(pm_geo_probe_total{venue="limitless"}[24h]) - series: 100 * avg_over_time(pm_geo_accessible{venue="limitless"}[1h]) - - - slug: forecastex - name: ForecastEx - tag: CFTC-regulated by Interactive Brokers, US users only - formula: "Fraction of 15-minute probe cycles returning HTTP 200 OK from each probe region, averaged over 24 hours." - queries: - p50: 100 * avg_over_time(pm_geo_accessible{venue="forecastex"}[24h]) - p90: 100 * avg_over_time(pm_geo_accessible{venue="forecastex",region="eu-west"}[24h]) - p99: 100 * avg_over_time(pm_geo_accessible{venue="forecastex",region="eu-west"}[24h]) - mean: 100 * avg_over_time(pm_geo_accessible{venue="forecastex"}[24h]) - success: vector(1) - sample_size: increase(pm_geo_probe_total{venue="forecastex"}[24h]) - series: 100 * avg_over_time(pm_geo_accessible{venue="forecastex"}[1h]) - - - slug: myriad - name: Myriad - tag: Single-region US East, no documented geographic restrictions - formula: "Fraction of 15-minute probe cycles returning HTTP 200 OK from each probe region, averaged over 24 hours." - queries: - p50: 100 * avg_over_time(pm_geo_accessible{venue="myriad"}[24h]) - p90: 100 * avg_over_time(pm_geo_accessible{venue="myriad",region="eu-west"}[24h]) - p99: 100 * avg_over_time(pm_geo_accessible{venue="myriad",region="eu-west"}[24h]) - mean: 100 * avg_over_time(pm_geo_accessible{venue="myriad"}[24h]) - success: vector(1) - sample_size: increase(pm_geo_probe_total{venue="myriad"}[24h]) - series: 100 * avg_over_time(pm_geo_accessible{venue="myriad"}[1h]) diff --git a/harnesses/pm-fee-comparison/Dockerfile b/harnesses/pm-fee-comparison/Dockerfile deleted file mode 100644 index 01d7ac64..00000000 --- a/harnesses/pm-fee-comparison/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -FROM golang:1.24-alpine AS builder - -WORKDIR /app -RUN apk add --no-cache git - -COPY go.mod go.sum* ./ -RUN go mod download || true - -COPY . . - -RUN CGO_ENABLED=0 GOOS=linux go build -o /app/pm-fee-comparison ./cmd/script - -FROM debian:bookworm-slim - -WORKDIR /app -RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/* - -COPY --from=builder /app/pm-fee-comparison /app/pm-fee-comparison - -EXPOSE 2112 - -CMD ["/app/pm-fee-comparison"] diff --git a/harnesses/pm-fee-comparison/cmd/script/fees.go b/harnesses/pm-fee-comparison/cmd/script/fees.go deleted file mode 100644 index f735f40f..00000000 --- a/harnesses/pm-fee-comparison/cmd/script/fees.go +++ /dev/null @@ -1,140 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "sort" - "time" -) - -const ( - feesLimitlessBase = "https://api.limitless.exchange" - feesUA = "OpenChainBench/1.0 (+https://openchainbench.com/methodology; contact@mobula.io)" -) - -var httpClientFees = &http.Client{Timeout: 20 * time.Second} - -// setStaticFees publishes known fixed fees once at startup. These are refreshed -// only when the published fee schedules change (manual update required). -func setStaticFees() { - now := float64(time.Now().Unix()) - - // Polymarket: 2% fee deducted from potential payout ($1 resolution). - // Fee = 2% × $1 = $0.02/contract → 200 bps of $1 gross notional. - pmFeeTakerBps.WithLabelValues("polymarket").Set(200) - pmFeeLastRefresh.WithLabelValues("polymarket").Set(now) - - // Kalshi: per-contract fee schedule, approximately $0.07/contract at $0.50 price. - // $0.07 / $1 resolution value = 7% = 700 bps. Source: kalshi.com published schedule. - pmFeeTakerBps.WithLabelValues("kalshi").Set(700) - pmFeeLastRefresh.WithLabelValues("kalshi").Set(now) - - // Manifold: play-money (mana) only, no real-money fee. - pmFeeTakerBps.WithLabelValues("manifold").Set(0) - pmFeeLastRefresh.WithLabelValues("manifold").Set(now) - - // Myriad: 2% protocol settlement fee = 200 bps. - pmFeeTakerBps.WithLabelValues("myriad").Set(200) - pmFeeLastRefresh.WithLabelValues("myriad").Set(now) - - fmt.Println("[fees] static: polymarket=200 kalshi=700 manifold=0 myriad=200 (bps)") -} - -// tradePricesSlice deserializes tradePrices.buy.market / tradePrices.sell.market. -// Each slice element is the price for one outcome (YES=index 0, NO=index 1). -type tradePricesBranch struct { - Market []float64 `json:"market"` -} - -type tradePrices struct { - Buy tradePricesBranch `json:"buy"` - Sell tradePricesBranch `json:"sell"` -} - -type limitlessFeeMarket struct { - Status string `json:"status"` - TradePrices tradePrices `json:"tradePrices"` - CollateralToken struct { - Symbol string `json:"symbol"` - } `json:"collateralToken"` - WinningOutcomeIndex *int `json:"winningOutcomeIndex"` -} - -type limitlessFeeResp struct { - Data []limitlessFeeMarket `json:"data"` -} - -// fetchLimitlessFee polls /markets/active page 1, computes the median -// half-spread in basis points across active USDC binary markets, and sets -// the pm_fee_taker_bps{venue="limitless"} gauge. -// -// Half-spread bps = ((bestAsk - bestBid) / 2) × 10000, where prices are -// expressed as fractions of the $1 resolution value. This is directly -// comparable to Polymarket's 200 bps (2% of $1 payout). -func fetchLimitlessFee() { - url := fmt.Sprintf("%s/markets/active?page=1&limit=25&sortBy=newest", feesLimitlessBase) - req, _ := http.NewRequest("GET", url, nil) - req.Header.Set("User-Agent", feesUA) - req.Header.Set("Accept", "application/json") - - resp, err := httpClientFees.Do(req) - if err != nil { - pmFeeFetchErrors.WithLabelValues("limitless", "network").Inc() - fmt.Printf("[fees][limitless] request error: %v\n", err) - return - } - defer resp.Body.Close() - body, _ := io.ReadAll(resp.Body) - - if resp.StatusCode != 200 { - pmFeeFetchErrors.WithLabelValues("limitless", fmt.Sprintf("http_%d", resp.StatusCode)).Inc() - fmt.Printf("[fees][limitless] status %d\n", resp.StatusCode) - return - } - - var r limitlessFeeResp - if err := json.Unmarshal(body, &r); err != nil { - pmFeeFetchErrors.WithLabelValues("limitless", "parse").Inc() - fmt.Printf("[fees][limitless] parse error: %v\n", err) - return - } - - var halfSpreads []float64 - for _, m := range r.Data { - if m.CollateralToken.Symbol != "USDC" { - continue - } - if m.WinningOutcomeIndex != nil { - continue - } - buy := m.TradePrices.Buy.Market - sell := m.TradePrices.Sell.Market - if len(buy) == 0 || len(sell) == 0 { - continue - } - ask := buy[0] - bid := sell[0] - // Sanity: bid < ask, both in (0, 1) - if bid <= 0 || ask <= 0 || bid >= ask || ask > 1 { - continue - } - halfSpreadBps := ((ask - bid) / 2) * 10000 - halfSpreads = append(halfSpreads, halfSpreadBps) - } - - if len(halfSpreads) == 0 { - fmt.Printf("[fees][limitless] no valid spread data (markets=%d)\n", len(r.Data)) - return - } - - sort.Float64s(halfSpreads) - median := halfSpreads[len(halfSpreads)/2] - - pmFeeTakerBps.WithLabelValues("limitless").Set(median) - pmFeeLastRefresh.WithLabelValues("limitless").Set(float64(time.Now().Unix())) - - fmt.Printf("[fees][limitless] markets_with_spread=%d median_half_spread_bps=%.1f\n", - len(halfSpreads), median) -} diff --git a/harnesses/pm-fee-comparison/cmd/script/main.go b/harnesses/pm-fee-comparison/cmd/script/main.go deleted file mode 100644 index 733b4394..00000000 --- a/harnesses/pm-fee-comparison/cmd/script/main.go +++ /dev/null @@ -1,43 +0,0 @@ -package main - -import ( - "fmt" - "os" - "os/signal" - "syscall" - "time" -) - -func main() { - fmt.Println("=== pm-fee-comparison harness ===") - fmt.Println("Effective taker fees in basis points per venue. Static for most venues; live spread for Limitless.") - fmt.Println("Exposes /metrics on :2112.") - - go func() { - if err := StartMetricsServer(":2112"); err != nil { - fmt.Printf("[pm-fee-comparison] metrics server error: %v\n", err) - os.Exit(1) - } - }() - - // Set static fees immediately at startup. - setStaticFees() - // Fetch Limitless live spread at startup then every hour. - fetchLimitlessFee() - - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) - - ticker := time.NewTicker(1 * time.Hour) - defer ticker.Stop() - - for { - select { - case <-sigChan: - fmt.Println("[pm-fee-comparison] shutting down") - return - case <-ticker.C: - fetchLimitlessFee() - } - } -} diff --git a/harnesses/pm-fee-comparison/cmd/script/metrics.go b/harnesses/pm-fee-comparison/cmd/script/metrics.go deleted file mode 100644 index 46a921a4..00000000 --- a/harnesses/pm-fee-comparison/cmd/script/metrics.go +++ /dev/null @@ -1,58 +0,0 @@ -package main - -import ( - "net/http" - - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promhttp" -) - -var ( - // pm_fee_taker_bps{venue} is the effective taker fee in basis points - // of gross notional ($1 resolution value per contract). - // - // Static venues (set once at startup): - // polymarket = 200 bps (flat 2% of potential payout) - // kalshi = 700 bps (approx $0.07/contract at $0.50; from published schedule) - // manifold = 0 bps (play-money, no real fee) - // myriad = 200 bps (2% protocol settlement fee) - // - // Live venue (refreshed hourly): - // limitless = median half-spread across active USDC markets (from tradePrices API) - pmFeeTakerBps = prometheus.NewGaugeVec( - prometheus.GaugeOpts{ - Name: "pm_fee_taker_bps", - Help: "Effective taker fee in basis points of gross notional ($1 resolution value per contract). Polymarket=200 (2% of payout, flat). Kalshi=700 (approx $0.07/contract at ATM, from published schedule). Limitless=live median half-spread across active USDC markets. Manifold=0 (play-money). Myriad=200 (2% settlement protocol fee).", - }, - []string{"venue"}, - ) - - pmFeeLastRefresh = prometheus.NewGaugeVec( - prometheus.GaugeOpts{ - Name: "pm_fee_last_refresh_seconds", - Help: "Unix timestamp of the last successful fee measurement per venue.", - }, - []string{"venue"}, - ) - - pmFeeFetchErrors = prometheus.NewCounterVec( - prometheus.CounterOpts{ - Name: "pm_fee_fetch_errors_total", - Help: "Total fetch failures per venue and error type.", - }, - []string{"venue", "error_type"}, - ) -) - -func init() { - prometheus.MustRegister(pmFeeTakerBps, pmFeeLastRefresh, pmFeeFetchErrors) -} - -func StartMetricsServer(addr string) error { - mux := http.NewServeMux() - mux.Handle("/metrics", promhttp.Handler()) - mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { - _, _ = w.Write([]byte("OK")) - }) - return http.ListenAndServe(addr, mux) -} diff --git a/harnesses/pm-fee-comparison/go.mod b/harnesses/pm-fee-comparison/go.mod deleted file mode 100644 index 1c3a0392..00000000 --- a/harnesses/pm-fee-comparison/go.mod +++ /dev/null @@ -1,17 +0,0 @@ -module github.com/mobula/pm-fee-comparison - -go 1.24 - -require github.com/prometheus/client_golang v1.20.5 - -require ( - github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/klauspost/compress v1.17.9 // indirect - github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.55.0 // indirect - github.com/prometheus/procfs v0.15.1 // indirect - golang.org/x/sys v0.22.0 // indirect - google.golang.org/protobuf v1.34.2 // indirect -) diff --git a/harnesses/pm-fee-comparison/go.sum b/harnesses/pm-fee-comparison/go.sum deleted file mode 100644 index d5318cf8..00000000 --- a/harnesses/pm-fee-comparison/go.sum +++ /dev/null @@ -1,24 +0,0 @@ -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= -github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= -github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= -github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= diff --git a/harnesses/pm-geographic-access/Dockerfile b/harnesses/pm-geographic-access/Dockerfile deleted file mode 100644 index a2cdfb58..00000000 --- a/harnesses/pm-geographic-access/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -FROM golang:1.24-alpine AS builder - -WORKDIR /app -RUN apk add --no-cache git - -COPY go.mod go.sum* ./ -RUN go mod download || true - -COPY . . - -RUN CGO_ENABLED=0 GOOS=linux go build -o /app/pm-geographic-access ./cmd/script - -FROM debian:bookworm-slim - -WORKDIR /app -RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/* - -COPY --from=builder /app/pm-geographic-access /app/pm-geographic-access - -EXPOSE 2112 - -CMD ["/app/pm-geographic-access"] diff --git a/harnesses/pm-geographic-access/cmd/script/main.go b/harnesses/pm-geographic-access/cmd/script/main.go deleted file mode 100644 index 945e8af5..00000000 --- a/harnesses/pm-geographic-access/cmd/script/main.go +++ /dev/null @@ -1,72 +0,0 @@ -package main - -import ( - "fmt" - "os" - "os/signal" - "strings" - "syscall" - "time" -) - -func main() { - region := loadRegion() - fmt.Printf("=== pm-geographic-access harness (region=%s) ===\n", region) - fmt.Println("HTTP probes to each venue's market listing endpoint. 200 OK = accessible, 4xx/error = blocked.") - fmt.Println("Exposes /metrics on :2112.") - - go func() { - if err := StartMetricsServer(":2112"); err != nil { - fmt.Printf("[pm-geographic-access] metrics server error: %v\n", err) - os.Exit(1) - } - }() - - // First probe immediately at startup. - runAllProbes(region) - - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) - - ticker := time.NewTicker(15 * time.Minute) - defer ticker.Stop() - - for { - select { - case <-sigChan: - fmt.Println("[pm-geographic-access] shutting down") - return - case <-ticker.C: - runAllProbes(region) - } - } -} - -// loadRegion reads the probe region from env vars. Same resolution order -// as pm-rate-limits so the Prometheus label space is consistent. -func loadRegion() string { - if r := strings.TrimSpace(os.Getenv("REGION")); r != "" { - return r - } - if r := normalizeRailwayRegion(os.Getenv("RAILWAY_REPLICA_REGION")); r != "" { - return r - } - return "eu-west" -} - -func normalizeRailwayRegion(raw string) string { - raw = strings.ToLower(strings.TrimSpace(raw)) - if raw == "" { - return "" - } - switch { - case strings.HasPrefix(raw, "us-"), strings.HasPrefix(raw, "northamerica"): - return "us-east" - case strings.HasPrefix(raw, "europe"), strings.HasPrefix(raw, "eu-"): - return "eu-west" - case strings.HasPrefix(raw, "asia"), strings.HasPrefix(raw, "ap-"): - return "sgp" - default: - return raw - } -} diff --git a/harnesses/pm-geographic-access/cmd/script/metrics.go b/harnesses/pm-geographic-access/cmd/script/metrics.go deleted file mode 100644 index 47d0ff8a..00000000 --- a/harnesses/pm-geographic-access/cmd/script/metrics.go +++ /dev/null @@ -1,58 +0,0 @@ -package main - -import ( - "net/http" - - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promhttp" -) - -var ( - // pm_geo_accessible{venue, region} = 1 if the venue's public market - // listing endpoint returned HTTP 200 from the probe region, 0 otherwise. - // Probe frequency: every 15 minutes. - pmGeoAccessible = prometheus.NewGaugeVec( - prometheus.GaugeOpts{ - Name: "pm_geo_accessible", - Help: "1 if the venue's public market listing endpoint returned HTTP 200 from the probe region, 0 if blocked (403), redirected to a block page, or timed out. Probed every 15 minutes. Does not assess legal access rights.", - }, - []string{"venue", "region"}, - ) - - pmGeoLastRefresh = prometheus.NewGaugeVec( - prometheus.GaugeOpts{ - Name: "pm_geo_last_refresh_seconds", - Help: "Unix timestamp of the last probe attempt per venue and region.", - }, - []string{"venue", "region"}, - ) - - pmGeoProbeTotal = prometheus.NewCounterVec( - prometheus.CounterOpts{ - Name: "pm_geo_probe_total", - Help: "Total probe attempts per venue and region.", - }, - []string{"venue", "region"}, - ) - - pmGeoFetchErrors = prometheus.NewCounterVec( - prometheus.CounterOpts{ - Name: "pm_geo_fetch_errors_total", - Help: "Total probe failures (network errors, timeouts) per venue and error type.", - }, - []string{"venue", "error_type"}, - ) -) - -func init() { - prometheus.MustRegister(pmGeoAccessible, pmGeoLastRefresh, pmGeoProbeTotal, pmGeoFetchErrors) -} - -func StartMetricsServer(addr string) error { - mux := http.NewServeMux() - mux.Handle("/metrics", promhttp.Handler()) - mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { - _, _ = w.Write([]byte("OK")) - }) - return http.ListenAndServe(addr, mux) -} diff --git a/harnesses/pm-geographic-access/cmd/script/probe.go b/harnesses/pm-geographic-access/cmd/script/probe.go deleted file mode 100644 index f32c45ce..00000000 --- a/harnesses/pm-geographic-access/cmd/script/probe.go +++ /dev/null @@ -1,109 +0,0 @@ -package main - -import ( - "fmt" - "io" - "net/http" - "time" -) - -const ( - probeUA = "OpenChainBench/1.0 (+https://openchainbench.com/methodology; contact@mobula.io)" - probeTimeout = 10 * time.Second -) - -type venueProbe struct { - Venue string - URL string -} - -// venueProbes is the list of market-listing endpoints to probe. -// Each URL returns a 200 when the venue is accessible from the probe region, -// a 403 when geo-blocked, or a redirect to a block page. -// -// Manifold is intentionally excluded: it is play-money (no real money at -// stake) and has no documented geographic restrictions — it would always -// score 100% from every region, making it uninteresting for this bench. -// ForecastEx (Interactive Brokers CFTC-regulated exchange) is included -// because it is US-regulated and may exhibit geo restrictions from non-US -// regions, mirroring the Kalshi vs Polymarket story for a third regulated venue. -var venueProbes = []venueProbe{ - { - Venue: "polymarket", - URL: "https://gamma-api.polymarket.com/markets?closed=false&active=true&limit=1", - }, - { - Venue: "kalshi", - URL: "https://api.elections.kalshi.com/trade-api/v2/markets/?limit=1", - }, - { - Venue: "limitless", - URL: "https://api.limitless.exchange/markets/active?page=1&limit=1&sortBy=newest", - }, - { - Venue: "myriad", - URL: "https://api-v2.myriadprotocol.com/markets?state=open&limit=1", - }, - { - // ForecastEx (Interactive Brokers CFTC-regulated exchange). The root - // domain returns 200 when accessible, or a geo-block response for - // regions where IBKR restricts access. No public unauthenticated API - // endpoint exists; we probe the market listing page directly. - Venue: "forecastex", - URL: "https://forecastex.com/markets", - }, -} - -var httpClientProbe = &http.Client{ - Timeout: probeTimeout, - // Follow redirects but detect block pages (redirect to geo-block URL). - CheckRedirect: func(req *http.Request, via []*http.Request) error { - if len(via) >= 5 { - return http.ErrUseLastResponse - } - return nil - }, -} - -// runAllProbes fires one HTTP GET per venue and updates the accessibility gauge. -func runAllProbes(region string) { - for _, p := range venueProbes { - runProbe(p, region) - } -} - -func runProbe(p venueProbe, region string) { - pmGeoProbeTotal.WithLabelValues(p.Venue, region).Inc() - - req, err := http.NewRequest("GET", p.URL, nil) - if err != nil { - pmGeoAccessible.WithLabelValues(p.Venue, region).Set(0) - pmGeoFetchErrors.WithLabelValues(p.Venue, "request_build").Inc() - return - } - req.Header.Set("User-Agent", probeUA) - req.Header.Set("Accept", "application/json") - - resp, err := httpClientProbe.Do(req) - if err != nil { - pmGeoAccessible.WithLabelValues(p.Venue, region).Set(0) - pmGeoLastRefresh.WithLabelValues(p.Venue, region).Set(float64(time.Now().Unix())) - pmGeoFetchErrors.WithLabelValues(p.Venue, "network").Inc() - fmt.Printf("[geo][%s][%s] network error: %v\n", p.Venue, region, err) - return - } - defer resp.Body.Close() - // Drain body to reuse connection. - _, _ = io.Copy(io.Discard, resp.Body) - - accessible := 0.0 - if resp.StatusCode == 200 { - accessible = 1.0 - } - - pmGeoAccessible.WithLabelValues(p.Venue, region).Set(accessible) - pmGeoLastRefresh.WithLabelValues(p.Venue, region).Set(float64(time.Now().Unix())) - - fmt.Printf("[geo][%s][%s] status=%d accessible=%.0f\n", - p.Venue, region, resp.StatusCode, accessible) -} diff --git a/harnesses/pm-geographic-access/go.mod b/harnesses/pm-geographic-access/go.mod deleted file mode 100644 index 5ce85e9d..00000000 --- a/harnesses/pm-geographic-access/go.mod +++ /dev/null @@ -1,17 +0,0 @@ -module github.com/mobula/pm-geographic-access - -go 1.24 - -require github.com/prometheus/client_golang v1.20.5 - -require ( - github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/klauspost/compress v1.17.9 // indirect - github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.55.0 // indirect - github.com/prometheus/procfs v0.15.1 // indirect - golang.org/x/sys v0.22.0 // indirect - google.golang.org/protobuf v1.34.2 // indirect -) diff --git a/harnesses/pm-geographic-access/go.sum b/harnesses/pm-geographic-access/go.sum deleted file mode 100644 index d5318cf8..00000000 --- a/harnesses/pm-geographic-access/go.sum +++ /dev/null @@ -1,24 +0,0 @@ -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= -github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= -github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= -github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= diff --git a/public/logos/predictit.svg b/public/logos/predictit.svg index 843795b6..4adac07c 100644 --- a/public/logos/predictit.svg +++ b/public/logos/predictit.svg @@ -1,4 +1,23 @@ - - - PI + + + + + + + + + + + + + + + + + + diff --git a/public/logos/smarkets.jpg b/public/logos/smarkets.jpg new file mode 100644 index 00000000..81b82878 Binary files /dev/null and b/public/logos/smarkets.jpg differ diff --git a/public/logos/smarkets.svg b/public/logos/smarkets.svg deleted file mode 100644 index 25a1ad70..00000000 --- a/public/logos/smarkets.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - S - diff --git a/src/lib/logo-manifest.ts b/src/lib/logo-manifest.ts index f1691a86..96dcca7b 100644 --- a/src/lib/logo-manifest.ts +++ b/src/lib/logo-manifest.ts @@ -67,7 +67,7 @@ const RAW: Record = { manifold: "/logos/manifold.svg", myriad: "/logos/myriad.png", predictit: "/logos/predictit.svg", - smarkets: "/logos/smarkets.svg", + smarkets: "/logos/smarkets.jpg", // ─── Perp DEX cohort (perps hub /perps) ─── drift: "/logos/drift.png",