Anteproof › Developer docs

Developer docs

The REST API and the MCP server give programmatic access to the same archive the website reads: dependency risk for a package or repository, the forecast log, question detail and the calibration record. Every number is a published forecast or a score of one; nothing is computed on request.

Quickstart Authentication /v1/risk /v1/entities /v1/forecasts /v1/questions /v1/calibration/families /v1/proof Other endpoints MCP server Keys and quotas Errors Resolution rules Terms of use

Quickstart

  1. Sign in at /account (email code or Google) and create an API key. Keys start with ap_ and are shown once.
  2. Ask about a dependency:
curl -s "https://api.anteproof.com/v1/risk?q=express" \
  -H "Authorization: Bearer ap_YOUR_KEY"

The base URL is https://api.anteproof.com. https://anteproof.com serves the same routes. All responses are JSON (UTF-8) except /v1/digest, which is Markdown. Timestamps are ISO-8601 in UTC. Probabilities are decimals in [0, 1]. The interactive OpenAPI explorer is at /docs/openapi and the schema at /openapi.json.

For worked examples — one answer shown in full with the activity behind it, and the commands that produce it — see /examples. Every figure on that page is fetched live, so it is also a check that the API is answering.

Authentication

Send the key in either header on every request:

Authorization: Bearer ap_YOUR_KEY
X-API-Key: ap_YOUR_KEY

A request that carries a key is authenticated and counted against your account's monthly allowance (Keys and quotas). A key that is unknown or revoked is refused with 401 rather than treated as anonymous, so a misconfigured client finds out. Requests without a key are the website's own traffic: they are served without metering or any service commitment, and are not intended for programmatic use.

Dependency risk

GET/v1/risk?q=

One answer about a package or repository for an agent or engineer choosing or auditing a dependency: the latest open maintenance forecasts, 90 days of activity summarised, the day the entity was last velocity-scored, the plain-language signals those numbers support, and the families' own calibration record. Every block comes from the archive as it stands; the answer names what is missing instead of estimating it.

ParameterMeaning
qRequired, 1–200 characters. An npm, PyPI or crates.io package name (express, @scope/pkg, requests, serde), a GitHub owner/repo or repository URL, or a prefixed name: npm:express, pypi:requests, crate:serde, repo:owner/name. A bare owner/name is read as a repository. Matching is case-insensitive; an ambiguous substring is a 404 that lists the near misses.

entity.dependents is how many repositories depend on this package, from ecosyste.ms dependent_repos_count — the same figure /v1/risk/top filters on. It is null when the registry never gave us one, which is not the same as 0, and it is null for most repositories, where the count belongs to the packages built from them.

GET/v1/entities/{entity_id}/risk

The same answer for an entity by id.

GET/v1/risk/top?limit=10&min_dependents=1000

The weekly digest's "Most at risk this week" table as JSON: the limit repositories (1–100, default 10) with the lowest open maintenance forecasts, among source repositories of packages with at least min_dependents dependent repositories (ecosyste.ms dependent_repos_count, default 1000). One row per repository with both family probabilities (maintenance.commits_90d, maintenance.release_90d, either may be null), ranked by min_probability ascending, with the qualifying packages, as_of, the chain head, the families' pooled track_record (withheld today: n and brier are null with a withheld block, see calibration_note below) and the standing disclaimer. open_questions (also given as open_maintenance_questions) counts open maintenance questions on any repository, so an empty rows is readable as "nothing open yet" versus "nothing above the threshold"; earliest_live_resolution is the date (ISO, UTC) the first of those open questions resolves — when the families' live track record starts — or null while nothing is open. Cached five minutes.

Example response

