diff --git a/app/api/v1/paid/[product]/route.ts b/app/api/v1/paid/[product]/route.ts index 65076eb..ebebf2d 100644 --- a/app/api/v1/paid/[product]/route.ts +++ b/app/api/v1/paid/[product]/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; import { getSeedProduct, paymentRequirement } from '@/lib/seed-products'; +import { buildSeedProductPayload } from '@/lib/seed-product-intelligence'; import { verifyPyrimidPaymentTx } from '@/lib/payment-verification'; function paymentRequired(req: NextRequest, product: NonNullable>) { @@ -24,77 +25,6 @@ function paymentRequired(req: NextRequest, product: NonNullable }) { const { product: productId } = await context.params; const product = getSeedProduct(productId); @@ -122,13 +52,15 @@ export async function GET(req: NextRequest, context: { params: Promise<{ product ); } + const payload = await buildSeedProductPayload(product.product_id, Object.fromEntries(req.nextUrl.searchParams.entries())); + return NextResponse.json({ product_id: product.product_id, vendor_id: product.vendor_id, payment_tx: verification.txHash, payment_amount: verification.amount?.toString(), buyer: verification.buyer, - ...payload(product.product_id, req, proof), + ...payload, routed_by: 'pyrimid', links: { docs: 'https://pyrimid.ai/quickstart', diff --git a/lib/seed-product-intelligence.ts b/lib/seed-product-intelligence.ts new file mode 100644 index 0000000..a5b2808 --- /dev/null +++ b/lib/seed-product-intelligence.ts @@ -0,0 +1,478 @@ +type Query = Record; + +type GitHubRepo = { + full_name: string; + html_url: string; + description: string | null; + stargazers_count: number; + forks_count: number; + open_issues_count: number; + updated_at: string; + pushed_at?: string; + topics?: string[]; + language?: string | null; + license?: { spdx_id?: string | null } | null; +}; + +type GitHubSearchResponse = { + total_count?: number; + items?: GitHubRepo[]; +}; + +type GitHubContent = { + content?: string; + encoding?: string; +}; + +type Capability = { + id: string; + label: string; +}; + +type SegmentDefinition = { + label: string; + queries: string[]; + keywords: string[]; + productIdeas: string[]; + buyerPain: string; +}; + +const SEGMENTS: Record = { + mcp: { + label: 'MCP servers', + queries: [ + 'mcp server language:TypeScript pushed:>2025-01-01', + '"model context protocol" server pushed:>2025-01-01', + ], + keywords: ['mcp', 'model context protocol', 'server', 'tool'], + productIdeas: ['premium-search', 'batch-enrich', 'private-export', 'risk-audit'], + buyerPain: 'agents need callable tools with predictable per-call cost and useful paid outputs', + }, + 'agent-frameworks': { + label: 'Agent frameworks', + queries: [ + 'agent framework tools language:TypeScript pushed:>2025-01-01', + 'ai agent plugin marketplace pushed:>2025-01-01', + ], + keywords: ['agent', 'framework', 'tool', 'plugin', 'marketplace'], + productIdeas: ['paid-plugin-registry', 'tool-routing', 'agent-evaluation', 'workflow-export'], + buyerPain: 'framework users need monetizable tools and paid workflow extensions', + }, + 'api-tools': { + label: 'AI/API tools', + queries: [ + 'ai api data enrichment language:TypeScript pushed:>2025-01-01', + 'api wrapper ai tools pushed:>2025-01-01', + ], + keywords: ['api', 'ai', 'data', 'enrich', 'search'], + productIdeas: ['data-enrich', 'document-analyze', 'search-export', 'scored-api-call'], + buyerPain: 'API vendors already have per-call value that maps well to x402 pricing', + }, +}; + +function githubHeaders(): Record { + const headers: Record = { + Accept: 'application/vnd.github+json', + 'User-Agent': 'pyrimid-seed-product-intelligence', + }; + + if (process.env.GITHUB_TOKEN) { + headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`; + } + + return headers; +} + +async function fetchJson(url: string, timeoutMs = 6000): Promise { + try { + const res = await fetch(url, { + headers: githubHeaders(), + cache: 'no-store', + signal: AbortSignal.timeout(timeoutMs), + }); + + if (!res.ok) return null; + return (await res.json()) as T; + } catch { + return null; + } +} + +async function searchGitHubRepos(query: string) { + const params = new URLSearchParams({ + q: query, + sort: 'updated', + order: 'desc', + per_page: '8', + }); + const data = await fetchJson(`https://api.github.com/search/repositories?${params.toString()}`); + + return { + query, + total_count: data?.total_count || 0, + items: data?.items || [], + }; +} + +function daysSince(iso?: string) { + if (!iso) return 9999; + const time = new Date(iso).getTime(); + if (Number.isNaN(time)) return 9999; + return Math.floor((Date.now() - time) / 86_400_000); +} + +function uniqueRepos(repos: GitHubRepo[]) { + const seen = new Set(); + const unique: GitHubRepo[] = []; + + for (const repo of repos) { + if (seen.has(repo.full_name)) continue; + seen.add(repo.full_name); + unique.push(repo); + } + + return unique; +} + +function keywordHits(repo: GitHubRepo, keywords: string[]) { + const text = [ + repo.full_name, + repo.description || '', + ...(repo.topics || []), + repo.language || '', + ].join(' ').toLowerCase(); + + return keywords.filter((keyword) => text.includes(keyword.toLowerCase())); +} + +function scoreVendorLead(repo: GitHubRepo, definition: SegmentDefinition) { + const hits = keywordHits(repo, definition.keywords); + const recencyDays = Math.min(daysSince(repo.pushed_at || repo.updated_at), daysSince(repo.updated_at)); + const starSignal = Math.min(20, Math.floor(Math.log10(repo.stargazers_count + 1) * 8)); + const forkSignal = Math.min(10, Math.floor(Math.log10(repo.forks_count + 1) * 5)); + const recencySignal = recencyDays <= 30 ? 20 : recencyDays <= 120 ? 12 : recencyDays <= 365 ? 6 : 0; + const keywordSignal = Math.min(25, hits.length * 7); + const licenseSignal = repo.license?.spdx_id ? 5 : 0; + const issueSignal = repo.open_issues_count > 0 ? 5 : 0; + const score = Math.min(100, 35 + starSignal + forkSignal + recencySignal + keywordSignal + licenseSignal + issueSignal); + + return { + vendor_name: repo.full_name, + url: repo.html_url, + source: 'github_search', + fit_score: score, + fit: score >= 85 ? 'very_high' : score >= 70 ? 'high' : score >= 55 ? 'medium' : 'low', + evidence: [ + `${repo.stargazers_count} stars`, + `${repo.forks_count} forks`, + `${recencyDays} days since last update`, + hits.length ? `keyword hits: ${hits.join(', ')}` : 'matched search query', + repo.license?.spdx_id ? `license: ${repo.license.spdx_id}` : 'license not detected', + ], + suggested_product: definition.productIdeas[0], + suggested_price_usdc: '$0.05-$0.25 per call', + suggested_affiliate_bps: 2000, + pitch: `Add an optional x402-gated ${definition.productIdeas[0]} endpoint, list it in Pyrimid, and let buyer agents route paid calls with affiliate attribution.`, + next_step: 'Open the repo docs, identify one high-value tool response, then propose a small paid endpoint plus catalog metadata.', + }; +} + +function curatedLead(segment: string, definition: SegmentDefinition) { + return { + vendor_name: `${definition.label} with data-heavy tools`, + url: `https://github.com/search?q=${encodeURIComponent(definition.queries[0])}&type=repositories`, + source: 'curated_fallback', + fit_score: 65, + fit: 'medium', + evidence: [ + `segment: ${segment}`, + `buyer pain: ${definition.buyerPain}`, + 'GitHub search was unavailable or returned no usable repositories', + ], + suggested_product: definition.productIdeas[0], + suggested_price_usdc: '$0.05-$0.25 per call', + suggested_affiliate_bps: 2000, + pitch: `Target ${definition.label.toLowerCase()} that expose reusable agent tools and can sell a premium response through x402.`, + next_step: 'Run the provided GitHub query manually, shortlist active projects, then pitch a narrow paid endpoint.', + }; +} + +async function vendorLeadDiscovery(query: Query) { + const segment = query.segment || 'mcp'; + const definition = SEGMENTS[segment] || SEGMENTS.mcp; + const searches = await Promise.all(definition.queries.map(searchGitHubRepos)); + const repos = uniqueRepos(searches.flatMap((search) => search.items)); + const leads = repos + .map((repo) => scoreVendorLead(repo, definition)) + .sort((a, b) => b.fit_score - a.fit_score) + .slice(0, 8); + + return { + segment, + segment_label: definition.label, + generated_at: new Date().toISOString(), + sources: searches.map((search) => ({ + type: 'github_search', + query: search.query, + total_count: search.total_count, + returned_count: search.items.length, + })), + scoring_model: { + base_fit: 35, + recency: 'up to 20 points for repositories pushed in the last 30 days', + keywords: 'up to 25 points for segment-specific terms in name, description, topics, or language', + traction: 'up to 30 points from stars, forks, license, and open issue activity', + }, + leads: leads.length ? leads : [curatedLead(segment, definition)], + }; +} + +function parseGitHubRepo(input: string) { + const trimmed = input.trim().replace(/\.git$/, ''); + const ownerRepo = trimmed.match(/^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/); + if (ownerRepo) return { owner: ownerRepo[1], repo: ownerRepo[2] }; + + try { + const url = new URL(trimmed); + if (url.hostname !== 'github.com') return null; + const [, owner, repo] = url.pathname.split('/'); + if (!owner || !repo) return null; + return { owner, repo: repo.replace(/\.git$/, '') }; + } catch { + return null; + } +} + +function decodeBase64Content(content?: GitHubContent | null) { + if (!content?.content || content.encoding !== 'base64') return ''; + try { + return Buffer.from(content.content, 'base64').toString('utf8').slice(0, 20_000); + } catch { + return ''; + } +} + +async function fetchGitHubRepo(owner: string, repo: string) { + const encodedOwner = encodeURIComponent(owner); + const encodedRepo = encodeURIComponent(repo); + const [metadata, readme, packageJson] = await Promise.all([ + fetchJson(`https://api.github.com/repos/${encodedOwner}/${encodedRepo}`), + fetchJson(`https://api.github.com/repos/${encodedOwner}/${encodedRepo}/readme`), + fetchJson(`https://api.github.com/repos/${encodedOwner}/${encodedRepo}/contents/package.json`), + ]); + + return { + metadata, + readme: decodeBase64Content(readme), + packageJson: decodeBase64Content(packageJson), + }; +} + +function detectCapabilities(text: string): Capability[] { + const options = [ + { id: 'search', label: 'Search', pattern: /search|query|retriev|lookup|index/ }, + { id: 'enrich', label: 'Enrichment', pattern: /enrich|classif|summar|extract|metadata/ }, + { id: 'analyze', label: 'Analysis', pattern: /analy|audit|score|rank|detect|inspect/ }, + { id: 'export', label: 'Export', pattern: /export|download|csv|json|report/ }, + { id: 'automation', label: 'Automation', pattern: /automate|workflow|agent|tool call|mcp/ }, + ]; + + return options + .filter((capability) => capability.pattern.test(text)) + .map(({ id, label }) => ({ id, label })); +} + +function inferAuditSignals(inputUrl: string, repo: GitHubRepo | null, readme: string, packageJson: string) { + const combined = [inputUrl, repo?.description || '', repo?.full_name || '', readme, packageJson, ...(repo?.topics || [])] + .join(' ') + .toLowerCase(); + const capabilities = detectCapabilities(combined); + const hasMcpSignal = /mcp|model context protocol|tools\/call|tools\/list|server\.tool/.test(combined); + const hasApiSignal = /api|endpoint|route|http|webhook|sdk/.test(combined); + const hasPaymentSignal = /x402|payment|paid|stripe|usdc|billing|subscription/.test(combined); + const hasAuthSignal = /auth|token|api key|oauth|bearer/.test(combined); + const hasRateLimitSignal = /rate limit|quota|limit|abuse|throttle/.test(combined); + + return { + capabilities, + has_mcp_signal: hasMcpSignal, + has_api_signal: hasApiSignal, + has_existing_payment_signal: hasPaymentSignal, + has_auth_signal: hasAuthSignal, + has_rate_limit_signal: hasRateLimitSignal, + }; +} + +function monetizationFitScore(repo: GitHubRepo | null, signals: ReturnType) { + let score = 40; + if (signals.has_mcp_signal) score += 20; + if (signals.has_api_signal) score += 10; + if (signals.capabilities.length >= 3) score += 15; + if (repo?.stargazers_count) score += Math.min(10, Math.floor(Math.log10(repo.stargazers_count + 1) * 5)); + if (repo?.pushed_at && daysSince(repo.pushed_at) <= 120) score += 10; + if (signals.has_existing_payment_signal) score += 5; + return Math.min(100, score); +} + +function paidToolRecommendations(signals: ReturnType) { + const capabilities = signals.capabilities.length + ? signals.capabilities + : [ + { id: 'analyze', label: 'Analysis' }, + { id: 'export', label: 'Export' }, + ]; + + return capabilities.slice(0, 4).map((capability, index) => ({ + tool_id: `paid-${capability.id}`, + name: `Paid ${capability.label}`, + route: `/api/v1/paid/${capability.id}`, + free_preview: 'Return schema, price, sample output, and x402 accepts[] metadata before payment.', + paid_output: `Return higher-value ${capability.label.toLowerCase()} results with provenance and machine-readable JSON.`, + suggested_price_usdc: index === 0 ? '$0.05' : index === 1 ? '$0.10' : '$0.25', + buyer_value: 'Agents can call this only when the paid response saves manual work or unlocks richer data.', + })); +} + +function catalogMetadata(url: string, repo: GitHubRepo | null, score: number) { + const baseName = repo?.full_name || url; + const slug = baseName + .toLowerCase() + .replace(/^https?:\/\//, '') + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-|-$/g, '') + .slice(0, 48); + + return { + vendor_id: slug || 'mcp-vendor', + product_id: `${slug || 'mcp'}-paid-tool`, + category: 'devtools', + tags: ['mcp', 'paid-tools', 'x402', 'base-usdc', score >= 75 ? 'high-fit' : 'needs-validation'], + method: 'GET', + endpoint: `${url.replace(/\/$/, '')}/api/v1/paid/{tool}`, + affiliate_bps: score >= 75 ? 2500 : 1500, + output_schema: { + type: 'object', + properties: { + result: { type: 'object' }, + provenance: { type: 'array' }, + routed_by: { const: 'pyrimid' }, + }, + }, + }; +} + +async function mcpServerAudit(query: Query) { + const target = query.url || query.repo || 'https://example.com/mcp'; + const parsedRepo = parseGitHubRepo(target); + const repoData = parsedRepo ? await fetchGitHubRepo(parsedRepo.owner, parsedRepo.repo) : null; + const repo = repoData?.metadata || null; + const readme = repoData?.readme || ''; + const packageJson = repoData?.packageJson || ''; + const signals = inferAuditSignals(target, repo, readme, packageJson); + const score = monetizationFitScore(repo, signals); + + return { + audit: { + url: target, + generated_at: new Date().toISOString(), + inspected: { + source: parsedRepo ? 'github_repo' : 'url_heuristics', + github_repo: parsedRepo ? `${parsedRepo.owner}/${parsedRepo.repo}` : null, + fetched: parsedRepo + ? { + repository_metadata: Boolean(repo), + readme: Boolean(readme), + package_json: Boolean(packageJson), + } + : {}, + note: parsedRepo + ? 'Public GitHub metadata and repo files were inspected through the GitHub API.' + : 'Arbitrary URLs are not fetched to avoid SSRF; recommendations use URL and path heuristics.', + }, + monetization_fit_score: score, + summary: score >= 75 + ? 'Strong candidate for x402 paid MCP/API tools.' + : score >= 55 + ? 'Moderate candidate; validate a high-value paid output before integration.' + : 'Early candidate; start with a preview endpoint and one narrow paid result.', + detected_signals: signals, + recommended_paid_tools: paidToolRecommendations(signals), + pricing: { + strategy: 'Keep previews free; price paid calls by data cost, compute cost, and buyer urgency.', + suggested_range_usdc: '$0.05-$0.25 per call', + upgrade_path: ['$0.05 preview-adjacent enrichment', '$0.10 structured analysis', '$0.25 batch/export/high-cost response'], + }, + x402_route_shape: { + unpaid_response: { + status: 402, + body: 'Return { error: "payment_required", accepts: [{ x402Version, scheme, network, asset, maxAmountRequired, payTo, resource, description }] }', + headers: ['X-PAYMENT-REQUIRED', 'Cache-Control: no-store'], + }, + paid_retry: 'Accept X-PAYMENT or X-PAYMENT-TX, verify Base USDC payment, then return the paid JSON result.', + }, + catalog_metadata: catalogMetadata(target, repo, score), + integration_steps: [ + 'Choose one high-value MCP tool response to put behind a paid route.', + 'Add a free preview that returns schema, sample output, and x402 accepts[] metadata.', + 'Verify X-PAYMENT or X-PAYMENT-TX before serving the paid response.', + 'Register vendor_id, product_id, endpoint, price, affiliate_bps, tags, and output_schema in the Pyrimid catalog.', + 'Publish agents.txt or llms.txt entries so buyer agents can discover the paid tool.', + ], + risk_notes: [ + signals.has_auth_signal + ? { severity: 'medium', risk: 'The project references auth/API keys.', mitigation: 'Never ask agents to send secrets through paid tool inputs; require scoped tokens or server-side credentials.' } + : { severity: 'low', risk: 'No obvious auth-secret signal detected.', mitigation: 'Still document which inputs are safe and which are rejected.' }, + signals.has_rate_limit_signal + ? { severity: 'medium', risk: 'Rate-limit or quota language detected.', mitigation: 'Expose quotas in catalog metadata and price high-cost calls above marginal cost.' } + : { severity: 'low', risk: 'No obvious quota signal detected.', mitigation: 'Add explicit request-size and abuse limits before launch.' }, + { severity: 'medium', risk: 'Paid endpoints can leak licensed or private data if outputs are not scoped.', mitigation: 'Return provenance, redact secrets, and avoid selling third-party data without rights.' }, + ], + }, + }; +} + +export async function buildSeedProductPayload(productId: string, query: Query) { + switch (productId) { + case 'mya-agent-enrichment': { + const agent = query.agent || 'demo-agent'; + return { + enrichment: { + agent, + category: 'developer-tools', + agent_readable_summary: `${agent} can monetize API calls by exposing paid tools through x402 and listing them in the Pyrimid catalog.`, + monetization_angle: 'Package one high-value tool as a paid MCP/API endpoint priced $0.05-$0.25 per call.', + suggested_cta: 'Claim listing -> add paid tool -> route purchases through Pyrimid.', + }, + }; + } + case 'mya-category-scout': { + const category = query.category || 'developer-tools'; + return { + category, + agents: [ + { name: 'MCP server vendors', fit: 'high', reason: 'Already expose tool interfaces; easiest path to paid tools.' }, + { name: 'AI API wrappers', fit: 'high', reason: 'Usage-based value maps cleanly to x402 per-call pricing.' }, + { name: 'agent directories', fit: 'medium', reason: 'Can route discovery traffic into paid vendor listings.' }, + ], + }; + } + case 'vendor-lead-discovery': + return vendorLeadDiscovery(query); + case 'mcp-server-audit': + return mcpServerAudit(query); + case 'x402-integration-plan': { + const service = query.service || 'agent-api'; + return { + plan: { + service, + route_shape: 'GET /api/paid/{tool} returns 402 until X-PAYMENT or X-PAYMENT-TX is supplied', + payment_network: 'Base USDC', + pyrimid_metadata: ['vendorId', 'productId', 'affiliateBps', 'endpoint', 'output_schema'], + launch_checklist: ['publish llms.txt', 'publish agents.txt', 'submit MCP server card', 'list product in Pyrimid catalog'], + }, + }; + } + default: + return { result: 'unknown_seed_product' }; + } +}