@builtbyecho/public-api-finder 0.5.6 → 0.5.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@builtbyecho/public-api-finder",
3
- "version": "0.5.6",
3
+ "version": "0.5.8",
4
4
  "description": "Find free/public APIs for agents and prototypes.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from 'node:child_process';
3
+
4
+ const cases = [
5
+ { q: 'crypto prices', args: ['--no-auth','--https','--cors','Yes'], topCategory: /cryptocurrency/i, top3Category: /cryptocurrency/i, mustName: /dexscreener|dexpaprika|defillama|geckoterminal|coinpaprika/i, forbid: /weather|joke|tvmaze|jobs/i },
6
+ { q: 'stock prices', args: ['--no-auth','--https','--cors','Yes'], topCategory: /finance/i, top3Category: /finance/i, forbid: /weather|joke|tvmaze|jobs/i },
7
+ { q: 'free stock quote API no auth', args: ['--no-auth','--https'], topCategory: /finance/i, mustName: /stooq|portfolio|alpha|finnhub|twelve/i, forbid: /weather|joke|anime|tv/i },
8
+ { q: 'solana token price market cap dex no key', args: ['--no-auth','--https'], topCategory: /cryptocurrency/i, top3Category: /cryptocurrency/i, mustName: /dexscreener|dexpaprika|geckoterminal|defillama/i },
9
+ { q: 'wallet balance transactions erc20 transfers api', args: [], topCategory: /cryptocurrency|openapi/i, mustName: /etherscan|alchemy|moralis|covalent|block/i },
10
+ { q: 'weather forecast no auth cors', args: ['--no-auth','--https','--cors','Yes'], topCategory: /weather/i, top3Category: /weather/i, mustName: /open-meteo|national weather|pirate/i, forbid: /joke|crypto|stocks/i },
11
+ { q: 'historical weather api', args: [], topCategory: /weather/i, mustName: /open-meteo|weather/i },
12
+ { q: 'weather alerts us api', args: ['--no-auth'], topCategory: /weather/i, mustName: /national weather|weather/i },
13
+ { q: 'geocoding api no auth', args: ['--no-auth','--https'], topCategory: /geocoding|location/i, mustName: /nominatim|geocod|zippopotam|ipinfo/i },
14
+ { q: 'reverse geocoding api', args: [], topCategory: /geocoding|location/i, mustName: /nominatim|geocod|mapbox|google/i },
15
+ { q: 'maps routing api', args: [], topCategory: /geocoding|location/i, mustName: /graphhopper|mapbox|google|here|tomtom/i, forbid: /linkedin/i },
16
+ { q: 'public transit api', args: [], topCategory: /transport|transit/i, mustName: /transitland|transport|mbta/i },
17
+ { q: 'jobs api remote', args: [], topCategory: /jobs|agents/i, mustName: /graphql jobs|adzuna|usajobs|search.gov|linkedin jobs/i },
18
+ { q: 'government jobs api', args: [], topCategory: /jobs|government/i, mustName: /usajobs|search.gov/i },
19
+ { q: 'company enrichment api', args: [], topCategory: /business|marketing|openapi/i, mustName: /clearbit|brandfetch|opencorporates|company/i },
20
+ { q: 'email validation api', args: [], topCategory: /email/i, mustName: /abstract|email|mail/i },
21
+ { q: 'phone number lookup api', args: [], topCategory: /communication|telecom|security|openapi/i, mustName: /numverify|twilio|phone|abstract/i },
22
+ { q: 'sms api', args: [], topCategory: /communication|messaging|telecom|openapi/i, mustName: /twilio|telnyx|vonage|sms|message/i },
23
+ { q: 'webhook testing api', args: [], mustName: /webhook|requestbin|pipedream|beeceptor|alchemy/i },
24
+ { q: 'payment processing api', args: [], topCategory: /payments|financial|openapi/i, mustName: /stripe|paypal|checkout|payment/i },
25
+ { q: 'subscriptions billing api', args: [], topCategory: /payments|openapi/i, mustName: /stripe|billing|subscription|chargebee/i },
26
+ { q: 'oauth identity api', args: [], topCategory: /authentication|security|development|openapi/i, mustName: /auth0|clerk|okta|oauth|openid/i },
27
+ { q: 'passwordless auth api', args: [], topCategory: /authentication|security|openapi/i, mustName: /auth0|clerk|magic|stytch|passwordless/i },
28
+ { q: 'screenshot api', args: [], topCategory: /development|media|documents|openapi/i, mustName: /urlbox|microlink|screenshot/i },
29
+ { q: 'link preview api', args: [], topCategory: /development|media|utility|openapi/i, mustName: /microlink|linkpreview|metadata|open graph/i },
30
+ { q: 'website metadata api', args: [], topCategory: /development|media|utility|openapi/i, mustName: /microlink|metadata|open graph|urlbox/i },
31
+ { q: 'pdf generation api', args: [], topCategory: /documents|development|utility|openapi/i, mustName: /pdf|document/i },
32
+ { q: 'ocr api', args: [], topCategory: /machine learning|ai|documents|openapi/i, mustName: /ocr|vision|mindee/i },
33
+ { q: 'image generation api', args: [], topCategory: /ai|machine learning|media|openapi/i, mustName: /stability|openai|replicate|image/i },
34
+ { q: 'text to speech api', args: [], topCategory: /ai|audio|machine learning|openapi/i, mustName: /elevenlabs|speech|voice|tts/i },
35
+ { q: 'speech to text api', args: [], topCategory: /ai|audio|machine learning|openapi/i, mustName: /assemblyai|deepgram|whisper|speech/i },
36
+ { q: 'vulnerability database api', args: [], topCategory: /security|development|open data/i, mustName: /osv|nvd|cve|vulnerab/i },
37
+ { q: 'cve lookup api', args: [], topCategory: /security|open data/i, mustName: /nvd|cve|osv/i },
38
+ { q: 'npm package metadata api', args: ['--no-auth'], topCategory: /development|open data/i, mustName: /npm|libraries.io|package/i },
39
+ { q: 'github repo stats api', args: [], topCategory: /development|open data/i, mustName: /github|gitlab/i },
40
+ { q: 'domain dns lookup api', args: [], topCategory: /security|development|openapi/i, mustName: /dns|google dns|whois|ssl/i },
41
+ { q: 'open data demographics api', args: [], topCategory: /government|open data/i, mustName: /census|data.gov|demographics/i },
42
+ { q: 'fake ecommerce api for testing', args: ['--no-auth','--https'], topCategory: /test data|shopping/i, mustName: /fake store|dummyjson|jsonplaceholder/i },
43
+ { q: 'joke api', args: ['--no-auth'], topCategory: /entertainment|jokes/i, mustName: /joke/i, forbid: /weather|crypto|stock/i },
44
+ { q: 'cat images random dog facts no auth', args: ['--no-auth'], mustName: /cat|dog|thecatapi|dog ceo/i, forbid: /weather|stock|crypto/i },
45
+ { q: 'food barcode ingredients allergens nutrition no auth cors', args: ['--no-auth','--https'], topCategory: /food/i, mustName: /open food facts/i },
46
+ { q: 'recipe from pantry ingredients avoid allergens api', args: [], topCategory: /food/i, mustName: /spoonacular|edamam|recipe|meal/i },
47
+ { q: 'air quality by coordinates pollutant measurements', args: [], topCategory: /environment|science|weather/i, mustName: /openaq|air quality|aqicn/i },
48
+ { q: 'historical currency exchange rates no auth cors', args: ['--no-auth','--https'], topCategory: /currency exchange/i, mustName: /frankfurter|currency/i, forbid: /\b(crypto|bitcoin|dex)\b/i },
49
+ { q: 'public holidays by country no auth', args: ['--no-auth','--https'], topCategory: /calendar|date/i, mustName: /nager|holiday|calendarific/i },
50
+ { q: 'vehicle vin decode recall safety data no auth', args: ['--no-auth'], topCategory: /transportation|government|open data/i, mustName: /nhtsa|vin|vehicle/i },
51
+ { q: 'real estate property value rent estimate api', args: [], topCategory: /real estate|property|openapi|finance/i, mustName: /rentcast|attom|zillow|real estate|property/i },
52
+ { q: 'flight status airport arrivals departures api', args: [], topCategory: /transportation|travel|openapi/i, mustName: /aviation|flight|airport|amadeus/i },
53
+ { q: 'hotel search booking availability api', args: [], topCategory: /travel|commerce|openapi/i, mustName: /hotel|booking|amadeus/i },
54
+ { q: 'brand colors fonts logo company domain enrichment', args: [], topCategory: /business|marketing|development|openapi/i, mustName: /brandfetch|clearbit|logo|brand/i },
55
+ ];
56
+
57
+ let failures = 0;
58
+ for (const [i, c] of cases.entries()) {
59
+ const res = spawnSync(process.execPath, ['src/cli.js', c.q, ...c.args, '--limit', '5', '--json'], { encoding: 'utf8' });
60
+ const rows = res.status === 0 ? JSON.parse(res.stdout || '[]') : [];
61
+ const top = rows[0];
62
+ const checks = [];
63
+ if (!top) checks.push('no results');
64
+ if (top && c.topCategory && !c.topCategory.test(top.category || '')) checks.push(`top category ${top.category} !~ ${c.topCategory}`);
65
+ if (top && c.top3Category) {
66
+ const bad = rows.slice(0, 3).filter(r => !c.top3Category.test(r.category || '')).map(r => `${r.name} (${r.category})`);
67
+ if (bad.length) checks.push(`top3 off-domain: ${bad.join(', ')}`);
68
+ }
69
+ if (c.mustName && !rows.slice(0, 3).some(r => c.mustName.test(`${r.name} ${r.description} ${r.url}`))) checks.push(`top3 missing ${c.mustName}`);
70
+ if (c.forbid && rows.slice(0, 5).some(r => c.forbid.test(`${r.name} ${r.category} ${r.description}`))) checks.push(`forbidden result ${c.forbid}`);
71
+ const ok = checks.length === 0;
72
+ if (!ok) failures++;
73
+ console.log(`${ok ? 'PASS' : 'FAIL'} ${i + 1}. ${c.q}`);
74
+ rows.slice(0, 3).forEach((r, idx) => console.log(` ${idx + 1}. ${r.name} | ${r.category} | auth=${r.auth} | cors=${r.cors} | score=${r.score}`));
75
+ if (!ok) console.log(` Reasons: ${checks.join('; ')}`);
76
+ }
77
+ console.log(`\n${cases.length - failures}/${cases.length} hardening cases passed`);
78
+ process.exitCode = failures ? 1 : 0;
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from 'node:child_process';
3
+
4
+ const cases = [
5
+ { q: 'free stock data for a frontend chart', args: ['--no-auth','--https'], expect: /finance/i, any: /stooq|portfolio|stock|finance/i, forbid: /weather|joke|anime/i },
6
+ { q: 'api for checking token liquidity on dexes', args: [], expect: /cryptocurrency/i, any: /dexscreener|dexpaprika|geckoterminal|defillama/i },
7
+ { q: 'browser safe weather forecast api', args: ['--no-auth','--https','--cors','Yes'], expect: /weather/i, any: /open-meteo|weather|pirate/i },
8
+ { q: 'whois and dns lookup for a domain', args: [], expect: /security|development|openapi/i, any: /whois|dns|ssl/i },
9
+ { q: 'send text messages to users api', args: [], expect: /communication|messaging|telecom|openapi/i, any: /twilio|sms|message|telnyx|vonage/i },
10
+ { q: 'make pdf from html api', args: [], expect: /documents|development|openapi/i, any: /pdf|html/i },
11
+ { q: 'find APIs for app screenshots', args: [], expect: /development|media|documents|openapi/i, any: /urlbox|microlink|screenshot|capture/i },
12
+ { q: 'check if npm package has vulnerabilities', args: [], expect: /security|development|open data/i, any: /osv|nvd|vulnerability|npm/i },
13
+ { q: 'lookup us census demographics by zip', args: [], expect: /government|open data|geocoding/i, any: /census|zippopotam|data.gov/i },
14
+ { q: 'temporary email inbox for tests', args: ['--no-auth','--https'], expect: /email/i, any: /mail|email|inbox/i },
15
+ { q: 'login users with magic link api', args: [], expect: /authentication|security|openapi/i, any: /stytch|magic|auth0|clerk|passwordless/i },
16
+ { q: 'mock webhooks during development', args: [], expect: /development|test data|openapi/i, any: /webhook|beeceptor|requestbin|mock/i },
17
+ { q: 'public holidays calendar for germany', args: ['--no-auth','--https'], expect: /calendar|date/i, any: /nager|holiday|calendar/i },
18
+ { q: 'random user data for frontend seed demo', args: ['--no-auth','--https'], expect: /test data/i, any: /random user|jsonplaceholder|dummyjson|fake/i },
19
+ { q: 'recipe nutrition search by ingredients', args: [], expect: /food/i, any: /spoonacular|edamam|open food|recipe|nutrition/i },
20
+ { q: 'flight arrivals by airport code', args: [], expect: /travel|transportation|openapi/i, any: /aviation|flight|airport|amadeus/i },
21
+ { q: 'rent estimate property valuation api', args: [], expect: /real estate|property|finance|openapi/i, any: /rentcast|attom|property|real estate/i },
22
+ { q: 'container image tags registry api', args: [], expect: /development|security|openapi/i, any: /docker|registry|container/i },
23
+ { q: 'open graph card preview for url', args: [], expect: /development|media|utility|openapi/i, any: /microlink|open graph|link preview|metadata/i },
24
+ { q: 'speech transcription from uploaded audio', args: [], expect: /ai|audio|machine learning|openapi/i, any: /assemblyai|deepgram|whisper|transcription/i },
25
+ // Over-filter / tight filter checks: should not pad unrelated junk.
26
+ { q: 'stock quote no auth cors yes', args: ['--no-auth','--https','--cors','Yes'], any: /portfolio|stooq|stock|finance/i, forbid: /weather|joke|tvmaze|anime|jobs/i, max: 5, allowEmpty: true },
27
+ { q: 'crypto exchange orderbook no auth cors yes', args: ['--no-auth','--https','--cors','Yes'], expect: /cryptocurrency/i, any: /0x|coinpaprika|dexpaprika|dex|crypto/i, forbid: /weather|stock|joke/i, max: 5 },
28
+ { q: 'oauth login no auth cors yes', args: ['--no-auth','--https','--cors','Yes'], forbid: /weather|joke|food|crypto|stock|calendar|holiday/i, max: 5, allowEmpty: true },
29
+ { q: 'webhook testing no auth cors yes', args: ['--no-auth','--https','--cors','Yes'], any: /webhook|beeceptor|jsonplaceholder|mock/i, forbid: /weather|crypto|stock/i, max: 5 },
30
+ { q: 'medical diagnosis api no auth cors yes', args: ['--no-auth','--https','--cors','Yes'], forbid: /weather|crypto|stock|joke|tvmaze|anime|email|animals|food/i, max: 5, allowEmpty: true },
31
+ ];
32
+
33
+ function checkRows(rows, c) {
34
+ const checks = [];
35
+ const top = rows[0];
36
+ if (!top && !c.allowEmpty) checks.push('no results');
37
+ if (top && c.expect && !c.expect.test(top.category || '')) checks.push(`top category ${top.category} !~ ${c.expect}`);
38
+ if (c.any && !(c.allowEmpty && rows.length === 0) && !rows.slice(0, 3).some(r => c.any.test(`${r.name} ${r.category} ${r.description} ${r.url}`))) checks.push(`top3 missing ${c.any}`);
39
+ if (c.forbid && rows.some(r => c.forbid.test(`${r.name} ${r.category} ${r.description}`))) checks.push(`forbidden ${c.forbid}`);
40
+ if (c.max && rows.length > c.max) checks.push(`too many rows ${rows.length} > ${c.max}`);
41
+ return checks;
42
+ }
43
+
44
+ let failures = 0;
45
+ for (const [i, c] of cases.entries()) {
46
+ const res = spawnSync(process.execPath, ['src/cli.js', c.q, ...c.args, '--limit', String(c.max || 5), '--json'], { encoding: 'utf8' });
47
+ const rows = res.status === 0 ? JSON.parse(res.stdout || '[]') : [];
48
+ const checks = checkRows(rows, c);
49
+ const ok = checks.length === 0;
50
+ if (!ok) failures++;
51
+ console.log(`${ok ? 'PASS' : 'FAIL'} ${i + 1}. ${c.q}`);
52
+ rows.slice(0, 3).forEach((r, idx) => console.log(` ${idx + 1}. ${r.name} | ${r.category} | auth=${r.auth} | cors=${r.cors} | score=${r.score}`));
53
+ if (!ok) console.log(` Reasons: ${checks.join('; ')}`);
54
+ }
55
+ console.log(`\n${cases.length - failures}/${cases.length} mutation cases passed`);
56
+ process.exitCode = failures ? 1 : 0;
package/src/cli.js CHANGED
@@ -13,7 +13,7 @@ const SOURCES = {
13
13
  };