{
  "query": "astral-sh/uv",
  "as_of": "2026-09-04T14:02:11.418223+00:00",
  "chain_head": {"head_hash": "9f0c…e41a", "head_id": 48211},
  "entity": {"id": 1207, "kind": "repo", "canonical": "astral-sh/uv", "url": "/entities/1207",
             "dependents": null},
  "repo": {"id": 1207, "canonical": "astral-sh/uv", "url": "/entities/1207"},
  "repo_link": "self",
  "maintenance": {
    "commits_90d": {
      "question_id": 40317,
      "title": "Will astral-sh/uv receive 180+ commits to its default branch in the 90 days to 2 Dec 2026?",
      "question": "at least K commits in the next 90 days",
      "probability": 0.22,
      "issued_at": "2026-09-04T06:10:42.910055+00:00",
      "resolves_at": "2026-12-03T00:00:00+00:00",
      "threshold": 180,
      "window_start": "2026-09-04",
      "window_end": "2026-12-02",
      "url": "/questions/40317"
    },
    "release_90d": {
      "question_id": 40318,
      "title": "Will astral-sh/uv publish a release by 2 Dec 2026?",
      "question": "at least one release in the next 90 days",
      "probability": 0.61,
      "issued_at": "2026-09-04T06:10:44.120380+00:00",
      "resolves_at": "2026-12-03T00:00:00+00:00",
      "threshold": 1,
      "window_start": "2026-09-04",
      "window_end": "2026-12-02",
      "url": "/questions/40318"
    }
  },
  "activity": {
    "window_days": 90,
    "source_health": {
      "commits_hourly": {
        "metric": "commits_hourly",
        "status": "degraded",
        "degraded_from": "2026-07-01",
        "healthy_through": "2026-06-30",
        "reference_per_day": 556.23,
        "reference_per_entity_per_day": 0.207263,
        "latest_ratio": 0.2625,
        "latest_population_ratio": 1.0,
        "floored_from": "2026-07-01",
        "reason": "commits_hourly is under a hard floor from 2026-07-01 and is never reported healthy on or after it — GH Archive's hourly files cap at ~155-165k events and push crowded the rest out; July 2026's push capture ran 25-30% below its stable level, verified against the GitHub API, while the relative measure still read 0.698 of its anchored reference"
      }
    },
    "data_through": "2026-09-03",
    "data_since": "2025-11-01",
    "coverage_days": {"commits_hourly": 309, "release_published": 309, "stars_hourly": 309},
    "metrics_present": ["commits_hourly", "release_published", "stars_hourly"],
    "withheld": {
      "reason": "source_collapsed",
      "metrics": ["commits_hourly", "release_published", "stars_hourly"],
      "degraded_from": "2025-11-01",
      "detail": "Our push, release and star measurements are not trustworthy from 2025-11-01, so the push, release and star figures that would describe this repository over it are withheld: they would report our own capture failure as this project's behaviour. A withheld figure is null; a real zero is 0."
    },
    "commits_last_30d": null,
    "commits_prev_30d": null,
    "commits_last_90d": null,
    "days_since_last_commit": null,
    "releases_last_90d": null,
    "releases_last_365d": null,
    "days_since_last_release": null,
    "stars_last_30d": null,
    "stars_prev_30d": null
  },
  "velocity": {"as_of": "2026-09-04", "composite": null, "stage": null,
               "withheld": {"reason": "insufficient_data", "metrics": ["composite", "stage"], "degraded_from": null,
                            "detail": "The velocity stage label is withheld. ... The velocity composite score is withheld, ..."}},
  "signals": [
    "measurement source degraded: our push data (commits_hourly) is unreliable from 2026-07-01 — fleet-wide capture is 26% of its usual level; push figures covering that period are not usable and no push trend is claimed for it",
    "measurement source collapsed: our release data (release_published) is unreliable from 2025-11-01 — fleet-wide capture is 2% of its usual level; release figures covering that period are not usable and no release trend is claimed for it",
    "measurement source collapsed: our star data (stars_hourly) is unreliable from 2026-04-01 — fleet-wide capture is 10% of its usual level; star figures covering that period are not usable and no star trend is claimed for it",
    "forecast: keeping its push pace over the next 90 days is unlikely (p=0.22)"
  ],
  "calibration_note": {
    "url": "/calibration?horizon=90",
    "min_n_for_a_verdict": 30,
    "forecaster_version": "m5-v2",
    "families": {
      "commits_90d": {
        "question": "at least K commits in the next 90 days",
        "live": {"n": null, "brier": null},
        "pastcast": {"n": null, "brier": null},
        "track_record": null,
        "withheld": {
          "reason": "resolutions_untrusted",
          "metrics": ["commits_hourly", "release_published"],
          "degraded_from": null,
          "detail": "No track record is published for these questions. Their outcomes were counted from a third-party archive of GitHub events that we have since stopped trusting, so a score would measure that archive rather than the projects. We withheld it instead of publishing a number we cannot stand behind."
        }
      },
      "release_90d": {
        "question": "at least one release in the next 90 days",
        "live": {"n": null, "brier": null},
        "pastcast": {"n": null, "brier": null},
        "track_record": null,
        "withheld": {"reason": "resolutions_untrusted", "metrics": ["commits_hourly", "release_published"], "degraded_from": null, "detail": "…"}
      }
    },
    "withheld": {"reason": "resolutions_untrusted", "metrics": ["commits_hourly", "release_published"], "degraded_from": null, "detail": "…"},
    "note": "No track record is published for these questions. Their outcomes were counted from a third-party archive of GitHub events …"
  }
}

Fields

