diff --git a/benchmarks/pm-resolution-delay.yml b/benchmarks/pm-resolution-delay.yml new file mode 100644 index 00000000..ebb599a6 --- /dev/null +++ b/benchmarks/pm-resolution-delay.yml @@ -0,0 +1,241 @@ +# OpenChainBench. Bench № 039 + +slug: pm-resolution-delay +number: "039" +title: Resolution delay, Polymarket vs Kalshi +seo_title: "Prediction market resolution delay live 2026" +seo_description: "Polymarket and Kalshi resolution time measured live: median delay from close to payout, broken down by category. Polymarket anchored on UMA ProposePrice, Kalshi on API-observed finalization." +subtitle: How long from market close to final settlement — Polymarket measured onchain via UMA ProposePrice→QuestionResolved, Kalshi measured via REST API polling (close_time to first observed finalized status). + +category: Trading +status: live +metric: Resolution delay +unit: s +higher_is_better: false + +seo_intro: | + The most asked question about prediction markets after "is it legal" is "how + long until I get paid". The answer circulating on social media for Polymarket + is that 93 percent of markets resolve within 2 hours. That number has no + published methodology and cannot be reproduced from Polymarket's own Gamma + API. This benchmark measures what can actually be observed: for Polymarket, + the time from the first onchain outcome proposal on UMA's optimistic oracle to + the onchain resolution, read directly from Polygon logs. For Kalshi, the time + from a market's scheduled close_time to the first poll at which its result is + non-null, measured every five minutes. Both venues are broken down by category + so the comparison is apples to apples. + +abstract: | + Polymarket: a listener follows the UMA CTF adapters on Polygon + (QuestionInitialized, QuestionResolved, QuestionReset) and the Optimistic + Oracle they call (ProposePrice, DisputePrice). Delay = first ProposePrice + block to QuestionResolved block, joined to Gamma for category tags. + + Kalshi: a poller hits the public /trade-api/v2/markets endpoint every 5 + minutes. Delay = market close_time to the first poll at which result != null. + Accuracy is ±5 min, negligible for delays measured in minutes to hours. No + auth required. + +methodology: + - "Polymarket anchor: the first OO ProposePrice block timestamp, not Gamma fields. Verified live: Gamma closedTime equals the QuestionResolved block timestamp (circular by construction) and endDate is a scheduled buffer most markets beat." + - "Polymarket sources: Polygon logs of the post-migration UMA CTF adapters (binary 0x65070BE9..., neg-risk 0x69c47De9...) and their Optimistic Oracle (0x2c0367a9...). Gamma supplies the market catalogue and category tags." + - "Polymarket join: questionID = keccak256 of the proposal ancillary data, verified against QuestionInitialized events." + - "Kalshi anchor: close_time from the public markets API. Delay = observed finalization time (first poll at which result != null) minus close_time. Accuracy bounded by 5-minute poll interval." + - "Kalshi source: public REST API /trade-api/v2/markets, no auth. 30-day lookback window. Categories mapped: Sports→sports, Politics→politics, Crypto/Cryptocurrency→crypto, everything else→other." + - "Categories: sports, politics, crypto, other, consistent across venues. Per-category rows are the honest unit of comparison because market types run different resolution windows." + - "Disputes (Polymarket only): a DisputePrice before resolution marks the market disputed. Disputed delays land in the disputed series and are excluded from the main category rows." + - "Pending backlog (Polymarket only): markets past their scheduled endDate with no onchain resolution, polled from Gamma every cycle." + - "Listener: 45s incremental Polygon log polling with a 7-day chunked backfill on restart. Kalshi poller ticks every 5 min. Both coexist in the same process." + +findings: + - "Polymarket across all markets: median onchain resolution delay {{p50:polymarket-all}} from first proposal to resolution, p99 at {{p99:polymarket-all}}. The within 2 hours share is in the table above, measured, and does not match the 93 percent figure that circulates without a methodology." + - "Kalshi across all markets: median resolution delay {{p50:kalshi-all}} from close_time to observed finalization. This measures a different thing than Polymarket: Kalshi's resolution process is internal, not onchain, so the anchor is the scheduled market close." + - "Categories are not comparable on a single median: sports, crypto and politics markets run different resolution processes on each venue." + - "Disputes (Polymarket only) are rare but expensive: disputed markets carry materially longer delays than the clean path." + +disclaimer: "Polymarket delay is measured from the first onchain outcome proposal, not from the real world event ending. Kalshi delay is measured from scheduled close_time with ±5 min accuracy. Both are lower bounds on total user wait." + +faq: + - q: "How long does Polymarket take to pay out after a market ends?" + a: "Measured onchain, the median delay from the first outcome proposal to resolution is currently {{p50:polymarket-all}} across all markets, and redemptions are claimable immediately after resolution. The full distribution matters: p99 is {{p99:polymarket-all}}, and the per category rows show that sports, crypto and politics markets run on different verification windows. Add the time between the real world event ending and the first proposal, which is not measurable onchain." + - q: "How long does Kalshi take to resolve?" + a: "Kalshi resolves via its own internal process, not through an onchain oracle. The delay measured here is from a market's scheduled close_time to the first API poll (every 5 minutes) at which a result appears. The current median is {{p50:kalshi-all}}. This reflects Kalshi's operational resolution speed, not an onchain settlement time." + - q: "Do 93 percent of Polymarket markets resolve within 2 hours?" + a: "We could not reproduce that figure. The fields in Polymarket's public Gamma API cannot produce it: closedTime is written at the moment of resolution (circular) and endDate is a scheduled buffer most markets beat. Measured from the first onchain outcome proposal, the within 2 hours share is in the table above and has been materially below 93 percent since this benchmark started." + - q: "What happens when a Polymarket market is disputed?" + a: "A dispute on UMA's optimistic oracle resets the question and routes it through UMA token holder voting, replacing the normal verification window with a voting period measured in days. The disputed label separates those markets so clean path percentiles stay honest." + - q: "Why is my Polymarket market still unresolved?" + a: "Three common reasons: the underlying event has not concluded despite the scheduled end date; no one has submitted a proposal yet (the clock only starts at first proposal); or the market is in a UMA dispute, which adds days. The pending backlog gauge counts all markets past their end date without onchain resolution." + +per_chain_explainer: + - slug: polymarket-all + h2: Polymarket resolution — all markets + body: "All Polymarket markets combined, measured from first UMA ProposePrice to QuestionResolved on Polygon. The median is {{p50:polymarket-all}}. Sports dominates volume; crypto runs fastest; politics carries conservative verification windows. p99 at {{p99:polymarket-all}} reflects the long tail of disputes and postponed events." + - slug: polymarket-sports + h2: Polymarket sports resolution + body: "Sports markets dominate Polymarket volume and currently resolve at {{p50:polymarket-sports}} median from first proposal. Nearly all disputes happen here: contested endings and stat corrections make sports the category where the oracle's verification window earns its keep." + - slug: polymarket-politics + h2: Polymarket politics resolution + body: "Politics markets resolve at {{p50:polymarket-politics}} median, the slow end. Political outcomes often need an authoritative source to publish before anyone proposes, and verification windows are conservative because these markets carry the largest open interest." + - slug: polymarket-crypto + h2: Polymarket crypto resolution + body: "Crypto price markets resolve at {{p50:polymarket-crypto}} median, the fastest Polymarket category. The outcome is machine checkable against price feeds the moment the window closes, so proposals arrive promptly and short verification windows suffice." + - slug: kalshi-all + h2: Kalshi resolution — all markets + body: "All Kalshi markets combined, measured from scheduled close_time to API-observed finalization (poll every 5 min). The median is {{p50:kalshi-all}}. Kalshi resolves via internal processes, not an onchain oracle, so the anchor is the market's scheduled close rather than an onchain proposal." + - slug: kalshi-sports + h2: Kalshi sports resolution + body: "Kalshi sports markets resolve at {{p50:kalshi-sports}} median from scheduled close. Sports outcomes can require official stat verification, so delays vary with league reporting speed." + - slug: kalshi-politics + h2: Kalshi politics resolution + body: "Kalshi politics markets resolve at {{p50:kalshi-politics}} median from scheduled close. Political outcomes on Kalshi are resolved by Kalshi's internal operations team against authoritative sources." + - slug: kalshi-crypto + h2: Kalshi crypto resolution + body: "Kalshi crypto markets resolve at {{p50:kalshi-crypto}} median from scheduled close. Machine-checkable outcomes against price feeds allow for prompt resolution." + +source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/pm-resolution-delay + +prometheus: + window: 24h + expected_freshness_seconds: 900 + freshness_metric: pmres_resolutions_total + +metric_panels: + - id: pending_backlog + label: Pending backlog (Polymarket) + description: "Polymarket markets past their scheduled end date with no onchain resolution yet. The queue a waiting bettor experiences; oldest entries are usually postponed events, not oracle failures." + metric: sum(pmres_pending_markets{}) + label_key: category + unit: count + higher_is_better: false + - id: disputes + label: Disputes (Polymarket) + description: "Polymarket resolutions that went through UMA's dispute process since the listener's backfill window. Rare, concentrated in sports." + metric: sum(pmres_disputes_total{}) + label_key: category + unit: count + higher_is_better: false + +ledger_columns: + - { label: "p50 (median delay)", slot: p50 } + - { label: "p99", slot: p99 } + - { label: "Pending", panel: pending_backlog } + - { label: "Disputes", panel: disputes } + +providers: + - slug: polymarket-all + name: "Polymarket — All" + tag: Every resolved Polymarket market across categories + formula: "Median seconds from first OO ProposePrice block to QuestionResolved block on Polygon. Success = share resolved within 2 hours of proposal." + queries: + p50: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket"})) + p90: 1000 * histogram_quantile(0.90, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket"})) + p99: 1000 * histogram_quantile(0.99, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket"})) + mean: 1000 * sum(pmres_resolution_delay_seconds_sum{venue="polymarket"}) / sum(pmres_resolution_delay_seconds_count{venue="polymarket"}) + success: clamp_max(sum(pmres_resolution_delay_seconds_bucket{venue="polymarket",le="7200"}) / sum(pmres_resolution_delay_seconds_count{venue="polymarket"}), 1) + sample_size: sum(pmres_resolutions_total{venue="polymarket"}) + series: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket"})) + + - slug: polymarket-sports + name: "Polymarket — Sports" + tag: Game and match markets, the volume majority + formula: "Median seconds from first proposal to resolution for Polymarket sports markets." + queries: + p50: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket",category="sports"})) + p90: 1000 * histogram_quantile(0.90, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket",category="sports"})) + p99: 1000 * histogram_quantile(0.99, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket",category="sports"})) + mean: 1000 * sum(pmres_resolution_delay_seconds_sum{venue="polymarket",category="sports"}) / sum(pmres_resolution_delay_seconds_count{venue="polymarket",category="sports"}) + success: clamp_max(sum(pmres_resolution_delay_seconds_bucket{venue="polymarket",le="7200",category="sports"}) / sum(pmres_resolution_delay_seconds_count{venue="polymarket",category="sports"}), 1) + sample_size: sum(pmres_resolutions_total{venue="polymarket",category="sports"}) + series: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket",category="sports"})) + + - slug: polymarket-crypto + name: "Polymarket — Crypto" + tag: Price markets, machine checkable outcomes + formula: "Median seconds from first proposal to resolution for Polymarket crypto price markets." + queries: + p50: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket",category="crypto"})) + p90: 1000 * histogram_quantile(0.90, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket",category="crypto"})) + p99: 1000 * histogram_quantile(0.99, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket",category="crypto"})) + mean: 1000 * sum(pmres_resolution_delay_seconds_sum{venue="polymarket",category="crypto"}) / sum(pmres_resolution_delay_seconds_count{venue="polymarket",category="crypto"}) + success: clamp_max(sum(pmres_resolution_delay_seconds_bucket{venue="polymarket",le="7200",category="crypto"}) / sum(pmres_resolution_delay_seconds_count{venue="polymarket",category="crypto"}), 1) + sample_size: sum(pmres_resolutions_total{venue="polymarket",category="crypto"}) + series: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket",category="crypto"})) + + - slug: polymarket-politics + name: "Polymarket — Politics" + tag: Election and policy markets, conservative windows + formula: "Median seconds from first proposal to resolution for Polymarket politics markets." + queries: + p50: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket",category="politics"})) + p90: 1000 * histogram_quantile(0.90, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket",category="politics"})) + p99: 1000 * histogram_quantile(0.99, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket",category="politics"})) + mean: 1000 * sum(pmres_resolution_delay_seconds_sum{venue="polymarket",category="politics"}) / sum(pmres_resolution_delay_seconds_count{venue="polymarket",category="politics"}) + success: clamp_max(sum(pmres_resolution_delay_seconds_bucket{venue="polymarket",le="7200",category="politics"}) / sum(pmres_resolution_delay_seconds_count{venue="polymarket",category="politics"}), 1) + sample_size: sum(pmres_resolutions_total{venue="polymarket",category="politics"}) + series: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket",category="politics"})) + + - slug: polymarket-other + name: "Polymarket — Other" + tag: Culture, weather, science and everything else + formula: "Median seconds from first proposal to resolution for uncategorized Polymarket markets." + queries: + p50: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket",category="other"})) + p90: 1000 * histogram_quantile(0.90, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket",category="other"})) + p99: 1000 * histogram_quantile(0.99, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket",category="other"})) + mean: 1000 * sum(pmres_resolution_delay_seconds_sum{venue="polymarket",category="other"}) / sum(pmres_resolution_delay_seconds_count{venue="polymarket",category="other"}) + success: clamp_max(sum(pmres_resolution_delay_seconds_bucket{venue="polymarket",le="7200",category="other"}) / sum(pmres_resolution_delay_seconds_count{venue="polymarket",category="other"}), 1) + sample_size: sum(pmres_resolutions_total{venue="polymarket",category="other"}) + series: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="polymarket",category="other"})) + + - slug: kalshi-all + name: "Kalshi — All" + tag: All Kalshi markets, close_time to observed finalization + formula: "Median seconds from Kalshi market close_time to first API poll observing a non-null result (5 min polling interval). Success = share resolved within 2 hours of close." + queries: + p50: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="kalshi"})) + p90: 1000 * histogram_quantile(0.90, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="kalshi"})) + p99: 1000 * histogram_quantile(0.99, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="kalshi"})) + mean: 1000 * sum(pmres_resolution_delay_seconds_sum{venue="kalshi"}) / sum(pmres_resolution_delay_seconds_count{venue="kalshi"}) + success: clamp_max(sum(pmres_resolution_delay_seconds_bucket{venue="kalshi",le="7200"}) / sum(pmres_resolution_delay_seconds_count{venue="kalshi"}), 1) + sample_size: sum(pmres_resolutions_total{venue="kalshi"}) + series: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="kalshi"})) + + - slug: kalshi-sports + name: "Kalshi — Sports" + tag: Kalshi sports markets + formula: "Median seconds from Kalshi sports market close_time to observed finalization." + queries: + p50: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="kalshi",category="sports"})) + p90: 1000 * histogram_quantile(0.90, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="kalshi",category="sports"})) + p99: 1000 * histogram_quantile(0.99, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="kalshi",category="sports"})) + mean: 1000 * sum(pmres_resolution_delay_seconds_sum{venue="kalshi",category="sports"}) / sum(pmres_resolution_delay_seconds_count{venue="kalshi",category="sports"}) + success: clamp_max(sum(pmres_resolution_delay_seconds_bucket{venue="kalshi",le="7200",category="sports"}) / sum(pmres_resolution_delay_seconds_count{venue="kalshi",category="sports"}), 1) + sample_size: sum(pmres_resolutions_total{venue="kalshi",category="sports"}) + series: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="kalshi",category="sports"})) + + - slug: kalshi-politics + name: "Kalshi — Politics" + tag: Kalshi politics markets + formula: "Median seconds from Kalshi politics market close_time to observed finalization." + queries: + p50: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="kalshi",category="politics"})) + p90: 1000 * histogram_quantile(0.90, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="kalshi",category="politics"})) + p99: 1000 * histogram_quantile(0.99, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="kalshi",category="politics"})) + mean: 1000 * sum(pmres_resolution_delay_seconds_sum{venue="kalshi",category="politics"}) / sum(pmres_resolution_delay_seconds_count{venue="kalshi",category="politics"}) + success: clamp_max(sum(pmres_resolution_delay_seconds_bucket{venue="kalshi",le="7200",category="politics"}) / sum(pmres_resolution_delay_seconds_count{venue="kalshi",category="politics"}), 1) + sample_size: sum(pmres_resolutions_total{venue="kalshi",category="politics"}) + series: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="kalshi",category="politics"})) + + - slug: kalshi-crypto + name: "Kalshi — Crypto" + tag: Kalshi crypto markets + formula: "Median seconds from Kalshi crypto market close_time to observed finalization." + queries: + p50: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="kalshi",category="crypto"})) + p90: 1000 * histogram_quantile(0.90, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="kalshi",category="crypto"})) + p99: 1000 * histogram_quantile(0.99, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="kalshi",category="crypto"})) + mean: 1000 * sum(pmres_resolution_delay_seconds_sum{venue="kalshi",category="crypto"}) / sum(pmres_resolution_delay_seconds_count{venue="kalshi",category="crypto"}) + success: clamp_max(sum(pmres_resolution_delay_seconds_bucket{venue="kalshi",le="7200",category="crypto"}) / sum(pmres_resolution_delay_seconds_count{venue="kalshi",category="crypto"}), 1) + sample_size: sum(pmres_resolutions_total{venue="kalshi",category="crypto"}) + series: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{venue="kalshi",category="crypto"})) diff --git a/benchmarks/polymarket-resolution-delay.yml b/benchmarks/polymarket-resolution-delay.yml deleted file mode 100644 index 9c058e1c..00000000 --- a/benchmarks/polymarket-resolution-delay.yml +++ /dev/null @@ -1,190 +0,0 @@ -# OpenChainBench. Bench № 039 - -slug: polymarket-resolution-delay -number: "039" -title: Polymarket resolution time, measured onchain -seo_title: "Polymarket resolution delay live 2026" -seo_description: "Polymarket resolution time live from Polygon: median delay from outcome proposal to onchain payout, sports vs politics vs crypto." -subtitle: Seconds from the first onchain outcome proposal to UMA resolution, measured per category from Polygon logs, with dispute rate and the live backlog of unresolved markets. - -category: Trading -status: live -metric: Resolution delay -unit: s -higher_is_better: false - -seo_intro: | - The most asked question about Polymarket after "is it legal" is "how long - until I get paid". The answer circulating on social media and repeated by - language models is that 93 percent of markets resolve within 2 hours. That - number has no published methodology, and the data Polymarket's own Gamma - API exposes cannot produce it: the closedTime field is written at the - moment of resolution (so measuring from it always yields zero) and endDate - is a scheduled buffer that most markets beat. This benchmark measures the - delay that can actually be observed: the time from the first onchain - outcome proposal on UMA's optimistic oracle to the onchain resolution of - the market, read directly from Polygon logs of Polymarket's UMA CTF - adapters. The table breaks the delay down by category, because sports, - crypto and politics markets run different verification windows, and a - single global median hides that. Dispute rate and the live backlog of - closed but unresolved markets are reported alongside. - -abstract: | - A listener follows Polymarket's UMA CTF adapters on Polygon - (QuestionInitialized, QuestionResolved, QuestionReset) and the - Optimistic Oracle they call (ProposePrice, DisputePrice). For every - resolved market, the recorded delay is the wall-clock time between the - block containing the first outcome proposal and the block containing the - resolution. Markets are joined to Polymarket's Gamma catalogue by - questionID (the keccak hash of the proposal's ancillary data, verified - against live events) and classified into sports, politics, crypto or - other. A dispute resets the question and routes it through UMA's - dispute process; those markets are counted separately so the dispute - penalty is visible instead of polluting the happy path percentiles. The - pending gauge counts markets past their scheduled end date that have not - resolved onchain yet, which is the backlog a bettor waiting for a payout - actually experiences. - -methodology: - - "Anchor: the first OO ProposePrice block timestamp, not Gamma fields. Verified live: Gamma closedTime equals the QuestionResolved block timestamp to the second (circular by construction) and endDate is a scheduled buffer that a majority of markets resolve before. Any 'resolves within X' claim computed from those fields is unfalsifiable." - - "Sources: Polygon logs of the post-migration UMA CTF adapters (binary 0x65070BE9..., neg-risk 0x69c47De9...) and their Optimistic Oracle (0x2c0367a9...), all address-configurable. Gamma supplies the market catalogue and category tags." - - "Join: questionID = keccak256 of the proposal ancillary data, verified against QuestionInitialized events. Neg-risk markets expose a different Gamma questionID; categories fall back to keyword classification for those, the delay itself is unaffected." - - "Categories: sports, politics, crypto, other, classified from Gamma tags plus keyword fallback. Categories run different UMA verification windows, so per category rows are the honest unit of comparison." - - "Disputes: a DisputePrice before resolution marks the market disputed and its delay lands in the disputed series. QuestionReset events are tracked so re-proposals do not create duplicate joins." - - "Pending backlog: markets past their scheduled endDate with no onchain resolution, polled from Gamma every cycle. This includes markets whose underlying event has not happened yet despite the date passing, so it is an upper bound." - - "Listener: 45s incremental Polygon log polling with a 7 day chunked backfill on restart, two public RPC endpoints with rotation and backoff. Counters re-count the backfill window after a restart; quantiles are computed on the cumulative histogram, which the backfill repopulates within minutes." - - "Delays are measured between block timestamps, so the resolution granularity is Polygon's block time (~2s), negligible against delays measured in minutes and hours." - - "What this does not measure: the gap between the real world event ending and someone submitting the first proposal. That interval is not observable onchain; the proposal is the first onchain signal that the outcome is known. Reported delays are therefore a lower bound on the total wait." - -findings: - - "Across all markets, the median onchain resolution delay is {{p50:all-markets}} from first proposal to resolution, with p99 at {{p99:all-markets}}. The within 2 hours share is on the table above, measured, and it does not match the 93 percent figure that circulates without a methodology." - - "Categories are not comparable on a single median: sports markets currently sit at {{p50:sports}}, crypto at {{p50:crypto}}, politics at {{p50:politics}}. Different market types run different UMA verification windows, which is why the leaderboard is per category." - - "Disputes are rare but expensive: the dispute counter on this page tracks how many resolutions went through UMA's dispute process, and disputed markets carry materially longer delays than the clean path." - - "The pending backlog gauge is the number a waiting bettor actually feels: markets past their scheduled end date with no onchain resolution yet. Its oldest entries are weeks old, usually markets whose underlying event was postponed rather than oracle failures." - -disclaimer: "Delay is measured from the first onchain outcome proposal, not from the real world event ending. The gap between event and proposal is not observable onchain, so total wait times experienced by users are at least the figures shown." - -faq: - - q: "How long does Polymarket take to pay out after a market ends?" - a: "Measured onchain, the median delay from the first outcome proposal to resolution is currently {{p50:all-markets}} across all markets, and redemptions are claimable immediately after resolution. The full distribution matters though: p99 is {{p99:all-markets}}, and the per category rows above show that sports, crypto and politics markets run on different verification windows. Add the time between the real world event ending and the first proposal, which happens offchain and is not measurable, so treat these figures as the floor." - - q: "Do 93 percent of Polymarket markets resolve within 2 hours?" - a: "We could not reproduce that figure, and the fields in Polymarket's public Gamma API cannot produce it: closedTime is written at the moment of resolution, so measuring from it always returns zero, and endDate is a scheduled buffer most markets beat. Measured from the first onchain outcome proposal to resolution on Polygon, the within 2 hours share is shown live in the table above and has been materially below 93 percent since this benchmark started. If you have a methodology that supports the 93 percent claim, we would genuinely like to see it." - - q: "What happens when a Polymarket market is disputed?" - a: "A dispute on UMA's optimistic oracle resets the question and routes it through the dispute process, where UMA token holders vote. That replaces the normal verification window with a voting period measured in days. Disputes are rare, the live dispute counter on this page tracks the measured rate, but when one hits a market you are waiting on, the delay moves from minutes or hours to days. The disputed label in our data separates those markets so the clean path percentiles stay honest." - - q: "Why is my Polymarket market still unresolved?" - a: "Three common reasons, in decreasing likelihood. First, the underlying event has not actually concluded, even if the market's scheduled end date passed; postponed games and extended deadlines dominate the pending backlog we track. Second, no one has submitted an outcome proposal to the oracle yet; the clock we measure only starts at the first proposal. Third, the market is in a UMA dispute, which adds days. The pending backlog gauge above counts all markets past their end date without onchain resolution." - - q: "How does Polymarket resolution actually work onchain?" - a: "Polymarket markets resolve through UMA's optimistic oracle on Polygon. Someone posts a bond and proposes the outcome (ProposePrice). A verification window follows during which anyone can dispute by posting a matching bond. If no dispute arrives, the proposal settles and Polymarket's UMA CTF adapter resolves the market (QuestionResolved), making winning shares redeemable. If a dispute arrives, UMA token holders vote. This benchmark reads exactly those events from Polygon logs, so the delays shown are the protocol's own timestamps, not estimates." - - q: "Which Polymarket categories resolve fastest?" - a: "The leaderboard above ranks categories by measured median delay, and the ordering is stable enough to plan around: crypto markets, which resolve against onchain or exchange prices, run the shortest verification windows, while politics markets sit at the long end. Sports land in between and dominate volume. The numbers update live, so check the table rather than trusting a snapshot quoted in an article, including this one." - -per_chain_explainer: - - slug: sports - h2: Polymarket sports resolution - body: "Sports markets dominate Polymarket's resolution volume and currently resolve at {{p50:sports}} median from first proposal. They are also where nearly all disputes happen: contested endings and stat corrections make sports the category where the oracle's verification window earns its keep. If you bet game markets, the practical wait is this row plus however long the league takes to make the result official, which happens before the onchain clock starts." - - slug: politics - h2: Polymarket politics resolution - body: "Politics markets resolve at {{p50:politics}} median from first proposal, the slow end of the cohort. Political outcomes often need an authoritative source to publish before anyone proposes, and verification windows are conservative because these markets carry the largest open interest. The headline 'Polymarket resolves in minutes' claims circulating online are sports and crypto numbers; this row is the honest expectation for election style markets." - - slug: crypto - h2: Polymarket crypto resolution - body: "Crypto price markets resolve at {{p50:crypto}} median, the fastest category. The outcome is machine checkable against price feeds the moment the window closes, so proposals arrive promptly and short verification windows suffice. High frequency up or down markets recycle continuously, which is why crypto contributes a large share of total resolutions despite modest open interest per market." - -source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/pm-resolution-delay - -prometheus: - window: 24h - expected_freshness_seconds: 900 - freshness_metric: pmres_resolutions_total - -metric_panels: - - id: pending_backlog - label: Pending backlog - description: "Markets past their scheduled end date with no onchain resolution yet. The queue a waiting bettor experiences; oldest entries are usually postponed events, not oracle failures." - metric: sum(pmres_pending_markets{}) - label_key: category - unit: count - higher_is_better: false - - id: disputes - label: Disputes - description: "Resolutions that went through UMA's dispute process since the listener's backfill window. Rare, and concentrated in sports." - metric: sum(pmres_disputes_total{}) - label_key: category - unit: count - higher_is_better: false - -# Surface the two LIVE signals in the ledger so the page reads as a UMA -# state dashboard, not just a historical characterization. The p50 is -# statistically stable (cumulative histogram), so the live answers are -# what shifts day to day: how many markets are stuck waiting, and how -# many have hit UMA's dispute process. -ledger_columns: - - { label: "p50 (median delay)", slot: p50 } - - { label: "p99", slot: p99 } - - { label: "Pending", panel: pending_backlog } - - { label: "Disputes", panel: disputes } - -providers: - - slug: all-markets - name: All markets - tag: Every resolved market across categories - formula: "Median seconds from first OO ProposePrice block to QuestionResolved block, cumulative histogram over the listener's window. The success column is the share resolved within 2 hours of proposal, the number the 93 percent claim pretends to be." - queries: - p50: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket)) - p90: 1000 * histogram_quantile(0.90, sum by (le) (pmres_resolution_delay_seconds_bucket)) - p99: 1000 * histogram_quantile(0.99, sum by (le) (pmres_resolution_delay_seconds_bucket)) - mean: 1000 * sum(pmres_resolution_delay_seconds_sum) / sum(pmres_resolution_delay_seconds_count) - success: clamp_max(sum(pmres_resolution_delay_seconds_bucket{le="7200"}) / sum(pmres_resolution_delay_seconds_count), 1) - sample_size: sum(pmres_resolutions_total) - series: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket)) - - - slug: sports - name: Sports - tag: Game and match markets, the volume majority - formula: "Median seconds from first proposal to resolution for sports markets. Success column is the share within 2 hours of proposal." - queries: - p50: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{category="sports"})) - p90: 1000 * histogram_quantile(0.90, sum by (le) (pmres_resolution_delay_seconds_bucket{category="sports"})) - p99: 1000 * histogram_quantile(0.99, sum by (le) (pmres_resolution_delay_seconds_bucket{category="sports"})) - mean: 1000 * sum(pmres_resolution_delay_seconds_sum{category="sports"}) / sum(pmres_resolution_delay_seconds_count{category="sports"}) - success: clamp_max(sum(pmres_resolution_delay_seconds_bucket{le="7200",category="sports"}) / sum(pmres_resolution_delay_seconds_count{category="sports"}), 1) - sample_size: sum(pmres_resolutions_total{category="sports"}) - series: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{category="sports"})) - - - slug: crypto - name: Crypto - tag: Price markets, machine checkable outcomes - formula: "Median seconds from first proposal to resolution for crypto price markets. Success column is the share within 2 hours of proposal." - queries: - p50: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{category="crypto"})) - p90: 1000 * histogram_quantile(0.90, sum by (le) (pmres_resolution_delay_seconds_bucket{category="crypto"})) - p99: 1000 * histogram_quantile(0.99, sum by (le) (pmres_resolution_delay_seconds_bucket{category="crypto"})) - mean: 1000 * sum(pmres_resolution_delay_seconds_sum{category="crypto"}) / sum(pmres_resolution_delay_seconds_count{category="crypto"}) - success: clamp_max(sum(pmres_resolution_delay_seconds_bucket{le="7200",category="crypto"}) / sum(pmres_resolution_delay_seconds_count{category="crypto"}), 1) - sample_size: sum(pmres_resolutions_total{category="crypto"}) - series: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{category="crypto"})) - - - slug: politics - name: Politics - tag: Election and policy markets, conservative windows - formula: "Median seconds from first proposal to resolution for politics markets. Success column is the share within 2 hours of proposal." - queries: - p50: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{category="politics"})) - p90: 1000 * histogram_quantile(0.90, sum by (le) (pmres_resolution_delay_seconds_bucket{category="politics"})) - p99: 1000 * histogram_quantile(0.99, sum by (le) (pmres_resolution_delay_seconds_bucket{category="politics"})) - mean: 1000 * sum(pmres_resolution_delay_seconds_sum{category="politics"}) / sum(pmres_resolution_delay_seconds_count{category="politics"}) - success: clamp_max(sum(pmres_resolution_delay_seconds_bucket{le="7200",category="politics"}) / sum(pmres_resolution_delay_seconds_count{category="politics"}), 1) - sample_size: sum(pmres_resolutions_total{category="politics"}) - series: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{category="politics"})) - - - slug: other - name: Other - tag: Culture, weather, science and everything else - formula: "Median seconds from first proposal to resolution for uncategorized markets. Success column is the share within 2 hours of proposal." - queries: - p50: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{category="other"})) - p90: 1000 * histogram_quantile(0.90, sum by (le) (pmres_resolution_delay_seconds_bucket{category="other"})) - p99: 1000 * histogram_quantile(0.99, sum by (le) (pmres_resolution_delay_seconds_bucket{category="other"})) - mean: 1000 * sum(pmres_resolution_delay_seconds_sum{category="other"}) / sum(pmres_resolution_delay_seconds_count{category="other"}) - success: clamp_max(sum(pmres_resolution_delay_seconds_bucket{le="7200",category="other"}) / sum(pmres_resolution_delay_seconds_count{category="other"}), 1) - sample_size: sum(pmres_resolutions_total{category="other"}) - series: 1000 * histogram_quantile(0.50, sum by (le) (pmres_resolution_delay_seconds_bucket{category="other"})) diff --git a/harnesses/pm-resolution-delay/cmd/script/chain.go b/harnesses/pm-resolution-delay/cmd/script/chain.go index 5fc245b7..e8314a7d 100644 --- a/harnesses/pm-resolution-delay/cmd/script/chain.go +++ b/harnesses/pm-resolution-delay/cmd/script/chain.go @@ -279,7 +279,7 @@ func (e *engine) markDisputed(qid string, ts int64, via string) { title := q.title e.mu.Unlock() cat := e.category(qid, title) - disputesTotal.WithLabelValues(cat).Inc() + disputesTotal.WithLabelValues("polymarket", cat).Inc() log.Printf("[dispute] qid=%s via=%s category=%s title=%q at=%s", qid, via, cat, title, time.Unix(ts, 0).UTC().Format(time.RFC3339)) } @@ -306,8 +306,8 @@ func (e *engine) resolve(qid string, ts int64) { disputed = "true" extra = fmt.Sprintf(" dispute_extra=%ds", ts-q.firstDisputedAt) } - resolutionDelay.WithLabelValues(cat, disputed).Observe(float64(delay)) - resolutionsTotal.WithLabelValues(cat, disputed).Inc() + resolutionDelay.WithLabelValues("polymarket", cat, disputed).Observe(float64(delay)) + resolutionsTotal.WithLabelValues("polymarket", cat, disputed).Inc() log.Printf("[resolve] qid=%s category=%s disputed=%s delay=%ds proposed=%s resolved=%s%s title=%q", qid, cat, disputed, delay, time.Unix(q.firstProposedAt, 0).UTC().Format(time.RFC3339), diff --git a/harnesses/pm-resolution-delay/cmd/script/kalshi_resolution.go b/harnesses/pm-resolution-delay/cmd/script/kalshi_resolution.go new file mode 100644 index 00000000..280adba7 --- /dev/null +++ b/harnesses/pm-resolution-delay/cmd/script/kalshi_resolution.go @@ -0,0 +1,193 @@ +package main + +// Kalshi resolution delay poller. +// +// Unlike Polymarket (UMA on-chain events, 2h floor), Kalshi resolves via its +// own internal process. Delay is measured as: +// observed_resolution_time - market.close_time +// +// where observed_resolution_time = first poll at which result != null. +// Accuracy is bounded by KALSHI_POLL_SECONDS (default 300 = 5 min), which +// is negligible for delays measured in hours. +// +// No auth required — uses the public /trade-api/v2/markets endpoint. + +import ( + "context" + "encoding/json" + "log" + "net/http" + "strings" + "sync" + "time" +) + +const ( + kalshiMarketsURL = "https://api.elections.kalshi.com/trade-api/v2/markets" + kalshiLookbackDays = 30 +) + +type kalshiMarketRecord struct { + Ticker string `json:"ticker"` + Title string `json:"title"` + CloseTime string `json:"close_time"` // RFC3339 + Status string `json:"status"` // "open", "closed", "finalized" + Result string `json:"result"` // "yes", "no", "" when unresolved + Category string `json:"category"` // "Sports", "Politics", "Crypto", etc. +} + +type kalshiMarketsResponse struct { + Markets []kalshiMarketRecord `json:"markets"` + Cursor string `json:"cursor"` +} + +type kalshiTracker struct { + mu sync.Mutex + watched map[string]time.Time // ticker → close_time (markets we're watching) + recorded map[string]struct{} // tickers already emitted to histogram + client *http.Client +} + +func newKalshiTracker() *kalshiTracker { + return &kalshiTracker{ + watched: make(map[string]time.Time), + recorded: make(map[string]struct{}), + client: &http.Client{Timeout: 15 * time.Second}, + } +} + +func (k *kalshiTracker) fetchPage(status, cursor string) ([]kalshiMarketRecord, string, error) { + req, err := http.NewRequest(http.MethodGet, kalshiMarketsURL, nil) + if err != nil { + return nil, "", err + } + q := req.URL.Query() + q.Set("limit", "200") + q.Set("status", status) + q.Set("order", "close_time") + q.Set("ascending", "false") + if cursor != "" { + q.Set("cursor", cursor) + } + req.URL.RawQuery = q.Encode() + req.Header.Set("User-Agent", userAgent) + + resp, err := k.client.Do(req) + if err != nil { + return nil, "", err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, "", nil + } + var body kalshiMarketsResponse + if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { + return nil, "", err + } + return body.Markets, body.Cursor, nil +} + +// normalizeKalshiCategory maps Kalshi's category strings to lowercase slugs +// comparable to Polymarket's categories. +func normalizeKalshiCategory(raw string) string { + switch strings.ToLower(raw) { + case "sports": + return "sports" + case "politics": + return "politics" + case "crypto", "cryptocurrency": + return "crypto" + case "economics", "finance": + return "other" + default: + return "other" + } +} + +// pollClosed fetches recently closed (unresolved) markets and adds them to +// the watch list so we notice when they become finalized. +func (k *kalshiTracker) pollClosed() { + cutoff := time.Now().Add(-kalshiLookbackDays * 24 * time.Hour) + markets, _, err := k.fetchPage("closed", "") + if err != nil { + log.Printf("[kalshi-res] pollClosed error: %v", err) + return + } + k.mu.Lock() + defer k.mu.Unlock() + for _, m := range markets { + ct, err := time.Parse(time.RFC3339, m.CloseTime) + if err != nil { + continue + } + if ct.Before(cutoff) { + break + } + if _, ok := k.watched[m.Ticker]; !ok { + k.watched[m.Ticker] = ct + } + } +} + +// pollFinalized fetches recently finalized markets and emits histogram +// observations for any we haven't recorded yet. +func (k *kalshiTracker) pollFinalized() { + cutoff := time.Now().Add(-kalshiLookbackDays * 24 * time.Hour) + now := time.Now() + + markets, _, err := k.fetchPage("finalized", "") + if err != nil { + log.Printf("[kalshi-res] pollFinalized error: %v", err) + return + } + + k.mu.Lock() + defer k.mu.Unlock() + for _, m := range markets { + ct, err := time.Parse(time.RFC3339, m.CloseTime) + if err != nil { + continue + } + if ct.Before(cutoff) { + break + } + if _, done := k.recorded[m.Ticker]; done { + continue + } + if m.Result == "" { + continue + } + k.recorded[m.Ticker] = struct{}{} + delete(k.watched, m.Ticker) + + delay := now.Sub(ct).Seconds() + if delay < 0 || delay > float64(kalshiLookbackDays*24*3600) { + continue + } + cat := normalizeKalshiCategory(m.Category) + resolutionDelay.WithLabelValues("kalshi", cat, "false").Observe(delay) + resolutionsTotal.WithLabelValues("kalshi", cat, "false").Inc() + log.Printf("[kalshi-res] resolved ticker=%s category=%s delay=%.0fs close_time=%s result=%s", + m.Ticker, cat, delay, m.CloseTime, m.Result) + } +} + +func runKalshiResolutionLoop(ctx context.Context) { + t := time.NewTicker(5 * time.Minute) + defer t.Stop() + + tracker := newKalshiTracker() + // Prime the watch list immediately on startup. + tracker.pollClosed() + tracker.pollFinalized() + + for { + select { + case <-ctx.Done(): + return + case <-t.C: + tracker.pollClosed() + tracker.pollFinalized() + } + } +} diff --git a/harnesses/pm-resolution-delay/cmd/script/main.go b/harnesses/pm-resolution-delay/cmd/script/main.go index 99df9185..ef2936d1 100644 --- a/harnesses/pm-resolution-delay/cmd/script/main.go +++ b/harnesses/pm-resolution-delay/cmd/script/main.go @@ -30,6 +30,7 @@ func main() { go healthLoop() go gamma.pendingLoop(ctx) + go runKalshiResolutionLoop(ctx) go func() { // Category map first, then the chain engine: backfilled resolutions // join against Gamma tags instead of falling back to keywords. diff --git a/harnesses/pm-resolution-delay/cmd/script/metrics.go b/harnesses/pm-resolution-delay/cmd/script/metrics.go index 7a75aa92..38078a1a 100644 --- a/harnesses/pm-resolution-delay/cmd/script/metrics.go +++ b/harnesses/pm-resolution-delay/cmd/script/metrics.go @@ -21,21 +21,23 @@ var ( // claim the bench fact-checks. delayBuckets = []float64{300, 900, 1800, 3600, 7200, 14400, 43200, 86400, 172800, 604800, 1209600} + // venue label added 2026-07 for multi-venue support (kalshi + future). + // Polymarket = "polymarket", Kalshi = "kalshi". resolutionDelay = promauto.NewHistogramVec(prometheus.HistogramOpts{ Name: "pmres_resolution_delay_seconds", - Help: "QuestionResolved block timestamp minus first OO ProposePrice block timestamp for the same questionID. Includes the UMA challenge window (~2h), so the floor is the market's liveness. disputed=true means at least one OO DisputePrice / adapter QuestionReset occurred before resolution.", + Help: "Time from resolution anchor to settlement. Polymarket: ProposePrice→QuestionResolved (UMA, ~2h floor). Kalshi: close_time→observed settlement (~minutes to hours). disputed=true = at least one UMA DisputePrice before resolution (Polymarket only).", Buckets: delayBuckets, - }, []string{"category", "disputed"}) + }, []string{"venue", "category", "disputed"}) resolutionsTotal = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "pmres_resolutions_total", - Help: "QuestionResolved events observed and successfully joined to a proposal. Re-counts the 7-day backfill window after a restart (no DB), so prefer rate()/increase() over raw values.", - }, []string{"category", "disputed"}) + Help: "Resolved markets recorded. Polymarket re-counts the 7-day backfill on restart; Kalshi accumulates since harness start.", + }, []string{"venue", "category", "disputed"}) disputesTotal = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "pmres_disputes_total", - Help: "Questions that saw their first OO DisputePrice (or adapter QuestionReset) before resolution. Counted once per question, not per dispute round.", - }, []string{"category"}) + Help: "Markets disputed before resolution (Polymarket/UMA only). Counted once per question.", + }, []string{"venue", "category"}) pendingMarkets = promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "pmres_pending_markets", diff --git a/src/components/site-header.tsx b/src/components/site-header.tsx index 032f8ac5..aef5dbd8 100644 --- a/src/components/site-header.tsx +++ b/src/components/site-header.tsx @@ -36,11 +36,6 @@ const NAV: NavItem[] = [ label: "Benchmarks", match: (p) => p === "/benchmarks" || p.startsWith("/benchmarks/"), }, - { - href: "/data-api", - label: "Data APIs", - match: (p) => p === "/data-api" || p.startsWith("/data-api/"), - }, { href: "/products", label: "Products", diff --git a/src/lib/pm-venue-context.ts b/src/lib/pm-venue-context.ts index 575363e0..fe7dda84 100644 --- a/src/lib/pm-venue-context.ts +++ b/src/lib/pm-venue-context.ts @@ -20,9 +20,11 @@ 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: "Offchain US" }, kalshi: { url: "https://kalshi.com", chainLabel: "Offchain US" }, limitless: { url: "https://limitless.exchange", chainLabel: "Base" }, myriad: { url: "https://myriad.markets", chainLabel: "Abstract L2" }, + manifold: { url: "https://manifold.markets", chainLabel: "Offchain" }, }; export const PM_FEED_META: Record = { @@ -109,7 +111,7 @@ export function benchRowsForVenue( tone: "teal", }, { - benchSlug: "polymarket-resolution-delay", + benchSlug: "pm-resolution-delay", label: "Resolution delay", blurb: "ProposePrice anchor to QuestionResolved block, median.", rank: rankWithinCohort(cohort.venues, "medianResolutionDelayMin", venue.slug),