# Lazaretto

> Deterministic pre-install verification for npm packages, AI agent skills and MCP tools. The free lockfile check matches every exactly pinned dependency against OSV and OpenSSF malicious-package advisories with no account. A paid scan adds behavioral analysis with file-and-line evidence. It reports credential theft, exfiltration, obfuscation, prompt injection and install-time droppers, and returns a signed attestation that verifies offline. No LLM runs in the scan path, so the same input yields the same verdict. A clear result means nothing matched, which is not a statement that an artifact carries no risk. Verdicts are bound to a SHA-256 of exactly what was analyzed.

Operated by Meridian Bridge Advisors LLC d/b/a Lazaretto. Contact: contact@lazaretto.dev.

## Free endpoints (no API key, no payment)

- POST https://lazaretto.dev/check : the same lockfile check rendered as plain text for a terminal, for humans you are helping. One line, no key: `curl -s https://lazaretto.dev/check --data-binary @package-lock.json`. GET it for usage.
- POST https://lazaretto.dev/v1/lockfile : check EVERY exactly-pinned dependency in a lockfile against malicious-package advisories in one call. Accepts package-lock.json, npm-shrinkwrap.json, yarn.lock, or pnpm-lock.yaml as the request body. An empty `malicious` list is an all-clear only when `unverified` is also empty AND `truncated` is false: packages past the per-request limit are in neither list and were never checked, so send them in a second call before calling the tree clean.
  curl -s -X POST https://lazaretto.dev/v1/lockfile -H 'content-type: application/json' --data @package-lock.json
- GET https://lazaretto.dev/v1/known-bad/{sha256} : exact known-bad hash lookup. A miss is not a verdict on the artifact.
- GET https://lazaretto.dev/v1/attestations/{subject} : ask whether anyone has attested a package identity, an MCP server endpoint, or a sha256, before you install or pay. When nobody has, an npm identity gets a free identity check against the OSV / OpenSSF advisory corpus instead, returned under `identity_check` with `found` still false: an identity check is unsigned, looks at the package identity rather than its code, and absence from the corpus is not a verdict.
- POST https://lazaretto.dev/v1/trial : mint a free developer key (10 full scans per day, refills daily, no card).
- GET https://lazaretto.dev/v1/health : liveness, latency, indicator counts and freshness.

## Paid endpoint

- POST https://lazaretto.dev/v1/scan : full deterministic behavioral scan of one artifact (npm package, GitHub repo, ClawHub skill, raw URL, or inline text) with evidence. $0.03 USDC per scan over x402 on Base (no account), or an X-API-Key with prepaid credits. An `error` verdict is never billed. Gate decisions on `risk` (critical/high/medium/low/none), not on `verdict` alone.
  x402 wire details: the 402 is DUAL-STACK. v2 clients (@x402/fetch and friends) read the PAYMENT-REQUIRED response header (x402Version 2, network eip155:8453, amount field); v1 clients (x402-fetch, x402-axios) read the JSON body (x402Version 1, network base, maxAmountRequired). Send the payment in PAYMENT-SIGNATURE (v2) or X-PAYMENT (v1); both are accepted.
  curl -s -X POST https://lazaretto.dev/v1/scan -H 'content-type: application/json' -d '{"target":{"type":"npm_package","ref":"chalk@5.6.1"},"depth":"full"}'
  (without payment this returns HTTP 402 with x402 payment requirements in `accepts`)
- POST https://lazaretto.dev/v1/scan/batch : behavioral scan of EVERY exactly-pinned dependency in a lockfile, not just their identities. One credit per package that returns a verdict, nothing for one that errors, capped at 25 packages per call. Send a package-lock.json, yarn.lock or pnpm-lock.yaml with an X-API-Key. Read `complete_coverage` before you trust the result: false means something was capped, errored, only partly readable, or skipped, so it is NOT a clean bill of health for the whole tree. When credits, time or the cap leave packages out, `not_scanned.by_reason` counts them by cause and `not_scanned.packages` names every one of them (in lockfile order), so the next call can send exactly those. Over MCP the same product is the `scan_lockfile_deep` tool.
  curl -s -X POST https://lazaretto.dev/v1/scan/batch -H "X-API-Key: KEY" -H 'content-type: application/json' --data @package-lock.json
