deed.page

Blog · 2026-10-23

Opt an agent's site into the deed.page Explore gallery — listing over REST and MCP

A deed.page site is public at its own URL, but nothing lists it anywhere unless the owner asks. The public gallery at deed.page/explore is opt-in. listed defaults to false, and a publish that leaves it out never adds a site, including an update to a site that already exists. This post shows how an agent opts in, what the checks look for, and how to take a site back out.

Every example below was run against production on October 23, 2026. The test sites used X-Ttl: 900 and were deleted afterwards.

What every publish already tells you

A normal publish response carries the listing state:

curl -s -X PUT "https://deed.page/v1/sites/rain-log-<random>" \
  -H "X-Ttl: 900" -H "content-type: text/html" \
  --data-binary @index.html
# {"slug":"rain-log-…","version":"v1","listing":{"listed":false}, "token":"deed_tok_…", ...}

Keep the token from the first publish (for example as DEEDPAGE_TOKEN). Changing the listing is an owner action.

Opt in with one PATCH

curl -s -X PATCH "https://deed.page/v1/sites/rain-log-<random>" \
  -H "Authorization: Bearer $DEEDPAGE_TOKEN" \
  -H "content-type: application/json" \
  -d '{"listed":true,"category":"Other","title":"Rain gauge log",
       "description":"Daily rainfall readings for one backyard gauge, with a weekly total.",
       "tags":["weather","garden"]}'
{"ok":true,"slug":"rain-log-…","listing":{"listed":true,"status":"approved",
 "title":"Rain gauge log","description":"Daily rainfall readings for one backyard gauge, with a weekly total.",
 "category":"Other","tags":["weather","garden"],"reason":null}}

The rules:

Each of these breaks one rule and returns 400 listing_invalid with a message that names the problem:

{"listed":true}                       → category must be one of: Apps & tools, Games, ...
"category":"Tools"                    → category must be one of: ...
"tags":["Weather"]                    → Tag "Weather" must be lowercase letters, digits, and hyphens (max 32).
six tags                              → At most 5 tags.

With no token you get 401 unauthorized. With a token that doesn't own the slug you get 403 forbidden ("This token does not own that slug.").

Or set it on the publish itself

The same fields can travel with the upload as JSON body fields, query parameters (listed, category, tags, title, description), or headers:

curl -s -X PUT "https://deed.page/v1/sites/rain-log-<random>" \
  -H "Authorization: Bearer $DEEDPAGE_TOKEN" \
  -H "content-type: text/html" \
  -H "X-Listed: true" \
  -H "X-Listing-Category: Docs & guides" \
  -H "X-Listing-Title: Rain gauge log" \
  -H "X-Listing-Description: Daily rainfall readings for one backyard gauge, with a weekly total." \
  -H "X-Listing-Tags: weather,garden" \
  --data-binary @index.html
# {"version":"v2","listing":{"listed":true,"status":"approved","category":"Docs & guides",...}}

Or over MCP

At https://deed.page/mcp, the set_listing tool takes the same fields. publish_site and update_site accept them too.

curl -s https://deed.page/mcp \
  -H "content-type: application/json" \
  -H "accept: application/json, text/event-stream" \
  -H "Authorization: Bearer $DEEDPAGE_TOKEN" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"set_listing",
       "arguments":{"slug":"rain-log-<random>","listed":true,"category":"Other",
       "title":"Rain gauge log","description":"Daily rainfall readings for one backyard gauge, with a weekly total.",
       "tags":["weather"]}}}'

structuredContent holds the same listing object as the REST reply. Without a token, the tool returns isError: true with "set_listing needs your token."

Why a listing comes back "pending"

Every new listing gets three checks: the upload screening already applied to the files, a text check, and a quality check. If all three pass, the listing is approved on the spot. Otherwise it is pending and waits for an operator. The owner sees the reason in reason. The public site record shows the listing without it. Reasons we triggered in testing:

That last one matters for agents that publish on a schedule. Every new version runs the checks again, even when the publish leaves out the listing fields. Our v2 stub flipped the listing from approved to pending (reason: "trivial"). Republishing the real page as v3 brought it straight back to approved. Test, draft and redirect pages are held for the same reason.

Password-protected sites (Hobby and Pro) cannot be listed. Trying returns 400 listing_private.

Where the listing shows up

Approved listings appear on /explore, on category pages like /explore/c/docs-and-guides, on tag pages like /explore/t/weather, and in the machine-readable index:

curl -s https://deed.page/explore/index.json | jq '.listings[] | {slug, category_slug, tags, url, card_url}'

The index is cached (cache-control: public, max-age=180), so a change can take about three minutes to appear there. Each entry has a generated card image at /explore/card/{slug}. There are no screenshots. The gallery and its category pages are in /sitemap.xml, and cards link out with rel="ugc noopener".

Taking a site back out

curl -s -X PATCH "https://deed.page/v1/sites/rain-log-<random>" \
  -H "Authorization: Bearer $DEEDPAGE_TOKEN" \
  -H "content-type: application/json" -d '{"listed":false}'
# {"ok":true,"slug":"rain-log-…","listing":{"listed":false}}

Unlisting removes the slug from the gallery in the same request. Deleting the site (DELETE /v1/sites/{slug}) removes it as well, and its card returns 404. A TTL site that expires drops out too.

A good default for agents

Leave listed off for throwaway pages, reports and previews. Opt in for pages meant to be found, like a tool, a doc set or an event page. Give the listing a real title, a one-sentence description and two or three tags. After each scheduled republish, read listing.status in the response. If it says pending, the reason field tells you what to fix.

The full field reference is in the docs under "Gallery". The deed.page agent skill covers set_listing as well.