FieldMeaning
entity, repo, repo_linkWhat the query matched and the repository the maintenance questions are asked about. repo_link says how the repository was found: self (the query was a repository), source_repo (the package's source repository as recorded by its registry, the normal case), external_ids (a connector recorded the package's repository), name_match (a unique repository whose name equals the package name — a labelled guess), or unknown (no repository; repo is null, both forecasts are null, and maintenance_unavailable says so in words).
maintenanceThe latest live forecast on the newest open question per family, or null when the family has no open question for this repository. threshold is the K the question asks for (for release_90d always 1); window_start/window_end are the inclusive calendar window; url is the public question page with the receipt and the full forecast history.
maintenance_unavailableWhy a family produced no forecast. Always present with one entry per family: null where maintenance carries a forecast, otherwise an object with reason, a one-sentence detail, needs_days (how many more days of history the repository needs; a number only when the failed gate counts collected days, min_days or min_history_days, where waiting closes the gap, otherwise null), needs_active_days (how many more days with activity, a commit or a release, the family needs; a number only when the failed gate is min_positive_days, which waiting alone does not clear, otherwise null), gate (the condition it failed) and gates (the gate ladder as far as it was evaluated, each with its own reading — the ladder stops at the first failure, so a later gate is unevaluated, not passed). reason is one of: repo_not_linked (the package has no source repository recorded, so there is nothing for a repository-scoped family to ask about), not_tracked (we hold no rows of this family's series for this repository at all — a gap in what we collect, not a claim that the project is quiet), not_a_candidate (we hold the series and it does not clear the family's gate; gate says which one, and needs_days or needs_active_days by how much), no_question_yet (it clears every gate but no question has been generated for it — generation runs in batches under a per-family cap, so a qualifying repository can wait), no_forecast_yet (an open question exists and carries no forecast — a gap in our pipeline, not a fact about the project), reason_withheld (the series every reason would be counted from was not capturing over this window, so stating one would report our own capture failure as the project's behaviour; see the withheld contract), or undetermined (we could not work it out, and we would rather say so than guess). The same sentences also appear in signals.
activitySums over daily rollups of the repository's commits, releases and stars. A metric absent from metrics_present reports null, never 0. data_through is the last day with any rollup; when it lags by more than 3 days the signals say so.
activity.withheldThe withheld contract, which holds everywhere in this API. A figure we are not willing to stand behind is null — never 0, never omitted, never a string — and the block that held it carries a sibling withheld, either null or {"reason", "metrics", "degraded_from", "detail"}. reason is one of source_degraded, source_collapsed, resolutions_untrusted or insufficient_data; metrics names the series involved; degraded_from is the date their figures stop being usable (null when no window of them is sound); detail is one sentence you can show a reader. So a real zero and a withheld figure never render the same: a real zero is 0, and its metric is not in withheld.metrics. A metric we hold no rows for at all is a third case — null figures, absent from both metrics_present and withheld.metrics. Each figure is judged over the window IT describes, so commits_prev_30d (which ends 30 days ago) can be withheld while a fresher figure is not, and the other way round.
activity.source_healthWhether the SOURCE behind each activity metric was still capturing over this window — a fleet-wide measure, not this repository: the metric's events per reporting entity per day, month by month, against an anchored reference (the best sustained level the series has held, which can rise but never erodes as an outage ages). status is healthy, degraded (under half that level), collapsed (under a seventh of it) or unknown (too little history, or a reporting population too far from its baseline for the months to be comparable). A source that has been degraded needs three consecutive months back at level before it is called healthy again, so one good month cannot clear it. healthy means "capturing consistently with its own best sustained level", never "complete". When it is degraded or collapsed, degraded_from is the date the figures stop being usable and every signal that would have asserted a change in this project's behaviour over a window reaching that date is withheld — a source that stops capturing looks exactly like a drop in the project's own activity, and we will not report the first as the second. The numbers go with the claim: every activity figure computed over a window reaching that date is null, with the reason in activity.withheld. floored_from is a HARD FLOOR — a fixed date, published in the code with its evidence, from which a metric is never reported healthy whatever the measured ratio says. The relative measure can only ever make a floored verdict stricter, never lift it: commits_hourly is floored from 2026-07-01 (July's push capture ran 25-30% below its stable level while the ratio still read 0.698 of the reference), stars_hourly from 2026-04-01, and release_published at every date it has — verified against the GitHub API, it captured only 30-70% of releases even in its best months, so it has no trusted era to compare against.
velocityThe day the entity was last scored, or null when it has never been scored. Both figures are currently withheld (reason: insufficient_data), for two separate reasons. stage is a CROSS-LAYER label — it compares a project's upstream layers against its downstream ones — and for a repository we hold only the upstream (builder) side, so two of its four labels cannot occur and the remaining two reduce to one undisclosed threshold. composite is withheld on its own account, not as a consequence: 40% of it ranks the entity's acceleration against the median of every other entity scored in the same run, so a repository with no commits at all outscores most live ones whenever the cohort is declining, and about one scored entity in ten sits exactly on the ±5.00 clip boundary where the number stops distinguishing magnitudes. Both arrive as null with the block's withheld sibling naming them. Those are not figures we failed to compute, and they must not be rendered as a dash or as "unknown". A velocity block that is PRESENT with null figures means we scored the entity and withheld the result; velocity: null means we never scored it.
signalsPlain-language flags derived only from the numbers above; each names the numbers it came from. A flag is a description of the record, not a recommendation.
calibration_noteThe two families' own resolved track record, per track. Currently withheld (reason: resolutions_untrusted): commits_90d and release_90d resolve by counting the very series that is under a hard floor above, so the outcomes those forecasts were scored against are unreliable and a Brier over them would not mean anything. n, brier and track_record are null — not 0, which would claim nothing has resolved when the truth is that we will not score what has.
chain_headThe forecast log's hash-chain head at the moment of the answer, so the answer can be tied to the receipt chain it was read from (/v1/chain).

What this endpoint is not: it does not call a package "safe" or "unsafe", it does not scan code or vulnerabilities, and a probability is not advice. It reports what a repository has done and what we forecast it will do, with the score of our previous forecasts of the same kind.

Entities

GET/v1/entities?q=

Case-insensitive substring search on the canonical name; exact match first, then shortest name. Current entities only (merged duplicates are excluded).

