AI Product Index

by 110kc3

Not rated
GitHub

About

A machine-readable directory ("SEO for AIs") where AI products register themselves so AI agents can discover them.

Details

Author
110kc3
Categories
Search, Knowledge Base, AI, Other

Setup

Install AI Product Index in your MCP client (Claude Desktop, Cursor, Windsurf, and others).

Repository: https://github.com/110kc3/seo

Follow the installation instructions in the repository README, then restart your MCP client.

A machine-readable directory ("SEO for AIs") whereAI products register themselves so AI agents can discover them. The customers are AI agents acting autonomously: an agent finds the site, readsllms.txt, and registers a product with zero human steps.

🟒Live, on real money, and proven.https://index.percall.devis deployed with the payment rail onmainnetβ€”POST /api/auditquotes $0.05 in USDC on Base and settles it to the receiving address. The rail completed its first end-to-end settlement on 2026-07-29: paid by the stockx402-fetchclient, settled on chain, replay refused, recorded on the revenue dashboard. Details inDEPLOY.mdβ†’ Phase 3.3. Migrated the same day fromindex.kc-it.pl, which stays attached and answers a method-preserving 308 β€” percall.dev is the umbrella domain for the paid-services portfolio this is becoming.

🟒Two products, one Worker, three hostnames(since 2026-08-03).percall.devserves the portfolio page;index.percall.devis the AI Product Index;router.percall.devisThe Routerβ€” live probing and routing at $0.005 per call, plus prepaid weekly watches at $0.005 per sweep. The one-shot Router endpoints settled real money on Base mainnet on 2026-08-03; watch delivery was subsequently proven through outage and recovery. Each host owns its own paths and 308s everything else, so there is exactly one address per document.

Documentation:NEXT.mdβ€”what's outstanding, and whose turn it isΒ·DEPLOY.mdβ€” how to get it live, phase by phase Β·ARCHITECTURE.mdβ€” how it works and why Β·TODO.mdβ€” the changelog, and the reasoning behind each change

- Registration is free and stays free: registeringisthe "purchase" β€” an agent opens a GitHub issue with listing JSON, a workflow validates and publishes it.
- The paid product isPOST /api/audit: an agent-readability audit of any URL, priced per call over x402 (HTTP 402). Its value does not depend on how many listings the registry holds, which is why it β€” not the tier upgrade β€” is what sits behind the payment gate.
- Paid tiers(verified,featured) exist in the schema and ranking; the
[upgrade]flow verifies x402 receipts on chain.

What a listed product gets: a crawlable HTML page with schema.org JSON-LD (/l/<slug>.html), presence in the JSON registry (/api/index.json+/listings/<slug>.json), sitemap inclusion, and llms.txt/llms-full.txt presence.

- Track A β€” human-paid GEO service (revenue now).Humans already pay $29–489/mo for AI visibility. The offer: done-for-you llms.txt + schema.org JSON-LD + agent-readability audit on the customer's own domain. Sold via the landing page's "For humans" section β€”[hire]issue or email. First jobs delivered by hand; no payment infra until someone pays.
- Track B β€” the agent registry (asset that ages).This repo. It markets Track A: every listing page is a live demo of the deliverable, and the funnel is built in (agent registers free β†’ operator sees the listing β†’ upsell). The seed listings are the registry itself, the Track A service, and the operator's seven other deployed sites (dogfood).

Static build in the repo root, served by aCloudflare Worker with static assets(wrangler.tomlβ†’worker/index.js). Zero runtime dependencies; plain-Node scripts. Build and test with Node β‰₯ 22 (the same version CI uses).

The Worker exists for the three things GitHub Pages structurally could not do:

That last row is the point: before the migration there was no way to tell whether a single agent had ever hit the site.

Source of truth(hand-edited or workflow-written):

- listings/<slug>.jsonβ€” one file per listing
- templates/β€” index.html, 404.html, llms.txt, robots.txt, openapi.yaml with{{BASE}}/{{REPO}}/{{COUNT}}/{{LISTINGS_HTML}}placeholders
- site.config.jsonβ€” base URL + repo slug (thesingle knob for migration)
- scripts/validate.mjsβ€” the security boundary: field rules,validate(),reconstruct(),esc(),jsonLd(), plusschemaJson()so the published schema is generated from the same constants that enforce it

