@builtbyecho/public-api-finder 0.1.0 → 0.5.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/README.md CHANGED
@@ -2,14 +2,20 @@
2
2
 
3
3
  Find free/public APIs for agents, prototypes, demos, and integrations.
4
4
 
5
- Powered by the curated [`public-api-lists/public-api-lists`](https://github.com/public-api-lists/public-api-lists) JSON dataset.
5
+ Powered by multiple sources plus a curated best-known API layer:
6
+
7
+ - [`public-api-lists/public-api-lists`](https://github.com/public-api-lists/public-api-lists) for fast curated JSON discovery
8
+ - [`public-apis/public-apis`](https://github.com/public-apis/public-apis) for the larger canonical README list
9
+ - [`APIs-guru/openapi-directory`](https://github.com/APIs-guru/openapi-directory) for OpenAPI-backed APIs
6
10
 
7
11
  ## Quick start
8
12
 
9
13
  ```bash
10
- npx @builtbyecho/public-api-finder "weather forecast" --no-auth --https
11
- npx @builtbyecho/public-api-finder "crypto prices" --category Cryptocurrency --limit 5
12
- npx @builtbyecho/public-api-finder "jobs" --json
14
+ npx --yes --package=@builtbyecho/public-api-finder -- public-api-finder "weather forecast" --no-auth --https
15
+ npx --yes --package=@builtbyecho/public-api-finder -- public-api-finder "crypto prices" --category Cryptocurrency --limit 5
16
+ npx --yes --package=@builtbyecho/public-api-finder -- public-api-finder "jobs" --json
17
+ npx --yes --package=@builtbyecho/public-api-finder -- public-api-finder "payments" --openapi
18
+ npx --yes --package=@builtbyecho/public-api-finder -- public-api-finder "weather forecast" --no-auth --https --check
13
19
  ```
14
20
 
15
21
  ## Why
@@ -24,16 +30,109 @@ The package includes an agent skill at:
24
30
  skills/public-api-finder/SKILL.md
25
31
  ```
26
32
 
27
- The skill tells agents to prefer the CLI first, then live-check docs/endpoints before building.
33
+ The skill tells agents to prefer the CLI first, then live-check docs/endpoints before building. Use `--check` when you want the CLI to annotate whether each result URL is reachable right now.
28
34
 
29
35
  ## CLI options
30
36
 
31
37
  ```text
32
38
  --category <name> Filter by category substring
39
+ --source <name> Filter by source: public-api-lists, public-apis, apis-guru, curated
33
40
  --no-auth Only APIs with Auth = No
34
41
  --https Only HTTPS APIs
35
42
  --cors <value> Filter by CORS: Yes, No, Unknown
43
+ --openapi Only APIs with OpenAPI specs
36
44
  --limit <n> Max results
45
+ --check Live-check result URLs and annotate reachability
37
46
  --json Emit JSON
38
47
  --refresh Refresh cache
39
48
  ```
49
+
50
+ ## Bankr x402 endpoint
51
+
52
+ This repo includes a Bankr x402 Cloud endpoint scaffold at:
53
+
54
+ ```text
55
+ x402/public-api-finder/index.ts
56
+ bankr.x402.json
57
+ ```
58
+
59
+ It is configured as a paid `POST` endpoint at **$0.01 USDC per successful request** on Base. Deploy it with Bankr when ready:
60
+
61
+ ```bash
62
+ bankr x402 deploy public-api-finder
63
+ ```
64
+
65
+ The endpoint accepts:
66
+
67
+ ```json
68
+ {
69
+ "query": "weather forecast no auth cors",
70
+ "limit": 5,
71
+ "noAuth": true,
72
+ "https": true,
73
+ "cors": "Yes"
74
+ }
75
+ ```
76
+
77
+ Suggested Bankr App prompt after deployment:
78
+
79
+ ```text
80
+ Build me a public app called Public API Finder. It should have a search box,
81
+ filters for no-auth, HTTPS, CORS, category, and result limit, and a green
82
+ button labeled “Pay $0.01 & Pull”. When clicked, call my x402 endpoint
83
+ https://x402.bankr.bot/<my-wallet>/public-api-finder with bankr.x402.fetch,
84
+ show the Bankr payment confirmation, then render ranked API cards with name,
85
+ category, auth, CORS, URL, score, and description. Make it public/unlisted so
86
+ I can share it with the Bankr team.
87
+ ```
88
+
89
+ ## Hosted app / Bankr-ready credits
90
+
91
+ The package also includes a tiny zero-dependency hosted API and landing page:
92
+
93
+ ```bash
94
+ npm start
95
+ # http://localhost:8787
96
+ ```
97
+
98
+ Pricing model: **1 credit = 1 successful enriched pull**. The default public price is `$0.01` per pull, so a 100-credit top-up is `$1.00`.
99
+
100
+ ### API
101
+
102
+ ```bash
103
+ # Create a Bankr-ready top-up order
104
+ curl -X POST http://localhost:8787/api/credits/topup \
105
+ -H 'content-type: application/json' \
106
+ -H 'x-api-key: acct_demo' \
107
+ -d '{"credits":100}'
108
+
109
+ # Search, spending 1 credit only when results are returned
110
+ curl -X POST http://localhost:8787/api/search \
111
+ -H 'content-type: application/json' \
112
+ -H 'x-api-key: acct_demo' \
113
+ -d '{"query":"weather forecast no auth cors","noAuth":true,"https":true,"cors":"Yes"}'
114
+ ```
115
+
116
+ Top-ups currently return Bankr payment instructions and create a pending order. After confirming payment, grant credits with an admin token:
117
+
118
+ ```bash
119
+ PUBLIC_API_FINDER_ADMIN_TOKEN=secret npm start
120
+
121
+ curl -X POST http://localhost:8787/api/admin/credits \
122
+ -H 'content-type: application/json' \
123
+ -H 'x-admin-token: secret' \
124
+ -d '{"account":"acct_demo","credits":100,"paymentRef":"bankr-tx-or-job-id"}'
125
+ ```
126
+
127
+ Environment knobs:
128
+
129
+ ```text
130
+ PORT Server port, default 8787
131
+ PUBLIC_API_FINDER_STATE Ledger JSON path, default ./state/public-api-finder-ledger.json
132
+ PUBLIC_API_FINDER_PULL_PRICE_CENTS Price per pull, default 1
133
+ PUBLIC_API_FINDER_CREDITS_PER_PULL Credits charged per successful search, default 1
134
+ PUBLIC_API_FINDER_ADMIN_TOKEN Required for manual credit grants
135
+ BANKR_RECEIVE_ADDRESS Address shown in Bankr payment instructions
136
+ BANKR_NETWORK Default Base
137
+ BANKR_ASSET Default USDC
138
+ ```
@@ -0,0 +1,38 @@
1
+ {
2
+ "network": "base",
3
+ "currency": "USDC",
4
+ "services": {
5
+ "public-api-finder": {
6
+ "price": "0.01",
7
+ "currency": "USDC",
8
+ "network": "base",
9
+ "methods": ["POST"],
10
+ "paymentScheme": "exact",
11
+ "description": "Find and rank free/public APIs for agents and app builders from a natural-language query.",
12
+ "schema": {
13
+ "input": {
14
+ "type": "object",
15
+ "properties": {
16
+ "query": { "type": "string", "description": "Natural-language API need, e.g. weather forecast no auth cors" },
17
+ "limit": { "type": "number", "description": "Max results, 1-20" },
18
+ "category": { "type": "string", "description": "Optional category substring" },
19
+ "noAuth": { "type": "boolean", "description": "Only return APIs with no auth/key requirement" },
20
+ "https": { "type": "boolean", "description": "Only return HTTPS APIs" },
21
+ "cors": { "type": "string", "description": "Filter CORS value: Yes, No, Unknown" }
22
+ },
23
+ "required": ["query"]
24
+ },
25
+ "output": {
26
+ "type": "object",
27
+ "properties": {
28
+ "query": { "type": "string" },
29
+ "price": { "type": "string" },
30
+ "charged": { "type": "boolean" },
31
+ "resultCount": { "type": "number" },
32
+ "results": { "type": "array" }
33
+ }
34
+ }
35
+ }
36
+ }
37
+ }
38
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@builtbyecho/public-api-finder",
3
- "version": "0.1.0",
3
+ "version": "0.5.1",
4
4
  "description": "Find free/public APIs for agents and prototypes.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,12 +8,17 @@
8
8
  },
9
9
  "files": [
10
10
  "src/",
11
+ "scripts/",
11
12
  "skills/",
13
+ "x402/",
14
+ "bankr.x402.json",
12
15
  "README.md",
13
16
  "LICENSE"
14
17
  ],
15
18
  "scripts": {
16
- "test": "node --test"
19
+ "test": "node --test",
20
+ "start": "node src/app.js",
21
+ "eval:quality": "node scripts/eval-quality.mjs"
17
22
  },
18
23
  "keywords": [
19
24
  "ai",
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from 'node:child_process';
3
+
4
+ const cases = [
5
+ { q: 'free frontend-safe weather alerts for US cities', args: ['--no-auth','--https','--cors','Yes'], expectCategory: /weather/i, expectAnyName: /weather|meteo|pirate/i },
6
+ { q: 'global weather forecast hourly no key browser', args: [], expectCategory: /weather/i, expectAuth: 'No', expectCors: 'Yes' },
7
+ { q: 'rain radar severe weather alerts us no signup', args: [], expectCategory: /weather/i, expectAuth: 'No' },
8
+ { q: 'crypto token metadata logos contract addresses', args: ['--category','Cryptocurrency'], expectCategory: /cryptocurrency/i, expectAnyName: /coinmarketcap|coingecko|coinpaprika|coinbase/i },
9
+ { q: 'ethereum gas prices no auth', args: [], expectCategory: /cryptocurrency|blockchain/i, expectAuth: 'No' },
10
+ { q: 'solana token price market cap dex no key', args: [], expectCategory: /cryptocurrency/i },
11
+ { q: 'intraday stock candles historical OHLC forex', args: [], expectCategory: /finance/i, expectAnyName: /polygon|twelve|alpha|finnhub/i },
12
+ { q: 'free stock quote API no auth', args: [], expectCategory: /finance/i, expectAuth: 'No' },
13
+ { q: 'company fundamentals earnings financial statements', args: [], expectCategory: /finance/i },
14
+ { q: 'reverse geocode lat lon to address no auth', args: ['--https'], expectCategory: /geocoding|location/i, expectAuth: 'No', expectAnyName: /nominatim|geocode/i },
15
+ { q: 'map tiles routing distance matrix places', args: [], expectCategory: /geocoding|location|maps/i, expectAnyName: /mapbox|openstreetmap|google|here|tomtom/i },
16
+ { q: 'timezone from latitude longitude no auth', args: [], expectCategory: /geocoding|location/i },
17
+ { q: 'remote developer jobs salary company listings', args: [], expectCategory: /jobs/i, expectAnyName: /adzuna|graphql|usajobs|search.gov/i },
18
+ { q: 'nba player stats team standings no auth', args: [], expectCategory: /sports/i, expectAuth: 'No', expectAnyName: /balldontlie|nba/i },
19
+ { q: 'soccer fixtures odds standings predictions api key', args: [], expectCategory: /sports/i, expectAnyName: /football|sports/i },
20
+ { q: 'anime character search images ratings no auth cors', args: ['--no-auth','--https'], expectCategory: /anime/i, expectAuth: 'No', expectAnyName: /jikan/i },
21
+ { q: 'movie database posters cast ratings imdb', args: [], expectCategory: /media|video|entertainment/i, expectAnyName: /tmdb|omdb/i },
22
+ { q: 'tv episode schedule cast search no auth', args: [], expectCategory: /video|media|entertainment/i, expectAnyName: /tvmaze|tmdb/i },
23
+ { q: 'news article search by topic and country', args: [], expectCategory: /news/i, expectAnyName: /news api|gnews|currents|guardian/i },
24
+ { q: 'election campaign finance donations candidates', args: [], expectCategory: /government|open data/i, expectAnyName: /fec|data.gov/i },
25
+ { q: 'congress bills votes representatives civic data', args: [], expectCategory: /government|open data/i },
26
+ { q: 'food barcode ingredients allergens nutrition no auth cors', args: ['--no-auth','--https'], expectCategory: /food/i, expectAuth: 'No', expectAnyName: /open food facts/i },
27
+ { q: 'checkout subscriptions invoices payments openapi', args: ['--openapi'], expectCategory: /payments|openapi/i, expectAnyName: /stripe|payments/i },
28
+ { q: 'validate disposable email mx deliverability', args: [], expectCategory: /email/i, expectAnyName: /abstract|mailbox|email/i },
29
+ { q: 'ip reputation vpn proxy privacy detection', args: ['--https'], expectCategory: /security|geocoding|location/i, expectAnyName: /ipqualityscore|proxycheck|ipinfo/i },
30
+ { q: 'word synonyms antonyms dictionary pronunciation', args: [], expectCategory: /dictionar|education/i, expectAnyName: /dictionary|wordsapi|wordnik|merriam/i },
31
+ { q: 'meal planning recipes by ingredients nutrition', args: [], expectCategory: /food/i, expectAnyName: /spoonacular|edamam|themealdb|open food/i },
32
+ { q: 'air quality by coordinates pollutant measurements', args: [], expectCategory: /environment|science|weather/i, expectAnyName: /openaq|air quality|aqicn/i },
33
+ { q: 'historical currency exchange rates no auth cors', args: ['--no-auth','--https'], expectCategory: /currency exchange/i, expectAuth: 'No', expectAnyName: /frankfurter|currency/i },
34
+ { q: 'public holidays by country next long weekend no auth', args: ['--no-auth','--https'], expectCategory: /calendar|date/i, expectAuth: 'No', expectAnyName: /nager|holiday/i },
35
+ { q: 'fake ecommerce cart products users for frontend demo', args: ['--no-auth','--https'], expectCategory: /test data|shopping/i, expectAuth: 'No', expectAnyName: /fake store|dummyjson|jsonplaceholder/i },
36
+ { q: 'gtfs transit stops routes realtime departures', args: [], expectCategory: /transport|transit/i, expectAnyName: /transitland|transport|mbta|transportapi/i },
37
+ { q: 'openapi spec for sms messaging send text', args: ['--openapi'], expectCategory: /communication|messaging|telecom|openapi/i, expectAnyName: /twilio|message|sms/i },
38
+ { q: 'file upload storage s3 compatible openapi', args: ['--openapi'], expectCategory: /cloud|storage|openapi/i, expectAnyName: /amazon|s3|storage|backblaze|management/i },
39
+ { q: 'public domain books search authors covers no auth', args: ['--no-auth','--https'], expectCategory: /books|education|open data/i, expectAuth: 'No', expectAnyName: /open library|google books|gutendex/i },
40
+ { q: 'podcast search episodes rss metadata', args: [], expectCategory: /podcasts|media|entertainment|music/i, expectAnyName: /podcast|listen notes|itunes/i },
41
+ { q: 'carbon intensity electricity grid emissions by region', args: [], expectCategory: /environment|science|open data/i, expectAnyName: /carbon|electricity|emissions/i },
42
+ { q: 'domain whois dns records ssl certificate lookup', args: [], expectCategory: /security|development|openapi/i, expectAnyName: /whois|dns|ssl|certificate/i },
43
+ { q: 'qr code generation api no auth', args: ['--no-auth','--https'], expectCategory: /development|utility|tools|test data|openapi/i, expectAuth: 'No', expectAnyName: /qr/i },
44
+ { q: 'shorten urls branded links analytics', args: [], expectCategory: /development|url shortener|utility|openapi/i, expectAnyName: /bitly|short/i },
45
+ { q: 'i need a free api for avatars profile pictures placeholder users no key', args: [], expectCategory: /test data|social|development|media/i, expectAuth: 'No', expectAnyName: /random user|dicebear|avatar|ui faces|placeholder/i },
46
+ { q: 'generate identicon avatar svg from wallet address no auth cors', args: [], expectAuth: 'No', expectAnyName: /avatar|dicebear|boring|identicon|blockies/i },
47
+ { q: 'nft metadata by contract token id ethereum polygon', args: [], expectCategory: /cryptocurrency|blockchain|openapi/i, expectAnyName: /alchemy|moralis|opensea|coinmarketcap|coinbase/i },
48
+ { q: 'wallet balance transactions erc20 transfers api', args: [], expectCategory: /cryptocurrency|blockchain|openapi/i, expectAnyName: /etherscan|alchemy|moralis|covalent|block/i },
49
+ { q: 'restaurant places nearby search opening hours photos', args: [], expectCategory: /geocoding|location|food|openapi/i, expectAnyName: /google|mapbox|foursquare|yelp|places/i },
50
+ { q: 'vehicle vin decode recall safety data no auth', args: [], expectCategory: /transportation|government|open data/i, expectAnyName: /nhtsa|vin|vehicle/i },
51
+ { q: 'license plate lookup vehicle owner', args: [], expectCategory: /transportation|government|openapi/i },
52
+ { q: 'address autocomplete typeahead places browser cors', args: [], expectCategory: /geocoding|location/i, expectCors: 'Yes', expectAnyName: /mapbox|nominatim|geo/i },
53
+ { q: 'free image search photos unsplash alternative no key', args: [], expectCategory: /photography|media|images|entertainment/i, expectAuth: 'No', expectAnyName: /pexels|pixabay|unsplash|wikimedia|openverse/i },
54
+ { q: 'cat images random dog facts no auth', args: [], expectAuth: 'No', expectAnyName: /cat|dog|thecatapi|dog ceo/i },
55
+ { q: 'jokes memes random quote api no auth', args: [], expectAuth: 'No', expectAnyName: /joke|meme|quote/i },
56
+ { q: 'translation language detect text api free', args: [], expectCategory: /language|translation|text analysis|openapi/i, expectAnyName: /libretranslate|google|microsoft|detect/i },
57
+ { q: 'sentiment analysis text moderation toxicity api', args: [], expectCategory: /machine learning|text analysis|ai|openapi/i, expectAnyName: /sentiment|moderation|toxicity|perspective/i },
58
+ { q: 'sms no twilio cheaper alternative openapi', args: ['--openapi'], expectCategory: /communication|messaging|telecom|openapi/i, expectAnyName: /sms|message|telnyx|vonage|messagebird|plivo/i },
59
+ { q: 'stripe alternative payments for crypto checkout x402', args: [], expectCategory: /payments|cryptocurrency|financial/i, expectAnyName: /stripe|paypal|coinbase|commerce|payment/i },
60
+ { q: 'calendar events create google calendar openapi oauth', args: ['--openapi'], expectCategory: /calendar|openapi/i, expectAnyName: /google|calendar/i },
61
+ { q: 'send email transactional api openapi', args: ['--openapi'], expectCategory: /email|communication|openapi/i, expectAnyName: /sendgrid|mailgun|postmark|resend|email/i },
62
+ { q: 'package tracking shipment carrier tracking no auth', args: [], expectCategory: /tracking|logistics|commerce|openapi/i, expectAnyName: /tracking|shippo|aftership|ups|fedex/i },
63
+ { q: 'open source vulnerability CVE lookup package security', args: [], expectCategory: /security|development|open data/i, expectAnyName: /nvd|cve|osv|security/i },
64
+ { q: 'github repo stars issues commits api no auth', args: [], expectCategory: /development|open data/i, expectAnyName: /github|gitlab/i },
65
+ { q: 'npm package downloads version metadata no key', args: [], expectCategory: /development|open data/i, expectAuth: 'No', expectAnyName: /npm|libraries.io|package/i },
66
+ { q: 'docker image tags vulnerabilities registry api', args: [], expectCategory: /development|security|openapi/i, expectAnyName: /docker|registry|hub|security/i },
67
+ { q: 'exchange rates but not crypto fiat only no auth', args: [], expectCategory: /currency exchange/i, expectAuth: 'No' },
68
+ { q: 'crypto prices but not stocks no auth', args: [], expectCategory: /cryptocurrency/i, expectAuth: 'No' },
69
+ { q: 'weather not climate current conditions no auth', args: [], expectCategory: /weather/i, expectAuth: 'No' },
70
+ { q: 'login oauth user profile social auth api openid', args: [], expectCategory: /authentication|security|development|openapi/i, expectAnyName: /auth0|clerk|okta|openid|oauth|google/i },
71
+ { q: 'create temporary email inbox receive messages api no auth', args: [], expectCategory: /email|test data|communication/i, expectAnyName: /mail|email|inbox|temp/i },
72
+ { q: 'sms verification otp phone number lookup api', args: [], expectCategory: /communication|messaging|telecom|security/i, expectAnyName: /twilio|vonage|telnyx|numverify|phone/i },
73
+ { q: 'phone number validation carrier line type country lookup', args: [], expectCategory: /communication|telecom|security|openapi/i, expectAnyName: /numverify|twilio|phone|abstract/i },
74
+ { q: 'bank routing number iban validation payments', args: [], expectCategory: /financial|finance|payments|openapi/i, expectAnyName: /iban|bank|routing|plaid|stripe/i },
75
+ { q: 'plaid alternative bank account transactions finance api', args: [], expectCategory: /finance|financial|payments|openapi/i, expectAnyName: /plaid|teller|truelayer|bank/i },
76
+ { q: 'tax rates sales tax by address api', args: [], expectCategory: /finance|government|commerce|openapi/i, expectAnyName: /tax|avalara|taxjar/i },
77
+ { q: 'address validation normalize usps deliverability api', args: [], expectCategory: /geocoding|location|openapi/i, expectAnyName: /smarty|usps|lob|address|geocod/i },
78
+ { q: 'timezone offset daylight savings from coordinates', args: [], expectCategory: /geocoding|location|time/i, expectAnyName: /timezone|timezonedb|ipgeolocation|google|abstract/i },
79
+ { q: 'public records business entity secretary of state api', args: [], expectCategory: /government|open data|business/i, expectAnyName: /opencorporates|business|company|data.gov/i },
80
+ { q: 'company enrichment domain employees logo api', args: [], expectCategory: /business|openapi|development|marketing/i, expectAnyName: /clearbit|brandfetch|company|logo|domain/i },
81
+ { q: 'logo from domain brand colors api no auth', args: [], expectCategory: /business|media|development|marketing/i, expectAnyName: /brandfetch|clearbit|logo|favicon/i },
82
+ { q: 'favicon screenshot website preview api', args: [], expectCategory: /development|media|utility|openapi/i, expectAnyName: /screenshot|favicon|microlink|urlbox/i },
83
+ { q: 'website metadata link preview open graph api no auth', args: [], expectCategory: /development|media|utility|openapi/i, expectAnyName: /microlink|linkpreview|metadata|open graph/i },
84
+ { q: 'pdf generation html to pdf api', args: [], expectCategory: /documents|development|utility|openapi/i, expectAnyName: /pdf|document/i },
85
+ { q: 'ocr extract text from image receipt api', args: [], expectCategory: /machine learning|ai|documents|openapi/i, expectAnyName: /ocr|vision|mindee|google|azure/i },
86
+ { q: 'speech to text transcription audio api', args: [], expectCategory: /ai|machine learning|audio|openapi/i, expectAnyName: /whisper|assemblyai|deepgram|speech/i },
87
+ { q: 'text to speech voice generation api', args: [], expectCategory: /ai|audio|machine learning|openapi/i, expectAnyName: /elevenlabs|speech|voice|tts/i },
88
+ { q: 'image generation ai api stable diffusion', args: [], expectCategory: /ai|machine learning|media|openapi/i, expectAnyName: /stability|openai|replicate|image/i },
89
+ { q: 'moderate images nudity violence safe search api', args: [], expectCategory: /ai|machine learning|security|openapi/i, expectAnyName: /moderation|sightengine|vision|safe/i },
90
+ { q: 'blockchain wallet risk sanctions screening api', args: [], expectCategory: /cryptocurrency|security|finance/i, expectAnyName: /chainalysis|trm|sanction|wallet|elliptic/i },
91
+ { q: 'sanctions ofac pep screening person company api', args: [], expectCategory: /security|government|finance|openapi/i, expectAnyName: /ofac|sanction|opensanctions|pep/i },
92
+ { q: 'zipcode to city state county demographics no auth', args: [], expectCategory: /geocoding|government|open data/i, expectAuth: 'No', expectAnyName: /zippopotam|census|zip|postal/i },
93
+ { q: 'school district boundary by address api', args: [], expectCategory: /education|government|geocoding|open data/i, expectAnyName: /school|district|census/i },
94
+ { q: 'real estate property value rent estimate api', args: [], expectCategory: /real estate|property|openapi|finance/i, expectAnyName: /rentcast|zillow|attom|real estate|property/i },
95
+ { q: 'mortgage rates loan calculator api', args: [], expectCategory: /finance|financial|real estate|openapi/i, expectAnyName: /mortgage|loan|rate/i },
96
+ { q: 'flight status airport arrivals departures api', args: [], expectCategory: /transportation|travel|openapi/i, expectAnyName: /aviation|flight|airport|amadeus/i },
97
+ { q: 'hotel search booking availability api', args: [], expectCategory: /travel|commerce|openapi/i, expectAnyName: /hotel|booking|amadeus/i },
98
+ { q: 'recipe from pantry ingredients avoid allergens api', args: [], expectCategory: /food/i, expectAnyName: /spoonacular|edamam|recipe|meal/i },
99
+ { q: 'calorie macro nutrition label parse api', args: [], expectCategory: /food|health|openapi/i, expectAnyName: /nutrition|edamam|spoonacular|food/i },
100
+
101
+ ];
102
+
103
+ let failures = 0;
104
+ for (const [i, c] of cases.entries()) {
105
+ const res = spawnSync(process.execPath, ['src/cli.js', c.q, ...c.args, '--limit', '5', '--json'], { encoding: 'utf8' });
106
+ if (res.status !== 0) {
107
+ failures++;
108
+ console.log(`FAIL ${i+1} ${c.q}: command failed ${res.stderr}`);
109
+ continue;
110
+ }
111
+ const rows = JSON.parse(res.stdout);
112
+ const top = rows[0];
113
+ const top3 = rows.slice(0,3);
114
+ const checks = [];
115
+ if (!top) checks.push('no results');
116
+ if (top && c.expectCategory && !c.expectCategory.test(top.category || '')) checks.push(`top category ${top.category} !~ ${c.expectCategory}`);
117
+ if (top && c.expectAuth && top.auth !== c.expectAuth) checks.push(`top auth ${top.auth} != ${c.expectAuth}`);
118
+ if (top && c.expectCors && top.cors !== c.expectCors) checks.push(`top cors ${top.cors} != ${c.expectCors}`);
119
+ if (c.expectAnyName && !top3.some(r => c.expectAnyName.test(r.name || '') || c.expectAnyName.test(r.description || '') || c.expectAnyName.test(r.url || ''))) checks.push(`top3 missing ${c.expectAnyName}`);
120
+ const ok = checks.length === 0;
121
+ if (!ok) failures++;
122
+ console.log(`${ok ? 'PASS' : 'FAIL'} ${i+1}. ${c.q}`);
123
+ rows.slice(0,3).forEach((r, idx) => console.log(` ${idx+1}. ${r.name} | ${r.category} | auth=${r.auth} | cors=${r.cors} | score=${r.score}`));
124
+ if (!ok) console.log(` Reasons: ${checks.join('; ')}`);
125
+ }
126
+ console.log(`\n${cases.length - failures}/${cases.length} passed`);
127
+ process.exitCode = failures ? 1 : 0;
@@ -5,14 +5,16 @@ description: Find and evaluate free/public APIs for projects, demos, agents, pro
5
5
 
6
6
  # Public API Finder
7
7
 
8
- Use this skill when a task needs a public API candidate. The agent-friendly path is the CLI first, then live-check docs/endpoints before coding.
8
+ Use this skill when a task needs a public API candidate. The CLI searches multiple sources: public-api-lists, public-apis, APIs.guru OpenAPI directory, and a curated best-known API layer for common domains like crypto, stocks, weather, maps, jobs, sports, media, news, government, and commerce. Use the CLI first, then live-check docs/endpoints before coding.
9
9
 
10
10
  ## Quick command
11
11
 
12
12
  ```bash
13
- npx @builtbyecho/public-api-finder "weather forecast" --no-auth --https
14
- npx @builtbyecho/public-api-finder "crypto prices" --category Cryptocurrency --limit 5
15
- npx @builtbyecho/public-api-finder "jobs" --json
13
+ npx --yes --package=@builtbyecho/public-api-finder -- public-api-finder "weather forecast" --no-auth --https
14
+ npx --yes --package=@builtbyecho/public-api-finder -- public-api-finder "crypto prices" --category Cryptocurrency --limit 5
15
+ npx --yes --package=@builtbyecho/public-api-finder -- public-api-finder "jobs" --json
16
+ npx --yes --package=@builtbyecho/public-api-finder -- public-api-finder "payments" --openapi
17
+ npx --yes --package=@builtbyecho/public-api-finder -- public-api-finder "weather forecast" --no-auth --https --check
16
18
  ```
17
19
 
18
20
  If npm is unavailable, use the bundled fallback script:
@@ -33,9 +35,10 @@ Recommend 2-5 APIs. Include:
33
35
  - HTTPS/CORS notes
34
36
  - One caveat to verify: rate limits, pricing, docs freshness, uptime, or terms
35
37
  - Minimal example request only after checking docs/live endpoint
38
+ - OpenAPI URL when available
36
39
 
37
40
  ## Heuristics
38
41
 
39
42
  Prefer APIs that are HTTPS-enabled, no-auth or simple API key, CORS `Yes` for frontend demos, well documented, and narrowly suited to the task.
40
43
 
41
- The curated list is not a production-readiness guarantee. Always verify before building around an API.
44
+ The curated list is not a production-readiness guarantee. Always verify before building around an API. Use `--check` for a quick live reachability check, but still inspect docs, terms, auth, and rate limits before committing to an integration.
package/src/app.js ADDED
@@ -0,0 +1,263 @@
1
+ import { createServer as createHttpServer } from 'node:http';
2
+ import { readFile, writeFile } from 'node:fs/promises';
3
+ import { dirname, join } from 'node:path';
4
+ import { mkdir } from 'node:fs/promises';
5
+ import { randomUUID } from 'node:crypto';
6
+ import { searchApis } from './cli.js';
7
+
8
+ const DEFAULT_PULL_PRICE_CENTS = 1;
9
+ const DEFAULT_CREDITS_PER_PULL = 1;
10
+
11
+ function json(res, status, body, headers = {}) {
12
+ const payload = JSON.stringify(body, null, 2);
13
+ res.writeHead(status, {
14
+ 'content-type': 'application/json; charset=utf-8',
15
+ 'cache-control': 'no-store',
16
+ ...headers,
17
+ });
18
+ res.end(payload);
19
+ }
20
+
21
+ function html(res, body) {
22
+ res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
23
+ res.end(body);
24
+ }
25
+
26
+ async function readJson(req) {
27
+ const chunks = [];
28
+ for await (const chunk of req) chunks.push(chunk);
29
+ if (!chunks.length) return {};
30
+ const raw = Buffer.concat(chunks).toString('utf8');
31
+ try {
32
+ return JSON.parse(raw);
33
+ } catch {
34
+ const err = new Error('Invalid JSON body');
35
+ err.status = 400;
36
+ throw err;
37
+ }
38
+ }
39
+
40
+ function accountFromRequest(req, body = {}) {
41
+ const auth = req.headers.authorization || '';
42
+ const bearer = auth.match(/^Bearer\s+(.+)$/i)?.[1];
43
+ return String(body.account || req.headers['x-api-key'] || bearer || '').trim();
44
+ }
45
+
46
+ function assertAccount(account) {
47
+ if (!account) {
48
+ const err = new Error('Missing account. Pass X-API-Key, Authorization: Bearer, or body.account.');
49
+ err.status = 401;
50
+ throw err;
51
+ }
52
+ }
53
+
54
+ class CreditLedger {
55
+ constructor(path) {
56
+ this.path = path;
57
+ this.data = null;
58
+ }
59
+
60
+ async load() {
61
+ if (this.data) return this.data;
62
+ try {
63
+ this.data = JSON.parse(await readFile(this.path, 'utf8'));
64
+ } catch {
65
+ this.data = { version: 1, accounts: {}, topups: {}, events: [] };
66
+ }
67
+ return this.data;
68
+ }
69
+
70
+ async save() {
71
+ await mkdir(dirname(this.path), { recursive: true });
72
+ await writeFile(this.path, JSON.stringify(this.data, null, 2));
73
+ }
74
+
75
+ async balance(account) {
76
+ const data = await this.load();
77
+ return data.accounts[account]?.credits || 0;
78
+ }
79
+
80
+ async grant(account, credits, meta = {}) {
81
+ assertAccount(account);
82
+ if (!Number.isFinite(credits) || credits <= 0) {
83
+ const err = new Error('credits must be a positive number');
84
+ err.status = 400;
85
+ throw err;
86
+ }
87
+ const data = await this.load();
88
+ data.accounts[account] ||= { credits: 0, createdAt: new Date().toISOString() };
89
+ data.accounts[account].credits += credits;
90
+ data.accounts[account].updatedAt = new Date().toISOString();
91
+ data.events.push({ id: randomUUID(), type: 'grant', account, credits, meta, at: new Date().toISOString() });
92
+ await this.save();
93
+ return data.accounts[account].credits;
94
+ }
95
+
96
+ async spend(account, credits, meta = {}) {
97
+ assertAccount(account);
98
+ const data = await this.load();
99
+ const current = data.accounts[account]?.credits || 0;
100
+ if (current < credits) {
101
+ const err = new Error(`Insufficient credits: need ${credits}, have ${current}`);
102
+ err.status = 402;
103
+ err.code = 'INSUFFICIENT_CREDITS';
104
+ err.balance = current;
105
+ throw err;
106
+ }
107
+ data.accounts[account].credits = current - credits;
108
+ data.accounts[account].updatedAt = new Date().toISOString();
109
+ data.events.push({ id: randomUUID(), type: 'spend', account, credits, meta, at: new Date().toISOString() });
110
+ await this.save();
111
+ return data.accounts[account].credits;
112
+ }
113
+
114
+ async createTopup(account, credits, usdCents, meta = {}) {
115
+ assertAccount(account);
116
+ const id = `topup_${randomUUID()}`;
117
+ const data = await this.load();
118
+ data.topups[id] = {
119
+ id,
120
+ account,
121
+ credits,
122
+ usdCents,
123
+ status: 'pending',
124
+ meta,
125
+ createdAt: new Date().toISOString(),
126
+ };
127
+ data.events.push({ id: randomUUID(), type: 'topup.created', account, topupId: id, credits, usdCents, meta, at: new Date().toISOString() });
128
+ await this.save();
129
+ return data.topups[id];
130
+ }
131
+ }
132
+
133
+ function bankrInstructions(topup, env) {
134
+ const receiveAddress = env.BANKR_RECEIVE_ADDRESS || env.PUBLIC_API_FINDER_RECEIVE_ADDRESS || 'SET_BANKR_RECEIVE_ADDRESS';
135
+ const dollars = (topup.usdCents / 100).toFixed(2);
136
+ return {
137
+ provider: 'bankr',
138
+ status: 'pending_manual_confirmation',
139
+ receiveAddress,
140
+ network: env.BANKR_NETWORK || 'Base',
141
+ asset: env.BANKR_ASSET || 'USDC',
142
+ memo: topup.id,
143
+ prompt: `Send $${dollars} ${env.BANKR_ASSET || 'USDC'} on ${env.BANKR_NETWORK || 'Base'} to ${receiveAddress} for ${topup.credits} Public API Finder credits. Memo/order id: ${topup.id}`,
144
+ };
145
+ }
146
+
147
+ function landingPage() {
148
+ return `<!doctype html>
149
+ <html lang="en">
150
+ <head>
151
+ <meta charset="utf-8" />
152
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
153
+ <title>Public API Finder</title>
154
+ <style>
155
+ body{font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;margin:0;background:#07111f;color:#e6f7ff}main{max-width:900px;margin:0 auto;padding:64px 24px}.eyebrow{color:#74e7ff;font-weight:700;letter-spacing:.08em;text-transform:uppercase}h1{font-size:clamp(42px,8vw,84px);line-height:.92;margin:12px 0 18px}p{font-size:20px;line-height:1.55;color:#b9d6e2}.card{background:rgba(255,255,255,.07);border:1px solid rgba(116,231,255,.25);border-radius:24px;padding:24px;margin-top:24px;box-shadow:0 24px 80px rgba(0,0,0,.35)}code{background:rgba(0,0,0,.35);padding:2px 6px;border-radius:7px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px}.price{font-size:36px;font-weight:800;color:#fff}.muted{color:#87a9b8;font-size:15px}</style>
156
+ </head>
157
+ <body><main>
158
+ <div class="eyebrow">Agent-native API discovery</div>
159
+ <h1>Find the right public API in one pull.</h1>
160
+ <p>Public API Finder ranks free/public APIs for prototypes, agents, and app builders. The CLI is free. The hosted API uses credits designed for Bankr-powered top-ups.</p>
161
+ <div class="grid">
162
+ <section class="card"><div class="price">$0.01</div><p>per successful enriched pull</p><p class="muted">1 credit = 1 ranked API search result set.</p></section>
163
+ <section class="card"><h2>Try the API</h2><p><code>POST /api/search</code><br/>Use <code>X-API-Key</code>, spend 1 credit only when results return.</p></section>
164
+ <section class="card"><h2>Bankr-ready</h2><p><code>POST /api/credits/topup</code><br/>Creates a Bankr payment prompt and pending credit order.</p></section>
165
+ </div>
166
+ </main></body></html>`;
167
+ }
168
+
169
+ export function createApp(options = {}) {
170
+ const env = options.env || process.env;
171
+ const ledger = options.ledger || new CreditLedger(options.statePath || env.PUBLIC_API_FINDER_STATE || join(process.cwd(), 'state', 'public-api-finder-ledger.json'));
172
+ const pullPriceCents = Number(env.PUBLIC_API_FINDER_PULL_PRICE_CENTS || DEFAULT_PULL_PRICE_CENTS);
173
+ const creditsPerPull = Number(env.PUBLIC_API_FINDER_CREDITS_PER_PULL || DEFAULT_CREDITS_PER_PULL);
174
+ const freeSearches = String(env.PUBLIC_API_FINDER_FREE_SEARCH || '').toLowerCase() === 'true';
175
+
176
+ return createHttpServer(async (req, res) => {
177
+ try {
178
+ const url = new URL(req.url || '/', 'http://localhost');
179
+ if (req.method === 'GET' && url.pathname === '/') return html(res, landingPage());
180
+ if (req.method === 'GET' && url.pathname === '/api/health') return json(res, 200, { ok: true, service: 'public-api-finder' });
181
+
182
+ if (req.method === 'GET' && url.pathname === '/api/credits') {
183
+ const account = accountFromRequest(req, Object.fromEntries(url.searchParams));
184
+ assertAccount(account);
185
+ return json(res, 200, { account, credits: await ledger.balance(account) });
186
+ }
187
+
188
+ if (req.method === 'POST' && url.pathname === '/api/credits/topup') {
189
+ const body = await readJson(req);
190
+ const account = accountFromRequest(req, body);
191
+ assertAccount(account);
192
+ const credits = Math.max(1, Math.floor(Number(body.credits || 100)));
193
+ const usdCents = Math.max(1, Math.floor(Number(body.usdCents || credits * pullPriceCents)));
194
+ const topup = await ledger.createTopup(account, credits, usdCents, { source: 'bankr', requestedBy: body.requestedBy || null });
195
+ return json(res, 201, { topup, payment: bankrInstructions(topup, env) });
196
+ }
197
+
198
+ if (req.method === 'POST' && url.pathname === '/api/admin/credits') {
199
+ if (!env.PUBLIC_API_FINDER_ADMIN_TOKEN || req.headers['x-admin-token'] !== env.PUBLIC_API_FINDER_ADMIN_TOKEN) {
200
+ return json(res, 403, { error: 'Forbidden' });
201
+ }
202
+ const body = await readJson(req);
203
+ const account = String(body.account || '').trim();
204
+ const credits = Math.floor(Number(body.credits || 0));
205
+ const balance = await ledger.grant(account, credits, { reason: body.reason || 'admin grant', paymentRef: body.paymentRef || null });
206
+ return json(res, 200, { account, credits: balance });
207
+ }
208
+
209
+ if (req.method === 'POST' && url.pathname === '/api/search') {
210
+ const body = await readJson(req);
211
+ const account = accountFromRequest(req, body);
212
+ assertAccount(account);
213
+ if (!body.query || typeof body.query !== 'string') return json(res, 400, { error: 'query is required' });
214
+ const results = await searchApis({
215
+ query: body.query,
216
+ category: body.category,
217
+ source: body.source,
218
+ noAuth: body.noAuth,
219
+ https: body.https,
220
+ openapi: body.openapi,
221
+ cors: body.cors,
222
+ limit: body.limit || 8,
223
+ check: body.check,
224
+ refresh: body.refresh,
225
+ });
226
+ let remaining = await ledger.balance(account);
227
+ let chargedCredits = 0;
228
+ if (!freeSearches && results.length) {
229
+ remaining = await ledger.spend(account, creditsPerPull, { query: body.query, resultCount: results.length });
230
+ chargedCredits = creditsPerPull;
231
+ }
232
+ return json(res, 200, {
233
+ query: body.query,
234
+ results,
235
+ billing: {
236
+ account,
237
+ chargedCredits,
238
+ remainingCredits: remaining,
239
+ unitPriceCents: pullPriceCents,
240
+ note: chargedCredits ? 'charged for successful enriched pull' : 'not charged',
241
+ },
242
+ });
243
+ }
244
+
245
+ return json(res, 404, { error: 'Not found' });
246
+ } catch (err) {
247
+ return json(res, err.status || 500, {
248
+ error: err.message || 'Internal server error',
249
+ code: err.code,
250
+ balance: err.balance,
251
+ });
252
+ }
253
+ });
254
+ }
255
+
256
+ export { CreditLedger };
257
+
258
+ if (process.argv[1] && import.meta.url === new URL(process.argv[1], 'file:').href) {
259
+ const port = Number(process.env.PORT || 8787);
260
+ createApp().listen(port, () => {
261
+ console.log(`public-api-finder app listening on http://localhost:${port}`);
262
+ });
263
+ }