ParameterMeaning
qRequired, 1–80 characters.
limit1–50, default 20.
{
  "entities": [
    {"id": 412, "kind": "npm_package", "canonical": "express"},
    {"id": 3391, "kind": "repo", "canonical": "expressjs/express"}
  ],
  "q": "express"
}

Entity kinds include repo, npm_package, pypi_package, crate, hf_model, company and topic. /v1/risk answers for the first four.

GET/v1/entities/{entity_id}

One entity's public record: identity, the day it was last velocity-scored (every figure in that block — composite, layer_scores and stage — is withheld, see above), the last 30 days of daily metrics (metrics: layer, metric, day, value), and every live question about it with its latest forecast.

{
  "id": 1207, "kind": "repo", "canonical": "astral-sh/uv",
  "velocity": {"as_of": "2026-09-04", "composite": null, "stage": null, "layer_scores": null,
               "withheld": {"reason": "insufficient_data",
                            "metrics": ["composite", "layer_scores", "stage"], "degraded_from": null,
                            "detail": "The velocity stage label is withheld. ... The velocity composite score is withheld, ..."}},
  "metrics": [
    {"layer": "builder", "metric": "commits_hourly", "day": "2026-08-06", "value": 3.0},
    …
  ],
  "questions": [
    {
      "id": 40317,
      "text": "Will GitHub repo astral-sh/uv's default branch receive at least 180 commits over the 90 days from 2026-09-04 to 2026-12-02 (inclusive)? (Commits as listed by the GitHub REST API.)",
      "title": "Will astral-sh/uv receive 180+ commits to its default branch in the 90 days to 2 Dec 2026?",
      "status": "open",
      "resolves_at": "2026-12-03T00:00:00+00:00",
      "resolver": "rollup_window",
      "resolution_spec": {"entity_id": "1207", "layer": "builder", "metric": "commits_hourly", "window_start": "2026-09-04", "window_end": "2026-12-02", "aggregate": "sum", "threshold": 180, "comparison": "gte", "scheme": "trailing_window_sum", "family": "commits_90d"},
      "latest_p": 0.22,
      "latest_forecast_at": "2026-09-04T06:10:42.910055+00:00",
      "outcome": null,
      "entity": "astral-sh/uv"
    }
  ]
}

The forecast log

GET/v1/forecasts

Every live forecast, newest first, with its question and its resolution once there is one. Backtests are excluded. A forecast whose question was later voided stays in the log with question_status "voided" and the recorded voided_reason.

ParameterMeaning
limit1–100, default 25.
offsetDefault 0.
{
  "forecasts": [
    {
      "id": 48211,
      "question_id": 40318,
      "entity_id": 1207,
      "entity": "astral-sh/uv",
      "question": "Will GitHub repo astral-sh/uv publish at least one release (as listed by the GitHub REST API) between 2026-09-04 and 2026-12-02 (inclusive)?",
      "title": "Will astral-sh/uv publish a release by 2 Dec 2026?",
      "resolver": "rollup_event_window",
      "resolves_at": "2026-12-03T00:00:00+00:00",
      "resolution_spec": {"entity_id": "1207", "layer": "builder", "metric": "release_published", "window_start": "2026-09-04", "window_end": "2026-12-02", "aggregate": "event_count", "threshold": 1, "comparison": "gte", "scheme": "event_window", "family": "release_90d"},
      "question_status": "open",
      "voided_reason": null,
      "p": 0.61,
      "issued_at": "2026-09-04T06:10:44.120380+00:00",
      "latency_ms": 8412,
      "outcome": null,
      "resolved_at": null
    }
  ],
  "limit": 25,
  "offset": 0
}

question is the exact wording that was forecast and timestamped; title is a shorter rendering of the same question. outcome is 1.0 (YES), 0.0 (NO) or null while open.

Question detail

GET/v1/questions/{question_id}

One question with every live forecast ever issued for it, oldest first, and its resolution if settled. The public page for the same question is /questions/{question_id}.

{
  "id": 40318,
  "text": "Will GitHub repo astral-sh/uv publish at least one release (as listed by the GitHub REST API) between 2026-09-04 and 2026-12-02 (inclusive)?",
  "title": "Will astral-sh/uv publish a release by 2 Dec 2026?",
  "kind": "binary",
  "resolver": "rollup_event_window",
  "opens_at": "2026-09-04T06:00:00+00:00",
  "resolves_at": "2026-12-03T00:00:00+00:00",
  "status": "open",
  "voided_reason": null,
  "resolution_spec": {"entity_id": "1207", "layer": "builder", "metric": "release_published", "window_start": "2026-09-04", "window_end": "2026-12-02", "aggregate": "event_count", "threshold": 1, "comparison": "gte", "scheme": "event_window", "family": "release_90d"},
  "entity_id": 1207,
  "entity": "astral-sh/uv",
  "outcome": null,
  "resolved_at": null,
  "disputed": null,
  "forecasts": [
    {"id": 48211, "p": 0.61, "issued_at": "2026-09-04T06:10:44.120380+00:00", "calibration_version": "m5-v2", "latency_ms": 8412}
  ]
}

Calibration by family

GET/v1/calibration/families

