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
ap_ and are shown once.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.
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.
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.
| Parameter | Meaning |
|---|---|
| q | Required, 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.
The same answer for an entity by id.
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.
{
"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 …"
}
}
| Field | Meaning |
|---|---|
entity, repo, repo_link | What 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). |
maintenance | The 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_unavailable | Why 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. |
activity | Sums 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.withheld | The 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_health | Whether 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. |
velocity | The 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. |
signals | Plain-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_note | The 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_head | The 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.
Case-insensitive substring search on the canonical name; exact match first, then shortest name. Current entities only (merged duplicates are excluded).
| Parameter | Meaning |
|---|---|
| q | Required, 1–80 characters. |
| limit | 1–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.
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"
}
]
}
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.
| Parameter | Meaning |
|---|---|
| limit | 1–100, default 25. |
| offset | Default 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.
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}
]
}
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.
| Parameter | Meaning |
|---|---|
| horizon | 7, 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.
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"
}
| Field | Meaning |
|---|---|
gate | The 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 n — n 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. |
maintenance | The 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.state | none: 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. |
note | The 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. |
| Endpoint | Returns |
|---|---|
GET /v1/calibration | snapshots: 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/head | The 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/latest | The 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/proof | The record in one JSON: gate verdict, the maintenance families' resolved record and its state, and the note that state earns (details). |
GET /v1/stats | Headline 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.
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.
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.
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 mcp add --transport http anteproof https://api.anteproof.com/mcp \
--header "Authorization: Bearer ap_YOUR_KEY"
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)".
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"}
}
}
}
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.
| Tool | Arguments | Returns |
|---|---|---|
| dependency_risk | query (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_entities | q (string), limit (1–50, default 20) | The /v1/entities search. |
| get_entity | entity_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_question | question_id (integer) | One question with every live forecast ever issued for it and its resolution if settled. |
| list_forecasts | limit (1–100, default 25), offset (default 0) | The forecast log, newest first, with titles. |
| get_calibration | horizon (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.
initialize, ping, tools/list) are not counted.X-Api-Calls-Used: 412
X-Api-Calls-Limit: 20000
X-Api-Calls-Reset: 2026-10-01
429 with code quota_exceeded and the reset date;
nothing is billed silently. Current usage per key and per day is on the account page and at
GET /v1/me/api-keys.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 and billing are handled on /account, which shows what is available to you; see also the terms and the refund policy.
Errors are JSON with a detail field: a string, or an object with a stable code.
| Status | When | Body |
|---|---|---|
| 401 | The key is unknown or revoked. | {"detail": "invalid or revoked API key"} |
| 404 | No 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."} |
| 409 | Creating a sixth key. | {"detail": {"code": "too_many_keys", "message": "an account may hold 5 active keys; revoke one first", "limit": 5}} |
| 422 | A 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": "…"}]} |
| 429 | The 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"}} |
| 503 | Keys 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.
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.
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.
rollup_window: the sum of the repository's daily commit counts (GitHub REST API commits
on the default branch) over the inclusive window. Resolves YES when the sum is at least K, NO otherwise.trailing_window_sum), so the question is "at least as many commits as last quarter". Ties resolve
YES.rollup_event_window: resolves YES if at least one release is listed for the repository
by the GitHub REST API in the inclusive window, NO otherwise. Threshold is always 1. The source moved from GH
Archive ReleaseEvents on 9 September 2026; a release is a release either way, so unlike commits_90d
the UNIT is unchanged — only which feed is believed about it. Questions issued before the switch keep resolving
on the series they were asked about.resolves_at is the first instant after the window closes; resolution
is recorded when the rollups for the last day of the window are complete.voided_reason and any forecast already issued stays in the log.calibration_note on every risk
answer.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]).