14
14
  const CACHE_PATH = process.env.PUBLIC_API_FINDER_CACHE || join(homedir(), '.cache', 'public-api-finder', 'all.json');
15
15
  const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
16
- const DATA_VERSION = 15;
16
+ const DATA_VERSION = 17;
17
17
 
18
18
  const ENRICHMENT_FIELDS = [
19
19
  'tags',
@@ -54,6 +54,15 @@ const TARGETED_BOOSTS = [
54
54
  [/\b(sanctions|ofac|pep|kyc|aml)\b/, /\b(opensanctions|ofac|sanctions|pep|kyc|aml|chainalysis)\b/i, 135],
55
55
  [/\b(wallet risk|crypto sanctions|blockchain wallet risk)\b/, /\b(chainalysis|trm|elliptic|sanctions|wallet risk)\b/i, 95],
56
56
  [/\b(crypto token metadata|token metadata|token logos|coin logos|logos contract addresses)\b/, /\b(coinmarketcap|coingecko|coinpaprika|coinbase|token logos|coin metadata|coin images)\b/i, 90],
57
+ [/\b(stock|stocks|stock quote|stock prices|equity|equities|ticker|tickers)\b/, /\b(stooq|portfolio optimizer|alpha vantage|polygon|finnhub|twelve data|stock|stocks|equity|ticker|quote)\b/i, 90],
58
+ [/\b(reverse geocoding|geocoding|maps routing|routing api|distance matrix|places)\b/, /\b(nominatim|geocod\.io|graphhopper|mapbox|foursquare|geocoding|routing|distance matrix|places)\b/i, 120],
59
+ [/\b(webhook testing|webhook debug|request bin|mock api)\b/, /\b(webhook\.site|beeceptor|requestbin|webhook|mock api|request bin)\b/i, 180],
60
+ [/\b(oauth identity|passwordless auth|openid|social auth|login oauth)\b/, /\b(auth0|clerk|stytch|magic|openid|oauth|passwordless|authentication|identity)\b/i, 190],
61
+ [/\b(cve lookup|vulnerability database|vulnerability data|package security)\b/, /\b(osv|nvd|cve|cvss|vulnerability|security advisories)\b/i, 190],
62
+ [/\b(cve lookup|cve api|cve search)\b/, /\b(osv|nvd|cve)\b/i, 260],
63
+ [/\b(open data demographics|demographics|census data)\b/, /\b(census|data\.gov|demographics|open data|government data)\b/i, 145],
64
+ [/\b(joke|jokes|meme|memes|random quote)\b/, /\b(official joke|joke api|jokes|quotable|quote)\b/i, 110],
65
+ [/\b(github repo|repo stats|stars issues commits)\b/, /\b(github rest|github|gitlab|repositories|stars|issues|commits)\b/i, 100],
57
66
  [/\b(zipcode|zip code|postal code)\b/, /\b(zippopotam|zip|postal|census)\b/i, 80],
58
67
  [/\b(real estate|property value|rent estimate)\b/, /\b(rentcast|attom|zillow|real estate|property|rent estimate)\b/i, 135],
59
68
  [/\b(mortgage|mortgage rates|loan calculator|loan rate|home loan)\b/, /\b(mortgage|home loan|loan rate)\b/i, 230],
@@ -388,9 +397,13 @@ const CURATED_APIS = [
388
397
  { name: 'GitHub REST API', url: 'https://docs.github.com/en/rest', description: 'GitHub repositories, stars, issues, commits, pull requests, releases, users, and org data API.', auth: 'No', https: true, cors: 'Yes', category: 'Development', source: 'curated', sourceWeight: 5 },
389
398
  { name: 'npm Registry API', url: 'https://github.com/npm/registry/blob/main/docs/REGISTRY-API.md', description: 'No-auth npm package metadata, versions, downloads, dist-tags, registry documents, and package data.', auth: 'No', https: true, cors: 'Yes', category: 'Development', source: 'curated', sourceWeight: 5 },
390
399
  { name: 'Docker Hub', url: 'https://docs.docker.com/docker-hub/api/latest/', description: 'Docker image repositories, tags, registry metadata, namespaces, vulnerabilities, and container image data API.', auth: 'apiKey', https: true, cors: 'Unknown', category: 'Development', source: 'curated', sourceWeight: 5 },
400
+ { name: 'Webhook.site', url: 'https://webhook.site/', description: 'Webhook testing, inspect HTTP requests, debug callbacks, request bins, and temporary webhook endpoints.', auth: 'No', https: true, cors: 'Yes', category: 'Development', source: 'curated', sourceWeight: 5 },
401
+ { name: 'Beeceptor', url: 'https://beeceptor.com/docs/', description: 'Webhook testing, mock APIs, request bin inspection, HTTP debugging, and endpoint simulation.', auth: 'No', https: true, cors: 'Unknown', category: 'Development', source: 'curated', sourceWeight: 5 },
391
402
 
392
403
  { name: 'Auth0', url: 'https://auth0.com/docs/api', description: 'OAuth, OpenID Connect, login, user profiles, social auth, authentication, and identity APIs.', auth: 'apiKey', https: true, cors: 'Unknown', category: 'Authentication', source: 'curated', sourceWeight: 5 },
393
404
  { name: 'Clerk', url: 'https://clerk.com/docs/reference/backend-api', description: 'Authentication, user profiles, organizations, sessions, OAuth, social login, and identity APIs.', auth: 'apiKey', https: true, cors: 'Unknown', category: 'Authentication', source: 'curated', sourceWeight: 5 },
405
+ { name: 'Stytch', url: 'https://stytch.com/docs/api', description: 'Passwordless login, magic links, OTP, OAuth, passkeys, sessions, and user authentication API.', auth: 'apiKey', https: true, cors: 'Unknown', category: 'Authentication', source: 'curated', sourceWeight: 5 },
406
+ { name: 'Magic', url: 'https://magic.link/docs/api', description: 'Passwordless authentication, magic links, wallets, login, users, and identity API.', auth: 'apiKey', https: true, cors: 'Unknown', category: 'Authentication', source: 'curated', sourceWeight: 5 },
394
407
  { name: 'Mail.tm', url: 'https://docs.mail.tm/', description: 'No-auth temporary email inboxes, disposable mail accounts, receive messages, and testing email API.', auth: 'No', https: true, cors: 'Yes', category: 'Email', source: 'curated', sourceWeight: 5 },
395
408
  { name: 'Twilio Verify', url: 'https://www.twilio.com/docs/verify/api', description: 'SMS OTP verification, phone verification, one-time passcodes, factors, and verification checks.', auth: 'apiKey', https: true, cors: 'Unknown', category: 'Telecom', source: 'curated', sourceWeight: 5 },
396
409
  { name: 'numverify', url: 'https://numverify.com/documentation', description: 'Phone number validation, carrier, country, location, line type, and international number lookup API.', auth: 'apiKey', https: true, cors: 'Unknown', category: 'Telecom', source: 'curated', sourceWeight: 5 },
@@ -734,7 +747,17 @@ function applyQueryHints(args) {
734
747
  if (!args.cors && /\b(cors|frontend-safe|browser-safe|frontend safe|browser safe)\b/.test(query)) args.cors = 'Yes';
735
748
  }
736
749
 
737
- const SEARCH_STOPWORDS = new Set(['a', 'an', 'and', 'api', 'apis', 'for', 'from', 'in', 'no', 'of', 'on', 'or', 'the', 'to', 'with']);
750
+ const SEARCH_STOPWORDS = new Set(['a', 'an', 'and', 'api', 'apis', 'auth', 'cors', 'for', 'from', 'in', 'key', 'no', 'of', 'on', 'or', 'the', 'to', 'with', 'yes']);
751
+
752
+
753
+ function normalizeSearchQuery(query) {
754
+ return String(query || '')
755
+ .replace(/\b(no auth|without auth|no api key|no key|unauthenticated)\b/gi, ' ')
756
+ .replace(/\b(cors|cors yes|frontend-safe|browser-safe|frontend safe|browser safe|https)\b/gi, ' ')
757
+ .replace(/\b(api|apis)\b/gi, ' ')
758
+ .replace(/\s+/g, ' ')
759
+ .trim();
760
+ }
738
761
 
739
762
  function tokenSet(text) {
740
763
  return new Set(String(text).toLowerCase().match(/[a-z0-9]+/g)?.filter(t => t.length > 1 && !SEARCH_STOPWORDS.has(t)) || []);
@@ -836,9 +859,47 @@ function intentPenalty(entry, queryText) {
836
859
  if (/\b(favicon|website preview|open graph|link preview|screenshot)\b/.test(queryText) && !/\b(microlink|urlbox|favicon|website metadata|open graph|link preview|screenshot)\b/.test(text)) penalty += 120;
837
860
  }
838
861
 
839
- if (/\b(school district|school boundary|district boundary)\b/.test(queryText) && /\b(linkedin|jobs scraper|lead|sales|recruiting)\b/.test(text)) {
862
+ if (/\b(school district|school boundary|district boundary|reverse geocoding|maps routing|routing api|open data demographics|demographics|census data)\b/.test(queryText) && /\b(linkedin|jobs scraper|lead|sales|recruiting|amazon product scraper|tiktok profile scraper)\b/.test(text)) {
863
+ penalty += 120;
864
+ }
865
+ const stockIntent = /\b(stock|stocks|stock quote|stock prices|equity|equities|ticker|tickers)\b/.test(queryText) && !/\b(not stocks?|not stock|non stocks?|non stock)\b/.test(queryText);
866
+ if (stockIntent && !/finance|financial|stock|stocks|equity|ticker|market data|portfolio|stooq|finnhub|polygon|alpha vantage|twelve data/.test(text)) {
867
+ penalty += 80;
868
+ }
869
+ if (stockIntent && /\b(quotable|joke|entertainment|food|weather|test data|placeholder)\b/.test(text)) {
870
+ penalty += 140;
871
+ }
872
+ if (/\bcrypto\b/.test(queryText) && /\bnot stocks?\b/.test(queryText) && /\b(finance|financial|stock|stocks|equity|ticker|portfolio|stooq|finnhub|polygon|sec edgar|predscope|valueray|econdb)\b/.test(text)) {
873
+ penalty += 240;
874
+ }
875
+ if (/\b(oauth identity|passwordless auth|openid|social auth|login oauth)\b/.test(queryText) && !/authentication|auth0|clerk|stytch|magic|openid|identity|passwordless|social auth/.test(text)) {
876
+ penalty += 160;
877
+ }
878
+ if (/\b(oauth identity|passwordless auth|openid|social auth|login oauth)\b/.test(queryText) && /\b(calendar|nager|holiday|open food|plaid|clearbit logo)\b/.test(text)) {
879
+ penalty += 180;
880
+ }
881
+ if (/\b(webhook testing|webhook debug|request bin|mock api)\b/.test(queryText) && !/webhook|beeceptor|requestbin|mock api|pipedream/.test(text)) {
882
+ penalty += 80;
883
+ }
884
+ if (/\b(cve lookup|vulnerability database|vulnerability data|package security)\b/.test(queryText) && !/osv|nvd|cve|cvss|vulnerability|security advisories/.test(text)) {
840
885
  penalty += 95;
841
886
  }
887
+ if (/\b(cve lookup|cve api|cve search)\b/.test(queryText) && !/osv|nvd|cve/.test(text)) {
888
+ penalty += 180;
889
+ }
890
+ if (/\b(joke api|jokes?|memes?)\b/.test(queryText) && !/joke|meme|quote/.test(text)) {
891
+ penalty += 70;
892
+ }
893
+
894
+ if (/\b(crypto|cryptocurrency|dex|orderbook)\b/.test(queryText) && /\b(currency exchange|frankfurter|national bank|vatcomply|exchange rates only|fiat)\b/.test(text) && !/\b(crypto|cryptocurrency|dex|token|blockchain|defi|coinpaprika|0x)\b/.test(text)) {
895
+ penalty += 160;
896
+ }
897
+ if (/\b(oauth|login|auth|authentication|passwordless)\b/.test(queryText) && /\b(calendar|holiday|nameday|nager|non-working days|food|weather|crypto|stock)\b/.test(text) && !/\b(auth|oauth|openid|login|identity|passwordless)\b/.test(text)) {
898
+ penalty += 140;
899
+ }
900
+ if (/\b(medical diagnosis|diagnosis|clinical|symptom checker)\b/.test(queryText) && /\b(food|crypto|currency|weather|joke|tv|geocoding|zippopotam)\b/.test(text) && !/\b(medical|health|clinical|diagnosis|symptom|fhir)\b/.test(text)) {
901
+ penalty += 180;
902
+ }
842
903
 
843
904
  if (!cat.includes('cryptocurrency') && /\b(wallet address|identicon|avatar|profile picture)\b/.test(queryText) && /\b(avatar|identicon|profile picture)\b/.test(text)) {
844
905
  penalty -= 20;
@@ -1097,8 +1158,38 @@ function sourceMatches(entry, source) {
1097
1158
  return (entry.sources || [entry.source]).some(s => String(s).toLowerCase() === source.toLowerCase());
1098
1159
  }
1099
1160
 
1161
+
1162
+ function passesIntentGate(entry, queryText) {
1163
+ const text = `${entry.name || ''} ${entry.category || ''} ${entry.description || ''} ${enrichedText(entry)}`.toLowerCase();
1164
+ if (/\b(currency exchange|exchange rates|fiat exchange|forex rates)\b/.test(queryText) && !/\bcrypto|bitcoin|dex|token\b/.test(queryText)) {
1165
+ if (/\bcurrency exchange\b/.test(String(entry.category || '').toLowerCase())) return true;
1166
+ if (/\b(frankfurter|national bank|vatcomply)\b/.test(String(entry.name || '').toLowerCase())) return true;
1167
+ if (/\b(crypto|crypto-currencies|cryptocurrency|stocks?|portfolio optimizer|econdb)\b/.test(text)) return false;
1168
+ return /\b(currency conversion|forex rates)\b/.test(text);
1169
+ }
1170
+ if (/\b(stock|stocks|stock quote|stock prices|equity|equities|ticker|tickers)\b/.test(queryText) && !/\b(not stocks?|not stock|non stocks?|non stock)\b/.test(queryText)) {
1171
+ return /\b(finance|financial|currency exchange)\b/.test(String(entry.category || '').toLowerCase())
1172
+ || /\b(stooq|portfolio optimizer|alpha vantage|finnhub|polygon|twelve data|sec edgar|econdb|tradier|predscope|valueray)\b/.test(text);
1173
+ }
1174
+ if (/\b(oauth|openid|passwordless|magic link)\b/.test(queryText) || /\blogin\b.*\b(auth|users?)\b/.test(queryText)) {
1175
+ return /\b(authentication|security)\b/.test(String(entry.category || '').toLowerCase())
1176
+ || /\b(auth0|clerk|stytch|magic|oauth|openid|identity|passwordless|social auth|login)\b/.test(text);
1177
+ }
1178
+ if (/\b(medical diagnosis|diagnosis|clinical|symptom checker)\b/.test(queryText)) {
1179
+ return /\b(medical|health|clinical|diagnosis|symptom|fhir)\b/.test(text);
1180
+ }
1181
+ if (/\b(crypto|cryptocurrency|dex|orderbook)\b/.test(queryText) && !/\b(not crypto|non crypto|fiat only)\b/.test(queryText)) {
1182
+ if (!/\bcryptocurrency\b/.test(String(entry.category || '').toLowerCase()) && /\b(finance|financial|currency exchange)\b/.test(String(entry.category || '').toLowerCase())) return false;
1183
+ return /\b(crypto|cryptocurrency|dex|defi|token|blockchain|coin|coinpaprika|0x|dexpaprika|geckoterminal|dexscreener)\b/.test(text);
1184
+ }
1185
+ return true;
1186
+ }
1187
+
1100
1188
  function filterEntries(entries, args) {
1101
- const q = tokenSet(args.query);
1189
+ const queryText = String(args.query || '').toLowerCase();
1190
+ const searchText = normalizeSearchQuery(args.query);
1191
+ const q = tokenSet(searchText);
1192
+ if (args.noAuth && (/\b(oauth|openid|passwordless|magic link)\b/.test(queryText) || /\blogin\b.*\b(auth|users?)\b/.test(queryText))) return [];
1102
1193
  return entries.flatMap(e => {
1103
1194
  if (args.category && !String(e.category || '').toLowerCase().includes(args.category.toLowerCase())) return [];
1104
1195
  if (args.source && !sourceMatches(e, args.source)) return [];
@@ -1106,13 +1197,15 @@ function filterEntries(entries, args) {
1106
1197
  if (args.https && !e.https) return [];
1107
1198
  if (args.openapi && !e.openapiUrl) return [];
1108
1199
  if (args.cors && String(e.cors || '').toLowerCase() !== args.cors.toLowerCase()) return [];
1200
+ if (q.size && !passesIntentGate(e, queryText)) return [];
1109
1201
  const matched = q.size ? textScore(e, q) : 1;
1110
1202
  const domain = q.size ? domainAdjustment(e, q) : 0;
1111
- const targeted = q.size ? targetedBoost(e, args.query.toLowerCase()) : 0;
1203
+ const targeted = q.size ? targetedBoost(e, queryText) : 0;
1112
1204
  if (q.size && matched === 0 && domain <= 0 && targeted <= 0) return [];
1113
- const s = q.size ? score(e, q, args.query.toLowerCase()) : 1;
1205
+ let s = q.size ? score(e, q, searchText.toLowerCase()) : 1;
1114
1206
  if (q.size && s <= 0) return [];
1115
- return [{ ...e, score: s + (e.sourceWeight || 0) }];
1207
+ const finalScore = s + (e.sourceWeight || 0);
1208
+ return [{ ...e, score: finalScore }];
1116
1209
  }).sort((a, b) => b.score - a.score || String(a.category).localeCompare(String(b.category)) || String(a.name).localeCompare(String(b.name))).slice(0, args.limit);
1117
1210
  }
1118
1211