The data behind the calibration page: the resolved-forecast pool broken down by question family, for two tracks. pastcast is the point-in-time backtest pool the launch gate judges, scoped to the current question scheme and the forecaster version named by the latest gate evaluation. live is every real-time forecast that has resolved, all versions. One row per question: the latest forecast, which is what resolution scored against.

ParameterMeaning
horizon7, 30, 90, long (over 90 days) or all. Omitted: the horizon the latest gate verdict scopes on. Maintenance families are at 90.
{
  "regime": {
    "forecaster_version": "m5-v2",
    "schemes": ["forecast_referenced"],
    "gate": {"created_at": "2026-08-29T03:14:07+00:00", "domain": "default", "as_of": "2026-08-28", "passes": false, "reasons": ["…"], "report": {"n": 414, "horizon_days": 7, "forecaster_version": "m5-v2", "…": "…"}, "withheld": null},
    "naive_baseline_brier": 0.25,
    "min_n_for_a_verdict": 30,
    "horizon": "90",
    "gate_horizon": "7",
    "horizons": [
      {"key": "7", "label": "up to 7 days", "n_pastcast": 414, "n_live": 212},
      {"key": "30", "label": "8 to 30 days", "n_pastcast": 96, "n_live": 40},
      {"key": "90", "label": "31 to 90 days", "n_pastcast": 0, "n_live": 1},
      {"key": "long", "label": "over 90 days", "n_pastcast": 0, "n_live": 0}
    ]
  },
  "pastcast": {
    "n": 0, "enough_to_judge": false, "base_rate": null, "mean_p": null, "brier": null,
    "brier_skill_score": null, "climatology_brier": null, "brier_skill_vs_base_rate": null,
    "z_bias": null, "spiegelhalter_z": null, "hit_rate": null,
    "bins": [{"lo": 0.0, "hi": 0.1, "n": 0, "mean_p": null, "observed": null}, "…"],
    "families": []
  },
  "live": {
    "n": 1, "enough_to_judge": false, "base_rate": 1.0, "mean_p": 0.8, "brier": 0.04,
    "brier_skill_score": 0.84, "climatology_brier": 0.0, "brier_skill_vs_base_rate": null,
    "z_bias": 0.5, "spiegelhalter_z": -0.5, "hit_rate": 1.0,
    "bins": ["…"],
    "families": [
      {"family": "release_90d", "label": "GitHub release, 90 days", "n": 1, "enough_to_judge": false, "base_rate": 1.0, "mean_p": 0.8, "brier": 0.04, "brier_skill_score": 0.84, "climatology_brier": 0.0, "brier_skill_vs_base_rate": null, "z_bias": 0.5, "spiegelhalter_z": -0.5, "hit_rate": 1.0, "bins": ["…"]}
    ]
  },
  "generated_at": "2026-09-04T14:02:11.418223+00:00"
}

Reading the numbers: brier is the mean squared error of the probabilities (0.25 is what always saying 50% scores; lower is better). brier_skill_score is 1 − brier / 0.25. z_bias and spiegelhalter_z are the two bias statistics the launch gate uses. Below min_n_for_a_verdict resolved rows, enough_to_judge is false and the numbers are shown but say little. The gate history, including every failing evaluation, is at /v1/calibration.

regime.gate and regime.latest_gate are under the same withheld contract as that history, through the same code path. A verdict scoped to a family whose resolutions came off the collapsed GH Archive series keeps its identity — when it ran, its domain, family, as-of date, horizon, forecaster version and its recorded verdict — and every statistic in its report is null (never 0, never an omitted key), with a sibling withheld block giving the reason. That includes report.market_comparison: a null there means withheld, not "no market comparison was run" — withheld is what tells the two apart, so read it before reading any number beside it. withheld is null on every verdict still published.

The record in one JSON

GET/v1/proof

What the landing page's proof strip shows, as data: the latest launch-gate verdict and the maintenance families' own resolved record, with a plain-words note that says exactly as much as the numbers earn. A launch post or a weekly report reads this once instead of retyping numbers that go stale. Every sentence is rendered from the archive; no date, count or skill figure is typed in. Cached five minutes.

