Skip to main content
Glama
README.md
# pkg-oracle — Dependency Trust Oracle

A pay-per-call MCP (Model Context Protocol) server that verifies an npm or
PyPI package **before** an AI coding agent writes it into a manifest.
Monetized per call with the real [x402](https://x402.gitbook.io/x402/)
protocol (signed EIP-3009 authorizations, settled through Coinbase's
hosted facilitator) in USDC on Base — no API keys, no signup, no dashboard.
An agent calls the tool, pays a fraction of a cent, gets a verdict.

## Why this exists

AI coding agents hallucinate package names and skip the verification a
human developer would normally do — they check one thing ("does this name
resolve?") and if it resolves, they install it. That gap is now an active
attack surface known as **slopsquatting**: attackers register the package
names LLMs are statistically likely to hallucinate, then wait.

`verify_package` closes that gap with a single tool call that:

1. Confirms the package **actually exists** on its registry (npm or PyPI).
2. Cross-references [OSV.dev](https://osv.dev) for known CVEs affecting the
   resolved version.
3. Pulls the package's [OpenSSF Scorecard](https://deps.dev) via deps.dev
   (branch protection, code review practices, maintenance signal, ...).
4. Runs a Levenshtein-distance **typosquat check** against a curated list
   of high-value popular package names, combined with the package's
   publish age — a near-miss spelling that's also brand new is the exact
   shape of a typosquat/slopsquat attack.

It returns one synthetic verdict an agent can act on without reasoning
about four different data sources itself: **ALLOW**, **WARN**, or
**BLOCK**.

## Frequently asked questions

### How do I check if an npm package is safe before installing it?

Call the `verify_package` MCP tool with the package name and ecosystem
(`npm` or `pypi`). One call confirms the package actually exists, checks
[OSV.dev](https://osv.dev) for known CVEs, pulls its OpenSSF Scorecard,
and flags typosquat-style name similarity to popular packages. It
returns a single verdict — `ALLOW`, `WARN`, or `BLOCK` — instead of four
separate reports to reason about.

### What is slopsquatting?

Slopsquatting is a supply-chain attack that exploits LLMs hallucinating
plausible-but-nonexistent package names. Attackers register those exact
names in advance; when a coding agent installs the "hallucinated"
package without checking, it actually installs the attacker's package.
pkg-oracle catches this by confirming the name is real, on the actual
registry, before anything gets installed.

### How do coding agents avoid installing a malicious or hallucinated package?

By calling `verify_package` before writing the dependency into a
manifest (`package.json`, `requirements.txt`, `pyproject.toml`). If the
name doesn't exist on the registry at all, or is a recent near-miss of a
popular package, the verdict is `BLOCK` — the strongest available signal
of hallucination or typosquatting.

### Is there an MCP server for verifying npm or PyPI packages?

Yes — pkg-oracle. It's a remote MCP server over Streamable HTTP: no
local install, no API key, no signup. Point any MCP client at
`https://mcp-snowy-dew-9447.fly.dev/mcp`.

### How much does it cost to verify a package?

The first 5 calls per wallet/IP are free. After that, $0.003 USDC per
call, paid via the [x402](https://x402.gitbook.io/x402/) protocol on
Base — no subscription, no API key, no dashboard.

### Does it work with Claude Desktop, Cursor, or Claude Code?

Yes, for the free tier — any Streamable-HTTP-compatible MCP client can
connect (see [Client configuration](#client-configuration) below).
Paying past the free tier needs an x402-aware client, since neither
Claude Desktop nor Cursor ships a built-in x402 signer yet.

## Architecture

```
Agent (Claude, Cursor, custom SDK / x402-aware MCP client)
        │  MCP tools/call verify_package  (_meta["x402/payment"] once paid)
        ▼
StreamableHTTPServerTransport (fresh per request, stateless mode)
        │
        ▼
McpServer → verify_package
        │
        ├─ under free tier? ──▶ raw handler (no charge)
        │
        └─ else ──▶ x402 payment wrapper (@x402/mcp)
                       │
                       ├──▶ CDP facilitator: verify signed payment, settle on-chain
                       └──▶ raw handler
                              │
                              ├──▶ registry.ts   (registry.npmjs.org | pypi.org)
                              ├──▶ osv.ts        (api.osv.dev)
                              ├──▶ depsdev.ts    (api.deps.dev — OpenSSF Scorecard)
                              └──▶ typosquat.ts  (fast-levenshtein vs. curated popular list)
                                     │
                                     ▼
                              oracle.ts aggregates → { verdict, findings[] }
```

Payment gating happens **per MCP tool**, not per HTTP route — `initialize`
and `tools/list` are never charged, only an actual `verify_package` call
past the free tier is.

## Quickstart

```bash
npm install
cp .env.example .env   # RECIPIENT_WALLET + CDP_API_KEY_ID/SECRET are required, even in dev
npm run build
npm start
```

Or for local iteration with hot reload:

```bash
npm run dev
```

Unlike most values in `.env.example`, `RECIPIENT_WALLET` and the two CDP
credentials have no safe placeholder — the resource server authenticates
against CDP's real hosted facilitator at boot, regardless of environment.
There's no way to run this server against fake/mock payment infrastructure.

The server listens on `PORT` (default `3000`) and exposes:

| Endpoint      | Method            | Purpose                                    |
| ------------- | ----------------- | ------------------------------------------- |
| `/mcp`        | `POST`            | MCP tool calls — `verify_package` is payment-gated past the free tier |
| `/mcp`        | `GET`, `DELETE`   | Streamable HTTP protocol completeness (no-ops) |
| `/health`     | `GET`             | Liveness check, always free                 |

## Environment variables

See [`.env.example`](.env.example) for the full list with defaults.

| Variable                     | Default                                     | Meaning |
| ----------------------------- | -------------------------------------------- | ------- |
| `RECIPIENT_WALLET`            | *(dev-only burn address)*                    | Base L2 wallet that receives USDC payments — required |
| `CDP_API_KEY_ID`              | *(none — required)*                          | Coinbase Developer Platform API key ID |
| `CDP_API_KEY_SECRET`          | *(none — required)*                          | Coinbase Developer Platform API key secret |
| `PRICE_ATOMIC_USDC`           | `3000` (= $0.003)                            | Price per call once the free tier is spent |
| `FREE_TIER_LIMIT`             | `5`                                          | Free calls per wallet/IP before payment is required |
| `UPSTREAM_TIMEOUT_MS`         | `8000`                                       | Timeout for registry/OSV/deps.dev calls |
| `NEW_PACKAGE_THRESHOLD_DAYS`  | `30`                                         | Age under which a package is "new" |
| `TYPOSQUAT_MAX_DISTANCE`      | `2`                                          | Max Levenshtein distance still flagged |

## The `verify_package` tool

**Input schema:**

```json
{
  "ecosystem": "npm | pypi",
  "name": "string (required)",
  "version": "string (optional — exact version to check)"
}
```

**Example call and response:**

```jsonc
// request
{ "ecosystem": "npm", "name": "expres" }

// response (verdict text + machine-readable JSON in content[])
{
  "verdict": "WARN",
  "findings": [
    {
      "code": "TYPOSQUAT_NAME_SIMILARITY",
      "message": "\"expres\" is close (distance 1) to the popular package \"express\". ..."
    }
  ],
  "registry": { "exists": true, "ageDays": 4800, "...": "..." },
  "osv": { "vulnerabilities": [], "highestSeverity": "UNKNOWN" },
  "scorecard": { "overallScore": 1.5, "sourceRepo": "github.com/..." },
  "typosquat": { "suspected": true, "distance": 1, "redFlag": false }
}
```

When no `version` is given, the oracle resolves the registry's **latest**
published version internally before querying OSV/deps.dev — querying OSV
with no version at all returns every vulnerability ever disclosed for the
package, patched or not, which would make a long-lived, well-maintained
package like `lodash` permanently read as `BLOCK`. A `CHECKED_LATEST_VERSION`
finding tells you which version was actually evaluated.

### Verdict logic (first match wins)

1. **BLOCK** — the name doesn't exist on the registry at all (the
   strongest possible hallucination/slopsquat signal).
2. **BLOCK** — a pinned version was requested but doesn't exist.
3. **BLOCK** — near-miss of a popular package name **and** published
   within `NEW_PACKAGE_THRESHOLD_DAYS`.
4. **BLOCK** — a known CRITICAL or HIGH severity vulnerability applies.
5. **WARN** — a MODERATE/LOW/UNKNOWN vulnerability, a new package without
   a typosquat match, an older near-miss name, or a low OpenSSF Scorecard.
6. **ALLOW** — nothing above fired.

## Monetization: x402 on Base, via the CDP facilitator

`verify_package` is wrapped with `@x402/mcp`'s `createPaymentWrapper` in
[`src/mcpServer.ts`](src/mcpServer.ts) — the real x402 "exact" scheme, not
a homegrown variant:

1. The caller is identified by `Authorization: Bearer <wallet>` (or an
   `X-Wallet-Address` header), falling back to the client IP Fly's edge
   proxy reports if neither is present. **This identity is self-declared,
   not verified** — nothing stops a client from rotating the header to
   claim a fresh free allowance. `FREE_TIER_LIMIT` defaults to `5`
   specifically because of this: the free tier is a landing ramp, not an
   authenticated quota, sized to keep casual abuse cheap to give away
   rather than to be unbeatable.
2. The first `FREE_TIER_LIMIT` calls per identity run the raw handler
   directly, no payment involved — a tool that requires payment on the
   very first call never gets tried by an agent, and never gets adopted.
3. Past the free tier, a call without a payment payload gets a payment
   error carrying a standard x402 `PaymentRequired` descriptor (`scheme:
   "exact"`, `network: "eip155:8453"`, `payTo: RECIPIENT_WALLET`,
   `amount: "3000"`). Any x402-aware MCP client — `x402MCPClient` from
   `@x402/mcp`, or an agent SDK with built-in x402 support — signs an
   EIP-3009 `transferWithAuthorization` (no gas, no waiting for
   confirmation) and retries with the payload in
   `_meta["x402/payment"]`. The server verifies the signature and settles
   the on-chain transfer through CDP's facilitator, then runs the tool.

**Funds are never custodied by Coinbase.** The facilitator only verifies
the client's signature and submits the resulting transfer on-chain
(`payTo` is `RECIPIENT_WALLET`, resolved at server startup from your own
env var) — CDP never holds the money mid-flight.

Settling through CDP's *hosted* facilitator (as opposed to a generic one)
is also what makes `verify_package` auto-discoverable in the
[x402 Bazaar](https://docs.cdp.coinbase.com/x402/bazaar): the
`bazaarResourceServerExtension` registered on the resource server plus the
`declareDiscoveryExtension({ toolName: "verify_package", ... })` call in
`mcpServer.ts` get indexed automatically the first time a real payment
settles — there's no separate registration step.

## Client configuration

`pkg-oracle` speaks MCP over Streamable HTTP, so any MCP-compatible client
pointed at `http://<host>:<port>/mcp` works for the free tier. Paying past
the free tier requires an x402-aware MCP client. Below are the common
integrations.

### Claude Desktop / Cursor (free tier only)

```json
{
  "mcpServers": {
    "pkg-oracle": {
      "type": "http",
      "url": "http://localhost:3000/mcp",
      "headers": {
        "Authorization": "Bearer 0xYourAgentWalletAddress"
      }
    }
  }
}
```

Neither ships a built-in x402 signer today, so once the free tier is spent
these clients will surface the payment-required error as a tool failure
rather than paying automatically.

### Custom agent with x402 support

```ts
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { wrapMCPClientWithPaymentFromConfig } from "@x402/mcp";
import { ExactEvmScheme } from "@x402/evm/exact/client";

const mcpClient = new Client({ name: "my-agent", version: "1.0.0" });
await mcpClient.connect(
  new StreamableHTTPClientTransport(new URL("http://localhost:3000/mcp")),
);

// account: any viem-compatible signer holding USDC on Base
const x402Client = wrapMCPClientWithPaymentFromConfig(mcpClient, {
  schemes: [{ network: "eip155:8453", client: new ExactEvmScheme(account) }],
});

const result = await x402Client.callTool("verify_package", {
  ecosystem: "npm",
  name: "expres",
});

if (result.paymentMade) {
  console.log("Paid:", result.paymentResponse?.transaction);
}
```

`autoPayment: true` (the default) signs and retries automatically once a
payment-required error is received — no manual 402 handling needed.

## Docker

```bash
docker build -t pkg-oracle .
docker run -p 3000:3000 \
  -e RECIPIENT_WALLET=0xYourWalletAddress \
  -e CDP_API_KEY_ID=your-cdp-key-id \
  -e CDP_API_KEY_SECRET=your-cdp-key-secret \
  -e FREE_TIER_LIMIT=5 \
  pkg-oracle
```

The image is a multistage, non-root, production-only build (dev
dependencies and TypeScript source are stripped from the final layer).

## Despliegue en producción (Fly.io)

`fly.toml` ya está configurado para desplegar directamente desde el
`Dockerfile` — HTTPS automático, health check contra `/health`. Pasos:

```bash
# 1. Instala flyctl y autentícate (una sola vez)
curl -L https://fly.io/install.sh | sh
fly auth login

# 2. Desde la raíz del proyecto: crea la app (usa el fly.toml existente)
fly launch --no-deploy   # detecta fly.toml, no lo sobreescribas si pregunta

# 3. Configura los secretos (nunca los pongas en fly.toml ni los commitees)
fly secrets set RECIPIENT_WALLET=0xTuWalletReal
fly secrets set CDP_API_KEY_ID=tu-cdp-key-id
fly secrets set CDP_API_KEY_SECRET=tu-cdp-key-secret

# 4. Despliega
fly deploy

# 5. Verifica
curl https://tu-app.fly.dev/health
```

Tu servidor queda con TLS ya resuelto en el dominio `*.fly.dev` que Fly
asigne (o el dominio custom que configures con `fly certs add`) — sin eso,
x402 no tiene sentido: el `resource` del reto de pago necesita ser una URL
real y segura para que un cliente x402 confíe en ella.

**Antes de escalar a más de una máquina:** el contador freemium vive en
memoria (LRU) por proceso. Con `min_machines_running = 1` (el valor por
defecto en `fly.toml`) esto no es un problema. Si escalas horizontalmente,
un mismo wallet obtendría `FREE_TIER_LIMIT` llamadas gratis *por
instancia* en vez de en total — en ese punto, mueve el contador a Redis
(Fly ofrece Upstash Redis como addon) antes de subir `min_machines_running`.

## Known limitations / honest scope notes

- **Popular-package list is curated, not a live top-1000 feed.** It's a
  hand-picked shortlist of the highest-value typosquat targets in each
  ecosystem (`src/services/popularPackages.ts`), not a synced download-rank
  API. Good enough to catch `expres` → `express`; won't catch a typo of a
  mid-tier package outside the list. Syncing against npm's/PyPI's real
  download-rank data on a cron is the natural next step.
- **The free-tier counter is in-memory (LRU)**, not persisted — it resets
  on restart and doesn't share state across multiple server instances.
  Fine at `min_machines_running: 1` (the default); back it with Redis
  before scaling horizontally (see Despliegue en producción above).
- **Payment replay protection is delegated to the protocol itself** — the
  EIP-3009 `nonce` in each signed authorization is enforced on-chain by
  USDC's contract, and CDP's facilitator rejects already-settled or
  expired authorizations. Nothing custom to maintain here.
- **Claude Desktop and Cursor have no built-in x402 signer** — they work
  fine for the free tier, but a payment-required response surfaces as a
  tool failure rather than being paid automatically. Use an x402-aware
  MCP client (see Client configuration) to actually pay past the free tier.
- **CVSS-vector severity estimation is a conservative heuristic**, used
  only when OSV doesn't supply an explicit `database_specific.severity`
  string (see the doc comment in `src/services/osv.ts`).