minifetch-api 1.1.1 → 1.2.0

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/README.md CHANGED
@@ -5,18 +5,27 @@
5
5
  </a>
6
6
  </div>
7
7
 
8
- **Fetch & extract data from web pages.** [Minifetch.com](https://minifetch.com) provides composable extraction APIs that humans and AI Agents can autonomously discover, orchestrate, and pay for— making web pages simple to access. Pay-as-you-go at competitive prices.
8
+ **Fetch & extract data from web pages.** [Minifetch.com](https://minifetch.com) provides composable extraction APIs for humans and AI Agents-- making web pages simple to access.
9
+
10
+ - **✅ [Sign up](https://minifetch.com/dashboard) for an account and get your first 125-250 fetches for free. 🎉🎉**
11
+ - ✅ Always pay-as-you-go at competitive prices.
12
+ - ⛔ No charge for blocked pages (403 errors).
13
+
14
+ ---
15
+ **👉 [Dashboard](https://minifetch.com/dashboard) | [Full API docs](https://minifetch.com/docs/api) | [LLMs.txt](https://minifetch.com/llms.txt) | Questions? Join our [Discord]([Discord](https://discord.gg/EM6ET8Dshm))**
9
16
 
10
- **Full [API docs are here](https://minifetch.com/docs/api)**, including example queries, response data & prices. For AI Agents, read the [LLMs.txt](https://minifetch.com/llms.txt) instead.
11
17
 
12
- **Payments** work with [x402](https://www.x402.org/) USDC stablecoin micropayments on Coinbase's Base & Solana blockchain networks. Transaction fees are free. If you'd like to use a traditional credit card & API key instead, sign up for the waitlist: [forms.gle/rkMi7T23bHJc8XFw9](https://forms.gle/rkMi7T23bHJc8XFw9)
13
18
 
14
- **Bulk fetch and extract** for building datasets is coming soon, sign up for the waitlist: [forms.gle/rkMi7T23bHJc8XFw9](https://forms.gle/rkMi7T23bHJc8XFw9)
19
+ ---
20
+ **Payments.** Two ways to pay:
21
+ 1. Credit card + API key. Get started free - [visit our dashboard to Sign Up](https://minifetch.com/dashboard). Your account will be auto-loaded with 125–250 API calls. Top up with your credit card later.
22
+ 2. x402. USDC stablecoin micropayments on Coinbase's Base & Solana networks.
15
23
 
16
24
  ## Prerequisites
17
25
 
18
26
  - Node.js v18+ & NPM
19
- - A valid Ethereum or Solana private key for making USDC payments.
27
+ - A valid API key from our [dashboard](https://minifetch.com/dashboard)
28
+ - *Or* an Ethereum or Solana private key for making USDC payments on Base or Solana networks.
20
29
 
21
30
  ## Install
22
31
 
@@ -27,82 +36,95 @@
27
36
  ```js
28
37
  import Minifetch from "minifetch-api";
29
38
 
30
- // Inititialize the client with your network choice & private key.
39
+ // First, initialize the client with your payment choice:
40
+
41
+ // 1. API Key Payments:
42
+ const client = new Minifetch({
43
+ apiKey: process.env.MINIFETCH_API_KEY
44
+ });
45
+
46
+ // 2. x402 Payments:
31
47
  // - Network options: "base" or "solana"
32
- // - Use private key from account that has a small amt of USDC
48
+ // - Private key from wallet that has a small amt of USDC
33
49
  const client = new Minifetch({
34
50
  network: "base",
35
51
  privateKey: process.env.BASE_PRIVATE_KEY,
36
52
  });
37
53
 
54
+ // Now you're ready to extract data from a url:
38
55
  try {
39
56
  const url = "example.com";
40
57
  const response = await client.checkAndExtractUrlPreview(url);
41
- // 200 "ok" responses will return the data and x402 payment info:
58
+ // 200 "ok" responses
42
59
  console.log("data: ", response.data);
43
- console.log("payment info: ", response.payment);
44
60
  } catch (err) {
45
- // No payment or charges for errors!
61
+ // No charge for errors or 403 blocked urls!
46
62
  console.log(err);
47
63
  }
48
64
  ```
49
65
 
50
- ### Client Methods Available
51
-
52
- After you initialize the client (above), you have the following methods available to use. The "checkAndExtract" API methods provided help you avoid paying for blocked URLs and return more granular info about why a URL may or may not return data. There is no charge for blocked upstream URLs.
53
-
54
- **Price list** and example data for each method [is here.](https://minifetch.com/docs/api#example-data)
55
-
56
- **Rate limit** Requests exceeding rate limits return `503` errors. This is intentional — back off and retry, max 5-10 queries per second. We'll add bulk fetch and extract in the future. Sign up for the [waitlist](https://forms.gle/rkMi7T23bHJc8XFw9).
57
-
58
- **Code examples** can be found in the github repository under /test.
66
+ ### Data Extraction Methods
59
67
 
60
- - **`client.checkAndExtractUrlPreview(url)`**
61
- - Extracts a light, token-efficient preview of a URL: title, description, and image (only).
68
+ After the Quick Start, you have the following methods to use. The "checkAndExtract" methods help to avoid paying for blocked URLs. **Price list** & example data [is here.](https://minifetch.com/docs/api#example-data)
62
69
 
63
- - **`client.checkAndExtractUrlContent(url, options)`**
64
- - Extracts a clean, token-efficient content summary as markdown from a URL. Removes ads, nav, scripts. Much more efficient than raw HTML fetches for LLMs.
65
- - Option: `{ includeMediaUrls: true }`, defaults to false
70
+ **Wrap** these in a **try/catch** just like in the Quickstart example above. **Code examples** can be also found in the [Github repository](https://github.com/Niche-Networks/minifetch-api/) in the /example- directories.
66
71
 
67
- - **`client.checkAndExtractUrlLinks(url)`**
68
- - Extracts all links from a URL categorized by type (internal/external/anchor) with SEO metadata. Detects image links, nofollow attributes, and analyzes external domain distribution.
69
-
70
- - **`client.checkAndExtractUrlMetadata(url, options)`**
71
- - Extracts rich structured metadata from a URL: title, description, og/twitter tags, json-ld, images, headings, response headers, and more. Perfect for SEO research or metadata indexing & analysis. Largest response size of the client methods available. Calling this method and setting verbosity to "full" is the drop-in replacement for the [`url-metadata`](https://www.npmjs.com/package/url-metadata) package.
72
- - Option: `{ verbosity: "full" }`, defaults to "standard"
73
- - Option: `{ includeResponseBody: true }`, defaults to false
74
-
75
- ***For maximum control, you can also use the following methods directly:***
76
-
77
- - **`client.preflightCheck(url)`**
78
- - Standalone call to check if a URL is allowed to be fetched according to the website's robots.txt file. Use this free endpoint before using our other paid endpoints to help avoid spending extra on un-fetchable URLs
79
- - **`client.extractUrlPreview(url)`**
80
- - **`client.extractUrlContent(url, options)`**
81
- - Option: `{ includeMediaUrls: true }`, defaults to false
82
- - **`client.extractUrlLinks(url)`**
83
- - **`client.extractUrlMetadata(url, options)`**
84
- - Option: `{ verbosity: "full" }`, defaults to "standard"
85
- - Option: `{ includeResponseBody: true }`, defaults to false
72
+ ```js
73
+ // Extracts a light, token-efficient preview of a URL:
74
+ // title, description, and image (only).
75
+ client.checkAndExtractUrlPreview(url);
76
+
77
+ // Extracts a clean, token-efficient content summary as markdown from a URL.
78
+ // Removes ads, nav, scripts. More efficient than raw HTML fetches for LLMs.
79
+ // Options:
80
+ // { includeMediaUrls: true }, defaults to false.
81
+ client.checkAndExtractUrlContent(url, options);
82
+
83
+ // Extracts all links from a URL categorized by type (internal/external/anchor).
84
+ // Detects image links, nofollow attributes, analyzes external domain distribution.
85
+ client.checkAndExtractUrlLinks(url);
86
+
87
+ // Extracts rich structured metadata from a URL:
88
+ // meta tags, json-ld, images, headings, response headers, + more.
89
+ // Perfect for SEO research or metadata indexing & analysis. Largest
90
+ // response size of client methods. Setting verbosity to "full" is the drop-in
91
+ // replacement for the [`url-metadata`](https://www.npmjs.com/package/url-metadata) package.
92
+ // Options:
93
+ // { verbosity: "full" }, defaults to "standard"
94
+ // { includeResponseBody: true }, defaults to false
95
+ client.checkAndExtractUrlMetadata(url, options);
96
+
97
+ // For max control, you can also use the following methods directly.
98
+ // Free - check robots.txt:
99
+ client.preflightCheck(url);
100
+ // Extract data:
101
+ client.extractUrlPreview(url);
102
+ client.extractUrlContent(url, options); // same options as above
103
+ client.extractUrlLinks(url);
104
+ client.extractUrlMetadata(url, options) // same options as above
86
105
 
106
+ ```
107
+ ---
87
108
  ### Error Types
88
- When you wrap the functions above in a try/catch, here are the errors you may encounter:
109
+ When you wrap the functions above in a try/catch, here are the errors you may encounter. You are never charged for errors.
89
110
 
90
111
  - **"RobotsBlockedError: URL is blocked by robots.txt"**
91
- - URL is explicitly blocked by the website's robots.txt will error like this.
112
+ - URL is explicitly blocked by the website's robots.txt, cannot be fetched.
92
113
  - **"Network Error: 402 Payment Required"**
93
114
  - Check your wallet -- likely you ran out of USDC to pay!
115
+ - **"Network Error: 429 Too Many Requests"**
116
+ - Back off and retry, max 5-10 requests per second.
94
117
  - **"Network Error: 502 Bad Gateway"**
95
- - URLs that pass the robots.txt check but are blocked anyway via 403 or other tactics may error like this.
118
+ - URLs that pass their robots.txt check but are blocked anyway via 403 or other tactics may error like this.
96
119
  - **"503 Service Temporarily Unavailable"**
97
- - Likely encountering Minifetch rate-limiting or upstream timeout errors on the target URL. You may try again but limit requests to 5-10 at a time. Bulk fetches coming soon.
120
+ - Likely encountering upstream timeout errors on the target URL.
98
121
  - **"InvalidURLError: Invalid url ${url}"**
99
- - The URL you passed in is malformed in some way, correct it and try again.
100
-
122
+ - The URL is malformed in some way, correct it and try again.
123
+ ---
101
124
  ### Service Limitations
102
125
  Minifetch only extracts publicly available metadata and content from pages accessible without authentication and javascript execution.
103
126
 
104
- What Minifetch does NOT do:
105
-
127
+ What Minifetch does *NOT* do:
106
128
  - Ignore robots.txt directives
107
129
  - Access authenticated or logged-in content
108
130
  - Create accounts or log into user sessions
@@ -112,8 +134,8 @@ What Minifetch does NOT do:
112
134
  What Minifetch does NOT do *currently* but may offer in the future as an add-on:
113
135
  - Javascript execution
114
136
 
115
- ## x402 Best Practices
116
-
137
+ ---
138
+ ### x402 Best Practices
117
139
  - LLMs & Agents should *never* have direct access to your private key. They *will* expose it!
118
140
  - Only keep a small amount of USDC in the account whose private key you use.
119
141
  - Keep that account separate from the rest of your onchain funds.
@@ -1,24 +1,25 @@
1
1
  import { initConfig } from "./init.js";
2
2
  import { validateAndNormalizeUrl } from "./utils/validation.js";
3
- import { handlePayment } from "./utils/payment.js";
3
+ import { handlePayment, handleApiKeyRequest } from "./utils/payment.js";
4
4
  import { InvalidUrlError, RobotsBlockedError, PaymentFailedError, ExtractionFailedError, NetworkError, } from "./types/errors.js";
5
5
  /**
6
- * Main Minifetch API client
7
- * Provides methods to check URLs and extract metadata/links/preview/content
6
+ * Main Minifetch API client.
7
+ * Supports two auth modes:
8
+ * - x402: crypto micropayments via Coinbase x402 (pass network + privateKey)
9
+ * - apiKey: Stripe-backed credits (pass apiKey: "mf_prod_..." or "mf_dev_...")
8
10
  */
9
11
  export class MinifetchClient {
10
12
  config;
11
13
  baseUrl;
12
14
  /**
13
- *
14
- * @param config
15
+ * @param config - Either { network, privateKey } for x402 or { apiKey } for API key auth
15
16
  */
16
17
  constructor(config) {
17
18
  this.config = initConfig(config);
18
19
  this.baseUrl = this.config.apiBaseUrl;
19
20
  }
20
21
  /**
21
- * Check if URL is allowed by robots.txt (free preflight check)
22
+ * Check if URL is allowed by robots.txt (free preflight check — no auth required)
22
23
  *
23
24
  * @param url
24
25
  * @throws {InvalidUrlError} if URL is invalid
@@ -26,17 +27,13 @@ export class MinifetchClient {
26
27
  */
27
28
  async preflightUrlCheck(url) {
28
29
  try {
29
- // Validate and normalize URL
30
30
  const normalizedUrl = validateAndNormalizeUrl(url);
31
- // Build request URL (free endpoint, no payment)
32
- const endpoint = `/api/v1/free/preflight/url-check?url=${encodeURIComponent(normalizedUrl)}`;
33
- const requestUrl = `${this.baseUrl}${endpoint}`;
31
+ const requestUrl = `${this.baseUrl}/api/v1/free/preflight/url-check?url=${encodeURIComponent(normalizedUrl)}`;
34
32
  const response = await fetch(requestUrl);
35
33
  if (!response.ok) {
36
34
  throw new NetworkError(`Preflight check failed: ${response.status} ${response.statusText}`);
37
35
  }
38
- const data = (await response.json());
39
- return data;
36
+ return (await response.json());
40
37
  }
41
38
  catch (error) {
42
39
  if (error instanceof InvalidUrlError || error instanceof NetworkError) {
@@ -46,230 +43,208 @@ export class MinifetchClient {
46
43
  }
47
44
  }
48
45
  /**
49
- * Extract URL metadata (paid, requires x402 payment)
46
+ * Extract URL metadata (paid endpoint)
50
47
  *
51
48
  * @param url
52
49
  * @param options
50
+ * @param options.verbosity - "standard" (default) or "full"
53
51
  * @param options.includeResponseBody
54
- * @param options.verbosity - Controls response detail level: "standard" (default) or "full"
55
52
  * @throws {InvalidUrlError} if URL is invalid
56
53
  * @throws {ExtractionFailedError} various reasons, check README
57
- * @throws {PaymentFailedError} if payment fails
54
+ * @throws {PaymentFailedError} if x402 payment fails
58
55
  * @throws {NetworkError} various reasons, check README
59
56
  */
60
57
  async extractUrlMetadata(url, options) {
61
58
  try {
62
- // Validate and normalize URL
63
59
  const normalizedUrl = validateAndNormalizeUrl(url);
64
- // Build request URL with optional params
65
60
  const params = new URLSearchParams({ url: normalizedUrl });
66
- if (options?.verbosity) {
61
+ if (options?.verbosity)
67
62
  params.set("verbosity", options.verbosity);
68
- }
69
- if (options?.includeResponseBody) {
63
+ if (options?.includeResponseBody)
70
64
  params.set("includeResponseBody", "true");
71
- }
72
- // Build request URL - convert params to string
73
- const endpoint = `/api/v1/x402/extract/url-metadata?${params.toString()}`;
74
- const requestUrl = `${this.baseUrl}${endpoint}`;
75
- // Make request with x402 payment handling
76
- const { response, payment } = await handlePayment(requestUrl, this.config);
77
- // Check if extraction succeeded
78
- if (!response.ok) {
79
- throw new ExtractionFailedError(normalizedUrl, `Metadata extraction failed: ${response.status} ${response.statusText}`);
80
- }
81
- const data = (await response.json());
82
- return {
83
- success: data.success,
84
- results: data.results,
85
- payment,
86
- };
65
+ const requestUrl = `${this.baseUrl}${this._extractPath("url-metadata")}?${params.toString()}`;
66
+ return await this._makeRequest(requestUrl, normalizedUrl, "Metadata extraction");
87
67
  }
88
68
  catch (error) {
89
- if (error instanceof InvalidUrlError ||
90
- error instanceof ExtractionFailedError ||
91
- error instanceof PaymentFailedError ||
92
- error instanceof NetworkError) {
93
- throw error;
94
- }
95
- throw new ExtractionFailedError(url, `Metadata extraction failed: ${error instanceof Error ? error.message : "Unknown error"}`);
69
+ return this._rethrowExtraction(error, url, "Metadata extraction");
96
70
  }
97
71
  }
98
72
  /**
99
- * Extract URL links (paid, requires x402 payment)
73
+ * Extract URL links (paid endpoint)
100
74
  *
101
75
  * @param url
102
76
  * @throws {InvalidUrlError} if URL is invalid
103
77
  * @throws {ExtractionFailedError} various reasons, check README
104
- * @throws {PaymentFailedError} if payment fails
78
+ * @throws {PaymentFailedError} if x402 payment fails
105
79
  * @throws {NetworkError} various reasons, check README
106
80
  */
107
81
  async extractUrlLinks(url) {
108
82
  try {
109
83
  const normalizedUrl = validateAndNormalizeUrl(url);
110
- const endpoint = `/api/v1/x402/extract/url-links?url=${encodeURIComponent(normalizedUrl)}`;
111
- const requestUrl = `${this.baseUrl}${endpoint}`;
112
- // Make request with x402 payment handling
113
- const { response, payment } = await handlePayment(requestUrl, this.config);
114
- if (!response.ok) {
115
- throw new ExtractionFailedError(normalizedUrl, `Links extraction failed: ${response.status} ${response.statusText}`);
116
- }
117
- const data = (await response.json());
118
- return {
119
- success: data.success,
120
- results: data.results,
121
- payment,
122
- };
84
+ const requestUrl = `${this.baseUrl}${this._extractPath("url-links")}?url=${encodeURIComponent(normalizedUrl)}`;
85
+ return await this._makeRequest(requestUrl, normalizedUrl, "Links extraction");
123
86
  }
124
87
  catch (error) {
125
- if (error instanceof InvalidUrlError ||
126
- error instanceof ExtractionFailedError ||
127
- error instanceof PaymentFailedError ||
128
- error instanceof NetworkError) {
129
- throw error;
130
- }
131
- throw new ExtractionFailedError(url, `Links extraction failed: ${error instanceof Error ? error.message : "Unknown error"}`);
88
+ return this._rethrowExtraction(error, url, "Links extraction");
132
89
  }
133
90
  }
134
91
  /**
135
- * Extract URL preview (paid, requires x402 payment)
92
+ * Extract URL preview (paid endpoint)
136
93
  *
137
94
  * @param url
138
95
  * @throws {InvalidUrlError} if URL is invalid
139
96
  * @throws {ExtractionFailedError} various reasons, check README
140
- * @throws {PaymentFailedError} if payment fails
97
+ * @throws {PaymentFailedError} if x402 payment fails
141
98
  * @throws {NetworkError} various reasons, check README
142
99
  */
143
100
  async extractUrlPreview(url) {
144
101
  try {
145
- // Validate and normalize URL
146
102
  const normalizedUrl = validateAndNormalizeUrl(url);
147
- // Build request URL
148
- const endpoint = `/api/v1/x402/extract/url-preview?url=${encodeURIComponent(normalizedUrl)}`;
149
- const requestUrl = `${this.baseUrl}${endpoint}`;
150
- // Make request with x402 payment handling
151
- const { response, payment } = await handlePayment(requestUrl, this.config);
152
- // Check if extraction succeeded
153
- if (!response.ok) {
154
- throw new ExtractionFailedError(normalizedUrl, `Preview extraction failed: ${response.status} ${response.statusText}`);
155
- }
156
- const data = (await response.json());
157
- return {
158
- success: data.success,
159
- results: data.results,
160
- payment,
161
- };
103
+ const requestUrl = `${this.baseUrl}${this._extractPath("url-preview")}?url=${encodeURIComponent(normalizedUrl)}`;
104
+ return await this._makeRequest(requestUrl, normalizedUrl, "Preview extraction");
162
105
  }
163
106
  catch (error) {
164
- if (error instanceof InvalidUrlError ||
165
- error instanceof ExtractionFailedError ||
166
- error instanceof PaymentFailedError ||
167
- error instanceof NetworkError) {
168
- throw error;
169
- }
170
- throw new ExtractionFailedError(url, `Preview extraction failed: ${error instanceof Error ? error.message : "Unknown error"}`);
107
+ return this._rethrowExtraction(error, url, "Preview extraction");
171
108
  }
172
109
  }
173
110
  /**
174
- * Extract URL content as markdown (paid, requires x402 payment)
111
+ * Extract URL content as markdown (paid endpoint)
175
112
  *
176
113
  * @param url
177
114
  * @param options
178
115
  * @param options.includeMediaUrls
179
116
  * @throws {InvalidUrlError} if URL is invalid
180
117
  * @throws {ExtractionFailedError} various reasons, check README
181
- * @throws {PaymentFailedError} if payment fails
118
+ * @throws {PaymentFailedError} if x402 payment fails
182
119
  * @throws {NetworkError} various reasons, check README
183
120
  */
184
121
  async extractUrlContent(url, options) {
185
122
  try {
186
- // Validate and normalize URL
187
123
  const normalizedUrl = validateAndNormalizeUrl(url);
188
- // Build request URL with optional params
189
124
  const params = new URLSearchParams({ url: normalizedUrl });
190
- if (options?.includeMediaUrls) {
125
+ if (options?.includeMediaUrls)
191
126
  params.set("includeMediaUrls", "true");
192
- }
193
- const endpoint = `/api/v1/x402/extract/url-content?${params.toString()}`;
194
- const requestUrl = `${this.baseUrl}${endpoint}`;
195
- // Make request with x402 payment handling
196
- const { response, payment } = await handlePayment(requestUrl, this.config);
197
- // Check if extraction succeeded
198
- if (!response.ok) {
199
- throw new ExtractionFailedError(normalizedUrl, `Content extraction failed: ${response.status} ${response.statusText}`);
200
- }
201
- const data = (await response.json());
202
- return {
203
- success: data.success,
204
- results: data.results,
205
- payment,
206
- };
127
+ const requestUrl = `${this.baseUrl}${this._extractPath("url-content")}?${params.toString()}`;
128
+ return await this._makeRequest(requestUrl, normalizedUrl, "Content extraction");
207
129
  }
208
130
  catch (error) {
209
- if (error instanceof InvalidUrlError ||
210
- error instanceof ExtractionFailedError ||
211
- error instanceof PaymentFailedError ||
212
- error instanceof NetworkError) {
213
- throw error;
214
- }
215
- throw new ExtractionFailedError(url, `Content extraction failed: ${error instanceof Error ? error.message : "Unknown error"}`);
131
+ return this._rethrowExtraction(error, url, "Content extraction");
216
132
  }
217
133
  }
218
134
  /**
219
- * Check URL and extract metadata in one call
220
- * Throws RobotsBlockedError if robots.txt blocks the URL
135
+ * Check URL then extract metadata in one call.
136
+ * Throws RobotsBlockedError if robots.txt blocks the URL.
221
137
  *
222
138
  * @param url
223
139
  * @param options
224
- * @param options.verbosity - Controls response detail level: "standard" (default) or "full"
225
- * @param options.includeResponseBody - Include raw response body in result
140
+ * @param options.verbosity - "standard" (default) or "full"
141
+ * @param options.includeResponseBody
226
142
  */
227
143
  async checkAndExtractUrlMetadata(url, options) {
228
- const checkResponse = await this.preflightUrlCheck(url);
229
- if (!checkResponse.results[0]?.data?.allowed) {
230
- throw new RobotsBlockedError(url, checkResponse.results[0]?.data?.message || "URL is blocked by robots.txt");
231
- }
144
+ await this._preflightOrThrow(url);
232
145
  return this.extractUrlMetadata(url, options);
233
146
  }
234
147
  /**
235
- * Check URL and extract links in one call
236
- * Throws RobotsBlockedError if robots.txt blocks the URL
148
+ * Check URL then extract links in one call.
149
+ * Throws RobotsBlockedError if robots.txt blocks the URL.
237
150
  *
238
151
  * @param url
239
152
  */
240
153
  async checkAndExtractUrlLinks(url) {
241
- const checkResponse = await this.preflightUrlCheck(url);
242
- if (!checkResponse.results[0]?.data?.allowed) {
243
- throw new RobotsBlockedError(url, checkResponse.results[0]?.data?.message || "URL is blocked by robots.txt");
244
- }
154
+ await this._preflightOrThrow(url);
245
155
  return this.extractUrlLinks(url);
246
156
  }
247
157
  /**
248
- * Check URL and extract preview in one call
249
- * Throws RobotsBlockedError if robots.txt blocks the URL
158
+ * Check URL then extract preview in one call.
159
+ * Throws RobotsBlockedError if robots.txt blocks the URL.
250
160
  *
251
161
  * @param url
252
162
  */
253
163
  async checkAndExtractUrlPreview(url) {
254
- const checkResponse = await this.preflightUrlCheck(url);
255
- if (!checkResponse.results[0]?.data?.allowed) {
256
- throw new RobotsBlockedError(url, checkResponse.results[0]?.data?.message || "URL is blocked by robots.txt");
257
- }
164
+ await this._preflightOrThrow(url);
258
165
  return this.extractUrlPreview(url);
259
166
  }
260
167
  /**
261
- * Check URL and extract content in one call
262
- * Throws RobotsBlockedError if robots.txt blocks the URL
168
+ * Check URL then extract content in one call.
169
+ * Throws RobotsBlockedError if robots.txt blocks the URL.
263
170
  *
264
171
  * @param url
265
172
  * @param options
266
173
  * @param options.includeMediaUrls
267
174
  */
268
175
  async checkAndExtractUrlContent(url, options) {
176
+ await this._preflightOrThrow(url);
177
+ return this.extractUrlContent(url, options);
178
+ }
179
+ // ---------------------------------------------------------------------------
180
+ // Private helpers
181
+ // ---------------------------------------------------------------------------
182
+ /**
183
+ * Returns the correct extract path segment based on auth mode.
184
+ * x402 → /api/v1/x402/extract/<endpoint>
185
+ * apiKey → /api/v1/extract/<endpoint>
186
+ *
187
+ * @param endpoint
188
+ */
189
+ _extractPath(endpoint) {
190
+ return this.config.authMode === "x402"
191
+ ? `/api/v1/x402/extract/${endpoint}`
192
+ : `/api/v1/extract/${endpoint}`;
193
+ }
194
+ /**
195
+ * Dispatch to the correct request handler based on auth mode, then
196
+ * normalize the response into PaidEndpointResponse.
197
+ * Note: payment field is only present for x402 responses.
198
+ *
199
+ * @param requestUrl
200
+ * @param normalizedUrl
201
+ * @param label - used in error messages
202
+ */
203
+ async _makeRequest(requestUrl, normalizedUrl, label) {
204
+ if (this.config.authMode === "x402") {
205
+ const { response, payment } = await handlePayment(requestUrl, this.config);
206
+ if (!response.ok) {
207
+ throw new ExtractionFailedError(normalizedUrl, `${label} failed: ${response.status} ${response.statusText}`);
208
+ }
209
+ const data = (await response.json());
210
+ return { success: data.success, results: data.results, payment };
211
+ }
212
+ else {
213
+ const { response } = await handleApiKeyRequest(requestUrl, this.config);
214
+ if (!response.ok) {
215
+ throw new ExtractionFailedError(normalizedUrl, `${label} failed: ${response.status} ${response.statusText}`);
216
+ }
217
+ const data = (await response.json());
218
+ // payment field intentionally omitted for apiKey auth — not applicable
219
+ return { success: data.success, results: data.results };
220
+ }
221
+ }
222
+ /**
223
+ * Preflight check helper — throws RobotsBlockedError if not allowed
224
+ *
225
+ * @param url
226
+ */
227
+ async _preflightOrThrow(url) {
269
228
  const checkResponse = await this.preflightUrlCheck(url);
270
229
  if (!checkResponse.results[0]?.data?.allowed) {
271
230
  throw new RobotsBlockedError(url, checkResponse.results[0]?.data?.message || "URL is blocked by robots.txt");
272
231
  }
273
- return this.extractUrlContent(url, options);
232
+ }
233
+ /**
234
+ * Re-throw known error types, wrapping unknowns in ExtractionFailedError
235
+ *
236
+ * @param error
237
+ * @param url
238
+ * @param label
239
+ */
240
+ _rethrowExtraction(error, url, label) {
241
+ if (error instanceof InvalidUrlError ||
242
+ error instanceof ExtractionFailedError ||
243
+ error instanceof PaymentFailedError ||
244
+ error instanceof NetworkError ||
245
+ error instanceof RobotsBlockedError) {
246
+ throw error;
247
+ }
248
+ throw new ExtractionFailedError(url, `${label} failed: ${error instanceof Error ? error.message : "Unknown error"}`);
274
249
  }
275
250
  }
package/dist/esm/init.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { VALID_NETWORKS } from "./types/config.js";
2
2
  import { ConfigurationError } from "./types/errors.js";
3
3
  /**
4
- * Default configuration values
4
+ * Default configuration values for x402 mode
5
5
  * Exported for testing purposes
6
6
  */
7
7
  export const DEFAULTS = {
@@ -19,32 +19,65 @@ export const DEFAULTS = {
19
19
  solana: "https://explorer.solana.com/tx",
20
20
  },
21
21
  };
22
+ /** Prod API key prefix */
23
+ const PROD_KEY_PREFIX = "mf_prod_";
24
+ /** Dev API key prefix */
25
+ const DEV_KEY_PREFIX = "mf_dev_";
22
26
  /**
23
- * Validate & initialize client configuration
27
+ * Validate & initialize client configuration.
28
+ * Accepts either X402Config (network + privateKey) or ApiKeyConfig (apiKey).
24
29
  *
25
30
  * @param config
26
31
  */
27
32
  export function initConfig(config) {
28
- // Validate private key is provided
33
+ if ("apiKey" in config) {
34
+ return initApiKeyConfig(config);
35
+ }
36
+ return initX402Config(config);
37
+ }
38
+ /**
39
+ * Initialize config for API key auth mode.
40
+ * Infers apiBaseUrl from the key prefix (mf_prod_* → prod, mf_dev_* → dev).
41
+ *
42
+ * @param config
43
+ */
44
+ function initApiKeyConfig(config) {
45
+ const { apiKey } = config;
46
+ if (!apiKey || apiKey.trim() === "") {
47
+ throw new ConfigurationError("API key is required");
48
+ }
49
+ if (!apiKey.startsWith(PROD_KEY_PREFIX) && !apiKey.startsWith(DEV_KEY_PREFIX)) {
50
+ throw new ConfigurationError(`Invalid API key format: must start with "${PROD_KEY_PREFIX}" or "${DEV_KEY_PREFIX}"`);
51
+ }
52
+ const apiBaseUrl = apiKey.startsWith(PROD_KEY_PREFIX)
53
+ ? "https://minifetch.com"
54
+ : "http://localhost:4021";
55
+ return {
56
+ authMode: "apiKey",
57
+ apiKey,
58
+ apiBaseUrl,
59
+ };
60
+ }
61
+ /**
62
+ * Initialize config for x402 crypto payment mode.
63
+ *
64
+ * @param config
65
+ */
66
+ function initX402Config(config) {
29
67
  if (!config.privateKey || config.privateKey.trim() === "") {
30
68
  throw new ConfigurationError("Private key is required");
31
69
  }
32
- // Validate network
33
70
  const network = config.network || DEFAULTS.network;
34
71
  if (!VALID_NETWORKS.includes(network)) {
35
72
  throw new ConfigurationError(`Invalid network: "${network}". Must be one of: ${VALID_NETWORKS.join(", ")}`);
36
73
  }
37
- // Validate private key format based on network
38
74
  validatePrivateKey(config.privateKey, network);
39
- // Build initalized config
40
- const apiBaseUrl = DEFAULTS.apiBaseUrls[network];
41
- const explorerUrl = DEFAULTS.explorerUrls[network];
42
- // console.log(`config.ts: network: ${network} apiBaseUrl: ${apiBaseUrl}`)
43
75
  return {
76
+ authMode: "x402",
44
77
  network,
45
78
  privateKey: config.privateKey,
46
- apiBaseUrl,
47
- explorerUrl,
79
+ apiBaseUrl: DEFAULTS.apiBaseUrls[network],
80
+ explorerUrl: DEFAULTS.explorerUrls[network],
48
81
  };
49
82
  }
50
83
  /**
@@ -55,13 +88,11 @@ export function initConfig(config) {
55
88
  */
56
89
  function validatePrivateKey(privateKey, network) {
57
90
  if (network === "solana" || network === "solana-devnet") {
58
- // Solana: base58 encoded, typically 88 characters
59
91
  if (!/^[1-9A-HJ-NP-Za-km-z]{87,88}$/.test(privateKey)) {
60
92
  throw new ConfigurationError("Invalid Solana private key format (expected base58 string)");
61
93
  }
62
94
  }
63
95
  else {
64
- // EVM: hex string, 64 characters (with or without 0x prefix)
65
96
  const cleanKey = privateKey.startsWith("0x") ? privateKey.slice(2) : privateKey;
66
97
  if (!/^[0-9a-fA-F]{64}$/.test(cleanKey)) {
67
98
  throw new ConfigurationError("Invalid EVM private key format (expected hex string that starts with 0x)");
@@ -16,50 +16,36 @@ import { PaymentFailedError, NetworkError } from "../types/errors.js";
16
16
  * @param config
17
17
  */
18
18
  export async function handlePayment(url, config) {
19
- if (!config.privateKey) {
20
- throw new PaymentFailedError("Private key required for payments");
21
- }
22
19
  try {
23
- // Create x402 client and signer based on network type
24
20
  const _x402Client = new x402Client();
25
21
  let payer;
26
- const isEvm = config.network.startsWith("base");
27
- const isSolana = config.network.startsWith("solana");
22
+ const isEvm = config.network?.startsWith("base");
23
+ const isSolana = config.network?.startsWith("solana");
28
24
  if (isEvm) {
29
- // Initialize EVM signer and register scheme
30
25
  const signer = privateKeyToAccount(config.privateKey);
31
- const evmSigner = signer; // Type assertion for EVM
26
+ const evmSigner = signer;
32
27
  registerExactEvmScheme(_x402Client, { signer: evmSigner });
33
28
  payer = signer.address;
34
29
  }
35
30
  else if (isSolana) {
36
- // Initialize Solana signer and register scheme
31
+ if (!config.privateKey)
32
+ throw new PaymentFailedError('privateKey is required for Solana payments');
37
33
  const privateKeyBytes = bs58.decode(config.privateKey);
38
34
  const signer = await createKeyPairSignerFromBytes(privateKeyBytes);
39
- const svmSigner = signer; // Type assertion for Solana
35
+ const svmSigner = signer;
40
36
  registerExactSvmScheme(_x402Client, { signer: svmSigner });
41
37
  payer = signer.address;
42
38
  }
43
39
  else {
44
40
  throw new PaymentFailedError(`Unsupported network: ${config.network}`);
45
41
  }
46
- // Wrap fetch with payment capabilities
47
42
  const fetchWithPayment = wrapFetchWithPayment(fetch, _x402Client);
48
- // Make request with payment handling
49
- // console.log("Attempting to fetch w payment:", url);
50
- const response = await fetchWithPayment(url, {
51
- method: "GET",
52
- });
43
+ const response = await fetchWithPayment(url, { method: "GET" });
53
44
  if (!response.ok) {
54
45
  throw new NetworkError(`Request failed: ${response.status} ${response.statusText}`);
55
46
  }
56
- // Extract payment receipt from response headers
57
47
  const httpClient = new x402HTTPClient(_x402Client);
58
48
  const paymentResponse = httpClient.getPaymentSettleResponse(name => response.headers.get(name));
59
- // DEBUG:
60
- // console.log("Payment response:");
61
- // console.log(paymentResponse);
62
- // Build payment info for user
63
49
  const payment = {
64
50
  success: true,
65
51
  payer,
@@ -78,8 +64,27 @@ export async function handlePayment(url, config) {
78
64
  throw new PaymentFailedError(`Payment failed: ${error instanceof Error ? error.message : "Unknown error"}`);
79
65
  }
80
66
  }
81
- // Helper fn to build explorer links
82
67
  /**
68
+ * Handle API key auth flow — simple Bearer token request, no crypto.
69
+ * No payment info is returned (not applicable for this auth mode).
70
+ *
71
+ * @param url
72
+ * @param config
73
+ */
74
+ export async function handleApiKeyRequest(url, config) {
75
+ const response = await fetch(url, {
76
+ method: "GET",
77
+ headers: {
78
+ Authorization: `Bearer ${config.apiKey}`,
79
+ },
80
+ });
81
+ if (!response.ok) {
82
+ throw new NetworkError(`Request failed: ${response.status} ${response.statusText}`);
83
+ }
84
+ return { response };
85
+ }
86
+ /**
87
+ * Build block explorer link for a transaction hash
83
88
  *
84
89
  * @param config
85
90
  * @param txHash
@@ -1,19 +1,20 @@
1
1
  import type { ClientConfig } from "./types/config.js";
2
2
  import type { PreflightCheckResponse, PaidEndpointResponse } from "./types/responses.js";
3
3
  /**
4
- * Main Minifetch API client
5
- * Provides methods to check URLs and extract metadata/links/preview/content
4
+ * Main Minifetch API client.
5
+ * Supports two auth modes:
6
+ * - x402: crypto micropayments via Coinbase x402 (pass network + privateKey)
7
+ * - apiKey: Stripe-backed credits (pass apiKey: "mf_prod_..." or "mf_dev_...")
6
8
  */
7
9
  export declare class MinifetchClient {
8
10
  private config;
9
11
  private baseUrl;
10
12
  /**
11
- *
12
- * @param config
13
+ * @param config - Either { network, privateKey } for x402 or { apiKey } for API key auth
13
14
  */
14
15
  constructor(config: ClientConfig);
15
16
  /**
16
- * Check if URL is allowed by robots.txt (free preflight check)
17
+ * Check if URL is allowed by robots.txt (free preflight check — no auth required)
17
18
  *
18
19
  * @param url
19
20
  * @throws {InvalidUrlError} if URL is invalid
@@ -21,15 +22,15 @@ export declare class MinifetchClient {
21
22
  */
22
23
  preflightUrlCheck(url: string): Promise<PreflightCheckResponse>;
23
24
  /**
24
- * Extract URL metadata (paid, requires x402 payment)
25
+ * Extract URL metadata (paid endpoint)
25
26
  *
26
27
  * @param url
27
28
  * @param options
29
+ * @param options.verbosity - "standard" (default) or "full"
28
30
  * @param options.includeResponseBody
29
- * @param options.verbosity - Controls response detail level: "standard" (default) or "full"
30
31
  * @throws {InvalidUrlError} if URL is invalid
31
32
  * @throws {ExtractionFailedError} various reasons, check README
32
- * @throws {PaymentFailedError} if payment fails
33
+ * @throws {PaymentFailedError} if x402 payment fails
33
34
  * @throws {NetworkError} various reasons, check README
34
35
  */
35
36
  extractUrlMetadata(url: string, options?: {
@@ -37,69 +38,69 @@ export declare class MinifetchClient {
37
38
  includeResponseBody?: boolean;
38
39
  }): Promise<PaidEndpointResponse>;
39
40
  /**
40
- * Extract URL links (paid, requires x402 payment)
41
+ * Extract URL links (paid endpoint)
41
42
  *
42
43
  * @param url
43
44
  * @throws {InvalidUrlError} if URL is invalid
44
45
  * @throws {ExtractionFailedError} various reasons, check README
45
- * @throws {PaymentFailedError} if payment fails
46
+ * @throws {PaymentFailedError} if x402 payment fails
46
47
  * @throws {NetworkError} various reasons, check README
47
48
  */
48
49
  extractUrlLinks(url: string): Promise<PaidEndpointResponse>;
49
50
  /**
50
- * Extract URL preview (paid, requires x402 payment)
51
+ * Extract URL preview (paid endpoint)
51
52
  *
52
53
  * @param url
53
54
  * @throws {InvalidUrlError} if URL is invalid
54
55
  * @throws {ExtractionFailedError} various reasons, check README
55
- * @throws {PaymentFailedError} if payment fails
56
+ * @throws {PaymentFailedError} if x402 payment fails
56
57
  * @throws {NetworkError} various reasons, check README
57
58
  */
58
59
  extractUrlPreview(url: string): Promise<PaidEndpointResponse>;
59
60
  /**
60
- * Extract URL content as markdown (paid, requires x402 payment)
61
+ * Extract URL content as markdown (paid endpoint)
61
62
  *
62
63
  * @param url
63
64
  * @param options
64
65
  * @param options.includeMediaUrls
65
66
  * @throws {InvalidUrlError} if URL is invalid
66
67
  * @throws {ExtractionFailedError} various reasons, check README
67
- * @throws {PaymentFailedError} if payment fails
68
+ * @throws {PaymentFailedError} if x402 payment fails
68
69
  * @throws {NetworkError} various reasons, check README
69
70
  */
70
71
  extractUrlContent(url: string, options?: {
71
72
  includeMediaUrls?: boolean;
72
73
  }): Promise<PaidEndpointResponse>;
73
74
  /**
74
- * Check URL and extract metadata in one call
75
- * Throws RobotsBlockedError if robots.txt blocks the URL
75
+ * Check URL then extract metadata in one call.
76
+ * Throws RobotsBlockedError if robots.txt blocks the URL.
76
77
  *
77
78
  * @param url
78
79
  * @param options
79
- * @param options.verbosity - Controls response detail level: "standard" (default) or "full"
80
- * @param options.includeResponseBody - Include raw response body in result
80
+ * @param options.verbosity - "standard" (default) or "full"
81
+ * @param options.includeResponseBody
81
82
  */
82
83
  checkAndExtractUrlMetadata(url: string, options?: {
83
84
  verbosity?: "standard" | "full";
84
85
  includeResponseBody?: boolean;
85
86
  }): Promise<PaidEndpointResponse>;
86
87
  /**
87
- * Check URL and extract links in one call
88
- * Throws RobotsBlockedError if robots.txt blocks the URL
88
+ * Check URL then extract links in one call.
89
+ * Throws RobotsBlockedError if robots.txt blocks the URL.
89
90
  *
90
91
  * @param url
91
92
  */
92
93
  checkAndExtractUrlLinks(url: string): Promise<PaidEndpointResponse>;
93
94
  /**
94
- * Check URL and extract preview in one call
95
- * Throws RobotsBlockedError if robots.txt blocks the URL
95
+ * Check URL then extract preview in one call.
96
+ * Throws RobotsBlockedError if robots.txt blocks the URL.
96
97
  *
97
98
  * @param url
98
99
  */
99
100
  checkAndExtractUrlPreview(url: string): Promise<PaidEndpointResponse>;
100
101
  /**
101
- * Check URL and extract content in one call
102
- * Throws RobotsBlockedError if robots.txt blocks the URL
102
+ * Check URL then extract content in one call.
103
+ * Throws RobotsBlockedError if robots.txt blocks the URL.
103
104
  *
104
105
  * @param url
105
106
  * @param options
@@ -108,4 +109,36 @@ export declare class MinifetchClient {
108
109
  checkAndExtractUrlContent(url: string, options?: {
109
110
  includeMediaUrls?: boolean;
110
111
  }): Promise<PaidEndpointResponse>;
112
+ /**
113
+ * Returns the correct extract path segment based on auth mode.
114
+ * x402 → /api/v1/x402/extract/<endpoint>
115
+ * apiKey → /api/v1/extract/<endpoint>
116
+ *
117
+ * @param endpoint
118
+ */
119
+ private _extractPath;
120
+ /**
121
+ * Dispatch to the correct request handler based on auth mode, then
122
+ * normalize the response into PaidEndpointResponse.
123
+ * Note: payment field is only present for x402 responses.
124
+ *
125
+ * @param requestUrl
126
+ * @param normalizedUrl
127
+ * @param label - used in error messages
128
+ */
129
+ private _makeRequest;
130
+ /**
131
+ * Preflight check helper — throws RobotsBlockedError if not allowed
132
+ *
133
+ * @param url
134
+ */
135
+ private _preflightOrThrow;
136
+ /**
137
+ * Re-throw known error types, wrapping unknowns in ExtractionFailedError
138
+ *
139
+ * @param error
140
+ * @param url
141
+ * @param label
142
+ */
143
+ private _rethrowExtraction;
111
144
  }
@@ -1,6 +1,6 @@
1
1
  import type { ClientConfig, InitializedConfig, Network } from "./types/config.js";
2
2
  /**
3
- * Default configuration values
3
+ * Default configuration values for x402 mode
4
4
  * Exported for testing purposes
5
5
  */
6
6
  export declare const DEFAULTS: {
@@ -19,7 +19,8 @@ export declare const DEFAULTS: {
19
19
  };
20
20
  };
21
21
  /**
22
- * Validate & initialize client configuration
22
+ * Validate & initialize client configuration.
23
+ * Accepts either X402Config (network + privateKey) or ApiKeyConfig (apiKey).
23
24
  *
24
25
  * @param config
25
26
  */
@@ -1,21 +1,31 @@
1
1
  export declare const VALID_NETWORKS: readonly ["base", "base-sepolia", "solana", "solana-devnet"];
2
2
  export type Network = (typeof VALID_NETWORKS)[number];
3
- export interface ClientConfig {
4
- /**
5
- * Blockchain network to use for payments
6
- *
7
- * @default 'base'
8
- */
3
+ /**
4
+ * Config for x402 crypto micropayment auth
5
+ */
6
+ export interface X402Config {
7
+ /** Blockchain network to use for payments @default 'base' */
9
8
  network: Network;
10
- /**
11
- * Private key for signing transactions -
12
- * hex string for EVM, base58 for Solana
13
- */
9
+ /** Private key for signing transactions — hex for EVM, base58 for Solana */
14
10
  privateKey: string;
15
11
  }
12
+ /**
13
+ * Config for Stripe-backed API key auth.
14
+ * apiBaseUrl is inferred from the key prefix:
15
+ * mf_prod_* → https://minifetch.com
16
+ * mf_dev_* → http://localhost:4021
17
+ */
18
+ export interface ApiKeyConfig {
19
+ apiKey: string;
20
+ }
21
+ export type ClientConfig = X402Config | ApiKeyConfig;
22
+ /** Discriminant for internal use after init */
23
+ export type AuthMode = "x402" | "apiKey";
16
24
  export interface InitializedConfig {
17
- network: Network;
18
- privateKey: string;
25
+ authMode: AuthMode;
26
+ network?: Network;
27
+ privateKey?: string;
28
+ explorerUrl?: string;
29
+ apiKey?: string;
19
30
  apiBaseUrl: string;
20
- explorerUrl: string;
21
31
  }
@@ -29,12 +29,12 @@ export interface PaidEndpointResponse {
29
29
  }>;
30
30
  /**
31
31
  * Payment information
32
- * Only present for paid endpoints when request was successful
32
+ * Only present for paid x402 endpoints when request was successful
33
33
  * */
34
34
  payment?: PaymentInfo;
35
35
  }
36
36
  /**
37
- * Payment information included with successful paid API responses
37
+ * Payment information included with successful paid x402 API responses
38
38
  */
39
39
  export interface PaymentInfo {
40
40
  /** Whether the payment was successful **/
@@ -14,3 +14,13 @@ export declare function handlePayment(url: string, config: InitializedConfig): P
14
14
  response: Response;
15
15
  payment?: PaymentInfo;
16
16
  }>;
17
+ /**
18
+ * Handle API key auth flow — simple Bearer token request, no crypto.
19
+ * No payment info is returned (not applicable for this auth mode).
20
+ *
21
+ * @param url
22
+ * @param config
23
+ */
24
+ export declare function handleApiKeyRequest(url: string, config: InitializedConfig): Promise<{
25
+ response: Response;
26
+ }>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minifetch-api",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Minifetch.com API Client. Fetch & extract data from web pages. For AI Agents & SEO research.",
5
5
  "type": "module",
6
6
  "main": "./dist/esm/index.js",