{
  "gate": {
    "n": 899, "bss": 0.113, "passes": false, "horizon_days": 7,
    "family": null, "family_label": "GDELT news tone, 863 of 900",
    "families": [{"family": "goldstein_scale", "label": "GDELT news tone (daily)", "n": 863}, "…"],
    "forecaster_version": "m5-v5", "as_of": "2026-09-03", "evaluated_at": "2026-09-04T03:14:07+00:00"
  },
  "maintenance": {
    "state": "none",
    "n_resolved": 0,
    "per_family": {
      "commits_90d": {"label": "GitHub commits, 90 days", "n": 0, "n_pastcast": 0, "n_live": 0, "brier": null,
                      "brier_skill_score": null, "brier_skill_ci95": null, "enough_to_judge": false},
      "release_90d": {"…": "…"}
    },
    "judged": null,
    "min_n_for_a_verdict": 30,
    "open_questions": 1842,
    "earliest_live_resolution": "2026-12-03",
    "calibration_url": "/calibration?horizon=90"
  },
  "note": {
    "headline": "No track record yet on the maintenance questions.",
    "body": "The first pastcast results on commits_90d and release_90d are expected within days. …",
    "text": "No track record yet on the maintenance questions. The first pastcast results … Details: https://anteproof.com/calibration?horizon=90"
  },
  "generated_at": "2026-09-04T14:02:11.418223+00:00"
}
FieldMeaning
gateThe latest launch-gate evaluation (null before the first one): n and bss (Brier skill over always saying 50%) from the verdict's own report, passes, the horizon and forecaster version it scoped on, and families, the pastcast pool at that horizon by question family, largest first. family_label names the pool in words: the gate's own family when it was scoped to one, otherwise the largest family followed by its own count over the pool total — "GDELT news tone, 863 of 900" — so the share is stated rather than implied, and the residual (37 rows here that are not GDELT) is a subtraction away. Both counts are families' own: the largest entry's n, and the sum of every entry's n. That total is not nn is the verdict's own figure from when it was evaluated, and the pool as it stands now can differ by a row or two. A pool with only one family is named bare, with no count.
maintenanceThe commits_90d and release_90d families' resolved rows on both tracks, scoped exactly as the calibration page scopes them, with Brier, Brier skill and a 95% interval per family (brier_skill_ci95, a normal approximation on the squared errors, used only to refuse a claim). judged pools the families with at least min_n_for_a_verdict rows, or is null. open_questions and earliest_live_resolution are the same two facts /v1/risk/top carries.
maintenance.statenone: nothing has resolved. early: rows exist but no family has 30 of them. no_skill: a family has 30 or more but the pooled skill's interval includes zero, so skill is not claimed. skill: 30 or more and the interval excludes zero.
noteThe wording the state earns: headline and body as the landing page prints them, and text, the two joined with the calibration URL, for a post or a report to paste verbatim.

Other endpoints

EndpointReturns
GET /v1/calibrationsnapshots: the latest calibration snapshot per domain; gate_history: every launch-gate evaluation ever run, verdicts included. A run scoped to a family whose resolutions came off the collapsed GH Archive series keeps its identity (when, domain, family, horizon, forecaster version, verdict) and has every statistic returned as null, with a withheld block giving the reason; every other run is returned verbatim.
GET /v1/chain/headThe forecast log's hash-chain head: head_hash, head_id, n_forecasts, max_issued_at. This is what the daily receipt anchors (anchor receipts).
GET /v1/chain/rows?after_id=&limit=The chain itself, keyset-paginated (up to 1000 rows): every forecast row's hashed fields plus prev_hash and row_hash as hex, in id order, so a head can be re-derived with no trust in us. Backtest rows are included; they are links in the chain. Pass the response's next_after_id back as after_id for the next page (null at the end).
GET /v1/anchor/latestThe latest daily anchor: the receipt that commits a chain head to OpenTimestamps and an RFC 3161 timestamp authority, with receipt_url / log_url pointing into the public log (anchor receipts), where the .ots and .tsr proofs live beside it. chain_head is the live head, so it normally sits ahead of the receipt's head_id — the receipt is written once a day, forecasts are appended all day. receipt is null when the serving box has no view of the log; the repo is authoritative either way.
GET /v1/proofThe record in one JSON: gate verdict, the maintenance families' resolved record and its state, and the note that state earns (details).
GET /v1/statsHeadline counts: live_forecasts, questions, resolutions, signal_events, sources.
GET /v1/digest?days=The period's digest as Markdown (text/markdown), 1–90 days.

Everything above is read-only. Account, watchlist, key, alert and billing routes under /v1/me and /v1/billing take a Clerk session token, not an API key, and are used by the account page.

SBOM import

POST/v1/me/watchlist/import?dry_run=
GET/v1/me/watchlist/pending?include_resolved=

Fill a watchlist from a dependency tree instead of one name at a time. The body is the SBOM document itself — raw bytes, not a multipart upload and not a wrapper object — with a Clerk session token, the same auth as the rest of /v1/me. Call /v1/me once first; without an account the import is a 404.

Formats. CycloneDX JSON and SPDX JSON. Only JSON — the CycloneDX and SPDX XML serialisations are not parsed. The format is detected from the document, not from a header or a query parameter: CycloneDX when bomFormat is "CycloneDX", or when the object carries both components and specVersion; SPDX when spdxVersion starts with SPDX, or when the object carries both packages and SPDXID. Anything else — including valid JSON that is neither, a JSON array, or a non-JSON body — is a 400 with {"code": "sbom_unreadable", "message": …}, the message written for the person who uploaded it. The document's own version string comes back as spec_version; we do not check it against a list of supported versions.

What is read. Every component's purl, of which four types resolve to things the archive tracks: pkg:npm, pkg:pypi, pkg:cargo and pkg:github. Versions and purl qualifiers are stripped — a watchlist is per package, not per version — and names are normalised the way each ecosystem does it (PyPI to PEP 503, npm and GitHub lowercased, Cargo left as written), then deduplicated, so react@17 and react@18 are one entry. CycloneDX nesting is followed: a component listed under another component's components is read like any other, and components_total counts the flattened tree (for SPDX it is the length of packages). An SPDX package with no purl falls back to a downloadLocation pointing at github.com. Everything that does not resolve is counted in unsupported_ecosystems under the key that says why: unknown for a component with no purl at all (an SPDX package with neither a purl nor a GitHub download location lands here too, and its name field is not guessed from), invalid_purl for a string that is not a purl, and the purl's own type for everything else — a type we do not track (maven, gem, golang), and also one we do track whose purl yielded no name, such as a pkg:github purl with no repo segment. So a key here names the reason, not necessarily an ecosystem we lack. Matching is against the archive's canonical name, case-insensitively and within the same ecosystem: a crate named requests is not a match for pkg:pypi/requests.

