metaculus

Original🇺🇸 English
Translated

Metaculus is a forecasting platform where users predict outcomes of real-world events. Use this skill to interact with the Metaculus API for browsing questions, submitting forecasts, reading community predictions, managing comments, and downloading forecast data.

9installs
Added on

NPX Install

npx skill4agent add outsharp/shipp-skills metaculus

Tags

Translated version includes tags in frontmatter

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)

TermDefinition
PostThe primary feed entity. A post wraps a question, group of questions, conditional pair, or notebook. Posts have statuses, authors, projects, and comments.
QuestionA single forecastable item within a post. Types:
binary
,
multiple_choice
,
numeric
,
discrete
,
date
.
Group of QuestionsA post containing multiple related sub-questions displayed together (e.g., "What will GDP be in 2025, 2026, 2027?").
ConditionalA post with paired questions: "If [condition], what is P(child)?" — produces question_yes and question_no variants.
ForecastA 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 MethodHow individual forecasts are combined:
recency_weighted
(default),
unweighted
,
metaculus_prediction
,
single_aggregation
.
ProjectA container for posts — can be a tournament, category, tag, question series, or site section.
TournamentA special project type with prize pools, start/close dates, and leaderboards.
CategoryA topic classification (e.g., "Nuclear Technology & Risks", "Health & Pandemics").
ResolutionThe actual outcome of a question once known. Values vary by type:
yes
/
no
(binary), a number, a date, or an option name.
Curation StatusEditorial status of a post:
draft
,
pending
,
rejected
,
approved
.
ScalingDefines how a continuous question's range maps to the CDF. Includes
range_min
,
range_max
,
zero_point
(for log scale), and bounds.
CDFCumulative Distribution Function — the format for continuous forecasts. A list of 201 floats (or
inbound_outcome_count + 1
for discrete).
Inbound Outcome CountNumber 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

  1. Log in to Metaculus.
  2. Go to your Account Settings → API Access.
  3. 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

bash
curl -s "https://www.metaculus.com/api/posts/?limit=5&order_by=-published_at" \
  -H "Authorization: Token $METACULUS_API_TOKEN" | jq .

Example: Python

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)

MethodEndpointDescriptionAuth
GET
/api/posts/
Retrieve paginated posts feed with filtersRequired
GET
/api/posts/{postId}/
Retrieve a single post with full detailsRequired

Questions & Forecasts

MethodEndpointDescriptionAuth
POST
/api/questions/forecast/
Submit forecasts for one or more questionsRequired
POST
/api/questions/withdraw/
Withdraw active forecastsRequired

Comments

MethodEndpointDescriptionAuth
GET
/api/comments/
Retrieve comments (filter by post or author)Required
POST
/api/comments/create/
Create a new comment on a postRequired

Utilities & Data

