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
28 changes: 14 additions & 14 deletions benchmarks/pm-geographic-access.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ slug: pm-geographic-access
number: "113"
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.
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
Expand All @@ -13,10 +13,10 @@ 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. 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 every
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
Expand Down Expand Up @@ -115,18 +115,18 @@ providers:
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
- 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="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="eu-west"}[24h])
mean: 100 * avg_over_time(pm_geo_accessible{venue="manifold"}[24h])
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="manifold"}[24h])
series: 100 * avg_over_time(pm_geo_accessible{venue="manifold"}[1h])
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
Expand Down
52 changes: 52 additions & 0 deletions harnesses/pm-cohort-stats/cmd/script/defillama.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,61 @@ func fetchAllDefillama() {
fmt.Printf("[defillama][%s] tvl=%.0f change_1d=%.2f%%\n", slug, row.TVL, row.Change1d)
}

// Fetch Polymarket trading volume from the DeFiLlama dexs summary endpoint.
// /protocols only carries TVL; the dexs/summary endpoint exposes the actual
// aggregate trading volume that covers all markets including recently settled
// ones. This overrides the gamma-api vol24h/vol30d with a more complete figure.
fetchDefillamaPolymarketVolume()

pmCohortStatsLastTickUnix.Set(float64(time.Now().Unix()))
}

// fetchDefillamaPolymarketVolume queries DeFiLlama's /summary/dexs endpoint
// for Polymarket's aggregate trading volume. The /protocols list only carries
// TVL; this endpoint exposes total24h and total30d which cover all settled
// and active markets — significantly more complete than summing volume24hr
// from the active-only gamma-api pass. The slug tried first is
// "polymarket-international"; "polymarket" is the fallback. If neither
// resolves (endpoint unavailable or network error) the function returns
// silently and the gamma-api values from the Polymarket fetcher carry forward.
func fetchDefillamaPolymarketVolume() {
type dexsResp struct {
Total24h float64 `json:"total24h"`
Total7d float64 `json:"total7d"`
Total30d float64 `json:"total30d"`
}

slugs := []string{"polymarket-international", "polymarket"}
var r dexsResp
var found bool
for _, s := range slugs {
url := fmt.Sprintf("%s/summary/dexs/%s", defillamaBase, s)
body, err := getJSONDefillama(httpClientDefillama, url)
if err != nil {
continue
}
if err := json.Unmarshal(body, &r); err != nil {
continue
}
if r.Total24h > 0 || r.Total30d > 0 {
found = true
break
}
}
if !found {
fmt.Printf("[defillama-vol][polymarket] dexs endpoint unavailable or zero; keeping gamma-api values\n")
return
}
if r.Total24h > 0 {
pmVenueVolume24hUsd.WithLabelValues("polymarket").Set(r.Total24h)
}
if r.Total30d > 0 {
pmVenueVolume30dUsd.WithLabelValues("polymarket").Set(r.Total30d)
}
fmt.Printf("[defillama-vol][polymarket] vol24h=%.0f vol7d=%.0f vol30d=%.0f\n",
r.Total24h, r.Total7d, r.Total30d)
}

// normalizeName lower-cases and strips spaces / punctuation so
// "Manifold Markets" and "manifold-markets" both index to the same key.
func normalizeName(s string) string {
Expand Down
7 changes: 6 additions & 1 deletion harnesses/pm-cohort-stats/cmd/script/myriad.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,14 @@ func fetchMyriadVenue(v Venue) {
}()

// --- Track A: open markets, sorted by lifetime volume_notional.
// Myriad's API occasionally returns 500 on this sort; fall back to
// volume_notional_24h and then to no sort before giving up.
openRows, ok := myriadFetchAll(v, "open", "volume_notional")
if !ok {
// Nothing publishable on a hard failure. Errors already bucketed.
time.Sleep(2 * time.Second)
openRows, ok = myriadFetchAll(v, "open", "volume_notional_24h")
}
if !ok {
return
}

Expand Down
2 changes: 1 addition & 1 deletion harnesses/pm-cohort-stats/cmd/script/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ var Registry = []Venue{
{Slug: "kalshi", Name: "Kalshi", Type: "offchain", Chain: ""},
{Slug: "limitless", Name: "Limitless", Type: "onchain", Chain: "base"},
{Slug: "manifold", Name: "Manifold", Type: "offchain", Chain: ""},
{Slug: "myriad", Name: "Myriad", Type: "offchain", Chain: ""},
{Slug: "myriad", Name: "Myriad", Type: "onchain", Chain: "abstract"},
}

// VenueBySlug returns the Venue with the given slug, or nil if not found.
Expand Down
15 changes: 11 additions & 4 deletions harnesses/pm-geographic-access/cmd/script/probe.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ type venueProbe struct {
// 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",
Expand All @@ -33,14 +40,14 @@ var venueProbes = []venueProbe{
Venue: "limitless",
URL: "https://api.limitless.exchange/markets/active?page=1&limit=1&sortBy=newest",
},
{
Venue: "manifold",
URL: "https://api.manifold.markets/v0/markets?limit=1",
},
{
Venue: "myriad",
URL: "https://api-v2.myriadprotocol.com/markets?state=open&limit=1",
},
{
Venue: "forecastex",
URL: "https://forecastex.com/api/v1/contracts",
},
}

var httpClientProbe = &http.Client{
Expand Down
4 changes: 1 addition & 3 deletions src/lib/pm-stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,7 @@ const PM_VENUES: VenueSeed[] = [
{ slug: "polymarket", name: "Polymarket", type: "onchain", chain: "polygon" },
{ slug: "kalshi", name: "Kalshi", type: "offchain" },
{ slug: "limitless", name: "Limitless", type: "onchain", chain: "base" },
{ slug: "myriad", name: "Myriad", type: "offchain" },
{ slug: "predictit", name: "PredictIt", type: "offchain" },
{ slug: "smarkets", name: "Smarkets", type: "offchain" },
{ slug: "myriad", name: "Myriad", type: "onchain", chain: "abstract" },
];

const PM_DATA_FEEDS: DataFeedSeed[] = [
Expand Down
Loading