Generated bynode scripts/build.mjs(committed; deterministic β€” no timestamps, build twice β†’ zero diff):index.html,404.html,llms.txt,llms-full.txt,robots.txt,openapi.yaml,sitemap.xml,api/index.json,api/schema.json,.well-known/agent.json(A2A card),.well-known/agents.json(the plural agents-manifest β€” a different spec, read by agent-readiness auditors),.well-known/security.txt(RFC 9116; itsExpiresis hardcoded in the template because the build may not stamp a timestamp, and a test fails once it passes),l/.html(thel/dir is wiped and rebuilt so removed listings can't leave stale pages).

Plus the surfaces whose whole job is to make the routes above findable by something that only knows the domain:.well-known/mcp.json(MCP server card β€” SEP-1649/2127 are draft, so it carries only the fields both drafts agree on),opensearch.xml(still the one format that turns a bare domain into a callable search box),feed.xml+feed.json(a directory that gains entries is a feed), and.well-known/ai-plugin.json(superseded, and probed often enough that answering costs less than the 404s). A test asserts the manifests cannot advertise a route the Worker does not have.

Write paths (the autonomous transactions)β€”.github/workflows/register.yml, gated on issue-title prefix (not a label, which REST-API agents couldn't set):

- [register]β€” new listing.scripts/process-issue.mjs(input via env only, never shell-interpolated): 20 KB cap β†’ parse (``json fence or bare body) β†’validate()β†’ unique slug + normalized-URL dedup β†’ ≀ 10 listings per GitHub account β†’ liveness check (product URL must answer < 400 in 10 s) β†’ write reconstructedlistings/<slug>.json.
-
[update]β€” full replacement of an existing listing; only the originalgithub_user(or the repo owner) may update;created/github_user/tierpreserved,updatedstamped.
-
[upgrade]β€” paid tier change ({"slug", "tier": "verified|featured", "rail": "x402", "receipt": {"transaction": "0x…"}}): ownership + shape checks, then on-chain receipt verification viascripts/x402-receipt.mjsβ€” the transaction must have succeeded, have enough confirmations, and contain an ERC-20Transferof at least the tier price in the configured asset topayments.x402_address. Spent transaction hashes are burned into the committedpayments.jsonledger so one payment cannot buy two upgrades. Rejectspayments_not_enabledwhile the rail is unconfigured;rail: "card"returnsmanual_reconciliation.
- All modes: build + commit + push with a reset-and-redo retry loop Γ—3 (not an Actions
concurrencygroup, which silently cancels queued runs; afterreset --hardthe dedup re-runs, so a lost race fails cleanly), then a machine-readable bot comment ({"ok":…,"code":…,"errors":…}) and issue close. Pages redeploys on the push (~1 min).

Tiers:free<verified<featuredβ€” paid tiers sort first in the index and get a badge. Manual flip (e.g. after an out-of-band payment):node scripts/set-tier.mjs <slug> <tier>, then commit + push.

Healthβ€”.github/workflows/health.yml(Mondays 04:17 UTC + manual dispatch):scripts/check-liveness.mjsre-checks every listing URL; strike state in committedhealth.json;3 consecutive weekly failures delist(page 404s, registry updated); failures/delistings reported as a GitHub issue.

Catalog livenessβ€”scripts/probe-catalogs.mjs, same weekly cron: neither upstream registry checks whether its entries still answer, so a rotating sample of both catalogs is probed and the results published atapi/{x402,mcp}/health.json. Every stride-th row rather than a contiguous window β€” these files are sorted, so a neighbourhood is not a sample (placeholder URLs are 1.6% of the MCP catalog but were 54% of its first 120 rows) β€” with the cursor advancing one per run, so a full pass still covers every entry exactly once.A 402 or 401 counts as answering: the question is whether anything is listening, and only transport failures and 5xx count against an entry. Two consecutive misses before anything is called dead; searchflags and never hides, since one weekly probe from one network path is evidence rather than proof.

MCP serverβ€”mcp/server.mjs: zero-dependency stdio JSON-RPC (initialize/ping/tools/list/tools/call). It imports its tool definitions fromworker/discovery.jsand forwardstools/callto the hosted/mcp, so the two servers cannot drift;tools/liststays offline because registry health checks introspect it in a sandbox with no network. Addsregister_product(opens the[register]issue; needs envGITHUB_TOKEN, public_repo), which is the only reason to prefer stdio β€” a token on a public Worker is a credential waiting to leak. Install:claude mcp add ai-product-index -- node <clone>/mcp/server.mjs, or skip the clone entirely withclaude mcp add --transport http ai-product-index https://index.percall.dev/mcp.

Security model: all HTML text/attributes through oneesc(); hrefs only from scheme-allowlisted (http/https, public-host) URL fields; JSON-LD<-escaped against</script>breakout; slug regex + resolved-path assertion stop path traversal; accepted objects rebuilt field-by-field from an allowlist (no__proto__write-through); workflow token scoped tocontents: write, issues: write.

The free/paid boundaryis a whitelist, not a delete.freeView()inscore.jsnames the fields the free tier keeps, so a field added to the audit later cannot leak into it by omission β€” and a test asserts a hypothetical new paid field stays out. The free tier deliberately answers "do I have a problem, and roughly where"; the paid tier answers "here is the code that fixes it".

Payment securityβ€” the facilitator verifies signatures and balances; it has no idea what we charge, soworker/x402.jsis what stops a client from paying one atomic unit to an address of its own choosing:

- every field of the client'sacceptedblock is compared against our own requirements (scheme, network, asset,payTo, amount), and theauthorization is checked independentlyβ€” a payload with a correct-lookingacceptedblock but an authorization paying elsewhere is rejected;
- amounts compare as
BigInt, so"1e5"," 10000"and"-10000"never pass as"10000";
- the nonce isreserved in KV before settling, so a concurrent replay loses the race instead of settling twice;
- the audit target passes
urlError()(public http/https hosts only)before any chargeβ€” nobody pays for a request we would reject.

node --test scripts/.test.mjs # validator, escaping, worker, payment-gate, receipt tests node scripts/build.mjs # regenerate everything (deterministic: build twice β†’ zero diff) npx wrangler dev # serve assets + Worker routes locally # simulate a registration without GitHub: ISSUE_BODY='{"slug":"x-y-z","name":"X","url":"https://example.com","description":"d","category":"api","pricing":"free"}' \ ISSUE_USER=you node scripts/process-issue.mjs # SKIP_LIVENESS=1 to skip the URL check

Usescripts/.test.mjs, notnode --test scripts/β€” Node 22 resolves a bare directory argument as a module and fails before running anything.

.github/workflows/deploy.ymlruns tests, asserts the committed build is not stale, thenwrangler deployon every push tomain. It skips the deploy with a notice while the two Cloudflare secrets are unset, somainstays green instead of collecting red Xs nobody reads.Deployed and green since 2026-07-25; the setup below is recorded for a rebuild, not outstanding work. One-time setup:
-
npx wrangler kv namespace create PAYMENTS→ put the id inwrangler.toml.
- Repo secrets
CLOUDFLARE_API_TOKEN(Workers Scripts: Edit) andCLOUDFLARE_ACCOUNT_ID.
- Point
index.percall.devat the Worker β€”gh workflow run cf-admin -f action=attach-domain -f hostname=index.percall.dev(the zone must be on the Cloudflare account and the token needs its zone rights).
- Optional, for
/api/stats.json:npx wrangler secret put CF_ACCOUNT_IDandCF_ANALYTICS_TOKEN(Account Analytics: Read). Without them the endpoint reportsstats_not_enabledrather than pretending.

Locally, run it withnpx wrangler dev --local --persist-to /tmp/seo-wstate. The--persist-tooutside the repo matters: the asset directory is the repo root, so wrangler's state dir otherwise lands in the watched tree and reload-loops forever.

Migration knob:site.config.json β†’ baseis the single source for every absolute URL; the build regenerates sitemap/canonical/JSON-LD/llms.txt/openapi from it. The three hardcoded URLs in.github/ISSUE_TEMPLATE/.ymlmust be edited by hand (issue-form text can't be templated).

Payments β€” rails, and how to switch between them

Rails differ only in where they settle and who they answer to, so they live as named profiles underpayments.x402.profileswith anactiveselector.Moving from rehearsal to real money is one word, not five edited fields.scripts/x402-config.mjs β†’ resolveX402()is the single resolver; both the Worker and the[upgrade]issue flow read it, so the two can never disagree about which chain and asset are being accepted.

Currentlyactive: "mainnet"β€” Base, USDC, real money.

The flip tomainnetwas made deliberately without first settling a testnet payment, so the profile's correctness rests on what was checked statically: the asset address against Circle's own page and the chain, the EIP-712 domain name against the token'sname(), and the facilitator's/supportedagainst the network. Reverting is the same one word.

The publicx402.orgfacilitator cannot settle Base mainnet.Its/supportedadvertiseseip155:84532and no mainnet at all, and the x402 docs say plainly not to treat it as a production path β€” somainnetpoints atPayAIfrom the official facilitator directory instead: no API key, and it advertises Base mainnet under both protocol versions. A third-party facilitator relays the transaction and pays the gas; it cannot redirect funds, because the authorization is signed to our address for our exact amount.

Check any profile against the chain and its facilitator before switching to it:

That reads the token's ownname(),symbol(),decimals()andversion()off chain and asks the facilitator what it will actually settle. It exists because two mistakes here are invisible until every payment fails: a wrong asset address, and a wrong EIP-712 domain name β€”asset_nameis published as the domain the payer signs against, and USDC calls itself"USDC"on Base Sepolia but"USD Coin"on Base mainnet, which is why it is a per-profile field.

Going live on mainnet was exactly that: the check above, the asset eyeballed on basescan once,"active": "mainnet", push. The live rail is nowcdp, which costs a CDP API key (wrangler secret put CDP_API_KEY_ID/CDP_API_KEY_SECRET, never insite.config.json) and buys a free tier of 1,000 tx/month plus cataloging in the x402 Bazaar. CDP's/supportedanswers 401, soverify-rail.mjssigns it with the same code the Worker uses when those two variables are present β€”gh workflow run cf-admin -f action=verify-cdpruns it on the runner, the only place the secrets exist.

Bazaar cataloging is not automatic just because the facilitator is CDP: the listing is built from discovery metadata attached to asettlement, so an endpoint can take real money indefinitely and never be listed./api/audittherefore publishes anoutputSchemawithdiscoverable: true(v1) and the same object underextensions.bazaar(v2) β€” a shape read off the live catalog rather than the docs.node scripts/bazaar-check.mjsanswers whether it worked.

Pricesare atomic units β€” USDC has 6 decimals, so50000= $0.05. The full audit is $0.05; one check, one live probe, one route query, and one weekly watch sweep are each $0.005; verified and featured listing tiers are $5 and $25. Agents can read the live terms at/api/x402/infowithout provoking a 402.

Card (humans)β€” a Stripe payment link inpayments.stripe_payment_link;rail: "card"upgrades answermanual_reconciliationand are flipped withscripts/set-tier.mjs. Stripe's own x402 product settles to a Stripe balance in fiat but is private preview behind an access request; adopting it later is afacilitator_urlchange, not a rewrite.

CDP authentication(worker/cdp-auth.js) is hand-rolled on WebCrypto rather than pulling in@coinbase/x402, which would dragviem,zodand the whole CDP SDK into a Worker for one signature. The contract was read off those published package sources: header{alg, kid, typ, nonce}withalgofEdDSA(Ed25519) orES256(EC), claims{sub, iss: "cdp", nbf, exp, jti, uris}. Theurisclaim binds each token to one method+host+path, so a/verifytoken cannot be replayed against/settle. A rail declaringauth: "cdp"with no credentials fails closed instead of firing unauthenticated and surfacing Coinbase's 401 as if the agent's payment were bad.

The read API will not change shape β€”tier`has been server-set on every listing since day one.

Search global news using natural language. Webz.io News Search API returns the most relevant articles and content, with filters for source, country, language, date, sentiment, and category.

RAG Search over your content powered by Inkeep

Production-ready RAG out of the box to search and retrieve data from your own documents.

Search and extract information about research papers from arXiv.

Search content using Azure AI Agent Service and Azure AI Search.

A self-hosted MCP server that indexes documentation from various sources and serves it to AI Agents with semantic search.

Read-only MCP server giving AI agents the web as compact, ranked, verified evidence. Local cross-encoder reranker, NLI claim verification, semantic cross-source agreement and contradiction detection. No API keys.

Search and discover 24,500+ MCP servers and AI agents. Semantic search, trust scores, vulnerability tracking.

A discovery and recommendation service for exploring MCP servers using natural language queries.

Explore and discover Model Context Protocol servers using natural language queries.

A meta-server for discovering and provisioning other MCP servers from a large database.

No reviews yet β€” be the first

Sign in to leave a review

Use Google, GitHub, or an email account so ratings stay tied to real people.

Email sign in

No reviews posted yet.