metaculus
Metaculus API
Metaculus is a forecasting platform where users predict outcomes of real-world events. Questions range across science, technology, politics, economics, and more. The platform aggregates individual forecasts into community predictions and scores forecasters on accuracy.
Check this skill and the official API documentation FREQUENTLY for updates.
Feedback: Contact the Metaculus team at api-requests@metaculus.com with questions, ideas, or feedback.
Source code & issues: github.com/Metaculus/metaculus
Key Concepts (Glossary)
| Term | Definition |
|---|---|
| Post | The primary feed entity. A post wraps a question, group of questions, conditional pair, or notebook. Posts have statuses, authors, projects, and comments. |
| Question | A single forecastable item within a post. Types: binary, multiple_choice, numeric, discrete, date. |
| Group of Questions | A post containing multiple related sub-questions displayed together (e.g., "What will GDP be in 2025, 2026, 2027?"). |
| Conditional | A post with paired questions: "If [condition], what is P(child)?" — produces question_yes and question_no variants. |
| Forecast | A user's prediction on a question. Format depends on question type: probability (binary), CDF (continuous), or distribution (multiple choice). |
| Community Prediction (CP) | The aggregated forecast from all users, computed via various aggregation methods. |
| Aggregation Method | How individual forecasts are combined: recency_weighted (default), unweighted, metaculus_prediction, single_aggregation. |
| Project | A container for posts — can be a tournament, category, tag, question series, or site section. |
| Tournament | A special project type with prize pools, start/close dates, and leaderboards. |
| Category | A topic classification (e.g., "Nuclear Technology & Risks", "Health & Pandemics"). |
| Resolution | The actual outcome of a question once known. Values vary by type: yes/no (binary), a number, a date, or an option name. |
| Curation Status | Editorial status of a post: draft, pending, rejected, approved. |
| Scaling | Defines how a continuous question's range maps to the CDF. Includes range_min, range_max, zero_point (for log scale), and bounds. |
| CDF | Cumulative Distribution Function — the format for continuous forecasts. A list of 201 floats (or inbound_outcome_count + 1 for discrete). |
| Inbound Outcome Count | Number of possible outcomes within a question's range (excluding out-of-bounds). Default is 200 for continuous; smaller for discrete. |
Base URL
All endpoints are served from:
https://www.metaculus.com
All paths below are relative to this base (e.g., GET /api/posts/ means GET https://www.metaculus.com/api/posts/).
Authentication
All API requests require authentication. Unauthenticated requests are rejected.
Getting Your Token
- Log in to Metaculus.
- Go to your Account Settings → API Access.
- Copy (or generate) your API token.
Using the Token
Add the Authorization header to every request. The token must be prefixed with the literal string Token followed by a space:
Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b
Example: curl
curl -s "https://www.metaculus.com/api/posts/?limit=5&order_by=-published_at" \
-H "Authorization: Token $METACULUS_API_TOKEN" | jq .
Example: Python
import os, requests
TOKEN = os.environ["METACULUS_API_TOKEN"]
HEADERS = {"Authorization": f"Token {TOKEN}"}
BASE = "https://www.metaculus.com"
resp = requests.get(f"{BASE}/api/posts/", headers=HEADERS, params={"limit": 5})
resp.raise_for_status()
data = resp.json()
Environment Variables
Never hardcode tokens. Store them as environment variables or in a .env file:
METACULUS_API_TOKEN=your-token-here
Rate Limits
Metaculus throttles requests to prevent abuse. If you receive a 429 Too Many Requests response, implement exponential backoff before retrying.
REST API Endpoints Overview
Feed (Posts)
| Method | Endpoint | Description | Auth |
|---|---|---|---|
GET |
/api/posts/ |
Retrieve paginated posts feed with filters | Required |
GET |
/api/posts/{postId}/ |
Retrieve a single post with full details | Required |
Questions & Forecasts
| Method | Endpoint | Description | Auth |
|---|---|---|---|
POST |
/api/questions/forecast/ |
Submit forecasts for one or more questions | Required |
POST |
/api/questions/withdraw/ |
Withdraw active forecasts | Required |
Comments
| Method | Endpoint | Description | Auth |
|---|---|---|---|
GET |
/api/comments/ |
Retrieve comments (filter by post or author) | Required |
POST |
/api/comments/create/ |
Create a new comment on a post | Required |
Utilities & Data
| Method | Endpoint | Description | Auth |
|---|---|---|---|
GET |
/api/posts/{postId}/download-data/ |
Download question data as a ZIP of CSVs | Required |
GET |
/api/projects/{projectId}/download-data/ |
Download full project data as a ZIP of CSVs | Required (admin/whitelisted) |
Data Model
Post → Question Hierarchy
A Post is the top-level entity in the feed. Each post contains exactly one of:
question— a single question (binary, numeric, date, multiple choice, or discrete)group_of_questions— multiple related sub-questionsconditional— a conditional pair (condition + child, producing question_yes and question_no)- A notebook (no question content)
The other fields will be null. For example, a post with a single binary question will have question populated and conditional/group_of_questions set to null.
Post
| Field | Type | Description |
|---|---|---|
id |
integer | Unique post ID |
title |
string | Full title |
short_title |
string | URL-friendly short title |
slug |
string | URL slug |
author_id |
integer | Author's user ID |
author_username |
string | Author's username |
projects |
object | Associated projects (see below) |
created_at |
datetime | When the post was created |
published_at |
datetime? | When the post was published |
open_time |
datetime? | When the question opened for forecasting |
edited_at |
datetime | Last edit timestamp |
curation_status |
string | draft, pending, rejected, or approved |
comment_count |
integer | Number of comments |
status |
string | open, upcoming, closed, resolved, draft, pending, rejected |
nr_forecasters |
integer | Number of unique forecasters |
question |
Question? | Single question (if applicable) |
conditional |
Conditional? | Conditional pair (if applicable) |
group_of_questions |
GroupOfQuestions? | Question group (if applicable) |
user_permission |
string | forecaster or viewer |
vote |
object | { score: int, user_vote: string? } |
forecasts_count |
integer | Total number of forecasts |
Post Projects Object
| Field | Type | Description |
|---|---|---|
site_main |
Project[] | Site-level project associations |
tournament |
Project[] | Tournaments this post belongs to |
category |
Category[] | Categories |
tag |
Tag[] | Tags |
question_series |
Project[] | Question series |
default_project |
Project | The post's primary/default project |
Project
| Field | Type | Description |
|---|---|---|
id |
integer | Project ID |
type |
string | site_main, tournament, etc. |
name |
string | Display name |
slug |
string? | URL slug |
prize_pool |
string | Prize pool amount (e.g., "0.00") |
start_date |
datetime? | Start date |
close_date |
datetime? | Close date |
is_ongoing |
boolean? | Whether the project is ongoing |
default_permission |
string | Default user permission (e.g., "forecaster") |
visibility |
string | normal, not_in_main_feed, unlisted |
Question
| Field | Type | Description |
|---|---|---|
id |
integer | Unique question ID (used in forecast submissions, not the post ID) |
title |
string | Question title |
description |
string | Full description (may be omitted unless include_descriptions=true) |
type |
string | binary, multiple_choice, numeric, discrete, date |
status |
string | upcoming, open, closed, resolved |
resolution |
string? | Resolution value (null if unresolved) |
resolution_criteria |
string | How the question will be resolved |
fine_print |
string | Additional resolution details |
created_at |
datetime | Creation timestamp |
open_time |
datetime | When forecasting opens |
scheduled_close_time |
datetime | When forecasting is scheduled to close |
actual_close_time |
datetime | When forecasting actually closed |
scheduled_resolve_time |
datetime | When resolution is scheduled |
actual_resolve_time |
datetime? | When it was actually resolved |
options |
string[] | Current options (multiple choice only) |
all_options_ever |
string[] | All options that have ever existed (multiple choice only) |
open_upper_bound |
boolean | Whether the upper bound is open |
open_lower_bound |
boolean | Whether the lower bound is open |
inbound_outcome_count |
integer? | Number of discrete outcomes in range (default 200 for continuous) |
unit |
string | Display unit (e.g., "$", "°C") |
label |
string? | Label for sub-questions |
scaling |
QuestionScaling | Range and scaling parameters |
aggregations |
object | Community prediction aggregations (see below) |
QuestionScaling
| Field | Type | Description |
|---|---|---|
range_min |
float? | Lower boundary of the input range |
range_max |
float? | Upper boundary of the input range |
zero_point |
float? | Log-scale zero point (null = linear scaling) |
open_upper_bound |
boolean? | Whether upper bound is open |
open_lower_bound |
boolean? | Whether lower bound is open |
inbound_outcome_count |
integer? | Number of outcomes within range |
continuous_range |
string[]? | Real-value locations where the CDF is evaluated |
Aggregations
Each question includes an aggregations object with up to four methods:
recency_weighted— Default; weights recent forecasts more heavilyunweighted— Equal weight for all forecastsmetaculus_prediction— Metaculus's proprietary predictionsingle_aggregation— Beta; admin-only
Each method contains:
| Field | Type | Description |
|---|---|---|
history |
object[] | Time series of aggregated forecasts |
latest |
object? | Most recent aggregated forecast |
score_data |
object? | Scoring information |
Each history/latest entry contains:
| Field | Type | Description |
|---|---|---|
start_time |
datetime | When this aggregation period started |
end_time |
datetime? | When this aggregation period ended |
forecast_values |
float[] | The aggregated forecast (probability for binary, CDF for continuous) |
forecaster_count |
integer | Number of contributing forecasters |
centers |
float[] | Median/center values |
interval_lower_bounds |
float[] | Lower confidence bounds |
interval_upper_bounds |
float[] | Upper confidence bounds |
means |
float? | Mean values |
Conditional
| Field | Type | Description |
|---|---|---|
id |
integer | Conditional ID |
condition |
Question | The condition question |
condition_child |
Question | The child question |
question_yes |
Question | "If condition = Yes" variant |
question_no |
Question | "If condition = No" variant |
GroupOfQuestions
| Field | Type | Description |
|---|---|---|
id |
integer | Group ID |
description |
string | Group description |
resolution_criteria |
string | Resolution criteria |
fine_print |
string | Additional details |
group_variable |
string | The variable that differs across sub-questions |
graph_type |
string | multiple_choice_graph or fan_graph |
questions |
Question[] | The sub-questions |
Comment
| Field | Type | Description |
|---|---|---|
id |
integer | Comment ID |
author |
object | { id, username, is_bot, is_staff } |
parent_id |
integer? | Parent comment ID (for replies) |
root_id |
integer? | Root comment ID in thread |
created_at |
datetime | Creation timestamp |
text |
string | Comment content |
on_post |
integer | Post ID the comment belongs to |
included_forecast |
boolean | Whether the user's last forecast is included |
is_private |
boolean | Whether the comment is private |
vote_score |
integer | Total vote score |
user_vote |
integer | Current user's vote (-1, 0, 1) |
Endpoints: Detailed Reference
GET /api/posts/ — Retrieve Posts Feed
Returns a paginated list of posts with extensive filtering and sorting.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
limit |
integer | Page size (default varies) |
offset |
integer | Pagination offset |
tournaments |
string[] | Filter by tournament slugs (e.g., metaculus-cup, aibq3) |
statuses |
string[] | Filter by status: upcoming, closed, resolved, open |
forecast_type |
string[] | Filter by type: binary, multiple_choice, numeric, discrete, date, conditional, group_of_questions, notebook |
categories |
string[] | Filter by category slugs (e.g., nuclear, health-pandemics) |
forecaster_id |
integer | Posts where this user has forecasted |
not_forecaster_id |
integer | Posts where this user has NOT forecasted |
for_main_feed |
boolean | Filter for main feed suitability |
with_cp |
boolean | Include community predictions (default: false). For groups, returns CP for top 3 sub-questions only. |
include_cp_history |
boolean | Include full CP history per aggregation method (default: false) |
include_descriptions |
boolean | Include description, fine_print, resolution_criteria fields |
order_by |
string | Sort field. Prefix with - for descending. Options below. |
open_time__gt |
datetime | Open time greater than (also supports __gte, __lt, __lte) |
published_at__gt |
datetime | Published time greater than (also supports __gte, __lt, __lte) |
scheduled_resolve_time__gt |
datetime | Scheduled resolve time greater than (also supports __gte, __lt, __lte) |
order_by options:
| Value | Description |
|---|---|
published_at |
Publication time |
open_time |
When forecasting opened |
vote_score |
Community vote score |
comment_count |
Number of comments |
forecasts_count |
Number of forecasts |
scheduled_close_time |
Scheduled close time |
scheduled_resolve_time |
Scheduled resolution time |
user_last_forecasts_date |
When the user last forecasted |
unread_comment_count |
Unread comments |
weekly_movement |
Weekly probability movement |
divergence |
Divergence metric |
hotness |
Composite trending score (decays after 3.5 days) |
score |
User forecasting performance (requires forecaster_id) |
Response:
{
"next": "https://www.metaculus.com/api/posts/?limit=20&offset=20",
"previous": null,
"results": [ /* array of Post objects */ ]
}
Example: Fetch open binary questions, newest first:
curl -s "https://www.metaculus.com/api/posts/?statuses=open&forecast_type=binary&order_by=-published_at&limit=10&with_cp=true" \
-H "Authorization: Token $METACULUS_API_TOKEN" | jq '.results[] | {id, title, status}'
Example: Fetch questions from a tournament:
curl -s "https://www.metaculus.com/api/posts/?tournaments=metaculus-cup&with_cp=true&limit=20" \
-H "Authorization: Token $METACULUS_API_TOKEN" | jq .
Example: Fetch questions you haven't forecasted on yet:
# Replace YOUR_USER_ID with your actual Metaculus user ID
curl -s "https://www.metaculus.com/api/posts/?not_forecaster_id=YOUR_USER_ID&statuses=open&limit=10" \
-H "Authorization: Token $METACULUS_API_TOKEN" | jq .
GET /api/posts/{postId}/ — Retrieve Post Details
Returns full details for a single post, including all sub-questions and aggregations.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
postId |
integer | The post ID |
Example:
curl -s "https://www.metaculus.com/api/posts/12345/" \
-H "Authorization: Token $METACULUS_API_TOKEN" | jq .
POST /api/questions/forecast/ — Submit Forecasts
Submit one or more forecasts. The request body is a JSON array of forecast objects.
Important: The
questionfield takes the question ID, not the post ID. Get the question ID frompost.question.id,post.group_of_questions.questions[].id, orpost.conditional.question_yes.id/post.conditional.question_no.id.
Binary Forecast
[
{
"question": 12345,
"probability_yes": 0.63
}
]
| Field | Type | Required | Description |
|---|---|---|---|
question |
integer | Yes | Question ID |
probability_yes |
float | Yes | Probability between 0 and 1 |
end_time |
datetime | No | Auto-withdraw timestamp |
Multiple Choice Forecast
[
{
"question": 12345,
"probability_yes_per_category": {
"Futurama": 0.5,
"Paperclipalypse": 0.3,
"Singularia": 0.2
}
}
]
| Field | Type | Required | Description |
|---|---|---|---|
question |
integer | Yes | Question ID |
probability_yes_per_category |
object | Yes | Map of option name → probability. Must sum to 1.0. |
end_time |
datetime | No | Auto-withdraw timestamp |
Continuous Forecast (Numeric / Date / Discrete)
[
{
"question": 12345,
"continuous_cdf": [0.0, 0.00005, 0.00010, "... 201 values total ...", 1.0]
}
]
| Field | Type | Required | Description |
|---|---|---|---|
question |
integer | Yes | Question ID |
continuous_cdf |
float[] | Yes | CDF array (see CDF generation section below) |
distribution_input |
object | No | Slider values for frontend display |
end_time |
datetime | No | Auto-withdraw timestamp |
Conditional Forecast
Submit forecasts for both the "if Yes" and "if No" questions:
[
{ "question": 111, "probability_yes": 0.499 },
{ "question": 222, "probability_yes": 0.501 }
]
Where 111 is conditional.question_yes.id and 222 is conditional.question_no.id.
Group Forecast
Submit forecasts for multiple sub-questions in a single request:
[
{ "question": 1, "probability_yes": 0.11 },
{ "question": 2, "probability_yes": 0.22 },
{ "question": 3, "probability_yes": 0.33 }
]
Example: Submit a binary forecast with curl:
curl -s -X POST "https://www.metaculus.com/api/questions/forecast/" \
-H "Authorization: Token $METACULUS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '[{"question": 12345, "probability_yes": 0.75}]'
Responses:
| Status | Description |
|---|---|
201 |
Forecasts submitted successfully |
400 |
Invalid request format |
POST /api/questions/withdraw/ — Withdraw Forecasts
Withdraw active forecasts. The request body is a JSON array of withdrawal objects.
[
{ "question": 12345 },
{ "question": 12346 }
]
Example:
curl -s -X POST "https://www.metaculus.com/api/questions/withdraw/" \
-H "Authorization: Token $METACULUS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '[{"question": 12345}]'
GET /api/comments/ — Retrieve Comments
Fetch comments with filters. Either post or author is required.
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
post |
integer | One of post/author | Post ID to filter by |
author |
integer | One of post/author | Author user ID to filter by |
limit |
integer | No | Number of comments to retrieve |
offset |
integer | No | Pagination offset |
is_private |
boolean | No | Filter private vs public (default: false) |
use_root_comments_pagination |
boolean | No | If true, pagination applies to root comments only; all child comments are included |
sort |
string | No | created_at (ascending) or -created_at (descending) |
focus_comment_id |
integer | No | Place this comment at the top of results |
Response:
{
"total_count": 42,
"count": 15,
"next": "https://www.metaculus.com/api/comments/?post=123&limit=10&offset=10",
"previous": null,
"results": [ /* array of Comment objects */ ]
}
total_count— Total root + child commentscount— Total root comments only
Example:
curl -s "https://www.metaculus.com/api/comments/?post=12345&sort=-created_at&limit=20" \
-H "Authorization: Token $METACULUS_API_TOKEN" | jq .
POST /api/comments/create/ — Create a Comment
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
on_post |
integer | Yes | Post ID to comment on |
text |
string | Yes | Comment content |
included_forecast |
boolean | Yes | Include the user's last forecast |
is_private |
boolean | Yes | Whether the comment is private |
parent |
integer | No | Parent comment ID (for replies) |
Example:
curl -s -X POST "https://www.metaculus.com/api/comments/create/" \
-H "Authorization: Token $METACULUS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"on_post": 12345,
"text": "I updated my forecast based on the latest data.",
"included_forecast": false,
"is_private": false
}'
Responses:
| Status | Description |
|---|---|
201 |
Comment created successfully (returns Comment object) |
400 |
Invalid request format |
GET /api/posts/{postId}/download-data/ — Download Question Data
Downloads forecast data as a ZIP file containing CSVs.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
postId |
integer | Post ID (the number after /questions/ in the URL) |
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
sub_question |
integer | No | Sub-question ID for group/conditional questions |
aggregation_methods |
string[] | No | Methods to include: recency_weighted, unweighted, metaculus_prediction, single_aggregation. Default: recency_weighted only |
include_bots |
boolean | No | Include bot forecasts in aggregation recalculation |
user_ids |
string[] | No | Specific user IDs (whitelisted users only). Requires aggregation_methods. |
minimize |
boolean | No | Default: true. If false, includes all data points (may produce large files) |
include_comments |
boolean | No | Default: false. If true, adds comment_data.csv |
include_scores |
boolean | No | Default: false. If true, adds score_data.csv |
ZIP Contents:
| File | Description |
|---|---|
question_data.csv |
Question metadata, scaling, resolution |
forecast_data.csv |
Individual and aggregated forecasts |
comment_data.csv |
Comments (if include_comments=true) |
score_data.csv |
Scores (if include_scores=true) |
Example:
curl -s "https://www.metaculus.com/api/posts/12345/download-data/?include_comments=true" \
-H "Authorization: Token $METACULUS_API_TOKEN" \
-o question_data.zip
GET /api/projects/{projectId}/download-data/ — Download Project Data
Downloads data for an entire project as a ZIP. Only available to site admins and whitelisted users.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
projectId |
integer | Project ID |
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
include_comments |
boolean | No | Include comments CSV |
include_scores |
boolean | No | Include scores CSV |
Generating Continuous CDFs
Continuous, numeric, date, and discrete questions require forecasts as a CDF (Cumulative Distribution Function). This section explains how to generate valid CDFs.
CDF Rules
-
Length: The CDF must have exactly
inbound_outcome_count + 1values. For most continuous questions, this is 201 values. For discrete questions, it depends on the question. -
Bounds:
- If
open_lower_bound == false(closed): first value must be 0.0 - If
open_lower_bound == true(open): first value must be ≥ 0.001 (at least 0.1% mass below lower bound) - If
open_upper_bound == false(closed): last value must be 1.0 - If
open_upper_bound == true(open): last value must be ≤ 0.999 (at least 0.1% mass above upper bound)
- If
-
Monotonicity: The CDF must be strictly increasing by at least
0.01 / inbound_outcome_countper step (i.e.,0.00005per step for the standard 200). -
Maximum step: No two adjacent values may differ by more than
0.2 * (200 / inbound_outcome_count).
Understanding Scaling
The CDF values correspond to evenly spaced points across the internal [0, 1] range. To map real-world values to CDF positions:
- Linear scaling (
zero_pointisnull):internal = (value - range_min) / (range_max - range_min) - Logarithmic scaling (
zero_pointis set): requires log transformation (see function below) - Date questions: convert ISO dates to unix timestamps first, then apply scaling
Python: Nominal Value to CDF Location
import datetime
import numpy as np
def nominal_location_to_cdf_location(
nominal_location: str | float,
question_data: dict,
) -> float:
"""Takes a location in nominal format (e.g. 123, "123",
or datetime in iso format) and scales it to metaculus's
"internal representation" range [0,1] incorporating question scaling"""
if question_data["type"] == "date":
scaled_location = datetime.datetime.fromisoformat(
nominal_location
).timestamp()
else:
scaled_location = float(nominal_location)
scaling = question_data["scaling"]
range_min = scaling.get("range_min")
range_max = scaling.get("range_max")
zero_point = scaling.get("zero_point")
if zero_point is not None:
# logarithmically scaled question
deriv_ratio = (range_max - zero_point) / (range_min - zero_point)
unscaled_location = (
np.log(
(scaled_location - range_min) * (deriv_ratio - 1)
+ (range_max - range_min)
)
- np.log(range_max - range_min)
) / np.log(deriv_ratio)
else:
# linearly scaled question
unscaled_location = (scaled_location - range_min) / (
range_max - range_min
)
return unscaled_location
Python: Generate CDF from Percentiles
def generate_continuous_cdf(
percentiles: dict,
question_data: dict,
below_lower_bound: float = None,
above_upper_bound: float = None,
) -> list[float]:
"""
Takes a set of percentiles and returns a corresponding CDF with
inbound_outcome_count + 1 values (typically 201).
percentiles: dict mapping percentile keys to nominal values
Keys must end in a number interpretable as a float in (0, 100).
Values are in the question's real-world scale.
Example:
{
"percentile_05": 25,
"percentile_25": 500,
"percentile_50": 650,
"percentile_75": 700,
"percentile_95": 990,
}
below_lower_bound: probability mass below range_min (for open lower bound)
above_upper_bound: probability mass above range_max (for open upper bound)
"""
percentile_locations = []
if below_lower_bound is not None:
percentile_locations.append((0.0, below_lower_bound))
if above_upper_bound is not None:
percentile_locations.append((1.0, 1 - above_upper_bound))
for percentile, nominal_location in percentiles.items():
height = float(str(percentile).split("_")[-1]) / 100
location = nominal_location_to_cdf_location(
nominal_location, question_data
)
percentile_locations.append((location, height))
percentile_locations.sort()
first_point, last_point = percentile_locations[0], percentile_locations[-1]
if (first_point[0] > 0.0) or (last_point[0] < 1.0):
raise ValueError(
"Percentiles must encompass bounds of the question"
)
def get_cdf_at(location):
previous = percentile_locations[0]
for i in range(1, len(percentile_locations)):
current = percentile_locations[i]
if previous[0] <= location <= current[0]:
return previous[1] + (current[1] - previous[1]) * (
location - previous[0]
) / (current[0] - previous[0])
previous = current
n_points = (question_data.get("inbound_outcome_count") or 200) + 1
continuous_cdf = [get_cdf_at(i / (n_points - 1)) for i in range(n_points)]
return continuous_cdf
Python: Standardize CDF for Submission
This function ensures your CDF satisfies all validation rules. It adds a small linear component to guarantee monotonicity and respects bound constraints.
def standardize_cdf(cdf, question_data: dict):
"""
Standardize a CDF for submission:
- No mass outside closed bounds
- Minimum mass outside open bounds
- Strictly increasing by at least the minimum step
- Caps maximum step size
"""
lower_open = question_data["open_lower_bound"]
upper_open = question_data["open_upper_bound"]
inbound_outcome_count = question_data.get("inbound_outcome_count") or 200
default_inbound_outcome_count = 200
cdf = np.asarray(cdf, dtype=float)
if not cdf.size:
return []
scale_lower_to = 0 if lower_open else cdf[0]
scale_upper_to = 1.0 if upper_open else cdf[-1]
rescaled_inbound_mass = scale_upper_to - scale_lower_to
def standardize(F: float, location: float) -> float:
rescaled_F = (F - scale_lower_to) / rescaled_inbound_mass
if lower_open and upper_open:
return 0.988 * rescaled_F + 0.01 * location + 0.001
elif lower_open:
return 0.989 * rescaled_F + 0.01 * location + 0.001
elif upper_open:
return 0.989 * rescaled_F + 0.01 * location
return 0.99 * rescaled_F + 0.01 * location
for i, value in enumerate(cdf):
cdf[i] = standardize(value, i / (len(cdf) - 1))
pmf = np.diff(cdf, prepend=0, append=1)
cap = 0.2 * (default_inbound_outcome_count / inbound_outcome_count)
def cap_pmf(scale: float) -> np.ndarray:
return np.concatenate(
[pmf[:1], np.minimum(cap, scale * pmf[1:-1]), pmf[-1:]]
)
def capped_sum(scale: float) -> float:
return float(cap_pmf(scale).sum())
lo = hi = scale = 1.0
while capped_sum(hi) < 1.0:
hi *= 1.2
for _ in range(100):
scale = 0.5 * (lo + hi)
s = capped_sum(scale)
if s < 1.0:
lo = scale
else:
hi = scale
if s == 1.0 or (hi - lo) < 2e-5:
break
pmf = cap_pmf(scale)
pmf[1:-1] *= (cdf[-1] - cdf[0]) / pmf[1:-1].sum()
cdf = np.cumsum(pmf)[:-1]
cdf = np.round(cdf, 10)
return cdf.tolist()
End-to-End Example: Submit a Continuous Forecast
import os, requests, numpy as np
TOKEN = os.environ["METACULUS_API_TOKEN"]
HEADERS = {"Authorization": f"Token {TOKEN}"}
BASE = "https://www.metaculus.com"
# 1. Fetch the question
post_id = 12345
resp = requests.get(f"{BASE}/api/posts/{post_id}/", headers=HEADERS)
resp.raise_for_status()
post = resp.json()
question = post["question"]
# 2. Define your percentile beliefs (in real-world units)
percentiles = {
"percentile_05": 25,
"percentile_25": 40,
"percentile_50": 55,
"percentile_75": 70,
"percentile_95": 90,
}
# 3. Generate and standardize the CDF
raw_cdf = generate_continuous_cdf(
percentiles,
question,
below_lower_bound=0.01,
above_upper_bound=0.01,
)
final_cdf = standardize_cdf(raw_cdf, question)
# 4. Submit
resp = requests.post(
f"{BASE}/api/questions/forecast/",
headers={**HEADERS, "Content-Type": "application/json"},
json=[{"question": question["id"], "continuous_cdf": final_cdf}],
)
resp.raise_for_status()
print("Forecast submitted!")
Common Patterns
Browse the Feed
# Newest open questions
curl -s "https://www.metaculus.com/api/posts/?statuses=open&order_by=-published_at&limit=10&with_cp=true" \
-H "Authorization: Token $METACULUS_API_TOKEN" | jq '.results[] | {id, title, status}'
Get Community Prediction for a Post
curl -s "https://www.metaculus.com/api/posts/12345/" \
-H "Authorization: Token $METACULUS_API_TOKEN" \
| jq '.question.aggregations.recency_weighted.latest'
Find Trending Questions
curl -s "https://www.metaculus.com/api/posts/?order_by=-hotness&statuses=open&limit=10&with_cp=true" \
-H "Authorization: Token $METACULUS_API_TOKEN" | jq '.results[] | {id, title}'
Find Questions by Category
curl -s "https://www.metaculus.com/api/posts/?categories=nuclear&statuses=open&with_cp=true&limit=10" \
-H "Authorization: Token $METACULUS_API_TOKEN" | jq '.results[] | {id, title}'
Find Questions in a Tournament
curl -s "https://www.metaculus.com/api/posts/?tournaments=aibq3&with_cp=true&limit=50" \
-H "Authorization: Token $METACULUS_API_TOKEN" | jq '.results[] | {id, title, status}'
Submit a Binary Forecast
curl -s -X POST "https://www.metaculus.com/api/questions/forecast/" \
-H "Authorization: Token $METACULUS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '[{"question": 12345, "probability_yes": 0.75}]'
Submit a Multiple Choice Forecast
curl -s -X POST "https://www.metaculus.com/api/questions/forecast/" \
-H "Authorization: Token $METACULUS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '[{
"question": 12345,
"probability_yes_per_category": {
"Option A": 0.4,
"Option B": 0.35,
"Option C": 0.25
}
}]'
Withdraw a Forecast
curl -s -X POST "https://www.metaculus.com/api/questions/withdraw/" \
-H "Authorization: Token $METACULUS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '[{"question": 12345}]'
Read Comments on a Post
curl -s "https://www.metaculus.com/api/comments/?post=12345&sort=-created_at&limit=20" \
-H "Authorization: Token $METACULUS_API_TOKEN" | jq '.results[] | {author: .author.username, text: .text}'
Post a Comment
curl -s -X POST "https://www.metaculus.com/api/comments/create/" \
-H "Authorization: Token $METACULUS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"on_post": 12345,
"text": "My analysis suggests a higher probability because...",
"included_forecast": true,
"is_private": false
}'
Reply to a Comment
curl -s -X POST "https://www.metaculus.com/api/comments/create/" \
-H "Authorization: Token $METACULUS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"on_post": 12345,
"parent": 67890,
"text": "Good point — I updated my forecast accordingly.",
"included_forecast": false,
"is_private": false
}'
Python: Paginate Through All Open Questions
import os, requests
TOKEN = os.environ["METACULUS_API_TOKEN"]
HEADERS = {"Authorization": f"Token {TOKEN}"}
BASE = "https://www.metaculus.com"
all_posts = []
offset = 0
limit = 100
while True:
resp = requests.get(
f"{BASE}/api/posts/",
headers=HEADERS,
params={
"statuses": "open",
"limit": limit,
"offset": offset,
"with_cp": True,
},
)
resp.raise_for_status()
data = resp.json()
results = data["results"]
all_posts.extend(results)
if data["next"] is None:
break
offset += limit
print(f"Fetched {len(all_posts)} open posts")
Python: Find Questions with Large Movement
import os, requests
TOKEN = os.environ["METACULUS_API_TOKEN"]
HEADERS = {"Authorization": f"Token {TOKEN}"}
BASE = "https://www.metaculus.com"
resp = requests.get(
f"{BASE}/api/posts/",
headers=HEADERS,
params={
"statuses": "open",
"order_by": "-weekly_movement",
"forecast_type": "binary",
"with_cp": True,
"limit": 10,
},
)
resp.raise_for_status()
for post in resp.json()["results"]:
q = post.get("question")
if q and q.get("aggregations"):
latest = q["aggregations"].get("recency_weighted", {}).get("latest")
if latest:
prob = latest.get("centers", [None])[0]
print(f"[{post['id']}] {post['title']} — CP: {prob}")
Question Type Quick Reference
| Type | Forecast Format | Key Fields |
|---|---|---|
binary |
probability_yes: float (0–1) |
— |
multiple_choice |
probability_yes_per_category: {option: float} (sum = 1.0) |
options, all_options_ever |
numeric |
continuous_cdf: float[] (201 values) |
scaling, open_lower_bound, open_upper_bound, unit |
date |
continuous_cdf: float[] (201 values) |
scaling (range_min/max are unix timestamps) |
discrete |
continuous_cdf: float[] (inbound_outcome_count + 1 values) |
inbound_outcome_count, scaling |
Post Status Lifecycle
draft → pending → approved → open → closed → resolved
→ rejected
| Status | Description |
|---|---|
draft |
Author is still editing |
pending |
Submitted for curation review |
rejected |
Rejected by curators |
open |
Approved and accepting forecasts |
upcoming |
Approved but not yet open for forecasting |
closed |
No longer accepting forecasts; awaiting resolution |
resolved |
Final outcome determined |
Error Handling
| Status Code | Meaning | Action |
|---|---|---|
200 |
Success | — |
201 |
Created | — |
400 |
Bad request | Check request format, CDF validity, required fields |
401 |
Unauthorized | Check your Authorization: Token ... header |
403 |
Forbidden | You don't have permission (e.g., project data download) |
404 |
Not found | Check the post/question/project ID |
429 |
Rate limited | Implement exponential backoff and retry |
500 |
Server error | Retry after a delay; if persistent, contact Metaculus |
Usage Tips
- Always pass
with_cp=truewhen listing posts if you need community predictions — aggregations are empty by default. - Use
include_cp_history=trueonly when you need historical CP data — it increases response size significantly. - Use
include_descriptions=trueonly when needed — descriptions can be large. - Question ID ≠ Post ID — Forecasts use the
question.id, not thepost.id. Always fetch the post first to get the question ID. - For group posts,
with_cp=trueon the list endpoint only returns CP for the top 3 sub-questions. Use the detail endpoint (/api/posts/{postId}/) for all sub-questions. - CDF generation is the trickiest part — use the provided Python functions or adapt them. Always run
standardize_cdf()before submitting. - Combine filters for targeted queries — e.g.,
statuses=open&forecast_type=binary&categories=nuclearnarrows results efficiently. - Paginate responsibly — use
limitandoffset. Don't fetch all posts at once. - The
order_by=scoresort requiresforecaster_id— it ranks questions by a specific user's performance. - Date questions use unix timestamps in
scaling.range_minandscaling.range_max— convert ISO dates to timestamps before mapping to CDF locations. - Respect rate limits — implement backoff when you receive 429 responses.
Resources
| Resource | URL |
|---|---|
| Metaculus Platform | metaculus.com |
| API Documentation | metaculus.com/api |
| Account Settings (API Token) | metaculus.com/accounts/settings |
| GitHub Issues | github.com/Metaculus/metaculus |
| Contact | api-requests@metaculus.com |