Six practical moves to serve both humans and AI agents from the same site, with a copy-paste checklist
Over the past four days, Claude Code and I turned smarts.md from a smart-contract docs site built for humans into one built for AI agents.
Those two things sound like the same site. They're not. A site for humans optimizes for readability, search, and load time. A site for AI agents has to answer a different set of questions:
robots.txt lock them out?Here are the six things I had Claude build. Each is independently useful, and you can drop each into your own site on its own.
robots.txtMost sites inherit a robots.txt from some template that implicitly denies any User-Agent it doesn't know. But after 2025, many AI crawlers are new User-Agents — GPTBot, ClaudeBot, PerplexityBot, Applebot-Extended, Google-Extended, CCBot, meta-externalagent, and more. If you want them in, you have to Allow them explicitly.
The smarts robots.txt opens with a policy note, then lists User-agent + Allow: / for 12 crawlers covering OpenAI, Anthropic, Perplexity, Google Gemini, Apple, Common Crawl, Meta, and Cohere. The stance is blunt: this site exists to be read by AI.
llms.txt — a menu for LLMsllms.txt is a convention proposed by llmstxt.org: drop a concise Markdown file at the site root that tells an LLM what this site is, how to use it, and where the key entry points are. It's your site's elevator pitch.
The smarts llms.txt is about 60 lines:
/{chain}/{address} vs. friendly slug)The insight: LLMs pay in tokens to read your docs, so they won't crawl the whole site. Hand them a distilled index and their hit rate goes up sharply.
.well-known/mcp.json — bet earlyThe MCP protocol hasn't standardized a discovery convention like /.well-known/openid-configuration yet. No public client crawls a fixed well-known path today.
But publishing the manifest is 30 lines:
def well_known_mcp
response.set_header("Cache-Control", "public, max-age=3600")
response.set_header("Access-Control-Allow-Origin", "*")
render json: {
name: "smarts",
version: "0.1.0",
description: "...",
protocol_version: "2024-11-05",
transports: [{ type: "sse", endpoint: "https://smarts.md/mcp/sse" }],
capabilities: { tools: true, resources: false, prompts: false },
tools: MCP_TOOLS.map { |t| { name: t[:name], description: t[:blurb] } }
}
end
The moment a discovery convention lands, I don't have to change a line. And it's self-documentation for developers — anyone curious about my MCP can just curl smarts.md/.well-known/mcp.json.
The trick: the tools array is derived from the MCP_TOOLS constant, single source of truth. An existing structural test forces every app/tools/*_tool.rb to appear in that constant, so the manifest inherits the "new tool won't be forgotten" guarantee for free.
.md variant: a WebFetch-friendly distillation of every pageThis one's my favorite. Claude Code's WebFetch tool has to clean and extract content from arbitrary HTML — that's expensive in tokens and unreliable. But what if you hand it .md directly?
Rails' respond_to supports this natively:
# config/routes.rb
get ":slug(.:format)", to: "contracts#show",
constraints: { slug: ContractSlugs::ROUTE_PATTERN, format: /html|md/ }
https://smarts.md/usdc-eth returns HTML; https://smarts.md/usdc-eth.md returns a 40-line Markdown: address, chain, classification, MCP endpoint, how to query via an AI agent, source links.
# USD Coin on Ethereum
- **Address:** `0xa0b8...`
- **Chain:** Ethereum
- **Classification:** ERC-20 Token
## Query via AI agent
- **MCP endpoint:** `https://smarts.md/mcp/sse`
- **Reference:** `usdc-eth`
- **Sample prompt:** "Tell me the current state of usdc-eth"
One fetch, all the structured content an AI agent wants, no DOM parser needed. Claude knocked this out fast — one controller, two .md.erb templates, (.:format) in routes, done.
This one's the reverse discovery: some of my visitors are already using Claude Code / Cursor / Windsurf. They land on a contract page, and the next thing they want is "let my AI take a look at this."
So every contract page renders a card with:
usdc-eth) or chain/address, with a copy buttonmcp.smarts.md for the one-line setupOne click of friction between "I'm looking at this contract" and "I'm asking my AI about it." The card uses a DaisyUI card and a generic Stimulus copy controller — 50 lines of erb. A regression test locks down the data-copy-text-value attributes so a future refactor can't silently paste the wrong string onto users' clipboards.
OpenGraph, Twitter Card, JSON-LD — these are the traditional search and social-card layer. Worth doing, because AI crawlers read them too.
The interesting choice is how JSON-LD should describe a smart contract. Claude went with a WebPage wrapping a SoftwareApplication:
{
"@type": "WebPage",
"about": {
"@type": "SoftwareApplication",
"name": "USD Coin",
"applicationCategory": "SmartContract",
"operatingSystem": "Ethereum",
"identifier": "0xa0b8..."
}
}
operatingSystem gets the chain name, identifier gets the address, additionalType gets the classification (e.g. "Uniswap V3 Pool"). schema.org has no dedicated SmartContract type, but SoftwareApplication with these fields is enough for an LLM to understand "this is a contract on Ethereum with a specific address."
The OG image is a 1200×630 summary_large_image; Twitter Card, breadcrumbs, softwareVersion, and license fields round it out.
The six items above came out of seven PRs over four days:
| PR | Files | Size |
|---|---|---|
feat/ai-crawler-discovery |
2 | ~126 loc |
feat/well-known-mcp-manifest |
4 | ~129 loc |
feat/markdown-contract-pages |
6 | ~298 loc |
feat/contract-mcp-card |
3 | ~126 loc |
feat/seo-meta-tags |
8 | ~292 loc |
feat/seo-enrichments |
8 | ~189 loc |
feat/og-card |
— | — |
Each is an independent PR with its own tests and commit message. That cadence is the highest-leverage thing about working with Claude — every PR is small enough to review at a glance, but stacked together the site grew a full AI surface area.
If you have a content-shaped site and want it AI-friendly, do this in order:
robots.txt/llms.txt (a distilled menu).md variant to your core content pages/.well-known/mcp.jsonMost SEO engineers won't tell you the first five — they're not in the traditional SEO playbook. But from four days of data on my end, the AI traffic curve and the human traffic curve are two completely independent series. You have to serve both.