metaculus

SKILL.md

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

  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

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

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 heavily
  • unweighted — Equal weight for all forecasts
  • metaculus_prediction — Metaculus's proprietary prediction
  • single_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 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

[
  {
    "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 comments
  • count — 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

  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

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

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
Weekly Installs
31
GitHub Stars
2
First Seen
Feb 18, 2026
Installed on
opencode30
github-copilot30
codex30
kimi-cli30
gemini-cli30
amp30