# npm Package Health Score API — Dependency Risk Scorer (`malonestar/npm-package-health-scorer`) Actor

npm package health score API. Rate any npm dependency 0-100 with a letter grade from keyless public signals: downloads, release recency & cadence, maintainers, license, deprecation, deps. One row per package with top risks. Keyless. A clean MCP tool for AI coding agents.

- **URL**: https://apify.com/malonestar/npm-package-health-scorer.md
- **Developed by:** [Kyle Maloney](https://apify.com/malonestar) (community)
- **Categories:** Developer tools, MCP servers, Agents
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.30 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## npm Package Health Score API — Dependency Risk Scorer

Score the health of any **npm package from 0 to 100** with a letter grade, using
only **keyless, public** signals — **download volume and momentum**, **release
recency and cadence**, **maintainer bus factor**, **license**, **version
maturity**, **dependency surface**, plus **SLSA build provenance**, **registry
signatures** and **install-script** detection. Give it a list of package names
and get back one clean, structured row per package so engineers and AI coding
agents can decide **whether a dependency is safe to adopt**.

No API key. No login. No GitHub token required.

### Who it's for

- **Engineering leads** vetting a dependency before it enters the tree.
- **DevSecOps / AppSec** teams triaging supply-chain risk across a lockfile.
- **OSPO and procurement** teams assessing license and maintenance posture.
- **AI coding agents** that need an "is this package safe to add?" tool.

### What it does

1. **Runs a live drift check before anything is billed** — a popularity canary,
   a document-shape check, and a negative control (a name that cannot exist must
   return 404). If npm's downloads API starts answering zero, the run fails
   rather than silently re-grading every package.
2. **Fetches the registry document and the 30-day download series**, each with
   three attempts and backoff.
3. **Scores eight weighted inputs** into a 0–100 composite and an A–F grade.
4. **Surfaces the supply-chain signals** already inside the registry document:
   SLSA provenance, registry signature, install scripts, bundled types, engine
   range, repository, package age, total versions, and the deprecation reason.

### How a failure is reported — read this before you trust a score

A score is only as good as its inputs, and a missing input changes the answer.
Measured live on 2026-08-01: **`express` scores 87 (grade A) with real download
data and 59 (grade C) when `api.npmjs.org` does not answer** — a two-grade swing,
with the row publishing `downloads_last_month: 0` and the rationale
"Low adoption: 0 downloads/month" about a package doing **505,265,187**
downloads a month.

So this actor distinguishes three states on every numeric input:

| Value | Meaning |
|---|---|
| A number | Fetched and real. |
| `0` | **The source answered, and the answer is zero.** |
| `null` | **Not fetched.** Never read this as zero. |

- `downloads_status` is `ok`, `no_data` (HTTP 404 — npm holds no download record,
  so `0` is a genuine answer), or `unavailable` (the API did not answer after
  three attempts, so `downloads_last_month` is `null`).
- `registry_status` is `ok`, `not_found` (HTTP 404 — a **verified** negative:
  the package does not exist), or `unavailable`.
- **`health_score` is published only when every weighted input was obtained.**
  Otherwise it is `null`, `score_status` is `"partial"`, `score_inputs_missing`
  names exactly what is missing, and `health_score_partial` carries the score
  renormalized over the weight that *was* evaluable.
- `maintainers`, `dependencies_count` and `deprecated` are `null` when the
  registry document could not be read — not `0`, `0` and `false`. (Previously an
  unread version object scored **full marks** for "Zero runtime dependencies".)
- If **every** package lookup fails, the run fails and emits nothing.

### Example input

```json
{
  "packages": ["express", "left-pad", "@babel/core"],
  "includeTrend": true
}
```

Scoped packages are supported. You can also pass a single `package` string, or a
comma/space-separated list.

### Output fields

| Field | Description |
|---|---|
| `package` | The package name as requested. |
| `latest_version` | Version on the `latest` dist-tag. |
| `health_score` | 0–100 composite. **`null` when any weighted input was unavailable.** |
| `grade` | `A` (≥85), `B` (≥70), `C` (≥55), `D` (≥40), `F`, or `N/A`. |
| `health_score_partial` | The score renormalized over the inputs that were obtained. Not comparable with a complete `health_score`. |
| `score_status` | `complete`, `partial`, or `unavailable`. |
| `score_inputs_missing` | Which weighted inputs could not be obtained. |
| `score_weight_evaluated_pct` | Share of the 100-point weight model actually evaluated. |
| `sub_scores` | The eight 0–100 sub-scores. A sub-score is `null` when its input was missing. |
| `last_publish_date` | Publish date of the latest version. |
| `days_since_publish` | Days since that publish. |
| `publishes_last_2y` | Releases in the last 730 days (the cadence input). |
| `publishes_last_1y` | Releases in the last 365 days. |
| `downloads_last_month` | 30-day download total. **`null` when the API did not answer.** |
| `download_trend_pct` | Momentum: second half of the window vs the first. |
| `maintainers` | Maintainer count (bus factor). `null` when the document was unreadable. |
| `license` | Declared license string. |
| `deprecated` | `true` / `false` / `null`. |
| `deprecated_message` | The reason npm publishes, e.g. `"use String.prototype.padStart()"`. |
| `dependencies_count` | Runtime dependencies on the latest version. |
| `dev_dependencies_count` | Declared devDependencies. |
| `top_risks` | Ranked plain-language risks. Never asserts anything that was not measured. |
| `supply_chain_risks` | Install scripts, missing provenance, missing signature, no repository, deprecation. Kept separate from `top_risks`. |
| `rationale` | One-paragraph summary of positives and risks. |
| `package_age_days` | Days since the first ever publish. |
| `first_publish_date` | Date of the first ever publish. |
| `total_versions` | Versions the registry holds. |
| `dist_tags` | All dist-tags, sorted. |
| `has_prerelease_tag` | True when tags other than `latest` exist (`next`, `beta`, `latest-4`, …). |
| `repository_url` | Source repository, normalized to a browsable https URL. |
| `homepage` | Declared package homepage. |
| `bugs_url` | Declared issue tracker. |
| `description` | Package description. |
| `keywords` | Package keywords. |
| `unpacked_size_bytes` | Installed size of the latest tarball. |
| `file_count` | Files in the published tarball. |
| `tarball_url` | Registry tarball URL. |
| `has_provenance` | **SLSA build-provenance attestation** — the strongest published supply-chain trust signal. |
| `has_registry_signature` | npm registry signature on the tarball. |
| `has_install_script` | **Arbitrary code runs on `npm install`.** |
| `has_types` | Ships bundled TypeScript definitions. |
| `engines_node` | Declared supported Node range. |
| `has_funding` | A funding target is declared. |
| `last_publisher` | npm account that published the latest version. |
| `npm_stars` | Users who have starred the package. |
| `registry_status` | `ok`, `not_found`, or `unavailable`. |
| `registry_error` | The transport error behind `unavailable`. |
| `downloads_status` | `ok`, `no_data`, or `unavailable`. |
| `downloads_error` | The transport error behind `unavailable`. |
| `row_partial` | True when any source did not answer for this package. |
| `error` | Present on a not-found or failed row. |
| `source_url` | npmjs.com page for the package. |
| `retrieved_at` | ISO timestamp of the lookup. |

**Populating the optional columns.** `deprecated_message` populates for a
deprecated package — the prefill (`express`, `left-pad`) exercises it, since
`left-pad` is deprecated with `"use String.prototype.padStart()"`.
`has_provenance` is `true` for packages published with SLSA attestations
(`@babel/core` as of 2026-08-01) and `false` for `express`. `engines_node`,
`has_funding` and `has_types` vary by package; the three-package example above
covers all of them. `registry_error`, `downloads_error` and `error` are `null`
on a healthy run — that is good news, not a dead column.

### Scoring model (weights sum to 100)

| Input | Weight | Notes |
|---|---|---|
| Download volume | 25 | log-scaled; 10M/month saturates. |
| Release recency | 20 | ≤30 days = 100, ≥730 days = 0. |
| Release cadence | 15 | ~8 releases in 2 years saturates. |
| Download trend | 10 | Second half of the 30-day window vs the first. |
| Maintainer count | 10 | Bus factor: 1 → 50, 2 → 75, 3+ → 100. |
| License | 10 | Permissive 100, copyleft 60, other 50, none 0. |
| Version maturity | 5 | ≥1.0.0 → 100. |
| Dependency count | 5 | 0 deps → 100, −5 per dep. |

A deprecated package is capped at 20 regardless of the other inputs. **The
weights are unchanged**, so scores remain comparable with earlier runs.

### Use as an MCP tool

Exposed to AI coding agents via `mcp.apify.com` as a "score this npm package"
tool. The three-state contract matters here: an agent must not read
`downloads_last_month: null` as zero adoption, or `health_score: null` as a bad
score. Check `score_status` first.

### FAQ

**Are scores comparable between runs?** Yes — the weights have not changed. Only
compare `health_score` values carrying `score_status: "complete"`.

**Why is `health_score` sometimes null?** Because a weighted input could not be
fetched. Use `health_score_partial` together with `score_weight_evaluated_pct`
for a labelled approximation.

**Does it detect a typo'd package name?** Yes — `registry_status: "not_found"`
with `health_score: null`, which is a verified negative rather than a low score.
A live negative control also fails the run if the registry ever starts returning
200 for names that do not exist.

**Is this a determination?** No. It is a screening tool over public registry
signals. It does not audit code, run tests, or evaluate transitive risk.

### Data sources (keyless, no anti-bot)

- `https://registry.npmjs.org/<package>` — the full packument.
- `https://api.npmjs.org/downloads/range/last-month/<package>` — daily downloads.

### Pricing (Pay Per Result)

Billed per dataset record returned: one row per package, including rows for
packages that do not exist (a verified negative is an answer). Runs that fail a
drift assertion, or lose every lookup, emit nothing and bill nothing.

### Related actors

- **SBOM Vulnerability Scanner** — scan a whole lockfile against OSV.dev and the
  CISA KEV catalog, then score the flagged packages here.

# Actor input Schema

## `packages` (type: `array`):

List of npm package names to score (e.g. "express", "lodash", "@babel/core"). Scoped packages are supported. You can also paste a comma- or space-separated string.

## `package` (type: `string`):

Convenience field to score just one package. Merged with the `packages` list if both are provided.

## `includeTrend` (type: `boolean`):

Compute the month-over-month download momentum (second half vs first half of the last 30 days) and include it in the score. Turn off to omit the trend field.

## Actor input object example

```json
{
  "packages": [
    "express",
    "left-pad"
  ],
  "includeTrend": true
}
```

# Actor output Schema

## `results` (type: `string`):

One scored record per npm package, in the default dataset.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "packages": [
        "express",
        "left-pad"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("malonestar/npm-package-health-scorer").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "packages": [
        "express",
        "left-pad",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("malonestar/npm-package-health-scorer").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "packages": [
    "express",
    "left-pad"
  ]
}' |
apify call malonestar/npm-package-health-scorer --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=malonestar/npm-package-health-scorer",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/TqSYDvW3JY2x2I0Ph/builds/iz2C5iE1bNWKZvYj4/openapi.json
