robots.txt for commerce. The open directory AI agents query before they buy.
Merchants publish a tiny shelf.json describing what they sell and what agents
are allowed to do. Agents make one lookup before transacting. Shelf Protocol is the
index in the middle — the handshake layer between agents and the commercial web.
agent ──lookup──▶ Shelf Protocol Registry ◀──publish── merchant
"can I buy here, and how?" "here's my shelf.json"
| Path | What it is |
|---|---|
spec/SPEC.md | The open standard. The shelf.json format + verification. |
spec/shelf.json.example | A sample merchant file. |
server/ | The registry API (FastAPI + SQLite). Register, lookup, search, verify, stats. |
sdk/shelfprotocol/ | The one-line lookup client a developer drops into an agent, plus the MCP server and the LangChain/CrewAI tool adapters. Pip-installable (pyproject.toml at repo root). |
demo/demo_agent.py | A shopping agent that uses Shelf Protocol to decide what it's allowed to buy. |
web/index.html | Developer landing page. |
tests/ | Test suites (cd tests && for t in test_*.py; do python3 $t; done). |
cd shelfprotocol
pip install -r server/requirements.txt
# 1. start the registry
python -m uvicorn server.main:app --port 8080
# interactive API docs at http://localhost:8080/docs
# 2. in another terminal, seed sample merchants
python -m server.seed
# 3. run the demo agent against it
SHELF_URL=http://localhost:8080 python demo/demo_agent.py
You'll watch an agent query Shelf Protocol, get back verified merchants, and either buy autonomously (within the merchant's declared limit) or escalate to a human.
Note: SQLite stores its file next to
server/db.pyby default. If you run on a network/mounted drive that errors withdisk I/O error, set a local path:export SHELF_DB=/tmp/shelf.sqlite.
POST /v1/merchants with a shelf.json body (see spec/shelf.json.example for the full schema):
curl -X POST http://localhost:8080/v1/merchants \
-H "Content-Type: application/json" \
-d '{
"merchant": {
"name": "Acme Coffee Co.",
"domain": "acme-coffee.example",
"categories": ["food.beverages.coffee"]
},
"agent_policy": {
"agents_allowed": true,
"max_autonomous_order_usd": 250
},
"checkout": {
"protocol": "AP2",
"endpoint": "https://acme-coffee.example/agent/checkout"
},
"catalog": {
"feed_url": "https://acme-coffee.example/.well-known/shelf-catalog.json"
}
}'
The response includes an api_key (save it — shown only once) and a verification_dns_record to add to your DNS. Once the TXT record is live, confirm ownership with:
export SHELF_API_KEY=osk_... # the api_key from the registration response
curl -X POST http://localhost:8080/v1/merchants/acme-coffee.example/verify \
-H "X-Api-Key: $SHELF_API_KEY"
To change your listing later (limits, checkout endpoint, feed URL — anything
except the domain itself), send the same body to PUT /v1/merchants/<domain>
with your X-Api-Key header. Verification status survives updates.
PUT is a full replace of the merchant-declared fields, not a partial patch —
first GET /v1/merchants/<domain> to see your current listing, edit the
field(s) you want to change, then PUT the whole thing back. This matters
most if you're editing a listing you didn't originally register yourself (see
Seeding & claiming below) — you won't know its current checkout/catalog
values otherwise, and a PUT that omits them clears them.
The registry looks up _shelfprotocol.<domain> in DNS and confirms the TXT record
carries your verification token. On success it flips trust.verified_domain to
true, which is required before can_buy() will allow an agent to purchase
autonomously (see below).
Local demo:
.exampledomains can never resolve in real DNS. Start the server withSHELF_DNS_CHECK=offto skip the TXT lookup (the api_key check still applies).
The registry rate-limits per client IP on /v1/*: 120 reads/min (lookup,
search, stats) and 10 writes/min (register, verify). Exceeding a limit
returns 429 with a Retry-After header. Tune with
SHELF_RATE_LIMIT_READS_PER_MIN / SHELF_RATE_LIMIT_WRITES_PER_MIN,
or disable for local demos and tests with SHELF_RATE_LIMIT=off.
The registry can pre-index stores from their public product feeds:
python -m server.importer domains.txt # one store domain per line
Imported listings are unclaimed: discoverable in search, but inert —
verified_domain: false and a $0 autonomous ceiling, so can_buy() refuses
them. A merchant takes ownership of its listing with:
curl -X POST http://localhost:8080/v1/merchants/acme-coffee.example/claim
That returns an api_key and a DNS record; the claim completes when the
/verify DNS check passes. Until then the listing can't be edited, so a
claimant who doesn't control the domain's DNS can never control its listing.
If you didn't ask to be here, that's a fair thing to be annoyed about. Here is the whole story, plainly.
Seeded listings are built only from product data a store already publishes
openly, and they are inert — unverified, $0 ceiling, so can_buy() refuses
them. They exist so the directory isn't empty on day one, not to represent
any endorsement or relationship. Stores were selected for one reason only:
they publish a machine-readable product feed, which is what the importer can
read. It was not a judgement about the business, and nobody was contacted or
evaluated beforehand.
If a store is yours, you have two options and both are free:
agents_allowed: false if you want agents kept out entirely.Removal is permanent, not just a delete. The record and its cached products go, and the domain is added to a suppression list the importer checks before it makes any request — so the next bulk import can't quietly put the store back. Opting out stays opted out:
python -m server.delist example.com "emailed 2026-08-24, asked to be removed"
Suppression blocks the importer, not the owner. If that store later decides it does want to be listed, registering normally clears the entry — an opt-out is not a ban.
There is deliberately no self-serve delete endpoint. An unauthenticated
DELETE on a domain nobody has proven they own is a way to erase a
competitor, not a safety feature, and an unclaimed listing has no api_key to
authenticate against. Removal stays a human running the command above until
the claim flow can authorize it properly.
Publishing a catalog is two steps: declare a catalog.feed_url when you
register (as in the example above), and host a shelf-catalog.json file at
that URL (copy spec/shelf-catalog.json.example as a starting point). Then
ask the registry to crawl it:
curl -X POST http://localhost:8080/v1/merchants/acme-coffee.example/catalog/refresh \
-H "X-Api-Key: $SHELF_API_KEY"
The fetch is guarded (HTTPS to a public host only, no redirects, 5s timeout,
1MB / 1000-item caps; SHELF_CATALOG_FETCH_GUARD=off relaxes the
scheme/IP checks for local demos). Every item also needs a unique sku and
a name, a non-negative price_usd, and a categories list if present —
if any item fails validation (including two items sharing a sku, a real
data-quality issue Shopify feeds sometimes have), the whole refresh is
rejected with 422 and a reason explaining exactly what's wrong and
which item, rather than silently dropping or guessing at bad data. Agents
then query the cache:
from shelfprotocol import catalog, products
catalog("acme-coffee.example", q="decaf") # one merchant's items
products(q="espresso", verified=True) # across all merchants, verified first
pip install shelfprotocol
from shelfprotocol import lookup, can_buy
profile = lookup("acme-coffee.example")
ok, why = can_buy(profile, amount_usd=40) # honors the merchant's declared limits
Defaults to the hosted registry at api.shelfprotocol.com; point at a
self-hosted one with SHELF_URL.
pip install "shelfprotocol[mcp]"
{
"mcpServers": {
"shelfprotocol": { "command": "shelfprotocol-mcp" }
}
}
Exposes lookup, search, can_buy, catalog, and products as tools —
add this to Claude Desktop, Claude Code, or any other MCP client and it can
check the registry before buying anything, without writing any code.
The MCP tool's can_buy(domain, amount_usd) is stricter than the raw SDK's
can_buy(profile, amount_usd, require_verified=True): it has no
require_verified argument at all, so a manipulated prompt can never talk
an agent into skipping domain verification through this tool. Code you write
yourself can still opt out deliberately with the SDK function directly.
pip install "shelfprotocol[langchain]" # or: "shelfprotocol[crewai]"
from langchain.agents import create_agent
from shelfprotocol.langchain_tools import get_tools
agent = create_agent(model, tools=get_tools())
from crewai import Agent
from shelfprotocol.crewai_tools import get_tools
buyer = Agent(role="Purchasing agent", goal="...", tools=get_tools())
Both give the agent the same five tools as the MCP server — shelf_lookup,
shelf_search, shelf_can_buy, shelf_catalog, shelf_products — so it can
find merchants, read their catalogs, and check a purchase against the
merchant's declared limits before spending anything.
Names carry a shelf_ prefix because framework tool lists are flat and
unnamespaced, and a bare search or products will collide with the web-search
tool most agent stacks already carry. Pass get_tools(prefix="") for the bare
names if you know yours won't clash.
shelf_can_buy is hardened the same way the MCP tool is: no require_verified
argument exists on it, so a manipulated prompt cannot talk the agent into
skipping domain verification. Deliberate opt-out stays available in code you
write yourself, via the SDK's can_buy() directly.
Note:
crewaipinsmcp~=1.28, which contradicts the[mcp]extra'smcp>=2.0, sopip install "shelfprotocol[mcp,crewai]"fails to resolve. That's intentional — the alternative is pip quietly backtrackingcrewaito a years-old release that these adapters were never tested against. Either extra alone installs fine, and you don't need both: the CrewAI adapter and the MCP server are two routes to the same tools.
No step requires a sales team. Revenue scales with the number of agents in the world, which is the bet.
If Google, Anthropic, or Shopify ships their own version and bakes it into their platform, the window narrows. Speed and openness (so it feels like a neutral standard, not a vendor product) are the defenses. Move fast; publish the spec publicly; get into a framework template early.
17 commits
Python
69.9%
HTML
30.1%
robots.txt for commerce. The open directory AI agents query before they buy.
Merchants publish a tiny shelf.json describing what they sell and what agents
are allowed to do. Agents make one lookup before transacting. Shelf Protocol is the
index in the middle — the handshake layer between agents and the commercial web.
agent ──lookup──▶ Shelf Protocol Registry ◀──publish── merchant
"can I buy here, and how?" "here's my shelf.json"
| Path | What it is |
|---|---|
spec/SPEC.md | The open standard. The shelf.json format + verification. |
spec/shelf.json.example | A sample merchant file. |
server/ | The registry API (FastAPI + SQLite). Register, lookup, search, verify, stats. |
sdk/shelfprotocol/ | The one-line lookup client a developer drops into an agent, plus the MCP server and the LangChain/CrewAI tool adapters. Pip-installable (pyproject.toml at repo root). |
demo/demo_agent.py | A shopping agent that uses Shelf Protocol to decide what it's allowed to buy. |
web/index.html | Developer landing page. |
tests/ | Test suites (cd tests && for t in test_*.py; do python3 $t; done). |
cd shelfprotocol
pip install -r server/requirements.txt
# 1. start the registry
python -m uvicorn server.main:app --port 8080
# interactive API docs at http://localhost:8080/docs
# 2. in another terminal, seed sample merchants
python -m server.seed
# 3. run the demo agent against it
SHELF_URL=http://localhost:8080 python demo/demo_agent.py
You'll watch an agent query Shelf Protocol, get back verified merchants, and either buy autonomously (within the merchant's declared limit) or escalate to a human.
Note: SQLite stores its file next to
server/db.pyby default. If you run on a network/mounted drive that errors withdisk I/O error, set a local path:export SHELF_DB=/tmp/shelf.sqlite.
POST /v1/merchants with a shelf.json body (see spec/shelf.json.example for the full schema):
curl -X POST http://localhost:8080/v1/merchants \
-H "Content-Type: application/json" \
-d '{
"merchant": {
"name": "Acme Coffee Co.",
"domain": "acme-coffee.example",
"categories": ["food.beverages.coffee"]
},
"agent_policy": {
"agents_allowed": true,
"max_autonomous_order_usd": 250
},
"checkout": {
"protocol": "AP2",
"endpoint": "https://acme-coffee.example/agent/checkout"
},
"catalog": {
"feed_url": "https://acme-coffee.example/.well-known/shelf-catalog.json"
}
}'
The response includes an api_key (save it — shown only once) and a verification_dns_record to add to your DNS. Once the TXT record is live, confirm ownership with:
export SHELF_API_KEY=osk_... # the api_key from the registration response
curl -X POST http://localhost:8080/v1/merchants/acme-coffee.example/verify \
-H "X-Api-Key: $SHELF_API_KEY"
To change your listing later (limits, checkout endpoint, feed URL — anything
except the domain itself), send the same body to PUT /v1/merchants/<domain>
with your X-Api-Key header. Verification status survives updates.
PUT is a full replace of the merchant-declared fields, not a partial patch —
first GET /v1/merchants/<domain> to see your current listing, edit the
field(s) you want to change, then PUT the whole thing back. This matters
most if you're editing a listing you didn't originally register yourself (see
Seeding & claiming below) — you won't know its current checkout/catalog
values otherwise, and a PUT that omits them clears them.
The registry looks up _shelfprotocol.<domain> in DNS and confirms the TXT record
carries your verification token. On success it flips trust.verified_domain to
true, which is required before can_buy() will allow an agent to purchase
autonomously (see below).
Local demo:
.exampledomains can never resolve in real DNS. Start the server withSHELF_DNS_CHECK=offto skip the TXT lookup (the api_key check still applies).
The registry rate-limits per client IP on /v1/*: 120 reads/min (lookup,
search, stats) and 10 writes/min (register, verify). Exceeding a limit
returns 429 with a Retry-After header. Tune with
SHELF_RATE_LIMIT_READS_PER_MIN / SHELF_RATE_LIMIT_WRITES_PER_MIN,
or disable for local demos and tests with SHELF_RATE_LIMIT=off.
The registry can pre-index stores from their public product feeds:
python -m server.importer domains.txt # one store domain per line
Imported listings are unclaimed: discoverable in search, but inert —
verified_domain: false and a $0 autonomous ceiling, so can_buy() refuses
them. A merchant takes ownership of its listing with:
curl -X POST http://localhost:8080/v1/merchants/acme-coffee.example/claim
That returns an api_key and a DNS record; the claim completes when the
/verify DNS check passes. Until then the listing can't be edited, so a
claimant who doesn't control the domain's DNS can never control its listing.
If you didn't ask to be here, that's a fair thing to be annoyed about. Here is the whole story, plainly.
Seeded listings are built only from product data a store already publishes
openly, and they are inert — unverified, $0 ceiling, so can_buy() refuses
them. They exist so the directory isn't empty on day one, not to represent
any endorsement or relationship. Stores were selected for one reason only:
they publish a machine-readable product feed, which is what the importer can
read. It was not a judgement about the business, and nobody was contacted or
evaluated beforehand.
If a store is yours, you have two options and both are free:
agents_allowed: false if you want agents kept out entirely.Removal is permanent, not just a delete. The record and its cached products go, and the domain is added to a suppression list the importer checks before it makes any request — so the next bulk import can't quietly put the store back. Opting out stays opted out:
python -m server.delist example.com "emailed 2026-08-24, asked to be removed"
Suppression blocks the importer, not the owner. If that store later decides it does want to be listed, registering normally clears the entry — an opt-out is not a ban.
There is deliberately no self-serve delete endpoint. An unauthenticated
DELETE on a domain nobody has proven they own is a way to erase a
competitor, not a safety feature, and an unclaimed listing has no api_key to
authenticate against. Removal stays a human running the command above until
the claim flow can authorize it properly.
Publishing a catalog is two steps: declare a catalog.feed_url when you
register (as in the example above), and host a shelf-catalog.json file at
that URL (copy spec/shelf-catalog.json.example as a starting point). Then
ask the registry to crawl it:
curl -X POST http://localhost:8080/v1/merchants/acme-coffee.example/catalog/refresh \
-H "X-Api-Key: $SHELF_API_KEY"
The fetch is guarded (HTTPS to a public host only, no redirects, 5s timeout,
1MB / 1000-item caps; SHELF_CATALOG_FETCH_GUARD=off relaxes the
scheme/IP checks for local demos). Every item also needs a unique sku and
a name, a non-negative price_usd, and a categories list if present —
if any item fails validation (including two items sharing a sku, a real
data-quality issue Shopify feeds sometimes have), the whole refresh is
rejected with 422 and a reason explaining exactly what's wrong and
which item, rather than silently dropping or guessing at bad data. Agents
then query the cache:
from shelfprotocol import catalog, products
catalog("acme-coffee.example", q="decaf") # one merchant's items
products(q="espresso", verified=True) # across all merchants, verified first
pip install shelfprotocol
from shelfprotocol import lookup, can_buy
profile = lookup("acme-coffee.example")
ok, why = can_buy(profile, amount_usd=40) # honors the merchant's declared limits
Defaults to the hosted registry at api.shelfprotocol.com; point at a
self-hosted one with SHELF_URL.
pip install "shelfprotocol[mcp]"
{
"mcpServers": {
"shelfprotocol": { "command": "shelfprotocol-mcp" }
}
}
Exposes lookup, search, can_buy, catalog, and products as tools —
add this to Claude Desktop, Claude Code, or any other MCP client and it can
check the registry before buying anything, without writing any code.
The MCP tool's can_buy(domain, amount_usd) is stricter than the raw SDK's
can_buy(profile, amount_usd, require_verified=True): it has no
require_verified argument at all, so a manipulated prompt can never talk
an agent into skipping domain verification through this tool. Code you write
yourself can still opt out deliberately with the SDK function directly.
pip install "shelfprotocol[langchain]" # or: "shelfprotocol[crewai]"
from langchain.agents import create_agent
from shelfprotocol.langchain_tools import get_tools
agent = create_agent(model, tools=get_tools())
from crewai import Agent
from shelfprotocol.crewai_tools import get_tools
buyer = Agent(role="Purchasing agent", goal="...", tools=get_tools())
Both give the agent the same five tools as the MCP server — shelf_lookup,
shelf_search, shelf_can_buy, shelf_catalog, shelf_products — so it can
find merchants, read their catalogs, and check a purchase against the
merchant's declared limits before spending anything.
Names carry a shelf_ prefix because framework tool lists are flat and
unnamespaced, and a bare search or products will collide with the web-search
tool most agent stacks already carry. Pass get_tools(prefix="") for the bare
names if you know yours won't clash.
shelf_can_buy is hardened the same way the MCP tool is: no require_verified
argument exists on it, so a manipulated prompt cannot talk the agent into
skipping domain verification. Deliberate opt-out stays available in code you
write yourself, via the SDK's can_buy() directly.
Note:
crewaipinsmcp~=1.28, which contradicts the[mcp]extra'smcp>=2.0, sopip install "shelfprotocol[mcp,crewai]"fails to resolve. That's intentional — the alternative is pip quietly backtrackingcrewaito a years-old release that these adapters were never tested against. Either extra alone installs fine, and you don't need both: the CrewAI adapter and the MCP server are two routes to the same tools.
No step requires a sales team. Revenue scales with the number of agents in the world, which is the bet.
If Google, Anthropic, or Shopify ships their own version and bakes it into their platform, the window narrows. Speed and openness (so it feels like a neutral standard, not a vendor product) are the defenses. Move fast; publish the spec publicly; get into a framework template early.
17 commits
Python
69.9%
HTML
30.1%