How to check a package's licence before installing it, step by step with curl
Last updated: 2026-10-13
To read the licence of a package an agent is about to install, send up to 10 names to POST /v1/check/packages (US$0.005 per batch). AttestPage looks each name up in its registry (npm, PyPI or crates.io) and returns the license field the registry holds, with exists, the latest version, the links and any OSV.dev advisories, under one signed receipt.
This guide covers the license field and what to decide from it. For the whole call and the other fields, see How to check packages before installing them. Commands use curl and jq against https://vehcdj664efetfrsolne5umanq.srv.us, then the same call as an MCP tool.
What the licence field is
- The registry's own text.
licenseis copied from the registry as the publisher wrote it. AttestPage does not read the package's files or its LICENSE file, and does not judge the text. - The latest version's licence. For npm and PyPI it is the latest version's licence field; for crates.io it is the default version's licence. A
versionyou send changes the advisories, not the licence. - Often an SPDX expression.
MIT,Apache-2.0andMIT OR Apache-2.0are SPDX forms. Older packages may hold free text such asSEE LICENSE IN LICENSE.txt, or a loose name likeBSD. For npm, a licence given as an object is read as itstype. - **
nullwhen there is nothing to read.** A name the registry does not list, a failed lookup, and a package whose licence field is empty or missing all givenull.
Price
US$0.005 per batch of 1 to 10 names, paid in USDC with x402 v2 on Base mainnet (real USDC). No account, key or sign-up. A request that fails validation gets 400 before payment and is never charged, and a batch where every registry lookup fails gets 502 and is not charged. All prices: pricing.json.
1. See an answer for free
The free sample checks four demo names. Three are listed, each with a different licence form, and one does not exist:
curl -s https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/packages | jq '.results[] | {name, exists, license}'
{
"name": "left-pad",
"exists": true,
"license": "WTFPL"
}
{
"name": "requests",
"exists": true,
"license": "Apache-2.0"
}
{
"name": "serde",
"exists": true,
"license": "MIT OR Apache-2.0"
}
{
"name": "left-pad-demo-missing",
"exists": false,
"license": null
}
2. Read the price from the 402 offer
Put the names you plan to install in a file. A request with a valid body gets the payment offer without being charged. The PAYMENT-REQUIRED header is base64 JSON:
cat > packages.json <<'EOF'
{ "packages": [{ "ecosystem": "npm", "name": "left-pad" }, { "ecosystem": "pypi", "name": "requests" }] }
EOF
curl -si -X POST https://vehcdj664efetfrsolne5umanq.srv.us/v1/check/packages -H 'content-type: application/json' -d @packages.json \
| grep -i '^payment-required:' | cut -d' ' -f2 | tr -d '\r' | base64 -d \
| jq '.accepts[0] | {scheme, network, amount, payTo}'
amount is in USDC base units (6 decimals), so 5000 is US$0.005.
3. Check the names
Each name needs an ecosystem (npm, pypi or crates) and a name. Put the batch in the same packages.json as step 2:
cat > packages.json <<'EOF'
{ "packages": [{ "ecosystem": "npm", "name": "left-pad" }, { "ecosystem": "pypi", "name": "requests" }] }
EOF
Pay with any x402 v2 client: it reads the offer from step 2, signs it and sends the same request again with a PAYMENT-SIGNATURE header. One x402 payment step by step shows this with plain fetch, and the MCP server does it for you. Save the answer as answer.json for the steps below.
4. Decide from the licence
List the licence next to the other fields that decide an install:
jq '.results[] | {name, exists, license, latest_version, homepage}' answer.json
Then sort each name into one of three groups. The allow list is yours.
license | What to do |
|---|---|
A form on your allow list, for example MIT, Apache-2.0 or BSD-3-Clause | Install. Open homepage or repository once to confirm it is the package you meant. |
An expression such as MIT OR Apache-2.0 | OR offers a choice: you may use the package under either. AND means every licence applies. Check each part against your list. |
Anything else: null, free text, UNLICENSED, SEE LICENSE IN …, or a name not on your list | Do not install on the agent's own say-so. Read the licence in the package's repository, or ask the person who owns the decision. |
A null licence is not a permissive one: with no licence stated you have no stated permission to use the code. When exists is false there is nothing to install, and the model may have invented the name.
To print every listed name whose licence is not on your allow list, with ALLOW a JSON array of the licences you accept:
ALLOW='["MIT","Apache-2.0","BSD-3-Clause","ISC"]'
jq --argjson allow "$ALLOW" '.results[] | select(.exists != false and ((.license // "") as $l | $allow | index($l) | not)) | {name, license}' answer.json
This is an exact match: MIT OR Apache-2.0 is not in the list above, so it is printed for you to read. The check reads what the registry says. It does not detect a registry licence that differs from the one in the package's files.
5. Keep the receipt
The receipt field is a compact JWS. It signs, for each name, whether it exists and its latest version, with the hash of the registry's answer, so you can keep it with the decision. Check it with the free verify route:
jq '{receipt}' answer.json \
| curl -s -X POST https://vehcdj664efetfrsolne5umanq.srv.us/v1/receipt/verify -H 'content-type: application/json' -d @- | jq .valid
With MCP
The same check is the MCP tool attestpage_check_packages, with the body from step 3 as its arguments. To use it from an MCP client with nothing to install, add the remote endpoint:
{
"mcpServers": {
"attestpage": { "type": "http", "url": "https://vehcdj664efetfrsolne5umanq.srv.us/mcp" }
}
}
One call to the remote endpoint with curl. _meta["attestpage/trial"] set to false asks for the payment offer rather than a trial call, so nothing is charged:
curl -s -X POST https://vehcdj664efetfrsolne5umanq.srv.us/mcp -H 'content-type: application/json' -d @- <<'EOF' \
| jq '.result.structuredContent.accepts[0] | {scheme, network, amount}'
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": { "name": "attestpage_check_packages",
"arguments": { "packages": [{ "ecosystem": "npm", "name": "left-pad" }, { "ecosystem": "pypi", "name": "requests" }] },
"_meta": { "attestpage/trial": false } } }
EOF
{
"scheme": "exact",
"network": "eip155:8453",
"amount": "5000"
}
The result has isError: true and the x402 payment requirements in structuredContent, with the same amount as step 2. Sign one of accepts, put the payload in _meta["x402/payment"] and call again: structuredContent is then the same JSON as answer.json, and _meta["x402/payment-response"] holds the settlement.
Questions
How much does a check cost?
US$0.005 per batch of up to 10 names. Checking 23 names takes 3 batches, so 3 calls.
Is this a licence compliance check?
No. It returns the licence text the registry holds for the latest version. It does not read the package's files, its dependencies' licences or your own policy. Use it to catch a missing or unexpected licence before an install, and make the final call yourself.
Why is license null for a package that exists?
The registry holds no licence text for the latest version, or only whitespace. Treat it as unknown and read the package's repository.
Does sending a version give that version's licence?
No. A version selects the version whose advisories are listed. license is still the latest version's (npm and PyPI) or the default version's (crates.io).