- POST https://lazaretto.dev/v1/scan with target type `mcp_server` : check an MCP SERVER before connecting to it. `ref` is the server's https endpoint. Lazaretto calls `initialize` and `tools/list` (never the server's own tools) and analyzes what it advertises to an agent: tool names, descriptions, parameter schemas, and its instructions string. That text is documentation a model obeys, so it is an instruction channel the server operator controls. Detects tool poisoning (hidden directives pointing the agent at private keys or agent config), parameters whose purpose is to carry secrets or conversation history out, and standing orders about another server's tools (cross-server shadowing). Evidence names the exact tool; `target_hash` covers the advertised tool set, so a server that changes its tools later does not inherit the verdict. It reads what a server SAYS, not what its code does. Over MCP the same product is the `scan_mcp_server` tool.
  curl -s -X POST https://lazaretto.dev/v1/scan -H "X-API-Key: KEY" -H 'content-type: application/json' -d '{"target":{"type":"mcp_server","ref":"https://the-server.example/mcp"}}'
- POST https://lazaretto.dev/v1/scan with target type `mcp_tools` : check tool definitions you ALREADY HOLD, with no network call to anyone. Most MCP servers run over stdio and have no endpoint that can be reached, so this is the only way to check them, and your client already read their tool list at startup. Put that JSON in `content`: a whole tools/list response, a {"tools":[...]} object, or a bare array. Same rules and same rendering as `mcp_server`, so a payload cannot be caught over the wire and missed here. Over MCP the same product is the `check_mcp_tools` tool.
- POST https://lazaretto.dev/v1/watch : register a dependency set once, then ask later whether any of it has been listed as malware since. One credit to create, free to read. Send a lockfile or {"packages":[...]}. We store the package IDENTITIES and what we knew at that moment, never lockfile contents or code. A watch is re-checked against the current advisory feed when you READ it: nothing runs on a schedule and nothing is pushed to you, so poll it (a daily CI job is enough). An optional https `webhook_url` is accepted and stored, but no deliveries are sent yet, so do not rely on it for alerts. Every verdict we issue is a statement about a MOMENT: chalk@5.6.1 was an ordinary dependency until it was not, and a check run the week before was correct and then quietly stopped being. A watch answers what a one-off check structurally cannot, which is whether something you ALREADY depend on has been listed as malware since you registered it.
  curl -s -X POST https://lazaretto.dev/v1/watch -H "X-API-Key: KEY" -H 'content-type: application/json' --data @package-lock.json
- GET https://lazaretto.dev/v1/watch/{id} with header `x-watch-token` : free. Returns `newly_malicious` (clean when you registered, listed now) separately from `still_malicious` (already listed then). Reading acknowledges an alert so it is not repeated. A 503 with `degraded:true` means we could not check, which is NOT an all-clear. DELETE the same URL to stop and delete the dependency list.
- Credit packs: POST https://lazaretto.dev/v1/credits/topup with {"bundle":"starter"|"pro"|"scale"}. starter: $3 = 150 scans; pro: $12 = 700 scans; scale: $25 = 1600 scans.
  Same dual-stack 402 as the scan (v2 header + v1 body). x402-fetch (v1) users: its default maxValue is $0.10, so pass an explicit maxValue to buy a bundle. Rate limits: scans and topup 120/min/IP; the free lockfile check 60/min/IP; known-bad lookups 60/min/IP.
- Humans without a wallet: the same bundles are sold for a card at https://lazaretto.dev/buy (Stripe hosted checkout; the API key appears once on the confirmation page).

## MCP server (for agents)

- Remote, zero install: add https://lazaretto.dev/mcp to any MCP client (Streamable HTTP). GET that URL for a description of the endpoint and its tool list, in HTML or JSON. Tools: check_lockfile (free; takes a whole lockfile OR a list of `name@version` strings, so a large tree need not go through your context), known_bad_lookup (free), verify_attestation (free), find_attestation (free; when nobody has attested the subject it falls back to a labelled identity check against the advisory corpus, which is not an attestation), get_free_key (free; issues the same developer key as POST https://lazaretto.dev/v1/trial, under the same one-per-source limit), scan_artifact (paid, one credit), scan_mcp_server (paid, one credit, checks an MCP server's advertised tools for poisoning before you connect), check_mcp_tools (paid, one credit, the same check on tool definitions you already hold, for stdio servers with no endpoint), scan_lockfile_deep (paid, one credit per package in the tree). Every tool declares an outputSchema and annotations.
- Local: `npx lazaretto-mcp` (npm package lazaretto-mcp, MIT).

## Machine contracts

- OpenAPI: https://lazaretto.dev/openapi.json
- Agent card: https://lazaretto.dev/.well-known/agent-card.json
- x402 discovery: https://lazaretto.dev/.well-known/x402
- Human docs: https://lazaretto.dev/docs/api

## Trust

- Real incidents distinguished, with reproduction commands: https://lazaretto.dev/caught
- Public corrections log: https://lazaretto.dev/corrections
- Data sources, licences, and coverage gaps: https://lazaretto.dev/sources
- How often we flag a package people install on purpose, with every corpus published in full: https://lazaretto.dev/benchmark (machine-readable: https://lazaretto.dev/benchmark.json)
