diff --git a/app/api/v1/paid/[product]/route.ts b/app/api/v1/paid/[product]/route.ts index 65076eb..e95458a 100644 --- a/app/api/v1/paid/[product]/route.ts +++ b/app/api/v1/paid/[product]/route.ts @@ -24,7 +24,258 @@ function paymentRequired(req: NextRequest, product: NonNullable { + const params = new URLSearchParams({ q: query, sort: 'updated', order: 'desc', per_page: '5' }); + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 4500); + try { + const res = await fetch(`https://api.github.com/search/repositories?${params}`, { + headers: { + accept: 'application/vnd.github+json', + 'user-agent': 'pyrimid-vendor-lead-discovery', + }, + signal: controller.signal, + next: { revalidate: 1800 }, + }); + if (!res.ok) return []; + const data = await res.json(); + return Array.isArray(data.items) ? data.items : []; + } catch { + return []; + } finally { + clearTimeout(timeout); + } +} + +async function vendorLeadDiscovery(segment: string) { + const queries = leadSearchQueries(segment); + const repos = (await Promise.all(queries.map(fetchGithubRepos))).flat(); + const seen = new Set(); + const leads = repos + .filter((repo) => { + if (seen.has(repo.full_name)) return false; + seen.add(repo.full_name); + return true; + }) + .map((repo) => { + const text = `${repo.full_name} ${repo.description || ''} ${(repo.topics || []).join(' ')}`; + const signals = [ + /mcp|model context protocol/i.test(text) ? 'mcp-surface' : null, + /api|sdk|server/i.test(text) ? 'api-or-tool-server' : null, + /search|data|crawl|scrape|export|analy/i.test(text) ? 'data-or-analysis-value' : null, + /payment|billing|x402|l402|paid|stripe/i.test(text) ? 'payment-aware' : null, + repo.homepage ? 'has-live-homepage' : null, + ].filter((signal): signal is string => Boolean(signal)); + const score = + 35 + + Math.min(25, Math.floor(repo.stargazers_count / 20)) + + signals.length * 8 + + (Date.now() - Date.parse(repo.updated_at) < 90 * 24 * 60 * 60 * 1000 ? 12 : 0); + const productId = slug(repo.name || repo.full_name); + return { + name: repo.full_name, + repo: repo.html_url, + homepage: repo.homepage || null, + description: repo.description || 'No repository description provided.', + stars: repo.stargazers_count, + last_updated: repo.updated_at, + score: Math.min(100, score), + fit: score >= 75 ? 'high' : score >= 58 ? 'medium' : 'watchlist', + signals, + suggested_paid_product: { + product_id: productId, + route_shape: `/api/paid/${productId}`, + starting_price_usdc: '0.05-0.25', + affiliate_bps: 2000, + }, + pyrimid_angle: + signals.includes('mcp-surface') + ? 'Package one existing MCP tool as a paid x402 endpoint and list it in the Pyrimid catalog.' + : 'Wrap the highest-value API call behind x402 and let agents resell it through Pyrimid affiliate routing.', + next_action: `Open an issue or PR proposing a paid ${productId} endpoint with x402 accepts[] metadata and Pyrimid catalog fields.`, + evidence: [ + { type: 'github_repo', url: repo.html_url }, + repo.homepage ? { type: 'homepage', url: repo.homepage } : null, + ].filter((item): item is { type: string; url: string } => Boolean(item)), + }; + }) + .sort((a, b) => b.score - a.score) + .slice(0, 8); + + return { + segment, + generated_at: new Date().toISOString(), + source: { + github_search: true, + queries, + fallback_used: leads.length === 0, + }, + scoring_model: { + max_score: 100, + signals: ['mcp-surface', 'api-or-tool-server', 'data-or-analysis-value', 'payment-aware', 'has-live-homepage', 'recently-updated', 'stars'], + }, + leads: + leads.length > 0 + ? leads + : [ + { + name: 'Sats4AI', + homepage: 'https://sats4ai.com', + description: 'L402-native AI API with many paid endpoints; useful as a reference vendor for Pyrimid catalog shaping.', + score: 72, + fit: 'medium', + signals: ['payment-aware', 'api-or-tool-server', 'has-live-homepage'], + suggested_paid_product: { product_id: 'sats4ai-agent-tools', route_shape: '/api/paid/sats4ai-agent-tools', starting_price_usdc: '0.05-0.25', affiliate_bps: 1500 }, + pyrimid_angle: 'Mirror one agent-facing endpoint as a Pyrimid-listed product and route purchases through Base USDC.', + next_action: 'Contact the vendor with a concrete catalog entry draft and x402 route example.', + evidence: [{ type: 'homepage', url: 'https://sats4ai.com' }], + }, + ], + next_actions: [ + 'Prioritize high-fit leads with MCP or data/API surfaces.', + 'Draft a one-route x402 integration issue or PR for the top lead.', + 'List product_id, price_usdc, affiliate_bps, endpoint, and output_schema before outreach.', + ], + }; +} + +function safeTargetUrl(rawUrl: string) { + try { + const url = new URL(rawUrl); + if (!['http:', 'https:'].includes(url.protocol)) return null; + if (/^(localhost|127\.|10\.|172\.(1[6-9]|2\d|3[0-1])\.|192\.168\.|0\.0\.0\.0)/i.test(url.hostname)) return null; + return url; + } catch { + return null; + } +} + +async function inspectMcpTarget(rawUrl: string) { + const target = safeTargetUrl(rawUrl); + if (!target) { + return { + fetch_status: 'skipped', + reason: 'URL must be public http(s); localhost and private-network targets are not fetched.', + sample: '', + }; + } + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 4500); + try { + const res = await fetch(target.toString(), { + signal: controller.signal, + headers: { accept: 'application/json,text/plain,text/html;q=0.8,*/*;q=0.5' }, + next: { revalidate: 1800 }, + }); + const text = (await res.text()).slice(0, 12_000); + return { + fetch_status: res.ok ? 'ok' : 'http_error', + http_status: res.status, + content_type: res.headers.get('content-type'), + sample: text, + }; + } catch { + return { fetch_status: 'failed', sample: '' }; + } finally { + clearTimeout(timeout); + } +} + +function auditFromSample(url: string, sample: string) { + const lower = sample.toLowerCase(); + const toolHints = [ + lower.includes('search') ? 'search' : null, + lower.includes('export') || lower.includes('csv') ? 'export' : null, + lower.includes('enrich') ? 'enrich' : null, + lower.includes('analy') || lower.includes('audit') ? 'analyze' : null, + lower.includes('summar') ? 'summarize' : null, + lower.includes('scrap') || lower.includes('crawl') ? 'crawl' : null, + ].filter((tool): tool is string => Boolean(tool)); + const parsed = safeTargetUrl(url); + const endpointSlug = slug(parsed?.hostname || 'mcp-server'); + const recommended = toolHints.length > 0 ? toolHints : ['search', 'enrich', 'export', 'analyze']; + + return { + recommended_paid_tools: recommended.map((tool) => ({ + tool, + product_id: `${endpointSlug}-${tool}`, + route_shape: `/api/paid/${tool}`, + suggested_price_usdc: tool === 'search' ? '0.01-0.05' : '0.05-0.25', + buyer_value: `${tool} is a repeatable agent action with clear per-call utility.`, + })), + monetization_readiness: { + has_mcp_or_tool_terms: /mcp|tool|json-rpc|capabilities|resources|prompts/i.test(sample), + has_payment_terms: /x402|l402|payment|invoice|usdc|price|billing/i.test(sample), + has_machine_readable_schema: /json|schema|openapi|tools|inputSchema|outputSchema/i.test(sample), + }, + x402_route_shape: 'Return HTTP 402 with accepts[] metadata until X-PAYMENT or X-PAYMENT-TX is supplied; on valid payment, execute the paid tool and return JSON.', + catalog_metadata: { + vendor_id: endpointSlug, + product_id: `${endpointSlug}-${recommended[0]}`, + endpoint: url, + method: 'GET or POST', + price_usdc: 50_000, + affiliate_bps: 2000, + output_schema: { type: 'object', properties: { result: { type: 'object' }, routed_by: { const: 'pyrimid' } } }, + }, + risk_notes: [ + 'Do not put secrets in MCP tool descriptions or paid endpoint responses.', + 'Block localhost/private-network URLs if this audit endpoint ever fetches arbitrary targets server-side.', + 'Keep paid outputs deterministic enough for buyer verification and refunds.', + ], + implementation_steps: [ + 'Pick one high-value tool and publish a paid route beside the free MCP server.', + 'Add x402 accepts[] metadata with Base USDC price, product_id, and max timeout.', + 'Register the product in the Pyrimid catalog with affiliate_bps.', + 'Add a smoke test that unauthenticated requests return 402 and paid requests return the expected JSON schema.', + ], + }; +} + +async function payload(productId: string, req: NextRequest, proof: string) { const query = Object.fromEntries(req.nextUrl.searchParams.entries()); switch (productId) { @@ -53,28 +304,20 @@ function payload(productId: string, req: NextRequest, proof: string) { } case 'vendor-lead-discovery': { const segment = query.segment || 'mcp'; - return { - segment, - leads: [ - { segment: 'mcp', target: 'MCP servers with paid/data-heavy tools', pitch: 'Add optional x402 payment gate + Pyrimid catalog listing.' }, - { segment: 'agent-frameworks', target: 'Agent frameworks with marketplace/plugin systems', pitch: 'Let builders sell tools to agents with Base USDC settlement.' }, - { segment: 'api-tools', target: 'AI API services with per-call cost', pitch: 'Turn API calls into agent-purchasable products.' }, - ], - }; + return vendorLeadDiscovery(segment); } case 'mcp-server-audit': { const url = query.url || 'https://example.com/mcp'; + const inspection = await inspectMcpTarget(url); return { audit: { url, - recommended_paid_tools: ['search', 'enrich', 'export', 'analyze'], - pricing: '$0.01-$0.25 per call depending on compute/data cost', - integration_steps: [ - 'Add 402 response with x402 accepts[] metadata', - 'Register vendor/product in Pyrimid catalog', - 'Expose tool schema in MCP server card', - 'Add affiliateBps for distribution agents', - ], + inspection: { + fetch_status: inspection.fetch_status, + http_status: 'http_status' in inspection ? inspection.http_status : null, + content_type: 'content_type' in inspection ? inspection.content_type : null, + }, + ...auditFromSample(url, inspection.sample), }, }; } @@ -128,7 +371,7 @@ export async function GET(req: NextRequest, context: { params: Promise<{ product payment_tx: verification.txHash, payment_amount: verification.amount?.toString(), buyer: verification.buyer, - ...payload(product.product_id, req, proof), + ...(await payload(product.product_id, req, proof)), routed_by: 'pyrimid', links: { docs: 'https://pyrimid.ai/quickstart', diff --git a/examples/mcp-paid-tool/README.md b/examples/mcp-paid-tool/README.md index 9755841..a2b3349 100644 --- a/examples/mcp-paid-tool/README.md +++ b/examples/mcp-paid-tool/README.md @@ -1,12 +1,63 @@ -# paid MCP tool pattern +# Paid MCP tool pattern Best fit: MCP servers with expensive data, scraping, enrichment, analytics, compliance checks, search, or model calls. +This example shows the smallest reproducible shape for turning an existing MCP tool into a paid API call that agents can discover through Pyrimid and purchase through x402 on Base USDC. + ## Tool design -- Free tool: `preview_*` returns schema, price, sample output, and payment requirement. -- Paid tool: `buy_*` returns HTTP 402/x402 requirement until paid. -- Discovery: publish server card, `llms.txt`, `agents.txt`, and Pyrimid catalog entry. +- Free MCP tool: `preview_vendor_search` returns schema, price, sample output, and a payment requirement. +- Paid HTTP endpoint: `GET /api/paid/vendor-search?q=...` returns HTTP 402/x402 metadata until paid. +- Paid MCP tool: `buy_vendor_search` calls the paid HTTP endpoint, retries with `X-PAYMENT` or `X-PAYMENT-TX`, then returns the paid JSON. +- Discovery: publish an MCP server card, `llms.txt`, `agents.txt`, and a Pyrimid catalog entry. + +Keep the free preview useful but incomplete. It should let buyer agents decide whether to pay without giving away the paid result. + +## Working endpoint shape + +Use the seed Pyrimid paid endpoint as a live reference: + +```bash +curl -i "https://pyrimid.ai/api/v1/paid/vendor-lead-discovery?segment=mcp" +``` + +Expected unauthenticated response: + +```http +HTTP/2 402 +content-type: application/json +x-payment-required: {"scheme":"x402",...} +x-pyrimid-vendor: pyrimid-growth +x-pyrimid-product: vendor-lead-discovery +``` + +Response body shape: + +```json +{ + "error": "payment_required", + "message": "Pay $0.25 USDC on Base through Pyrimid, then retry with X-PAYMENT or X-PAYMENT-TX.", + "accepts": [ + { + "scheme": "x402", + "network": "base", + "asset": "USDC", + "maxAmountRequired": "250000", + "resource": "https://pyrimid.ai/api/v1/paid/vendor-lead-discovery?segment=mcp" + } + ], + "docs": "https://pyrimid.ai/quickstart" +} +``` + +After the buyer pays through the Pyrimid router, retry with a payment proof: + +```bash +curl -s "https://pyrimid.ai/api/v1/paid/vendor-lead-discovery?segment=mcp" \ + -H "X-PAYMENT-TX: 0xYOUR_BASE_TX_HASH" | jq +``` + +Successful paid responses should be deterministic JSON that a buyer or verifier can inspect. For vendor discovery, that means leads, scores, evidence URLs, suggested product IDs, and next actions. For an MCP audit, that means paid-tool recommendations, pricing, route shape, catalog metadata, and risk notes. ## Minimal product metadata @@ -25,6 +76,125 @@ Best fit: MCP servers with expensive data, scraping, enrichment, analytics, comp } ``` +Recommended metadata fields: + +| Field | Why it matters | +| --- | --- | +| `vendor_id` | Stable vendor identifier used for settlement and stats. | +| `product_id` | Stable product identifier used by agents and affiliates. | +| `description` | One-sentence value proposition for buyer agents. | +| `price_usdc` | Integer micro-USDC amount. `50000` means $0.05. | +| `affiliate_bps` | Commission paid to routing agents. `3000` means 30%. | +| `endpoint` | Public HTTP endpoint that returns 402 before payment. | +| `output_schema` | JSON schema for paid result verification. | + +## Minimal paid route + +```ts +export async function GET(req: Request) { + const proof = req.headers.get("x-payment") || req.headers.get("x-payment-tx"); + const product = { + vendor_id: "your-mcp-server", + product_id: "paid_search", + price_usdc: 50000, + price_display: "$0.05", + endpoint: "https://your-service.com/api/paid/search", + affiliate_bps: 3000 + }; + + if (!proof) { + return Response.json( + { + error: "payment_required", + message: `Pay ${product.price_display} USDC on Base through Pyrimid.`, + accepts: [ + { + scheme: "x402", + network: "base", + asset: "USDC", + maxAmountRequired: String(product.price_usdc), + resource: product.endpoint, + payTo: "0xc949AEa380D7b7984806143ddbfE519B03ABd68B" + } + ], + docs: "https://pyrimid.ai/quickstart" + }, + { + status: 402, + headers: { + "X-PAYMENT-REQUIRED": JSON.stringify({ scheme: "x402", product_id: product.product_id }), + "X-Pyrimid-Vendor": product.vendor_id, + "X-Pyrimid-Product": product.product_id, + "Cache-Control": "no-store" + } + } + ); + } + + // Verify the Base payment tx before returning paid content. + // In production, check recipient, amount, asset, chain, product_id, and replay status. + return Response.json({ + product_id: product.product_id, + result: { + query: new URL(req.url).searchParams.get("q") || "", + rows: [] + }, + routed_by: "pyrimid" + }); +} +``` + +## MCP wrapper tools + +Expose two tools from the MCP server: + +```json +[ + { + "name": "preview_paid_search", + "description": "Preview the paid search product, price, output schema, and x402 payment requirement." + }, + { + "name": "buy_paid_search", + "description": "Purchase and run paid search through Pyrimid x402 routing. Requires a Base USDC payment proof." + } +] +``` + +`preview_paid_search` should never require payment. It returns: + +```json +{ + "product_id": "paid_search", + "price_usdc": 50000, + "endpoint": "https://your-service.com/api/paid/search", + "output_schema": { + "type": "object", + "properties": { + "rows": { "type": "array" }, + "routed_by": { "const": "pyrimid" } + } + } +} +``` + +`buy_paid_search` should: + +1. Call the paid endpoint without payment and surface the 402 metadata. +2. Ask the buyer runtime to pay using x402/Base USDC. +3. Retry with `X-PAYMENT` or `X-PAYMENT-TX`. +4. Return the paid JSON, not the payment secret. + +## Reproducibility checklist + +- `curl -i ` returns HTTP 402 with `accepts[]`, `X-Pyrimid-Vendor`, and `X-Pyrimid-Product`. +- `accepts[0].resource` exactly matches the paid endpoint URL. +- `price_usdc` is an integer in micro-USDC and matches `maxAmountRequired`. +- Paid response includes `product_id`, a typed result object, and `routed_by: "pyrimid"`. +- Catalog entry includes endpoint, price, affiliate basis points, tags, and output schema. +- MCP server exposes a free preview tool and a paid execution tool. +- Docs link to https://pyrimid.ai/quickstart so buyer agents can reproduce the payment flow. + ## Why route through Pyrimid? - Agents can find your tool in one catalog.