Skip to main content
Glama
README.md
# CF Package Check

**Is this WordPress plugin, npm package or PyPI project abandoned?**

A pay-per-call answer computed live from the public registries, on Cloudflare
Workers. Charged per request with [x402](https://x402.org), and exposed to
agents as an MCP tool. Every answer lists the signals behind its score.

> **Signals, not a verdict.** The score is computed from public registry
> metadata; it is not a security or code-quality review. Check the project
> before deciding.

**Live:** `https://cf-package-check.cf-exclusion-check.workers.dev`
**Network:** Base mainnet (`eip155:8453`), USDC, via the Coinbase CDP
facilitator. Payments are real. Settlement happens *after* the handler
succeeds, so a request that cannot be answered is never charged.

This is booth #2. Its payment, discovery, MCP, rate-limiting and logging code is
copied from booth #1 ([cf-exclusion-check](https://github.com/cfgenius145-sketch/cf-exclusion-check)).

---

## Endpoints

| endpoint | price | notes |
|---|---|---|
| `GET /v1/check?ecosystem=…&name=…` | **$0.02** | status, score, months since update, last release, signals |
| `GET /v1/report?ecosystem=…&name=…` | **$0.10** | everything above + breakdown, raw registry facts, GitHub facts, version history, support/issue stats, recommendation |
| `GET /v1/health` | free | registry reachability, cache and payment configuration |
| `GET /.well-known/x402` (also `…/x402.json`) | free | discovery document, `discoverable: true` |
| `POST /mcp` | $0.02 per `package_check` call | JSON-RPC; discovery and `package_sources` are free |

`ecosystem` is `wordpress`, `npm` or `pypi`. `name` is the npm package
(`express`, `@types/node`), the PyPI project (`requests`; normalised per PEP
503), or the WordPress.org plugin slug (`contact-form-7`, as in
`wordpress.org/plugins/<slug>/`).

Only `GET` is served on the paid paths. The query is validated **after** the
paywall: CDP's Bazaar probes the bare resource URL and requires a 402, so a bare
`GET /v1/check` answers with the payment challenge. A paid request with a
malformed query gets 400, and a response of 400 or above is never settled, so it
costs nothing.

### `/v1/check` response

```json
{
  "ecosystem": "npm",
  "name": "request",
  "exists": true,
  "status": "deprecated",
  "score": 100,
  "months_since_update": 78,
  "last_release": { "version": "2.88.2", "date": "2020-02-11T16:35:36.122Z" },
  "signals": [
    "deprecated on npm: \"request has been deprecated, see https://github.com/request/request/issues/3142\"",
    "last update 78 months ago (2020-02-11, 2.88.2)",
    "50,778,952 downloads last month"
  ],
  "as_of": "2026-09-11T07:36:46.761Z",
  "disclaimer": "Signals, not a verdict. …"
}
```

`status` is one of `active`, `stale`, `abandoned`, `deprecated`, `closed`.
`score` is abandonment risk, 0–100. `as_of` is when the registry facts were read
(up to 24 h old on a cache hit). A package that does not exist is an answer, not
an error: `exists: false`, with `status` and `score` null.

### `/v1/report` adds

`score_breakdown` (every rule that fired and its points), `recommendation`,
`registry` (the raw facts per ecosystem), `github` (archived, last push, open
issues, stars — or why it was not checked), `versions` (count, first release,
releases in the last 12 months, the ten newest), `support` (WordPress support
threads and resolution rate; GitHub open issues + PRs), `partial` (optional
enrichment that was skipped), `sources` (the upstream URLs used), `cached`,
`checked_at`.

---

## Scoring

Deterministic: a pure function of the registry facts and the current date
(`src/score.ts`, asserted rule by rule in `test/score.test.ts`).

| rule | points |
|---|---|
| months since last update: **0–5** | 0 |
| **6–11** | 25 |
| **12–23** | 50 |
| **24–35** | 70 |
| **36+** (or no dated release at all) | 90 |
| GitHub repository archived | +20 |
| WordPress "tested up to" ≥ 3 major releases behind current WordPress | +10 |
| npm: < 100 downloads last month **and** first published > 2 years ago | +10 |
| cap | 100 |

| registry flag | score | status |
|---|---|---|
| closed on WordPress.org, or unpublished from npm | 100 | `closed` |
| deprecated on npm, or every release yanked on PyPI | 100 | `deprecated` |

Without a flag: **score < 25 → `active`, < 50 → `stale`, otherwise
`abandoned`.** A score of 100 reached by stacking rules is still `abandoned`;
`deprecated` and `closed` are reserved for the registry itself saying so.

**Months** = `floor(days since last update / 30.4375)`, so band edges are exact
whole months.

**"Last update"** is chosen per ecosystem, and the report states which:

| ecosystem | last update means | why not the obvious field |
|---|---|---|
| npm | newest publish of **any** version | express's `latest` tag (5.2.1) dates from 2025-12-01, but 4.22.2 shipped 2026-05-11 on the maintained 4.x line. `time.modified` moves on metadata-only changes and is never scored. |
| PyPI | newest file upload to any release | adding wheels to an old release is maintenance |
| WordPress | `last_updated` | the only date the API has; it also moves on readme-only commits |

WordPress majors are `X.Y` (6.8, 6.9, 7.0 …), so "3 majors behind" is
`(10·X + Y)` of current minus that of tested-up-to.

### Worked examples (2026-09-11)

| package | result | why |
|---|---|---|
| wordpress `contact-form-7` | active, 0 | updated 2026-08-17, tested up to 7.1 = current |
| wordpress `wp-cycle` | abandoned, 100 | 168 months (90) + tested up to 3.4.2, 37 majors behind 7.1 (+10) |
| wordpress `exec-php` | closed, 100 | closed 2018-12-14, Security Issue |
| npm `express` | active, 0 | last publish 4 months ago (4.22.2) |
| npm `request` | deprecated, 100 | deprecated by its maintainers |
| npm `flux` | abandoned, 100 | 41 months (90) + repo archived as facebookarchive/flux (+20), capped |
| pypi `requests` | active, 0 | last upload 3 months ago |
| pypi `pycrypto` | abandoned, 90 | last upload 2014-06-20 |

---

## Paying

An unpaid call returns **402** with the machine-readable challenge in the
`payment-required` header (base64 JSON, x402 v2):

```json
{"x402Version":2,"accepts":[{"scheme":"exact","network":"eip155:8453",
  "amount":"20000","asset":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  "payTo":"0xCa28eb92657F8a81aFb5493b3a04A740B204d816","maxTimeoutSeconds":60,
  "extra":{"name":"USD Coin","version":"2","paymentFlow":"authorization"}}],
 "extensions":{"bazaar":{…}}}
```

`amount` is atomic USDC (6 decimals): `20000` = $0.02, `100000` = $0.10.

```bash
curl -i "https://cf-package-check.cf-exclusion-check.workers.dev/v1/check?ecosystem=npm&name=express"   # 402

node scripts/x402-client.mjs                          # pays $0.02 for /v1/check npm express
node scripts/x402-client.mjs /v1/report pypi requests # pays $0.10
```

The client refuses to pay unless `/v1/health` answers 200 and the payer holds at
least $0.05, then waits for the receipt and checks the USDC `Transfer` to
`PAY_TO` on chain. Gas is paid by the facilitator under EIP-3009, so a payer
needs USDC only.

## MCP

```json
{
  "mcpServers": {
    "cf-package-check": {
      "type": "http",
      "url": "https://cf-package-check.cf-exclusion-check.workers.dev/mcp"
    }
  }
}
```

```bash
curl -X POST https://cf-package-check.cf-exclusion-check.workers.dev/mcp \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

curl -X POST https://cf-package-check.cf-exclusion-check.workers.dev/mcp \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call",
       "params":{"name":"package_check","arguments":{"ecosystem":"npm","name":"express"}}}'
# -> 402 until paid; an x402-capable client pays and retries
```

| tool | price | returns |
|---|---|---|
| `package_check` | $0.02 per call | the `/v1/check` body as `structuredContent`; the text leads with the status |
| `package_sources` | free | upstream endpoints, the scoring table, the cache policy |

`initialize`, `ping` and `tools/list` are free, so an agent can learn the tool
and its price before paying. Bad arguments are answered as a free tool error
before the paywall. A registry outage is **HTTP 503**, not a 200 carrying an
error, so the payment is never settled for it.

---

## How it works

Stateless. Each answer is read live from:

| ecosystem | upstream |
|---|---|
| WordPress | `api.wordpress.org/plugins/info/1.2/` (heavy fields switched off: 42 KB → 17 KB) and `core/version-check/1.7/` |
| npm | `registry.npmjs.org/{name}` (streamed, see below), `/{name}/latest`, `api.npmjs.org/downloads/point/last-month/{name}` |
| PyPI | `pypi.org/pypi/{name}/json` |
| GitHub (optional) | `api.github.com/repos/{owner}/{repo}` when the registry links a repo; unauthenticated |

**npm packuments are never parsed whole.** typescript's is 15.6 MB and a full
`JSON.parse` costs ~31 ms of CPU against the free plan's 10 ms. The packument is
streamed, searched byte-wise for `"time":{`, and only that object is decoded; the
stream is cancelled once it closes. `docs/SOURCES.md` has the measurements.

**WordPress answers 404 for both "closed" and "never existed"**; only the body
tells them apart. Closed plugins are reported as `closed` with the date and
reason, not as a typo.

### Cache and free-tier discipline

- Registry facts are cached in KV for **24 h** — one combined record per
  package, because the free plan allows 1,000 KV writes a day. When optional
  enrichment was skipped (GitHub rate-limited, download count or current
  WordPress version unavailable) the TTL is **1 h** so it is retried soon.
- Registry **failures are never cached** and are answered **503**, which the
  `authorization` payment flow never settles.
- No request scans anything unbounded: npm reads are capped by
  `MAX_PACKUMENT_BYTES` (32 MiB), PyPI by 16 MB, WordPress by 2 MB.
- `/v1/health` probes are memoised for 60 s per isolate, because health is never
  rate limited.
- D1 holds only the request audit log.

## Limits

| | |
|---|---|
| rate limit, free | 10 / min per (salted-hash) IP — approximate: Cloudflare's rate-limit binding is eventually consistent per location, and a live burst passed 12 requests before the first 429 |
| rate limit, paid (request carries a payment header) | 60 / min, same caveat |
| never rate limited | `/v1/health`, `/.well-known/x402` |
| freshness | facts up to 24 h old; `as_of` says exactly |
| GitHub | unauthenticated, 60 requests/hour per egress IP shared with other Workers; when limited, the archived rule is skipped and the answer says so |
| ecosystems | WordPress.org plugins, npm, PyPI only |
| CPU | the largest packuments (typescript, aws-sdk, next) answered 200 on an edge preview; if production ever returns 1102 for them, lower `MAX_PACKUMENT_BYTES` and they get a clean, uncharged 503 |
| D1 | free-tier D1 quotas are **account-wide**, shared with booth #1. Audit-log writes fail soft (logged to the console) when the account's budget is spent — which happened on launch day: see below |

## Mainnet verification (2026-09-11)

| check | result |
|---|---|
| unpaid `/v1/check`, `/v1/report`, MCP `package_check` | `402`, `eip155:8453`, `20000` / `100000` atomic USDC to `0xCa28…d816`, `paymentFlow: authorization`, bazaar extension present (`scripts/live-test.mjs`, 26/26) |
| paid `GET /v1/check?ecosystem=npm&name=express` | `200` — active, score 0 |
| settlement | [`0x8b9bc81b018f1cb8bc546ef8143726a30d4d73a2aa8895259aafa8c3c91606cf`](https://basescan.org/tx/0x8b9bc81b018f1cb8bc546ef8143726a30d4d73a2aa8895259aafa8c3c91606cf), Base block 51162136, status `success` |
| on-chain transfer | USDC `Transfer` of **20000 atomic ($0.02)** from payer `0xE8eA…1117` to `PAY_TO`; balances payer 1.00 → 0.98, PAY_TO 0.00 → 0.02 |
| Bazaar, first attempt | **not listed**: bare `GET /v1/check` answered 400 because input was validated before the paywall, and CDP `/validate` rejected it ("Endpoint returned HTTP 400 instead of 402") |
| fix + indexing payment | input now validated after the paywall (version `80c7a879`); CDP `/validate` accepts bare `/v1/check` and `/v1/report`; paid `/v1/check` settled in [`0xe1874c320dbb74ad5cc1818cedc9b122deac9485e880a29f1a150e0c6e8c5e5b`](https://basescan.org/tx/0xe1874c320dbb74ad5cc1818cedc9b122deac9485e880a29f1a150e0c6e8c5e5b), block 51180211, 20000 atomic USDC to `PAY_TO` |
| Bazaar | **listed** about 10 minutes after that settlement: the merchant lookup shows `…/v1/check` (lastUpdated 2026-09-11T18:02:47Z). `/v1/report` is not listed because it has never been paid for. Keepalive due before 2026-10-11 (see below). |
| audit log | **the paid request's row was not written**: the account's D1 free-tier quota was already spent by booth #1 (13.4M rows read in 24h), so most inserts that day failed with `D1_ERROR: Your account has exceeded D1's free tier daily row read limit` — only 6 rows landed, intermittently, in the first 15 minutes of traffic. The request was served; the settlement is on chain; Workers Logs recorded the invocation. |

## Staying listed in the Bazaar (30-day keepalive)

CDP's Bazaar indexes a route after a payment for it **settles through the CDP
facilitator**, and removes it again:

- **after 30 days without a settlement.** Every listed route needs at least one
  settled paid call inside every 30-day window. Run a keepalive at least every
  three weeks: `node scripts/x402-client.mjs` ($0.02, `/v1/check`), and
  `node scripts/x402-client.mjs /v1/report npm express` ($0.10) if the report
  route should stay listed too. A route that has never been paid for is not
  listed at all.
- **when its bare URL stops answering `402 Payment Required`.** Keep bare
  `GET /v1/check` and `GET /v1/report` returning the challenge; that is why input
  is validated after the paywall.

Ranking is recomputed every six hours. To check, with no API key and no payment:

```bash
# would CDP accept the route for indexing?
curl -s -X POST https://api.cdp.coinbase.com/platform/v2/x402/validate \
  -H 'Content-Type: application/json' \
  -d '{"resource":"https://cf-package-check.cf-exclusion-check.workers.dev/v1/check","method":"GET"}'

# everything the Bazaar lists for this payout address (shared with booth #1)
curl -s "https://api.cdp.coinbase.com/platform/v2/x402/discovery/merchant?payTo=0xCa28eb92657F8a81aFb5493b3a04A740B204d816"
```

## Development

```bash
npm install
npm test                         # 129 tests, on recorded registry fixtures
npm run typecheck
npm run fixtures:record          # re-record test/fixtures from the live registries

npx wrangler dev                 # local; set PAY_TO="" in .dev.vars to disable payment
BASE_URL=http://127.0.0.1:8787 node scripts/live-test.mjs --open   # live registries
node scripts/live-test.mjs       # paywall checks against the deployed Worker (spends nothing)
```

Deploying:

```bash
npx wrangler d1 execute cf_package_check --remote --file migrations/0001_init.sql
npx wrangler deploy
openssl rand -hex 32 | npx wrangler secret put IP_HASH_SALT
npx wrangler secret put CDP_API_KEY_ID        # same CDP key as booth #1
npx wrangler secret put CDP_API_KEY_SECRET
```

Without the CDP secrets the paid paths answer 503 "payment facilitator
unavailable" (no payment taken) — the public facilitator does not support Base
mainnet.

## Privacy

- Raw client IPs are never stored: they are HMAC'd with `IP_HASH_SALT`, and with
  no salt no hash is written at all.
- The queried package is logged only as a salted hash, matching booth #1's
  schema.
- No analytics.

## Future work

- **Chrome Web Store** — it has no public API for extension metadata, so it is
  out of scope until one exists or a sanctioned data source appears.
- Other ecosystems (crates.io, RubyGems, Packagist, Maven Central) would slot in
  as further adapters behind the same scoring.

## Licence

MIT. Registry data belongs to its publishers (WordPress.org, npm, the Python
Package Index, GitHub). This project is not affiliated with or endorsed by the
WordPress Foundation, npm, Inc., GitHub, Inc. or the Python Software Foundation.

Maintenance

ActivityMaintained
ResponsivenessNo issues