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
70 changes: 61 additions & 9 deletions benchmarks/pm-fee-comparison.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ seo_description: "What does it cost to trade on Polymarket, Kalshi, Limitless an
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: draft
status: live
metric: Effective taker fee
unit: pct
higher_is_better: false
Expand Down Expand Up @@ -44,31 +44,83 @@ disclaimer: "Fee schedules change without notice. Verify current fees directly w

source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/pm-fee-comparison

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."
prometheus:
window: 24h
expected_freshness_seconds: 7200

rank_matrix_query: label_replace(pm_fee_taker_bps, "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"}
p90: pm_fee_taker_bps{venue="polymarket"}
p99: pm_fee_taker_bps{venue="polymarket"}
mean: pm_fee_taker_bps{venue="polymarket"}
success: vector(1)
sample_size: vector(1)
series: pm_fee_taker_bps{venue="polymarket"}

- 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"}
p90: pm_fee_taker_bps{venue="kalshi"}
p99: pm_fee_taker_bps{venue="kalshi"}
mean: pm_fee_taker_bps{venue="kalshi"}
success: vector(1)
sample_size: vector(1)
series: pm_fee_taker_bps{venue="kalshi"}

- slug: limitless
name: Limitless
tag: CLOB venue, fee structure via spread, no published fee schedule
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"}
p90: pm_fee_taker_bps{venue="limitless"}
p99: pm_fee_taker_bps{venue="limitless"}
mean: pm_fee_taker_bps{venue="limitless"}
success: pm_fee_last_refresh_seconds{venue="limitless"} > bool 0
sample_size: vector(1)
series: pm_fee_taker_bps{venue="limitless"}

- 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"}
p90: pm_fee_taker_bps{venue="manifold"}
p99: pm_fee_taker_bps{venue="manifold"}
mean: pm_fee_taker_bps{venue="manifold"}
success: vector(1)
sample_size: vector(1)
series: pm_fee_taker_bps{venue="manifold"}

- 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"}
p90: pm_fee_taker_bps{venue="myriad"}
p99: pm_fee_taker_bps{venue="myriad"}
mean: pm_fee_taker_bps{venue="myriad"}
success: vector(1)
sample_size: vector(1)
series: pm_fee_taker_bps{venue="myriad"}

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."

105 changes: 92 additions & 13 deletions benchmarks/pm-geographic-access.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

slug: pm-geographic-access
number: "113"
status: draft
status: live
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, Manifold and Myriad, reported as percent of regions returning a successful response.
category: Trading
Expand All @@ -16,20 +16,18 @@ abstract: |
CFTC settlement. Polymarket US (QCX), a separate CFTC-regulated entity, launched in December 2025
and is accessible to US users. Kalshi is CFTC-regulated and accessible to US users without
restriction. Minnesota enacted a law banning prediction markets effective August 1, 2026, which is
being challenged in court by the CFTC. This bench probes each venue's market listing endpoint from
nodes in 12 regions (US East, US West, EU West, EU Central, UK, Canada, Singapore, Japan, Australia,
Brazil, India, UAE) 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 regions returning a successful response. The bench does not assess legal access
rights, only observable HTTP-level behavior.
being challenged in court by the CFTC. 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 in 12 geographic regions once every 15 minutes. The request carries an identifying User-Agent and no VPN or proxy. Response codes, redirect chains and response body patterns are recorded."
- "Blocked classification: a response is classified as blocked if the HTTP status code is 403, if the response is a redirect to a URL containing terms such as block, geo, unavailable, or restricted, or if the TCP connection is reset before a response is received. Timeouts over 10 seconds are classified as unreachable rather than blocked."
- "Metric definition: the pct metric is the share of probe regions returning a 200 OK response to the venue's market listing endpoint in the trailing 24-hour window. A venue accessible from all 12 regions scores 100 pct. A venue blocked from all US probes but accessible from 9 of 12 regions scores 75 pct."
- "Polymarket disambiguation: the harness probes both polymarket.com (international exchange, geoblocked for US IPs) and the Polymarket US (QCX) endpoint separately and reports them as distinct rows. The combined accessibility row shows the union of regions that can reach either endpoint."
- "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 US probe on a venue's endpoint does not mean US users are legally permitted to trade on that venue. 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 or the Minnesota ban in 2026), the probe series will reflect the change within one probe cycle (15 minutes). The bench changelog records the date and description of each regime change."
- "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."
Expand All @@ -41,23 +39,104 @@ disclaimer: "Geographic access status changes as regulatory environments evolve.

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_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="us-east"}[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="us-east"}[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="us-east"}[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: manifold
name: Manifold
tag: Play-money, US-accessible, no 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="manifold"}[24h])
p90: 100 * avg_over_time(pm_geo_accessible{venue="manifold",region="eu-west"}[24h])
p99: 100 * avg_over_time(pm_geo_accessible{venue="manifold",region="us-east"}[24h])
mean: 100 * avg_over_time(pm_geo_accessible{venue="manifold"}[24h])
success: vector(1)
sample_size: increase(pm_geo_probe_total{venue="manifold"}[24h])
series: 100 * avg_over_time(pm_geo_accessible{venue="manifold"}[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="us-east"}[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])
22 changes: 22 additions & 0 deletions harnesses/pm-fee-comparison/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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"]
Loading
Loading