Packages we do not track yet. They are not dropped. Each is returned in unmatched and remembered against your account — pending_new counts the ones that were new to it — and GET /v1/me/watchlist/pending lists them back with resolved_entity_id: null. That list is the queue our universe seeder works from: it looks each name up on its registry, bounded and paced (at most 200 registry lookups per resolver run, one a second), and names past that bound wait for a later run rather than being discarded. A name the registry does not have is marked unresolvable and not retried for seven days. When an entity does appear, a daily sweep sets resolved_entity_id on your pending row, visible with ?include_resolved=true. Marking is not watching: the sweep does not add the entity to your watchlist — re-run the import, or watch it directly, to do that. We do not publish a turnaround time for this, and you should not assume one.

What comes back. A parseable document is always 200, even when nothing in it could be used and even when your plan's watchlist is already full — a partial import is the point, so the cap is reported, not raised as an error. components_total is what was in the document, supported how many distinct packages came out of it, matched how many of those are entities we track, and added / already_watched / over_cap split those between newly watched, already on the list, and the ones the plan's limit had no room for. not_covered is separate from unmatched: those are names our conflict policy excludes from coverage entirely, and they are neither matched nor remembered as demand.

POST /v1/me/watchlist/import
Authorization: Bearer <Clerk session token>

{"bomFormat": "CycloneDX", "specVersion": "1.5", "components": [ … ]}

{
  "format": "cyclonedx", "spec_version": "1.5", "dry_run": false,
  "components_total": 214, "supported": 198,
  "matched": 41, "added": 3, "already_watched": 0, "over_cap": 38,
  "unmatched": [{"ecosystem": "npm", "name": "left-pad"}, "…"],
  "not_covered": [{"ecosystem": "npm", "name": "webex"}],
  "pending_new": 157,
  "unsupported_ecosystems": {"maven": 12, "unknown": 4},
  "count": 3, "limit": 3, "plan": "free"
}

Limits. 5 MB per document, checked against the Content-Length header when there is one and against the body either way; over it is a 413 with {"code": "sbom_too_large", "max_bytes": 5242880}. There is no cap on how many components a document may hold under that size. The watchlist cap is the plan's — Free 3, Pro 100 (Plans) — and it is the only cap the import enforces; /account shows the plan you are on. ?dry_run=true reports the same counts and writes nothing at all: no watchlist rows and no pending rows, so pending_new is 0 in a dry run whatever the document holds. We do not keep the document — only the ecosystem-and-name pairs of the packages we could not match. Those rows have no expiry and there is no endpoint that removes one; they are held against the account and go when the account does.

Alerts (Pro)

Pro accounts get one email a day, on days something on their watchlist changed: a maintenance probability (the 90-day commit and release questions) at or below a line you set (default 35%), our two latest forecasts on any watched question differing by 15 points or more, or a watched question resolving. Each item says what we forecast and links to the question; nothing fires twice. GET/PUT /v1/me/alerts read and change the three rules and the email switch (402 on Free); every email carries a signed unsubscribe link. A probability is not advice.

MCP server

The same functions as tools, for agents. Transport is streamable HTTP (stateless, JSON responses) at

https://api.anteproof.com/mcp

Authentication is an API key sent as Authorization: Bearer ap_… or X-API-Key, or a sign-in through OAuth where the client supports it (the server publishes RFC 9728 metadata at /.well-known/oauth-protected-resource/mcp). initialize, ping and tools/list are open, so a client or registry can inspect the tools without a key; every tool call needs one and counts against the same monthly allowance as REST.

Claude Code

claude mcp add --transport http anteproof https://api.anteproof.com/mcp \
  --header "Authorization: Bearer ap_YOUR_KEY"

Claude.ai and the Claude desktop app

Settings → Connectors → Add custom connector. Name: Anteproof. URL: https://api.anteproof.com/mcp. Connect, then sign in when prompted (the same email or Google sign-in as the account page). Usage counts against the signed-in account's allowance and appears on /account under "Claude (OAuth)".

Cursor

Add to .cursor/mcp.json (project) or ~/.cursor/mcp.json (global):

{
  "mcpServers": {
    "anteproof": {
      "url": "https://api.anteproof.com/mcp",
      "headers": {"Authorization": "Bearer ap_YOUR_KEY"}
    }
  }
}

Other clients

Any client that speaks MCP over streamable HTTP works with the URL and header above. Clients that pass a per-user configuration object (Smithery-style, base64 JSON in the config query parameter) may put the key in its apiKey field instead of a header. The registry listing is com.anteproof/anteproof.

Tools

