// ToolPick: tool definition used by the generic app (routes, inputs, views, copy).
import { liveCheck, terms, InputError, Incomplete, DISCLAIMER, LIMITS, PRIVACY, ENGINE_VERSION, USER_AGENT, CDP_URL, PAYAI_URL, NETWORKS, OUR_HOSTS } from './engine.js';
import { LITE_EXAMPLE, PICK_EXAMPLE, MARKET_EXAMPLE } from './examples.js';
import { withDeadline } from './deadline.js';
export { InputError, Incomplete, DISCLAIMER, LIMITS, PRIVACY, ENGINE_VERSION, USER_AGENT };
export const NAME = 'ToolPick';
export const SLUG = 'toolpick';
export const SOURCE_URL = 'https://agentgate-source.pages.dev/x402-toolpick/';
export const NPM_URL = 'https://agentgate-source.pages.dev/x402-toolpick/'; // npm paused: self-host from source (key `npm` kept for compatibility)
export const SELF_HOST = { source: 'https://agentgate-source.pages.dev/x402-toolpick/', git: 'https://agentgate-source.pages.dev/x402-toolpick/x402-toolpick.git', tarball: 'https://agentgate-source.pages.dev/x402-toolpick/x402-toolpick.tar.gz', run: 'npm install --omit=dev --ignore-scripts && node src/mcp-stdio.js', note: 'npm publishing is paused; run the stdio MCP server from source' };
export const SOURCES = { cdp_discovery: CDP_URL, payai_discovery: PAYAI_URL, live_check: 'one unpaid request to each candidate endpoint (pick tier)' };
export const STALE_MS = 12 * 3600_000;
export const MAX_AGE_MS = 72 * 3600_000;
export const DESCRIPTION = 'Which paid API should my agent call? ToolPick searches every paid x402 resource listed on the public x402 discovery indexes (36,000+ endpoints, merged from the CDP discovery API and the PayAI list) and ranks them for a task by relevance, real 30-day usage (calls and unique payers reported by the index), recency and price, with filters for max price, network (Base, Solana, Polygon, Arbitrum...) and minimum payers. The pick tier live-checks the top candidates (one unpaid request each: do they answer 402 with terms right now, does the price match the listing) and names a best verified pick plus the category price range; the market tier gives sellers and builders the competition and pricing picture for a category (price quartiles, payer-weighted price, top sellers by estimated revenue). Fail-closed: a stale or missing index is refused (not charged) and unverified candidates are never called verified. Paid per call over x402 or with prepaid credits; free preview and remote MCP.';
const NEED = { type: 'string', description: 'The capability you need, in plain words, e.g. "solana token safety", "web search", "pdf to text", "hyperliquid funding rates" (2-200 chars)' };
const MAXP = { type: 'number', description: 'Only resources priced at or below this many USD per call, e.g. 0.01' };
const NETP = { type: 'string', enum: ['any', ...NETWORKS], description: 'Only resources payable on this network (default any)' };
const MINP = { type: 'integer', description: 'Only resources with at least this many unique payers in the last 30 days (default 0)' };
export const ROUTES = [
{ tier: 'lite', path: '/v1/lite', key: 'priceLite', env: 'X402_PRICE_LITE', dflt: '$0.003', op: 'find_paid_api_lite',
summary: 'Top 5 paid x402 APIs for a task, ranked by relevance, 30-day payers/calls, recency and price (from the index, no live check)',
desc: 'Find the paid x402 API for a task: top 5 resources from the merged public x402 discovery indexes, ranked by relevance to your need, index-reported 30-day payers and calls, recency and price. Each result: service, resource URL, method, description, USDC price, networks, 30-day calls and payers, repeat_buyers, last called time. Filters: max_price, network, min_payers. No live check (use /v1/pick). No matches: 404, never charged.',
returns: 'Top 5 resources (service, URL, method, price, networks, 30-day calls/payers, repeat_buyers, last called), match and host counts, index age, confidence, sources_failed.',
input: { need: 'solana token safety' }, schema: { properties: { need: NEED, max_price: MAXP, network: NETP, min_payers: MINP }, required: ['need'] }, example: LITE_EXAMPLE },
{ tier: 'pick', path: '/v1/pick', key: 'pricePick', env: 'X402_PRICE_PICK', dflt: '$0.01', op: 'pick_paid_api',
summary: 'Top 10 paid x402 APIs for a task with live 402 checks of the top 5, a best verified pick, cheaper alternatives and the category price range',
desc: 'Pick the paid x402 API to call: top 10 ranked resources for your need (relevance, 30-day payers and calls, recency, price; filters), a live check of the top 5 (one unpaid request each, 3 s timeout: 402 with x402 terms, live price and networks, matches the listing?), the best verified pick, the cheapest verified alternative and category price stats. Fail-closed: a failed check is never called verified; none verified means best_pick null, low confidence.',
returns: 'best_pick, cheapest_verified, top 10 results with live check for the top 5, category stats, index age, confidence, sources_failed.',
input: { need: 'web search', max_price: 0.02 }, schema: { properties: { need: NEED, max_price: MAXP, network: NETP, min_payers: MINP }, required: ['need'] }, example: PICK_EXAMPLE },
{ tier: 'market', path: '/v1/market', key: 'priceMarket', env: 'X402_PRICE_MARKET', dflt: '$0.02', op: 'x402_market_report', allowEmpty: true,
summary: 'x402 market report for a category (or the whole index): competition, price quartiles, payer-weighted price, demand, top sellers by estimated revenue',
desc: 'Market report for sellers and builders: for a category (need, e.g. token safety) or the whole x402 index, resource and seller-host counts, hosts with 3+ payers, listed price quartiles, the price where buyers are (payer-weighted median), 30-day calls, estimated 30-day revenue (calls x price, an upper bound), top sellers and the resources with most payers. Use it to price a new endpoint or test a niche for paying demand. limit 1-25 (default 10).',
returns: 'Category stats (resources, hosts, price quartiles, buyer price quartiles, payer-weighted median, calls, est. revenue), top sellers, top resources by payers, index age, confidence.',
input: { need: 'token safety' }, schema: { properties: { need: { ...NEED, description: `${NEED.description}; omit for the whole index` }, network: NETP, limit: { type: 'integer', description: 'Rows per list, 1-25 (default 10)' } } }, example: MARKET_EXAMPLE },
];
export const PREVIEW = { path: '/v1/preview', tier: 'preview', input: { need: 'web search' }, schema: { properties: { need: NEED, max_price: MAXP, network: NETP }, required: ['need'] }, returns: 'match count and the top 3 service names with price, networks and 30-day payers (no URLs, no live checks)' };
function parseNeed(raw, required) {
const n = raw.need ?? raw.query ?? raw.q ?? raw.task;
if (n === undefined || n === '') { if (required) throw new InputError('"need" is required: the capability you need, e.g. "web search"'); return ''; }
if (typeof n !== 'string' || n.trim().length < 2 || n.length > 200) throw new InputError('"need" must be a string of 2-200 characters');
if (!terms(n).length) throw new InputError('"need" must name a capability, e.g. "solana token safety" or "pdf to text"');
return n.trim();
}
export function parseInput(tier, raw = {}) {
const v = {};
v.need = parseNeed(raw, tier !== 'market');
if (raw.network !== undefined && raw.network !== '' && raw.network !== 'any') {
const n = String(raw.network).toLowerCase().replace(/^arbitrum-one$/, 'arbitrum');
if (!NETWORKS.includes(n)) throw new InputError(`"network" must be one of any, ${NETWORKS.join(', ')}`);
v.network = n;
}
if (tier === 'market') {
const lim = raw.limit === undefined || raw.limit === '' ? 10 : Number(raw.limit);
if (!Number.isInteger(lim) || lim < 1 || lim > 25) throw new InputError('"limit" must be an integer from 1 to 25');
v.limit = lim;
return v;
}
if (raw.max_price !== undefined && raw.max_price !== '') {
const p = Number(String(raw.max_price).replace(/^\$/, ''));
if (!Number.isFinite(p) || p < 0 || p > 100) throw new InputError('"max_price" must be a number of USD from 0 to 100, e.g. 0.01');
v.maxPrice = p;
}
if (tier !== 'preview' && raw.min_payers !== undefined && raw.min_payers !== '') {
const m = Number(raw.min_payers);
if (!Number.isInteger(m) || m < 0 || m > 100000) throw new InputError('"min_payers" must be an integer from 0 to 100000');
v.minPayers = m;
}
return v;
}
function freshness(info, now) {
const age = now - info.at;
const failed = [...(info.sources_failed || [])];
if (age > MAX_AGE_MS) throw new Incomplete('The x402 index is older than 72 hours and is being refreshed; not charged, retry later', ['index_stale']);
let confidence = 'high';
if (age > STALE_MS) { failed.push('index_stale'); confidence = age > 24 * 3600_000 ? 'low' : 'medium'; }
else if (failed.length) confidence = 'medium';
return { index: { refreshed_at: new Date(info.at).toISOString(), age_s: Math.round(age / 1000), stale: age > STALE_MS, counts: info.counts }, failed, confidence };
}
/** ctx: { fetchImpl, now, store } */
export async function run(tier, v, ctx) {
const now = ctx.now ?? Date.now();
let res;
try { res = await ctx.store.query(tier === 'preview' ? 'preview' : tier, v, now); } catch (e) { console.warn(`toolpick store: ${String(e?.message || e).slice(0, 200)}`); throw new Incomplete('Index store unavailable; not charged, retry shortly', ['index_store']); }
if (!res?.info) throw new Incomplete('The x402 index is being built (first refresh takes about a minute); not charged, retry shortly', ['index_not_ready']);
const f = freshness(res.info, now);
const r = res.result;
const base = { need: v.need || null, filters: { max_price: v.maxPrice ?? null, network: v.network || 'any', min_payers: v.minPayers ?? 0 }, terms: r.terms, index: f.index };
if (tier === 'market') {
if (v.need && !r.stats.resources) throw new InputError(`No x402 resources match "${v.need}"`, 404, 'no_matches');
return { ...base, filters: { network: v.network || 'any' }, ...r, confidence: f.confidence, sources_failed: f.failed, checked_at: new Date(now).toISOString() };
}
if (!r.matches) throw new InputError(`No x402 resources match "${v.need}" with these filters`, 404, 'no_matches');
const out = { ...base, matches: r.matches, hosts: r.hosts, results: r.results };
let confidence = f.confidence; const failed = f.failed;
if (tier === 'pick') {
const checks = await Promise.all(r.results.slice(0, 5).map((x) => (OUR_HOSTS.test(x.host)
? Promise.resolve({ status: 'not_probed', reason: 'own tool (ToolPick does not probe its own endpoints)' })
: withDeadline(liveCheck({ u: x.resource, m: x.method, p: x.price_usd }, { fetchImpl: ctx.fetchImpl }), 3500, 'live check').catch((e) => ({ status: 'unreachable', error: String(e?.message || e).slice(0, 80) })))));
checks.forEach((c, i) => {
const x = r.results[i];
x.live_check = c;
x.verified_live = c.status === 'live' ? true : (c.status === 'not_probed' ? null : false);
if (c.status === 'unreachable' || c.status === 'server_error') failed.push(`live_check:${x.host}`);
});
const ok = r.results.filter((x) => x.verified_live === true && x.live_check.price_matches_index !== false && (v.maxPrice === undefined || (x.live_check.live_price_usd ?? x.price_usd) <= v.maxPrice));
out.best_pick = ok[0] ? { rank: ok[0].rank, service: ok[0].service, resource: ok[0].resource, method: ok[0].method, price_usd: ok[0].live_check.live_price_usd ?? ok[0].price_usd, networks: ok[0].live_check.live_networks?.length ? ok[0].live_check.live_networks : ok[0].networks, payers_30d: ok[0].payers_30d, why: 'highest-ranked candidate that answered 402 with matching x402 terms in the live check' } : null;
const cheap = ok.filter((x) => x.price_usd !== null).sort((a, b) => a.price_usd - b.price_usd)[0];
out.cheapest_verified = cheap && cheap !== ok[0] ? { rank: cheap.rank, service: cheap.service, resource: cheap.resource, price_usd: cheap.price_usd } : null;
if (!out.best_pick) { confidence = 'low'; out.pick_note = 'No top-5 candidate passed the live check; results are index data only. Not verified.'; }
out.stats = r.stats;
}
return { ...out, confidence, sources_failed: [...new Set(failed)], checked_at: new Date(now).toISOString() };
}
export function paidView(tier, r) { return r; }
export function previewView(r) {
return { need: r.need, matches: r.matches, hosts: r.hosts, top: r.results.slice(0, 3).map((x) => ({ service: x.service, price_usd: x.price_usd, networks: x.networks, payers_30d: x.payers_30d })), index_age_s: r.index.age_s, confidence: r.confidence, sources_failed: r.sources_failed, checked_at: r.checked_at, note: 'Preview: service names only. URLs, usage detail and live checks are on the paid tiers.', disclaimer: DISCLAIMER };
}
export function fullLinks(origin, v, prices) {
const q = encodeURIComponent(v.need);
return { lite: `${origin}/v1/lite?need=${q}`, pick: `${origin}/v1/pick?need=${q}`, market: `${origin}/v1/market?need=${q}`, prices, adds: 'resource URLs, methods, 30-day calls/payers and last-called times for the top 5 (lite); top 10 with live 402 checks, best verified pick and price range (pick); competition and pricing report for the category (market)' };
}
export const MCP_INSTRUCTIONS = 'Call find_paid_api with the capability you need (e.g. "web search", "solana token safety", "pdf to text") to see how many paid x402 APIs offer it and the top 3 services with price and 30-day payers. Resource URLs, live 402 checks and a best verified pick are on the paid x402 routes (/v1/lite, /v1/pick); /v1/market gives sellers the competition and pricing picture. Listings are self-described by sellers; ToolPick ranks, it does not endorse.';
export const MCP_TOOLS = [
{ name: 'find_paid_api', title: 'Find a paid x402 API for a task (free preview)', description: 'Which paid API should my agent call? Searches the 36,000+ paid x402 resources on the public x402 discovery indexes and returns how many match your need, how many seller hosts there are, and the top 3 services ranked by relevance, 30-day unique payers and calls, recency and price (service name, USD price, networks, payers). Optional max_price and network filters. Free preview tier. The paid x402 routes add resource URLs and usage detail for the top 5 (/v1/lite), live 402 checks of the top candidates with a best verified pick (/v1/pick) and a market report with price quartiles and top sellers (/v1/market).', inputSchema: { type: 'object', properties: { need: NEED, max_price: MAXP, network: NETP }, required: ['need'] }, annotations: { readOnlyHint: true, openWorldHint: true, idempotentHint: true } },
];
export function previewText(pv, origin, v, prices) {
return `ToolPick "${pv.need}": ${pv.matches} matching paid x402 resources on ${pv.hosts} hosts. Top: ${pv.top.map((t) => `${t.service} ($${t.price_usd}, ${t.payers_30d ?? 'n/a'} payers/30d, ${t.networks.join('/')})`).join('; ')}. Index ${Math.round(pv.index_age_s / 60)} min old, confidence ${pv.confidence}${pv.sources_failed.length ? `; failed: ${pv.sources_failed.join(', ')}` : ''}.\nURLs + usage: ${origin}/v1/lite?need=${encodeURIComponent(v.need)} (${prices.lite}); live-checked best pick: /v1/pick (${prices.pick}); market report: /v1/market (${prices.market}). x402 USDC on Base, Polygon, Arbitrum One or Solana.\n${DISCLAIMER}`;
}
export async function probeSources({ fetchImpl }) {
const probe = async (name, url) => { const t0 = Date.now(); try { const r = await withDeadline(fetchImpl(url, { headers: { 'user-agent': USER_AGENT, accept: 'application/json' } }), 6000, name); try { await r.body?.cancel(); } catch { /* ignore */ } return [name, { status: r.status, ms: Date.now() - t0 }]; } catch (e) { return [name, { error: String(e?.message || e).slice(0, 80), ms: Date.now() - t0 }]; } };
return Object.fromEntries(await Promise.all([probe('cdp_discovery', `${CDP_URL}?limit=1`), probe('payai_discovery', `${PAYAI_URL}?limit=1`)]));
}
export const LANDING = {
title: 'ToolPick: find, compare and live-check paid x402 APIs for your agent (x402 API + MCP)',
h1: 'Tool<span>Pick</span>',
lead: 'Which paid API should my agent call? ToolPick searches <b>every paid x402 resource on the public x402 discovery indexes</b> (36,000+ endpoints) and ranks them for your task by relevance, real 30-day usage (calls and unique payers), recency and price. The pick tier live-checks the top candidates (one unpaid request each: do they answer 402 with x402 terms right now, does the price match the listing?) and names a best verified pick; the market tier shows sellers and builders the competition and the price where buyers actually are. Fail-closed: a stale index is refused and unverified candidates are never called verified. ToolPick ranks, it does not endorse, and it never pays or holds funds.',
snippet: (origin) => `// Agent: pick a paid API for a task, then call it with the same x402 client
const pick = await (await pay('${origin}/v1/pick?need=' + encodeURIComponent('web search') + '&max_price=0.02')).json();
console.log(pick.best_pick?.service, pick.best_pick?.resource, pick.best_pick?.price_usd);
const market = await (await pay('${origin}/v1/market?need=token%20safety')).json();
console.log(market.stats.price_with_buyers_usd, market.top_sellers.slice(0, 3));`,
};
agentgate-source: code mirror on Cloudflare Pages. All repos