nanoparse-mcp 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/README.md +25 -12
- package/examples/agent-conversation.md +11 -0
- package/examples/costs.md +7 -7
- package/examples/pay-with-x402.md +272 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/nanoparse-mcp)
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](https://nanoparse.app)
|
|
6
|
+
[](https://checkmcp.dev/mcp/nanoparse-app)
|
|
7
|
+
[](https://mcplookup.com/server/io.github.nanoparse-dev/nanoparse-mcp)
|
|
8
|
+
[](https://hookmcp.com)
|
|
6
9
|
|
|
7
10
|
**The easiest way for AI agents to read the web.** NanoParse MCP is a hosted Model Context Protocol server at `https://nanoparse.app/mcp` that turns any URL into clean Markdown your agent can actually use — with machine-readable trust signals attached.
|
|
8
11
|
|
|
9
12
|
No install. No wallet key. No `.env`. Connect once and your agent can read the web.
|
|
10
13
|
|
|
11
|
-
**Why agents choose NanoParse over Firecrawl or Jina Reader:**
|
|
14
|
+
**Why agents choose NanoParse over Firecrawl or Jina Reader:** every page comes back with **Litmus** — 20 machine-readable signals that tell your agent *whether to trust and cite the page* before it reads a word. Pay $0.002 per parse via x402 once the free tier runs out. Full JavaScript/SPA rendering, native GFM tables. No account, no subscription, no API key.
|
|
12
15
|
|
|
13
16
|
## Quick start
|
|
14
17
|
|
|
@@ -18,6 +21,7 @@ Add this to your MCP client config:
|
|
|
18
21
|
{
|
|
19
22
|
"mcpServers": {
|
|
20
23
|
"nanoparse": {
|
|
24
|
+
"type": "http",
|
|
21
25
|
"url": "https://nanoparse.app/mcp"
|
|
22
26
|
}
|
|
23
27
|
}
|
|
@@ -43,41 +47,48 @@ That's it. Your agent can now fetch pages from inside any MCP-compatible client
|
|
|
43
47
|
|
|
44
48
|
## Real output
|
|
45
49
|
|
|
46
|
-
Every parse returns `markdown` + `metadata` + `litmus`.
|
|
50
|
+
Every parse returns `markdown` + `metadata` + `litmus`. Here is the full `litmus` object from a real parse of an arXiv paper (`examples/example-output.json` — markdown truncated there, litmus complete and unmodified):
|
|
47
51
|
|
|
48
52
|
```json
|
|
49
53
|
{
|
|
50
54
|
"litmus": {
|
|
51
55
|
"source": { "type": "academic", "authority": 0.85, "rationale": "academic source" },
|
|
52
|
-
"freshness": { "published": null, "stale_warning": "No publish date found — verify independently" },
|
|
56
|
+
"freshness": { "published": null, "age": "unknown", "stale_warning": "No publish date found — verify independently", "modified": null, "has_correction": false, "correction_note": null },
|
|
53
57
|
"tldr": "Abstract page for arXiv paper 2401.14295: Demystifying Chains, Trees, and Graphs of Thoughts",
|
|
54
58
|
"key_figures": [ { "value": "2,247", "context": "Thu, 25 Jan 2024 16:34:00 UTC (2,247 KB)" } ],
|
|
59
|
+
"structural_trust_score": { "level": "medium", "trust_factors": ["High-authority source", "No publish date found — verify independently"] },
|
|
60
|
+
"hedge_language": { "density": "low", "markers_found": 0, "per_500_words": 0 },
|
|
61
|
+
"content_density": { "thin_content": false, "substantive_sentence_ratio": 0.76 },
|
|
62
|
+
"syndication": { "detected": false, "note": null },
|
|
63
|
+
"paywall": { "detected": false, "note": null },
|
|
55
64
|
"content_type": "academic_paper",
|
|
56
65
|
"reading_time": { "minutes": 4 },
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
66
|
+
"is_archived": { "archive_org": true, "archive_is": false },
|
|
67
|
+
"outbound_authority": { "score": 0.54, "top_domains": ["ui.adsabs.harvard.edu", "scholar.google.com"] },
|
|
68
|
+
"boilerplate_ratio": { "ratio": 0.08, "confidence": "high" }
|
|
60
69
|
}
|
|
61
70
|
}
|
|
62
71
|
```
|
|
63
72
|
|
|
64
|
-
The agent learns *before reading a word*: this is an academic source (0.85 authority), no publish date
|
|
73
|
+
The agent learns *before reading a word*: this is an academic source (0.85 authority), structural trust is **medium** — high authority, but no publish date found, so verify independently. It also gets the one-sentence summary, the key numbers, and confirmation there's no paywall or syndication flag. See [`examples/agent-conversation.md`](examples/agent-conversation.md) for a full worked round-trip.
|
|
65
74
|
|
|
66
75
|
## Payments
|
|
67
76
|
|
|
68
|
-
- **
|
|
69
|
-
- **After that: $0.
|
|
77
|
+
- **10 free parses per month with a signed wallet** (sign the claim message — no payment), or 3 anonymous free tries per network. No account, no API key, no human required.
|
|
78
|
+
- **After that: $0.002 per parse** (flat, no subscriptions) via **x402** — an open micropayment protocol. Your agent's wallet signs the payment automatically. To give your agent a Coinbase wallet, run `npx @coinbase/payments-mcp` and connect it alongside NanoParse.
|
|
79
|
+
|
|
80
|
+
Copy-paste recipe for both paths — the wallet-MCP one with no code, and a ~40-line REST client — including the exact `402 → sign → retry` loop and the error codes worth branching on: [`examples/pay-with-x402.md`](examples/pay-with-x402.md).
|
|
70
81
|
|
|
71
82
|
## How payments work under the hood
|
|
72
83
|
|
|
73
|
-
When an agent without free quota calls `nanoparse_fetch`, the endpoint responds with an **HTTP 402 — Payment Required** carrying x402 payment instructions. The agent's wallet signs a $0.
|
|
84
|
+
When an agent without free quota calls `nanoparse_fetch`, the endpoint responds with an **HTTP 402 — Payment Required** carrying x402 payment instructions. The agent's wallet signs a $0.002 USDC (Base) transfer, the facilitator settles it, and the parse proceeds. The hosted endpoint handles the entire challenge → settlement flow — the agent never needs a private key on your machine.
|
|
74
85
|
|
|
75
86
|
## Pricing vs Firecrawl
|
|
76
87
|
|
|
77
88
|
| | NanoParse | Firecrawl Hobby |
|
|
78
89
|
|---|---|---|
|
|
79
|
-
| Billing | **$0.
|
|
80
|
-
| 5,000 parses | **$
|
|
90
|
+
| Billing | **$0.002 per parse, flat** | $16/mo subscription, credits expire monthly |
|
|
91
|
+
| 5,000 parses | **$25.00** | $16 base — but ~$145 with JSON/extraction modes (9× credit burn) |
|
|
81
92
|
| Account / API key | **None** | Required |
|
|
82
93
|
| Agent can pay directly (x402) | **Yes** | No |
|
|
83
94
|
|
|
@@ -110,5 +121,7 @@ is the current, supported integration.
|
|
|
110
121
|
- Quick start & integration: [nanoparse.app/quickstart](https://nanoparse.app/quickstart)
|
|
111
122
|
- Compare & pricing: [nanoparse.app/compare](https://nanoparse.app/compare)
|
|
112
123
|
- API + MCP endpoint: [nanoparse.app](https://nanoparse.app)
|
|
124
|
+
- Security policy: [SECURITY.md](SECURITY.md) — report a vulnerability to hello@nanoparse.app
|
|
125
|
+
(also published at `https://nanoparse.app/.well-known/security.txt`)
|
|
113
126
|
|
|
114
127
|
MIT licensed.
|
|
@@ -36,6 +36,13 @@ word of the paper.
|
|
|
36
36
|
{ "value": "2,247", "context": "Thu, 25 Jan 2024 16:34:00 UTC (2,247 KB)" },
|
|
37
37
|
{ "value": "2,290", "context": "Sat, 30 Mar 2024 16:36:33 UTC (2,290 KB)" }
|
|
38
38
|
],
|
|
39
|
+
"structural_trust_score": {
|
|
40
|
+
"level": "medium",
|
|
41
|
+
"trust_factors": [
|
|
42
|
+
"High-authority source",
|
|
43
|
+
"No publish date found — verify independently"
|
|
44
|
+
]
|
|
45
|
+
},
|
|
39
46
|
"content_type": "academic_paper",
|
|
40
47
|
"reading_time": { "minutes": 4 },
|
|
41
48
|
"hedge_language": { "density": "low", "markers_found": 0, "per_500_words": 0 },
|
|
@@ -47,6 +54,10 @@ word of the paper.
|
|
|
47
54
|
|
|
48
55
|
## How the agent reasons about it
|
|
49
56
|
|
|
57
|
+
- **Structural trust: medium** — Litmus flags this as high-authority
|
|
58
|
+
(academic, 0.85) but with no publish date found, so the verdict is "verify
|
|
59
|
+
independently," not "trust blindly." The agent treats it as a solid source
|
|
60
|
+
it still needs to date-check.
|
|
50
61
|
- **Authority 0.85, academic** — high-trust source class. Claims from this
|
|
51
62
|
paper can be weighed accordingly. (Litmus scores the *source class*, not
|
|
52
63
|
the truth of the content — the agent still reads and thinks.)
|
package/examples/costs.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Cost comparison: NanoParse vs Firecrawl
|
|
2
2
|
|
|
3
|
-
Pricing verified 2026-08-28. NanoParse: **$0.
|
|
3
|
+
Pricing verified 2026-08-28 (Firecrawl); NanoParse updated to the current flat rate. NanoParse: **$0.002 per parse, flat** — no
|
|
4
4
|
subscription, no account, no API key. Litmus signals, full JS/SPA rendering,
|
|
5
5
|
and GFM tables are included in every parse — there are no feature
|
|
6
6
|
multipliers.
|
|
@@ -19,11 +19,11 @@ Two things the base rate hides:
|
|
|
19
19
|
|
|
20
20
|
1. **Credits do not roll over.** A Hobby subscription is $16 every month —
|
|
21
21
|
use 1,000 of 5,000 credits and you still paid $16. NanoParse bills only
|
|
22
|
-
what you use: $
|
|
22
|
+
what you use: $5 for exactly 1,000 parses.
|
|
23
23
|
2. **Feature multipliers.** JSON extraction and enhanced mode each add
|
|
24
24
|
credits per page (up to 9 credits/page combined). At Hobby that makes a
|
|
25
|
-
fully-featured parse ~$0.029/page — ~
|
|
26
|
-
structured output, and rendering are included in NanoParse's $0.
|
|
25
|
+
fully-featured parse ~$0.029/page — ~6× NanoParse's flat rate. Litmus,
|
|
26
|
+
structured output, and rendering are included in NanoParse's $0.002 with
|
|
27
27
|
no multiplier.
|
|
28
28
|
|
|
29
29
|
## What a month of 5,000 parses costs
|
|
@@ -31,9 +31,9 @@ Two things the base rate hides:
|
|
|
31
31
|
| | NanoParse | Firecrawl Hobby |
|
|
32
32
|
|---|---|---|
|
|
33
33
|
| Subscription | None — pay per parse | $16/mo, billed every month |
|
|
34
|
-
| 5,000 plain scrapes | **$
|
|
35
|
-
| 5,000 with extraction/features | **$
|
|
36
|
-
| Use only 1,000 of your quota | **$
|
|
34
|
+
| 5,000 plain scrapes | **$25.00** | $16 (base rate) |
|
|
35
|
+
| 5,000 with extraction/features | **$25.00** | ~$145 (9× credit burn) |
|
|
36
|
+
| Use only 1,000 of your quota | **$5.00** | $16 (credits expire) |
|
|
37
37
|
| Account / API key needed | No | Yes |
|
|
38
38
|
| Agent can pay directly (x402) | Yes | No |
|
|
39
39
|
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# Paying for a parse with x402
|
|
2
|
+
|
|
3
|
+
NanoParse answers an unauthenticated call with **HTTP 402** and machine-readable
|
|
4
|
+
terms — the price, the token, the chain, the recipient, and what a valid signature
|
|
5
|
+
must look like. No account, no API key, no card, no subscription.
|
|
6
|
+
|
|
7
|
+
- **10 free parses per month with a signed wallet**, or 3 anonymous free tries per
|
|
8
|
+
network.
|
|
9
|
+
- **After that: $0.002 USDC on Base per parse**, flat.
|
|
10
|
+
- Settlement runs **after** a page is read. A page that cannot be read — bot wall,
|
|
11
|
+
consent wall, paywall, JS shell with no text — is reported with a named reason
|
|
12
|
+
and **never charged**.
|
|
13
|
+
|
|
14
|
+
Every parse returns clean Markdown plus **Litmus** — 20 machine-readable signals
|
|
15
|
+
(source authority, freshness, structural trust, content density, syndication,
|
|
16
|
+
paywall state) in the same response, not as a second billed call.
|
|
17
|
+
|
|
18
|
+
Two ways to pay. Pick one.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Option A — give the agent a wallet, write no code
|
|
23
|
+
|
|
24
|
+
Add NanoParse to your MCP client, and give the agent a wallet that can sign:
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"mcpServers": {
|
|
29
|
+
"nanoparse": {
|
|
30
|
+
"url": "https://nanoparse.app/mcp"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
npx @coinbase/payments-mcp
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
(The Coinbase agentic wallet MCP — self-custody, funded via Apple Pay. Docs:
|
|
41
|
+
<https://docs.cdp.coinbase.com/agentic-wallet/mcp/welcome>.)
|
|
42
|
+
|
|
43
|
+
Then the loop is two calls:
|
|
44
|
+
|
|
45
|
+
1. `nanoparse_fetch(url)`. Once the free tier is spent, the tool returns an
|
|
46
|
+
`isError` result whose **`structuredContent` is the PaymentRequired object** —
|
|
47
|
+
`x402Version`, `accepts[]`, `extensions`. It is the quote, not a failure.
|
|
48
|
+
2. Sign `accepts[0]` and retry the same tool with
|
|
49
|
+
`payment_signature` = the base64url JSON envelope. This argument works in
|
|
50
|
+
**every** MCP client; there are no custom headers to set. A client that can
|
|
51
|
+
set headers may instead send the same payload as `Payment-Signature`, or put
|
|
52
|
+
the envelope object in `_meta["x402/payment"]`.
|
|
53
|
+
|
|
54
|
+
`nanoparse_status(address)` is free and tells the agent what it needs to know
|
|
55
|
+
before spending: free parses remaining, wallet and USDC balance, whether the next
|
|
56
|
+
call will go through without payment.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Option B — a REST client that pays its own way
|
|
61
|
+
|
|
62
|
+
~40 lines of Node with [`viem`](https://viem.sh). No SDK, no server-side key:
|
|
63
|
+
the wallet is the caller's.
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
// read-page.mjs — read one page, paying $0.002 in USDC on Base only if asked.
|
|
67
|
+
// Usage: npm i viem && WALLET_KEY=0x... node read-page.mjs https://example.com
|
|
68
|
+
//
|
|
69
|
+
// 1. Try the free tier (10/month with a signed wallet, or 3 anonymous tries): a plain POST.
|
|
70
|
+
// 2. A 402 is the quote, not an error. Read the terms out of it.
|
|
71
|
+
// 3. Sign an EIP-3009 permit over those terms (gas is the facilitator's).
|
|
72
|
+
// 4. Retry with the signed envelope; poll /status (free) until the page lands.
|
|
73
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
74
|
+
|
|
75
|
+
const ENDPOINT = "https://nanoparse.app/fetch";
|
|
76
|
+
const account = privateKeyToAccount(process.env.WALLET_KEY);
|
|
77
|
+
const b64 = (o) => Buffer.from(JSON.stringify(o)).toString("base64");
|
|
78
|
+
|
|
79
|
+
async function readPage(url) {
|
|
80
|
+
const post = (headers) =>
|
|
81
|
+
fetch(ENDPOINT, {
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
84
|
+
body: JSON.stringify({ url }),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// 1. Free tier first — no payment, no key, no account.
|
|
88
|
+
let res = await post();
|
|
89
|
+
if (res.status !== 402) {
|
|
90
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
|
|
91
|
+
return poll(await res.json());
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 2. The terms. `amount` is micro-USDC ("2000" = $0.002) and `payTo` is the
|
|
95
|
+
// only address a permit should ever be signed for.
|
|
96
|
+
const terms = (await res.json()).payment_required.accepts[0];
|
|
97
|
+
const now = Math.floor(Date.now() / 1000);
|
|
98
|
+
const nonce = "0x" + Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString("hex");
|
|
99
|
+
const authorization = {
|
|
100
|
+
from: account.address,
|
|
101
|
+
to: terms.payTo,
|
|
102
|
+
value: terms.amount,
|
|
103
|
+
validAfter: String(now - 5),
|
|
104
|
+
validBefore: String(now + 895), // within maxTimeoutSeconds from the challenge
|
|
105
|
+
nonce,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// 3. EIP-3009 transferWithAuthorization. The EIP-712 domain belongs to the
|
|
109
|
+
// token contract, so take it from the challenge: extra.name is the domain
|
|
110
|
+
// name "USD Coin" — signing against the ticker "USDC" fails verification.
|
|
111
|
+
const signature = await account.signTypedData({
|
|
112
|
+
domain: {
|
|
113
|
+
name: terms.extra.name,
|
|
114
|
+
version: terms.extra.version,
|
|
115
|
+
chainId: 8453, // Base
|
|
116
|
+
verifyingContract: terms.asset,
|
|
117
|
+
},
|
|
118
|
+
types: {
|
|
119
|
+
TransferWithAuthorization: [
|
|
120
|
+
{ name: "from", type: "address" },
|
|
121
|
+
{ name: "to", type: "address" },
|
|
122
|
+
{ name: "value", type: "uint256" },
|
|
123
|
+
{ name: "validAfter", type: "uint256" },
|
|
124
|
+
{ name: "validBefore", type: "uint256" },
|
|
125
|
+
{ name: "nonce", type: "bytes32" },
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
primaryType: "TransferWithAuthorization",
|
|
129
|
+
message: {
|
|
130
|
+
...authorization,
|
|
131
|
+
value: BigInt(authorization.value),
|
|
132
|
+
validAfter: BigInt(authorization.validAfter),
|
|
133
|
+
validBefore: BigInt(authorization.validBefore),
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// 4. Retry with the envelope the challenge quoted: accepted[] echoed verbatim.
|
|
138
|
+
res = await post({
|
|
139
|
+
"Payment-Signature": b64({ x402Version: 2, accepted: terms, payload: { signature, authorization } }),
|
|
140
|
+
});
|
|
141
|
+
const job = await res.json();
|
|
142
|
+
if (!res.ok) throw new Error(`${job.error_code}: ${job.message}`);
|
|
143
|
+
return poll(job); // 202 { jobId, statusUrl }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// /status is free and unauthenticated: watch a job without spending anything.
|
|
147
|
+
async function poll(job) {
|
|
148
|
+
for (;;) {
|
|
149
|
+
const out = await (await fetch(job.statusUrl)).json();
|
|
150
|
+
if (out.status === "completed") return out; // { markdown, metadata, litmus }
|
|
151
|
+
if (out.status !== "processing") throw new Error(`${out.status}: ${out.error_code ?? ""}`);
|
|
152
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
console.log((await readPage(process.argv[2])).markdown);
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## The 402 is the quote
|
|
162
|
+
|
|
163
|
+
Captured live, free tier exhausted, no payment attached:
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"error": "payment_required",
|
|
168
|
+
"error_code": "free_tier_exhausted",
|
|
169
|
+
"free_calls_used": 10,
|
|
170
|
+
"free_calls_limit": 10,
|
|
171
|
+
"payment_required": {
|
|
172
|
+
"x402Version": 2,
|
|
173
|
+
"accepts": [
|
|
174
|
+
{
|
|
175
|
+
"scheme": "exact",
|
|
176
|
+
"network": "eip155:8453",
|
|
177
|
+
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
178
|
+
"amount": "2000",
|
|
179
|
+
"payTo": "0x57a1d06873db575baeae1f6af30862a75c7bd570",
|
|
180
|
+
"maxTimeoutSeconds": 60,
|
|
181
|
+
"extra": { "assetTransferMethod": "eip3009", "name": "USD Coin", "version": "2" }
|
|
182
|
+
}
|
|
183
|
+
]
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The same terms travel base64url-encoded in the `payment-required` response header,
|
|
189
|
+
and the recipient is repeated as a CAIP-style `pay-to: ethereum:base:0x…` header.
|
|
190
|
+
`access-control-allow-origin: *` and the `Content-Type, Payment-Signature,
|
|
191
|
+
Authorization` request headers are allowed on this route, so a browser-side agent
|
|
192
|
+
can send a signed payment too.
|
|
193
|
+
|
|
194
|
+
Four fields carry the weight:
|
|
195
|
+
|
|
196
|
+
- **`amount`** is micro-USDC — `"2000"` is $0.002 at six decimals. There is no
|
|
197
|
+
currency string to parse.
|
|
198
|
+
- **`network`** is a CAIP-2 chain id (`eip155:8453` is Base) and **`asset`** is the
|
|
199
|
+
USDC contract on it, so the client never guesses which USDC.
|
|
200
|
+
- **`extra.name` / `extra.version`** are the EIP-712 domain of the token contract:
|
|
201
|
+
`"USD Coin"` v2. Signing against `"USDC"` produces a signature that fails
|
|
202
|
+
verification — the domain is part of the digest.
|
|
203
|
+
- **`payTo`** is the only address a permit should ever be signed for. A payload
|
|
204
|
+
pointing anywhere else is rejected before any signature is checked.
|
|
205
|
+
|
|
206
|
+
## What the server checks before the facilitator is called
|
|
207
|
+
|
|
208
|
+
A wrong shape costs nothing — it answers `400 invalid_payment_payload`:
|
|
209
|
+
|
|
210
|
+
- `x402Version` is exactly `2`.
|
|
211
|
+
- `accepted` repeats the terms verbatim: `scheme`, `network`, `asset`, `amount`,
|
|
212
|
+
`payTo`.
|
|
213
|
+
- `payload.signature` is `0x` + 130 hex characters (65-byte packed `r‖s‖v`).
|
|
214
|
+
- `payload.authorization.value` equals `amount` exactly.
|
|
215
|
+
- `payload.authorization.nonce` is `0x` + 64 hex characters, and single-use
|
|
216
|
+
on-chain — which is what makes "retry the same payload" safe rather than hopeful.
|
|
217
|
+
- `validAfter` is in the past and `validBefore` in the future.
|
|
218
|
+
|
|
219
|
+
## Retry policy, as error codes
|
|
220
|
+
|
|
221
|
+
| `error_code` | What to do |
|
|
222
|
+
|---|---|
|
|
223
|
+
| `insufficient_usdc` | Fund the wallet and sign a **fresh** envelope. |
|
|
224
|
+
| `invalid_payment_payload`, `invalid_payment` | The shape or signature is wrong. Fix it; retrying unchanged cannot succeed. |
|
|
225
|
+
| `permit_expired`, `permit_not_yet_valid` | Sign a fresh permit. |
|
|
226
|
+
| `settlement_failed` | Ambiguous settle. Retry with the **same** envelope — it is idempotent. |
|
|
227
|
+
| `screening_declined` | Compliance screening declined. Do not retry. |
|
|
228
|
+
| `location_blocked` | A jurisdiction the facilitator does not serve. |
|
|
229
|
+
| `self_send_not_allowed` | Payer and recipient are the same address. |
|
|
230
|
+
| `content_insufficient` | The page rendered but yielded almost no text (JS shell, login wall). **Not charged.** |
|
|
231
|
+
| `domain_blocked` | The host repeatedly returned nothing parseable. **Not charged.** Do not retry. |
|
|
232
|
+
|
|
233
|
+
There is no refund path because nothing is charged up front: settlement runs after
|
|
234
|
+
a successful read, so a failed render simply never settles.
|
|
235
|
+
|
|
236
|
+
## Check the recipient before you sign
|
|
237
|
+
|
|
238
|
+
`/.well-known/x402` publishes the address the endpoint claims, and the 402 repeats
|
|
239
|
+
it. Compare them before signing anything:
|
|
240
|
+
|
|
241
|
+
```js
|
|
242
|
+
const accept = terms; // accepts[0] from the 402
|
|
243
|
+
const disc = await (await fetch("https://nanoparse.app/.well-known/x402")).json();
|
|
244
|
+
if (!disc.ownershipProofs.map((a) => a.toLowerCase()).includes(accept.payTo.toLowerCase()))
|
|
245
|
+
throw new Error("recipient not published by the endpoint");
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
That buys consistency, not identity — both documents come from the same origin —
|
|
249
|
+
but it is one GET and it catches a swapped recipient.
|
|
250
|
+
|
|
251
|
+
## What was verified
|
|
252
|
+
|
|
253
|
+
- The client under **Option B** was run verbatim against the live endpoint on
|
|
254
|
+
2026-10-06 with a fresh, unfunded throwaway wallet: free tier exhausted → `402`
|
|
255
|
+
→ signed envelope → retry → `insufficient_usdc: Payer's USDC balance is too low
|
|
256
|
+
for this call (needs 2000 micro-USDC on Base)`. The shape and signature were
|
|
257
|
+
accepted; nothing was charged.
|
|
258
|
+
- The **Option A** MCP loop was verified the same way: `tools/call` returned
|
|
259
|
+
`isError: true` with `structuredContent` = the PaymentRequired object, and the
|
|
260
|
+
retry carrying `payment_signature` answered
|
|
261
|
+
`-32000 / insufficient_usdc` for the unfunded payer.
|
|
262
|
+
- The settle leg — USDC actually moving to the treasury on Base — is exercised
|
|
263
|
+
end to end by our own labeled canary through this same paid path.
|
|
264
|
+
|
|
265
|
+
## Docs
|
|
266
|
+
|
|
267
|
+
- Quick start: <https://nanoparse.app/quickstart>
|
|
268
|
+
- Machine spec: <https://nanoparse.app/openapi.json>
|
|
269
|
+
- Agent-facing description: <https://nanoparse.app/llms.txt>
|
|
270
|
+
- x402 discovery: <https://nanoparse.app/.well-known/x402>
|
|
271
|
+
|
|
272
|
+
MIT licensed.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nanoparse-mcp",
|
|
3
|
-
"version": "2.0.
|
|
4
|
-
"description": "NanoParse MCP
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"description": "NanoParse MCP \u2014 hosted Model Context Protocol server for AI agents. Any URL becomes clean Markdown with attribution signals. $0.002 per page via x402, no account or API key.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
7
7
|
"model-context-protocol",
|