ToolArgumentsReturns
dependency_riskquery (string, required)The /v1/risk answer. Description as published to clients: "Calibrated, timestamped maintenance-risk forecast for an open-source package or GitHub repository, for choosing or auditing a dependency. Pass an npm, PyPI or crates.io package name, a GitHub owner/repo or URL, or a prefixed name (npm:express, pypi:requests, crate:serde, repo:owner/name). Returns the latest open forecasts that the repository keeps its commit pace (commits_90d) and ships a release (release_90d) in the next 90 days — each with its question id, probability, issue time and resolution date; 90 days of commit, release and star activity summarised (commits last 30 vs prior 30 days, days since last commit and last release, releases in the last year); a velocity block carrying only the day the entity was last scored, its composite and stage both withheld; plain-language signals derived from those numbers (e.g. 'no commits in 60 days', 'release cadence slowing'); and a calibration note with the family's resolved-forecast count and Brier score, or 'no track record yet'. A package is linked to its repository when the archive knows the link; repo_link says how (self, source_repo = the registry's recorded repository, external_ids, name_match = a labelled guess, unknown). Nothing is estimated on the fly: every number comes from the archive and the answer names what is missing. Not found = no tracked match; try the exact package name or owner/repo." A miss is returned as a tool error carrying the same text as the REST 404.
search_entitiesq (string), limit (1–50, default 20)The /v1/entities search.
get_entityentity_id (integer)One entity's record: identity, the day it was last velocity-scored (the figures withheld), 30-day metric history, every live question with its latest forecast.
get_questionquestion_id (integer)One question with every live forecast ever issued for it and its resolution if settled.
list_forecastslimit (1–100, default 25), offset (default 0)The forecast log, newest first, with titles.
get_calibrationhorizon (7, 30, 90, long, all, or omitted for the gate's horizon)The calibration record: gate verdict and Brier skill by family for both tracks.

All six tools are annotated read-only, non-destructive and idempotent. The server's instructions to the model end with: probabilities are estimates, not advice.

API keys and quotas

X-Api-Calls-Used: 412
X-Api-Calls-Limit: 20000
X-Api-Calls-Reset: 2026-10-01

Rate limits

The monthly allowance is the only limit the API itself enforces today; there is no separate per-second limit in the API. Traffic that looks abusive may be throttled or blocked at the network edge. Cache answers you re-use: forecasts change at most a few times a day, and a question's record is immutable once issued.

Plans

Plans and billing are handled on /account, which shows what is available to you; see also the terms and the refund policy.

Errors

Errors are JSON with a detail field: a string, or an object with a stable code.

StatusWhenBody
401The key is unknown or revoked.{"detail": "invalid or revoked API key"}
404No tracked entity or question matches.{"detail": "no tracked package or repository matches 'nextjs'. Near misses: vercel/next.js (repo), vercel/next-auth (repo). Try the exact npm/PyPI/crates.io name, a GitHub owner/repo, or a prefix like npm:name / pypi:name."}
409Creating a sixth key.{"detail": {"code": "too_many_keys", "message": "an account may hold 5 active keys; revoke one first", "limit": 5}}
422A parameter is missing or out of range (for example q longer than 200 characters, or an unknown horizon).FastAPI's validation shape: {"detail": [{"loc": ["query", "q"], "msg": "…", "type": "…"}]}
429The monthly allowance is spent.{"detail": {"code": "quota_exceeded", "message": "20000 of 20000 calls used this month on the pro plan; resets 2026-10-01", "limit": 20000, "used": 20000, "plan": "pro", "resets_on": "2026-10-01"}}
503Keys or accounts are not enabled on the deployment you are talking to.{"detail": "API keys are not enabled on this deployment"}

The MCP endpoint answers in JSON-RPC: HTTP 401 with error code -32001 when no valid credential is presented (the WWW-Authenticate header points at the OAuth metadata), HTTP 429 with -32000 and the same data as the REST quota error when the allowance is spent, and HTTP 503 with -32002 while the transport is starting. A tool that finds nothing returns a tool error with the REST 404 text.

Resolution rules

Every question is generated by a fixed rule, timestamped with its exact wording and resolution specification, forecast, and later resolved by the same rule from the archived daily rollups. Forecasts are append-only and hash-chained (/v1/chain), and the chain head is anchored daily.

commits_90d — "at least K commits to the default branch in 90 days"

The unit changed on 9 September 2026. This family asked about pushes — GH Archive PushEvents — until then, and asks about commits from the GitHub REST API now. A push can carry any number of commits, so the two counts are not comparable and neither are their thresholds. Questions issued before the switch keep resolving on the series they were asked about: each question records its own metric, and its text names the unit it counts. Read the question, not this heading, when interpreting a specific forecast.

release_90d — "at least one release in 90 days"

General

Terms of use, in short

Anteproof is a publisher of scheduled, impersonal, general-audience forecasts. A probability is a statement of what we expect, scored later in public; it is not advice, a rating, a recommendation or a guarantee, and we never describe a package as safe. Use of the API is governed by the terms of service; how account and request data is handled is in the privacy policy. Only projects and packages whose maintaining organisations are in the United States, the European Union or allied jurisdictions are added on request; see the terms for the provenance policy. Questions? /support (or [email protected]).