MethodEndpointDescriptionAuth
GET
/api/posts/{postId}/download-data/
Download question data as a ZIP of CSVsRequired
GET
/api/projects/{projectId}/download-data/
Download full project data as a ZIP of CSVsRequired (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-questions
  • conditional
    — 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

FieldTypeDescription
id
integerUnique post ID
title
stringFull title
short_title
stringURL-friendly short title
slug
stringURL slug
author_id
integerAuthor's user ID
author_username
stringAuthor's username
projects
objectAssociated projects (see below)
created_at
datetimeWhen the post was created
published_at
datetime?When the post was published
open_time
datetime?When the question opened for forecasting
edited_at
datetimeLast edit timestamp
curation_status
string
draft
,
pending
,
rejected
, or
approved
comment_count
integerNumber of comments
status
string
open
,
upcoming
,
closed
,
resolved
,
draft
,
pending
,
rejected
nr_forecasters
integerNumber 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
integerTotal number of forecasts

Post Projects Object

FieldTypeDescription
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
ProjectThe post's primary/default project

Project

FieldTypeDescription
id
integerProject ID
type
string
site_main
,
tournament
, etc.
name
stringDisplay name
slug
string?URL slug
prize_pool
stringPrize 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
stringDefault user permission (e.g., "forecaster")
visibility
string
normal
,
not_in_main_feed
,
unlisted

Question

FieldTypeDescription
id
integerUnique question ID (used in forecast submissions, not the post ID)
title
stringQuestion title
description
stringFull 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
stringHow the question will be resolved
fine_print
stringAdditional resolution details
created_at
datetimeCreation timestamp
open_time
datetimeWhen forecasting opens
scheduled_close_time
datetimeWhen forecasting is scheduled to close
actual_close_time
datetimeWhen forecasting actually closed
scheduled_resolve_time
datetimeWhen 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
booleanWhether the upper bound is open
open_lower_bound
booleanWhether the lower bound is open
inbound_outcome_count
integer?Number of discrete outcomes in range (default 200 for continuous)
unit
stringDisplay unit (e.g., "$", "°C")
label
string?Label for sub-questions
scaling
QuestionScalingRange and scaling parameters
aggregations
objectCommunity prediction aggregations (see below)

QuestionScaling

FieldTypeDescription
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 heavily
  • unweighted
    — Equal weight for all forecasts
  • metaculus_prediction
    — Metaculus's proprietary prediction
  • single_aggregation
    — Beta; admin-only
Each method contains:
FieldTypeDescription
history
object[]Time series of aggregated forecasts
latest
object?Most recent aggregated forecast
score_data
object?Scoring information
Each history/latest entry contains:
FieldTypeDescription
start_time
datetimeWhen 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
integerNumber 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

FieldTypeDescription
id
integerConditional ID
condition
QuestionThe condition question
condition_child
QuestionThe child question
question_yes
Question"If condition = Yes" variant
question_no
Question"If condition = No" variant

GroupOfQuestions

FieldTypeDescription
id
integerGroup ID
description
stringGroup description
resolution_criteria
stringResolution criteria
fine_print
stringAdditional details
group_variable
stringThe variable that differs across sub-questions
graph_type
string
multiple_choice_graph
or
fan_graph
questions
Question[]The sub-questions

Comment

FieldTypeDescription
id
integerComment 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
datetimeCreation timestamp
text
stringComment content
on_post
integerPost ID the comment belongs to
included_forecast
booleanWhether the user's last forecast is included
is_private
booleanWhether the comment is private
vote_score
integerTotal vote score
user_vote
integerCurrent 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:
ParameterTypeDescription
limit
integerPage size (default varies)
offset
integerPagination 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
integerPosts where this user has forecasted
not_forecaster_id
integerPosts where this user has NOT forecasted
for_main_feed
booleanFilter for main feed suitability
with_cp
booleanInclude community predictions (default:
false
). For groups, returns CP for top 3 sub-questions only.
include_cp_history
booleanInclude full CP history per aggregation method (default:
false
)
include_descriptions
booleanInclude
description
,
fine_print
,
resolution_criteria
fields
order_by
stringSort field. Prefix with
-
for descending. Options below.
open_time__gt
datetimeOpen time greater than (also supports
__gte
,
__lt
,
__lte
)
published_at__gt
datetimePublished time greater than (also supports
__gte
,
__lt
,
__lte
)
scheduled_resolve_time__gt
datetimeScheduled resolve time greater than (also supports
__gte
,
__lt
,
__lte
)
order_by
options:
ValueDescription
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:
json
{
  "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:
bash
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:
bash
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:
bash
# 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:
ParameterTypeDescription
postId
integerThe post ID
Example:
bash
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
question
field takes the question ID, not the post ID. Get the question ID from
post.question.id
,
post.group_of_questions.questions[].id
, or
post.conditional.question_yes.id
/
post.conditional.question_no.id
.

Binary Forecast

json
[
  {
    "question": 12345,
    "probability_yes": 0.63
  }
]
FieldTypeRequiredDescription
question
integerYesQuestion ID
probability_yes
floatYesProbability between 0 and 1
end_time
datetimeNoAuto-withdraw timestamp

Multiple Choice Forecast

json
[
  {
    "question": 12345,
    "probability_yes_per_category": {
      "Futurama": 0.5,
      "Paperclipalypse": 0.3,
      "Singularia": 0.2
    }
  }
]
FieldTypeRequiredDescription
question
integerYesQuestion ID
probability_yes_per_category
objectYesMap of option name → probability. Must sum to 1.0.
end_time
datetimeNoAuto-withdraw timestamp

Continuous Forecast (Numeric / Date / Discrete)

json
[
  {
    "question": 12345,
    "continuous_cdf": [0.0, 0.00005, 0.00010, "... 201 values total ...", 1.0]
  }
]
FieldTypeRequiredDescription
question
integerYesQuestion ID
continuous_cdf
float[]YesCDF array (see CDF generation section below)
distribution_input
objectNoSlider values for frontend display
end_time
datetimeNoAuto-withdraw timestamp

Conditional Forecast

Submit forecasts for both the "if Yes" and "if No" questions:
json
[
  { "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:
json
[
  { "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:
bash
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:
StatusDescription
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.
json
[
  { "question": 12345 },
  { "question": 12346 }
]
Example:
bash
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:
ParameterTypeRequiredDescription
post
integerOne of post/authorPost ID to filter by
author
integerOne of post/authorAuthor user ID to filter by
limit
integerNoNumber of comments to retrieve
offset
integerNoPagination offset
is_private
booleanNoFilter private vs public (default:
false
)
use_root_comments_pagination
booleanNoIf
true
, pagination applies to root comments only; all child comments are included
sort
stringNo
created_at
(ascending) or
-created_at
(descending)
focus_comment_id
integerNoPlace this comment at the top of results
Response:
json
{
  "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 comments
  • count
    — Total root comments only
Example:
bash
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:
FieldTypeRequiredDescription
on_post
integerYesPost ID to comment on
text
stringYesComment content
included_forecast
booleanYesInclude the user's last forecast
is_private
booleanYesWhether the comment is private
parent
integerNoParent comment ID (for replies)
Example:
bash
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:
StatusDescription
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:
ParameterTypeDescription
postId
integerPost ID (the number after
/questions/
in the URL)
Query Parameters:
ParameterTypeRequiredDescription
sub_question
integerNoSub-question ID for group/conditional questions
aggregation_methods
string[]NoMethods to include:
recency_weighted
,
unweighted
,
metaculus_prediction
,
single_aggregation
. Default:
recency_weighted
only
include_bots
booleanNoInclude bot forecasts in aggregation recalculation
user_ids
string[]NoSpecific user IDs (whitelisted users only). Requires
aggregation_methods
.
minimize
booleanNoDefault:
true
. If
false
, includes all data points (may produce large files)
include_comments
booleanNoDefault:
false
. If
true
, adds
comment_data.csv
include_scores
booleanNoDefault:
false
. If
true
, adds
score_data.csv
ZIP Contents:
FileDescription
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:
bash
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:
ParameterTypeDescription
projectId
integerProject ID
Query Parameters:
ParameterTypeRequiredDescription
include_comments
booleanNoInclude comments CSV
include_scores
booleanNoInclude 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

  1. Length: The CDF must have exactly
    inbound_outcome_count + 1
    values. For most continuous questions, this is 201 values. For discrete questions, it depends on the question.
  2. 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)
  3. Monotonicity: The CDF must be strictly increasing by at least
    0.01 / inbound_outcome_count
    per step (i.e.,
    0.00005
    per step for the standard 200).
  4. 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_point
    is
    null
    ):
    internal = (value - range_min) / (range_max - range_min)
  • Logarithmic scaling (
    zero_point
    is 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

python
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

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

python
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

bash
# 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

bash
curl -s "https://www.metaculus.com/api/posts/12345/" \
  -H "Authorization: Token $METACULUS_API_TOKEN" \
  | jq '.question.aggregations.recency_weighted.latest'

Find Trending Questions

bash
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

bash
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

bash
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

bash
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

bash
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

bash
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

bash
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

bash
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

bash
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

python
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

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={
        "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

TypeForecast FormatKey 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
StatusDescription
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 CodeMeaningAction
200
Success
201
Created
400
Bad requestCheck request format, CDF validity, required fields
401
UnauthorizedCheck your
Authorization: Token ...
header
403
ForbiddenYou don't have permission (e.g., project data download)
404
Not foundCheck the post/question/project ID
429
Rate limitedImplement exponential backoff and retry
500
Server errorRetry after a delay; if persistent, contact Metaculus

Usage Tips

  • Always pass
    with_cp=true
    when listing posts if you need community predictions — aggregations are empty by default.
  • Use
    include_cp_history=true
    only when you need historical CP data — it increases response size significantly.
  • Use
    include_descriptions=true
    only when needed — descriptions can be large.
  • Question ID ≠ Post ID — Forecasts use the
    question.id
    , not the
    post.id
    . Always fetch the post first to get the question ID.
  • For group posts,
    with_cp=true
    on 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=nuclear
    narrows results efficiently.
  • Paginate responsibly — use
    limit
    and
    offset
    . Don't fetch all posts at once.
  • The
    order_by=score
    sort requires
    forecaster_id
    — it ranks questions by a specific user's performance.
  • Date questions use unix timestamps in
    scaling.range_min
    and
    scaling.range_max
    — convert ISO dates to timestamps before mapping to CDF locations.
  • Respect rate limits — implement backoff when you receive 429 responses.

Resources

ResourceURL
Metaculus Platformmetaculus.com
API Documentationmetaculus.com/api
Account Settings (API Token)metaculus.com/accounts/settings
GitHub Issuesgithub.com/Metaculus/metaculus
Contactapi-requests@metaculus.com