# What changed, newest first.

By Avatar 8 (Avatar 8 LLC) · Published October 31, 2026 · Updated October 31, 2026


## 1.15.3

Pages and the custom-domain moment.

- Attaching a custom domain to a Free site that still shows the badge returns `badge_note` (REST and MCP `add_domain`): one sentence for the human about the one-time $9 clean publish or Hobby. Nothing else in the response changes.

- New guides: `/for/cursor`, `/for/codex`, and `/for/claude-code`.

- `/trust` shows whether Google Safe Browsing is on right now and states the abuse review target (one business day). `/support` and `/faq` answer billing, refunds, commercial use, and handing a site over. The terms mention yearly billing, the clean publish, and the fair-use transfer allowance.

- `/data` is refreshed with numbers read on October 31, 2026, and lists retention (site still live after 7 days) separately from the return rate (published again on a later day).

- The here.now rows were re-checked against here.now/llms.txt on October 31, 2026. The home page shows a shorter comparison and a "For platforms and agencies" box.

## 1.15.2

Cost control follow-up.

- CDN caching now works on warm function instances. 1.15.0 only let the CDN keep a file when the site record had been read in the last 0.5 s, and warm instances serve from a copy up to 2 s old, so most asset requests were never cached. Now an older copy is checked against the stored record once, on a CDN miss, and the file is cached only if nothing changed.

- Fewer storage writes: page-view and visit counters flush every 60 s (was 10 s), and fair-use transfer counts every 5 minutes, carrying owners with under 1 MB pending for up to an hour. Counts are unchanged; they appear a little later.

## 1.15.1

Ways to pay, an account page, and clearer upgrade copy.

- Yearly billing: Hobby $30/year and Pro $150/year. `POST /api/v1/billing/checkout` and MCP `get_upgrade_link` take `interval` (`month`, the default, or `year`); a yearly checkout adds `interval` and `price_usd_year`. Signed `/upgrade/` links accept `?interval=year`. Existing monthly prices and subscriptions are unchanged. New admin route `/v1/admin/billing/annual-prices` (GET reports, POST creates any missing yearly price on the existing products).

- `/account`: paste the owner token, or get a one-time sign-in link sent to a verified recovery email (15 minutes, single use; same answer whether or not the address is on file). Shows plan, sites, transfer this month, and clean sites, and opens Stripe checkout (monthly or yearly, or the $9 clean publish) or the Stripe billing portal. Agent seat tokens are refused. Pages are `no-store` and `noindex`.

- The "Published with deed.page" badge now links to `/clean`, which names the site (from `?site=` or the referrer), explains the $9 one-time clean publish and Hobby, and lets the owner pay with the owner token. Visitors get a short "Not the owner?" section.

- New pages: `/upgrade` (every way to pay), `/changelog`, and `/platforms` (paid pilot for agent platforms, from $99/mo plus usage).

- Pricing shows yearly prices, the $9 clean publish, a Platform row, and the monthly fair-use transfer caps. The "Which plan" rule for agents no longer says to stay on Free until a limit: it tells the human once, when the page is for other people, that the badge comes off with the $9 clean publish or Hobby.

- skill.md opens with "save the token first". Pro support email is ops@avatar33.com.

## 1.15.0

Cost control and gallery hygiene.

- Hosted-site assets (anything that is not HTML) are cached at Vercel's CDN for up to 24 hours (`Vercel-CDN-Cache-Control: public, max-age=86400`, browser `Cache-Control: public, max-age=60`) with the cache tag `site-`. Every write of a site record (publish, delete, restore, listing change, password, takedown) purges that tag at once and again about 4 seconds later, so a new version is visible right away. Not cached: HTML (the badge and visit counts depend on it), password-protected sites, Range requests, files over 8 MB, and responses built from a site record older than 0.5 s. `DEED_CDN_CACHE=0` turns it off. Missing files on a live site (404) are cached the same way.

- A matching `If-None-Match` on an asset now answers 304 before the file is read.

- The request proxy (middleware) no longer runs for hosted-site hosts (`{slug}.deed.page`, version hosts); it only passed them through. Custom domains still go through it.

- Each function instance keeps a 64 MB in-memory cache of recently served files (files are content-addressed and never change).

- Visit analytics are buffered per site and day and written once per ~10 seconds instead of on every view; per-hit raw records are no longer written (nothing read them). Counts are unchanged.

- Page-view analytics (`/v1/sites/{slug}/analytics`) now count HTML documents only, matching visit analytics. Assets no longer add to `views`.

- Monthly fair-use transfer per owner: Free 10 GB, Hobby 100 GB, Pro 500 GB per calendar month (UTC), counted on bytes deed.page's servers send for the owner's sites (CDN cache hits are not counted). Over the cap, the owner's sites return a 429 page that says what happened and how to restore them; whoami adds `transfer` (`used_bytes`, `cap_gb`, `over`, and at 80% or more `message_for_user` and `upgrade_url`), and publish responses add `transfer_warning` at 80% or more. Upgrading lifts the block within about a minute. New limit name `quota_transfer` in the limit-hit log. `DEED_TRANSFER_ENFORCE=0` keeps counting without blocking.

- `/explore/t/*` tag pages are `noindex, follow` and are no longer in the sitemap. Adult, sexual, and gambling tags (for example adult, nsfw, porn, xxx, 18-plus, sexy, gambling, casino) are never shown as public tags; their tag pages return 404. Listed sites keep their other tags.

## 1.14.1

Plan-limit `agent_note` now says to show `message_for_user` (the same sentence as `agent_hint`), including on the per-token publish cap ("Don't retry before reset_at."). `user_message` is unchanged. An oversized first publish's token note says the same.

## 1.14.0

Plan-limit errors now carry a short relay sentence. Every plan-limit denial (429 `publish_rate` on publish, upload finalize, and version restore; 413 `quota_bytes` including restore; 403 `quota_sites`, `quota_domains`, `webhook_limit`; 403 `plan_required` for passwords, `visit_range`, `analytics_range`, `analytics_csv`, `badge_removal`, and `agent_seats`) keeps its existing fields and adds `upgrade` (`{plan, price, lifts}` from the live plan config), `message_for_user`, and `agent_hint` (the same sentence, for the agent to show the human verbatim, with the price and `upgrade_url`). A 429 also sends `Retry-After` and `retry_after_seconds`. MCP tool errors (`/mcp` and `/mcp/chatgpt`) put that sentence in the first text block; the JSON follows. `/mcp/chatgpt` still has no signed checkout link. A successful publish at 80% or more of the hourly publish cap adds `rate_warning` with the same `upgrade_url` (trigger `publish_rate`), at most once per hour per owner. Opening that link counts under `upgrade_funnel.by_trigger` as `publish_rate`.

## 1.13.0

Visit analytics. Human HTML page views and daily unique visitors on published sites. A view is a browser GET of an HTML document. Images, CSS, JavaScript, and fonts are not counted. Bots are filtered by user agent (known crawlers, headless browsers, empty and script clients) and by prefetch or non-document fetch metadata. A visitor is an HMAC of the IP and the user agent with a salt that changes each UTC day. The raw IP and the full user agent are not stored. Each hit is stored on its own key, and the daily counters are a separate object updated with a create-if-absent version claim so parallel requests do not drop increments. Counting runs after the response. Free reads 7 days, Hobby 30, Pro 90. `GET /v1/sites/{slug}/visits` and MCP `site_visits` (28 tools on `/mcp`, 25 on `/mcp/chatgpt`) return visitors, page views, top pages, top referrers, and a per-day series. A longer range is 403 `plan_required` (`limit: "visit_range"`) with the existing upgrade link. `https://deed.page/analytics?slug=` is the owner page (token required; a wrong token is rejected). It is not a path on a site subdomain. Publish responses add `analytics_url` and `visit_analytics` and leave `clean_publish` unchanged. `GET /v1/admin/views?from=&to=` (admin token) returns totals, a per-day series, top sites, and `bot_filtered_share`. `GET /v1/admin/stats` adds `monthly_visits` for the last 30 UTC days. The crawler rollup at `GET /v1/sites/{slug}/analytics` is unchanged (90 days on Free and Hobby, 365 plus CSV on Pro).

Upload screening no longer rejects a default Retype (retype.com) build as `brand_impersonation`. The brand heuristic (a brand name plus `type="password"` in one file) ran on every text file, and Retype's minified `resources/js/retype.js` contains both `input[type="password"]` (it clears password fields when it caches a page) and `window.__DOCS_INSTAGRAM__` (its own navigation module). The brand check now skips `.js`, `.mjs`, `.cjs`, `.css`, and `.map` assets, which are served with their own Content-Type and `nosniff` and never render as a page. HTML, SVG, extensionless files, and inline scripts in HTML are checked as before, and kit markers and off-site password forms still apply to every text file. `scripts/publish.sh` now deletes its temp archive and clears the trap before it hands off to `publish-large.mjs` (exec skipped the EXIT trap, so the archive stayed in `$TMPDIR`). The `update_site` description now says not to call `update_site` (not `publish_site`) for files over about 3.5 MB.

## 1.12.0

Large uploads from the shell. `scripts/publish.sh` switches to the manifest upload (`/v1/uploads`) when the archive is over 4 MB (`DEEDPAGE_SPLIT_BYTES`, default 4000000) or when `PUT /v1/sites` returns a non-JSON 413. `scripts/publish-large.mjs` no longer imports a Blob client SDK: it PUTs each missing blob to `upload.url` with `upload.headers`, four at a time, with one retry on a 5xx or a network error. It reads and saves `~/.config/deed-page/token` the same way `publish.sh` does, sends `X-Client` and `ref`, and prints the live URL on the last stdout line. A single file over 100 MB is refused. Both scripts are served at `/cli/publish.sh` and `/cli/publish-large.mjs`, and the same bytes are in `skill/deed-page/scripts/`. MCP `plan_upload` adds `commands` (one curl PUT per missing, not-yet-stored blob, then a finalize line that uses `$DEEDPAGE_TOKEN`) and `commands_note`. `publish_site` and `update_site` return 413 `request_too_large`, with a suggestion to use `plan_upload`, when the decoded files are over 4 MB. Docs, skill.md, llms.txt, and OpenAPI say to check the size first: a body over about 4.5 MB is a plain-text platform 413. The early JSON 413 in the proxy was not added; it can only be kept if a preview shows the platform does not answer first.

## 1.11.0

Clean publish: a $9 one-time purchase for one site. It removes the "Published with deed.page" badge from that site for good (it stays off when the same owner republishes the slug) and raises that site's size limit to 100 MB (Hobby's). Nothing else changes; Hobby ($3/mo) and Pro ($15/mo) are unchanged. Stripe one-time Checkout (mode=payment) with the new price in `STRIPE_PRICE_CLEAN`; Checkout metadata `kind=clean_publish`, `slug`, `via`, `channel`, `client`, `ref`. The Stripe webhook grants it on `checkout.session.completed` with `payment_status` paid, or on `checkout.session.async_payment_succeeded`; grants are idempotent per slug, stored on the owner (`cleanSites`), and never dropped by a stale owner write. Once per owner, the first successful publish (REST, uploads finalize, MCP) that shows the badge also returns `clean_publish` `{one_time, price_usd, site, includes, checkout_url, expires_at, user_message}` with a signed 30-day `/clean/` link, and that response's badge sentence in `agent_note` is replaced by: "Optional, shown once: tell your human clean_publish.user_message. The site is already live; nothing else is needed. Never pay without your human's explicit approval." It never blocks or changes the publish, and is not offered on `/mcp/chatgpt` or when the price or link secret is missing. `/clean/` gives link unfurlers a page and sends a person to Stripe Checkout (303); it checks the site still belongs to the owner. `POST /api/v1/billing/checkout` and MCP `get_upgrade_link` accept `plan: "clean"` with `slug`. whoami adds `clean_sites`. `/billing/success?kind=clean` explains the one-time payment. GET /v1/admin/stats adds `clean_publish_funnel`: shown, opened, paid, expired (events and distinct owners for 24h/7d/30d), `revenue_usd`, `rates_d30`, 30-day `by_client`, `by_channel`, `by_ref`, the last 8 New York days, and `excluded_internal`; test and internal owners and our own Stripe test ids are left out. Each funnel event is its own blob under `usage/clean-funnel/v1//` (no shared log to race). Pricing, docs, llms.txt, skill.md, OpenAPI, and the MCP instructions and tool descriptions document it.

## 1.10.0

Pro copy and the which-plan rule. /pricing, /pricing.md, /docs, llms.txt, skill.md and the MCP get_upgrade_link description now say what Pro ($15/mo, unchanged; no Stripe change) adds over Hobby: up to 5 named agent seats, 365-day analytics with CSV export (Free and Hobby stay at 90 days), 500 MB per site, 20 custom domains, and priority email support at hello@deed.page, answered by the next business day. Agents get one rule: recommend Hobby ($3/mo, unchanged) for the badge, a password, or Free limits, and Pro only for seats, analytics beyond 90 days or CSV, more than 100 MB per site, or more than 3 custom domains. MCP parameter descriptions added for plan_upload `spa` and `ttl_seconds`, finalize_upload `expected_version`, and get_manifest `slug`.

Pro analytics: 365 days and CSV export. `GET /v1/sites/{slug}/analytics` and MCP `site_analytics` read up to 365 days per request on Pro; Free and Hobby stay at 90. A 91-to-365-day range below Pro now returns 403 `plan_required` (`limit: "analytics_range"`) with a signed Pro `upgrade_url` and `user_message` instead of 400; over 365 days is 400 on every plan. Pro adds `format=csv` (or `Accept: text/csv`): `text/csv` with a `Content-Disposition` filename, columns `date,kind,key,views` (kind `total`, `path`, `referrer`, `bot`), cells starting with = + - @ prefixed with '. MCP `site_analytics` takes `format` and returns the CSV text in `csv`. Below Pro, CSV is 403 `plan_required` (`limit: "analytics_csv"`). New checkout triggers `analytics_range` and `analytics_csv`. The JSON body adds `max_days` and `csv_export`. Day files are read 16 at a time. Plans gain `analyticsDays` and `analyticsCsv` (also in whoami `plan`). The /docs MCP tool list now includes `agent_seats`.

Pro agent seats. Pro owners can create up to 5 named agent seats: separate tokens under one owner, each with a label and its own revocation, all sharing the owner's sites, domains, webhooks and limits. New routes `GET /v1/seats` (any token; shows status active, revoked, or paused, `created_at`, hourly `last_used_at`, and `current_seat`), `POST /v1/seats` `{label}` (owner token only; the seat token is returned once; 409 `seat_label_taken`, 403 `seat_limit`), and `DELETE /v1/seats/{id}` (owner revokes any seat; a seat revokes only itself). New MCP tool `agent_seats` (action list, create, revoke): 27 tools on `/mcp`; hidden on `/mcp/chatgpt`, which stays at 24. Free and Hobby get 403 `plan_required` with `limit: "agent_seats"` and a signed Pro `upgrade_url` (new checkout trigger `agent_seats`). Seat tokens get 403 `seat_forbidden` for rotate, revoke scope `all`, seat management, the recovery email, and billing. Owner revoke scope `all`, email recovery, and operator recovery also revoke every seat. When the owner leaves Pro, seat tokens return 401 `seat_inactive` (not revoked) and work again after an upgrade. Limit-hit rows, funnel events, signed upgrade links (`s`), and Checkout metadata (`seat`) now carry the seat id when a seat token caused them; GET /v1/admin/stats adds `agent_seats` (owners with seats, active and revoked seats, 30-day limit hits from owner vs seat tokens, funnel events from seats). whoami adds `token_kind`, `seat`, and `agent_seats {used, limit}`.

Badge-free month: end-of-month nudge and tracking. On days 27 to 30 of the month, publish responses (REST and MCP) carry `badge_month` with `ends_at`, `days_left`, `remove` ("Hobby $3/mo"), a signed 30-day `upgrade_url` (trigger `badge_month_expiring`), `pricing_url`, and a `user_message`, plus a sentence in `agent_note`; `/mcp/chatgpt` gets it without the signed link. For 30 days after the month ends, the publish `badge` offer's link uses the trigger `badge_month_expired`. `badge_removal`, `badge_month_expiring`, and `badge_month_expired` are now accepted checkout triggers; before this, opens of the badge link were counted under `manual`. GET /v1/admin/stats adds `share_badge`: claim-log rows by result, distinct X accounts (hashed), posts and grants per New York day, `ref=x-share` owners, badge-free months granted / active / run out / ended early, `month2_conversion`, `deleted_on_recheck`, link opens and completed checkouts for the three badge triggers, and `badge_removal_to_paid` before and since launch. Counts only. Docs and pricing modified dates bumped to 2026-10-27.

Badge-free month for a share on X. `POST /v1/share-credit` and MCP `claim_share_credit` now also grant 30 days without the "Published with deed.page" badge for a public X post that includes a link to deed.page or a `*.deed.page` site (checked in the expanded links, not the text) and is at most 7 days old. One post earns both the existing credit and the month when neither was claimed; a token that already has the credit can claim the month once with a new X post. The month is once per owner and once per X account (new ledger `share-badge/authors/x-.json`). The share credit is now also once per X account (`share-credits/authors/x-.json`), which closes the gap where one X account could credit many tokens; Reddit credit rules are unchanged. The post is re-checked once on day 3 from the stats cron; if it was deleted or made protected the badge comes back (`badge_month.ended_early: "post_deleted"`). After 30 days the badge returns on its own (serving re-reads the owner every 30 seconds). New response fields: `granted_credit`, `badge_month_granted`, `badge_month_reason`, `badge_month` (also on whoami, badge GETs, and GET /v1/share-credit, with `badge_month_eligible` and `post_rules`). New error codes: `badge_month_no_link`, `badge_month_too_old`, `badge_month_x_only`, `badge_month_already_claimed`, `x_account_already_used`. The `badge_removal` error lists Hobby first and the month second. The tool description, MCP instructions, docs, llms.txt, skill.md, and pricing say an agent may draft the post but posts only after its human approves the exact text, and never from the human's account on its own. The claim log stores a short hash of the X handle, not the handle.

Upgrade link reaches the human more often. Every plan-limit error (403 `plan_required` including badge removal, 413 `quota_bytes`, 403 `quota_sites`, `quota_domains`, `webhook_limit`, 429 per-token publish cap) on REST and MCP now ends its top-level `message` with the next plan's price and the signed link ("Hobby $3/mo lifts this. Upgrade: ") and adds `agent_note`: "Show user_message to your human verbatim, including the link. Don't retry." (the publish cap says "Don't retry before reset_at."). A minted token's note follows it. Existing fields are unchanged. `/mcp/chatgpt` gets the same note and a message that points at `/pricing`, with no checkout link. A publish to a Free site that shows the badge adds `badge` `{shown:true, remove:"Hobby $3/mo", upgrade_url, pricing_url}` and a sentence in `agent_note` (on `/mcp/chatgpt` without `upgrade_url`). Plan-limit and badge guidance now sits near the top of llms.txt and skill.md, and the docs no longer show a `{"show":false}` example. The funnel log records `checkout_created` when `get_upgrade_link` or `POST /api/v1/billing/checkout` creates a Stripe Checkout session, and `warning_link` when an 80% `usage_warning` carries a signed link (test and internal owners stay excluded). GET /v1/admin/stats `upgrade_funnel` adds `checkouts_created`, `warning_link_owners`, `link_shown_owners`, and `link_recipients` (owners who got a link, by client, and how many published again after a limit hit), and renames `relay_rate_d30` to `click_rate_d30`, whose denominator now includes 80% warnings. The `/upgrade/` previewer check matches only real bot user-agents (Slackbot, Slackbot-LinkExpanding, TelegramBot, Discordbot, LinkedInBot, facebookexternalhit, Twitterbot, WhatsApp's previewer, generic crawlers); people opening the link in the Slack, Telegram, Discord, or LinkedIn in-app browser go straight to Stripe Checkout.

MCP Go client compatibility. `/mcp` and `/mcp/chatgpt` now send `Content-Type: application/json` with no charset parameter on JSON-RPC replies, batches, parse errors, and the GET discovery document; go-sdk v0.5.0 rejected `application/json; charset=utf-8`. REST routes are unchanged. The `set_site_access` input schema gives `password` a single type (`"string"`) instead of `["string","null"]`, which mcp-go v0.3.1 could not parse. Behavior is unchanged: a password of 8 to 200 characters sets the gate, and an empty string or null clears it. No other tool schema used an array type. Tool counts stay 26 on `/mcp` and 24 on `/mcp/chatgpt`.

Agents may pay after human approval. skill.md, llms.txt, docs, OpenAPI, and the MCP instructions and `get_upgrade_link` description no longer say an agent must never pay. The rule is now: show your human the price (Hobby $3/mo) and the checkout link; an agent may complete the payment itself only if its human explicitly approved this purchase and it has a payment tool the human approved; never pay without that approval. To confirm an upgrade landed, call `whoami` (GET /v1/whoami) and check `plan.id`; no new tool, so `/mcp` stays at 26 tools and `/mcp/chatgpt` at 24.

Pre-limit size warning. A successful publish (`/v1/sites`, `/v1/uploads` finalize, MCP `publish_site`, `update_site`, `finalize_upload`) whose site is at 80% or more of the plan's per-site size limit (20 MB of Free's 25 MB, or 40 MB of 50 MB with the share credit; 80 MB of Hobby's 100 MB) now adds `usage_warning` with the size, the limit, the percent used, the next plan and price, a signed 30-day `upgrade_url` (trigger `size_warning`, now also accepted as `upgrade_trigger` on checkout), and a `user_message` for the human. `/mcp/chatgpt` gets the same warning without a checkout link. 413 `quota_bytes` on publish, upload plan, and finalize now adds `suggestion` and a message saying that retrying the same upload fails the same way: compress large files, remove unused ones, or split the site.

Share-for-credit: a Free token can get a one-time bump of +25 sites (50 total) and 50 MB per site by sending the URL of a public X or Reddit post about deed.page to POST /v1/share-credit `{"url":"..."}` or the new MCP tool `claim_share_credit` (26 tools on `/mcp`; hidden on `/mcp/chatgpt`, which stays at 24 because the response carries a checkout link). The server checks the post with public endpoints only (X syndication with an oEmbed fallback, Reddit's public JSON): it must be public, mention deed.page or link to a deed.page site, be from the last 30 days, and not have been claimed before. One grant per token and per post; paid tokens are refused; claims are rate-limited per token and per address and logged to `usage/share-credits.json`. The credit stays below Hobby (250 sites, 100 MB per site), and the success response keeps a signed Hobby `upgrade_url` (trigger `share_credit`, so credit claimers' upgrades can be counted on their own). Free quota_sites and quota_bytes errors mention the credit after the upgrade link, only when the token has not used it and it would cover the request. whoami adds `share_credit`. GET /v1/share-credit shows the state. Pricing, docs, llms.txt, skill.md, and OpenAPI document it.

Upgrade funnel tracking. Each limit-hit row now also stores the owner's normalised signup client, an internal flag, and whether the response carried a signed checkout link. Opening a signed `/upgrade/` link (a person, not a link unfurler) and Stripe `checkout.session.completed` / `checkout.session.expired` for deed.page sessions append to a new funnel log (`usage/funnel-events.json`, 31 days; checkout events de-duplicated by session id). GET /v1/admin/stats adds `upgrade_funnel` (clicks, completed and expired checkouts as events and distinct owners for 24h/7d/30d, 30-day breakdowns by signup client, trigger, and via, the last 8 New York days, `relay_rate_d30`, `excluded_internal`), and `limit_hits` adds `by_source`, `link_owners`, and `excluded_internal`. Test owners (`is_test`), internal labels, `DEED_INTERNAL_PRINCIPALS`, and our own Stripe test subscription and customer (`DEED_INTERNAL_STRIPE_IDS` extends the list) are left out of every count, including existing `limit_hits` counts. GET /v1/admin/billing keeps `upgrade_trigger`, `via`, `first_source`, and `how_heard` in subscription metadata, marks internal subscriptions and charges, and adds `net_collected_excluding_internal_minor_units`. Expired checkouts are only counted if the Stripe webhook endpoint is subscribed to `checkout.session.expired`.

Removing the "Published with deed.page" badge is now a paid-plan perk. Free sites always show it; Hobby and Pro never do (unchanged). On Free, `{"show":false}` on PUT/POST /v1/sites/{slug}/badge, PUT/POST /v1/badge, or MCP `set_badge` returns 403 `plan_required` with the standard upgrade fields (`limit: "badge_removal"`, a signed `upgrade_url` for Hobby, `user_message`) and is logged as a `badge_removal` limit hit. Deliberate decision: Free sites and tokens that turned the badge off before 2026-10-25 keep that setting, and re-sending `{"show":false}` for them still succeeds. `{"show":true}` always works. A hide request from a paid plan stores nothing, so the badge returns after a downgrade to Free. Pricing, terms (updated October 25, 2026), docs, llms.txt, OpenAPI, skill.md, and the MCP tool text say so.

A first publish without a token that is over the Free size limit (413 quota_bytes on `/v1/sites`, `/v1/uploads`, or MCP `publish_site` / `plan_upload`) now creates the owner and returns `token`, `token_note`, `agent_note`, and `next_steps`, the same fields as a successful first publish, alongside the upgrade fields. `upgrade_url` is the signed 30-day checkout link for that token, and the hit is recorded in the limit-hit log. Nothing is published. The new-token caps (10 per address, 30 per network prefix, per hour) still apply; past them the error has no token and `upgrade_url` stays `pricing_url`. Before this, the REST path minted an owner and dropped its token, and the upload path minted nothing and logged nothing.

The token snippets in skill.md, llms.txt, the docs, and the `agent_note` and `next_steps` of a newly minted token now work the same in sh, bash, and zsh. The publish examples build the header with `set --` and pass `"$@"` to curl. The old `${DEEDPAGE_TOKEN:+-H "Authorization: Bearer ..."}` form became one argument in zsh, so curl sent no Authorization header and a publish to a new slug could mint a second owner. The save command writes `~/.config/deed-page/token` only when the token is non-empty, with the file at mode 600 and the directory at 700, so an authenticated response can no longer clear a saved token. The load command keeps a `DEEDPAGE_TOKEN` that is already set. skill.md publish examples capture the response, save a newly minted token, and print the response with the token masked.

Plan-limit errors are now an upgrade path. quota_sites (403), quota_bytes (413), quota_domains (403), webhook_limit (403), plan_required (403), and the per-token publish rate_limited (429) keep their status, code, message, retry_after, and Retry-After header, and add `limit`, `plan`, `upgrade_plan`, `upgrade_price_usd_month`, `upgrade_url`, `pricing_url`, `reset_at`, and `user_message`. `upgrade_url` is a signed link (`/upgrade/`, HMAC, 30 days) that opens a fresh Stripe Checkout for that owner and plan; link unfurlers get a page instead of a session. The same fields appear in MCP tool errors on /mcp, including errors relayed from REST handlers (retry_after, version, and suggestion are now kept there too). /mcp/chatgpt keeps its informational errors: limit, plan, reset_at, pricing_url, and a user_message without a checkout link. Per-IP limits are unchanged and carry no upgrade fields. POST /api/v1/billing/checkout and get_upgrade_link take an optional `plan` (default: the next plan up), accept every limit name as `upgrade_trigger`, and return `user_message`. Checkout metadata records `via` (api, mcp, or limit_link). The limit-hit log stores the plan, and GET /v1/admin/stats `limit_hits` adds `days` (last 8 America/New_York days: hits, owners, free owners, and by limit). `scripts/publish.sh` prints `user_message` on failure.

Free-plan sites now show a small "Published with deed.page" link (to https://deed.page/?ref=badge) fixed in the bottom-right corner of HTML pages. Hobby and Pro never show it, and neither does a Free override of a paid Stripe plan. It is added only to full HTML responses: not to range requests, other file types, password prompts, password-protected pages, 404s, or pages that set a Content-Security-Policy meta tag. It uses no script, and fixed positioning means the layout does not move. Badged responses carry `X-Deed-Badge: added` and their own ETag; `Range: bytes=0-` returns the stored bytes, and `scripts/verify-deed.mjs` now uses that. Owners can hide it per site (`PUT /v1/sites/{slug}/badge {"show":false}`) or for every site of a token (`PUT /v1/badge`), or with the new MCP tool `set_badge` (25 tools). whoami adds `badge`. Pricing and terms say so; terms updated October 24, 2026.

A publish that mints a token now also returns `agent_note` and `next_steps`: save the token to `~/.config/deed-page/token` (mode 600) and reuse it as the Bearer token so later sites stay under the same owner. The token value is not repeated in them. skill.md has an install step near the top and a token-file section; llms.txt and the docs describe the same file. `skill/deed-page/scripts/publish.sh` reads and saves that file (never printing the token), sends `X-Client` (default `deed-page-skill`) and `ref=skill-script`.

`growth` network bursts are now judged against each network's usual rate: within 60 minutes, at least 5 owners and, for a network with 6+ active hours, at least 3x its median owners per active hour. Steady signups through a shared egress (many users of one hosted agent) are no longer excluded; a spike above that rate still is.

GET /v1/admin/stats adds `growth`: the last 14 America/New_York days of `new_sites`, `new_owners_raw` (equal to `signups.days[].owners`), and `real_owners`, which drops internal labels, app-generated owners (minted from a *.deed.page page or a known app X-Client), network bursts (5 or more owners from one prefix inside 60 minutes), and owners whose sites were all removed within 24 hours. Each dropped owner is counted once in `excluded`. It also returns distinct networks per day (counts of salted hashes only), a cohort funnel per signup day (published again on a later day, live site on day 7, recovery email, paid), and a client x ref cross-tab. `doc_reads` adds an `Exa` bucket for ExaUser and Exabot. Existing fields are unchanged.

GET /v1/admin/stats was staying on one Blob snapshot. The rebuild ran after the response, and the promise was not returned to `after`, so the platform did not keep the invocation alive and the snapshot was never rewritten. No error was logged. A copy older than 5 minutes is now rebuilt before the response, a cron rebuilds it every 5 minutes, and the response adds `cached_at` and `stale` (also `X-Deed-Stats-Cached-At`, `X-Deed-Stats-Age`, and `X-Deed-Stats-Stale`). Existing count fields are unchanged. A refresh that fails or exceeds its budget is logged at error level.

A `heard_from` answer replaces an email address, IP address, or phone number with `[redacted]` before it is stored. A URL or hostname is kept.

GET /v1/admin/stats counts an owner with no first-touch record as entry `unattributed` and client `unknown`, instead of entry `web`. `is_test` also covers a ref that contains `smoketest`, starts with `smoke` or `test`, or ends with `-probe`. Slug rules are unchanged. The stats response is reused for 60 seconds; an older copy is returned immediately and the next one is built after the response.

An optional `heard_from` answer (JSON field, `X-Deed-Heard-From`, or the MCP argument; trimmed to 200 characters, with control characters and HTML removed) is stored once on a new owner, with a normalised `heard_from_category`. GET /v1/admin/stats `signups.days` adds `by_heard_from` (top categories) and `answered`. Raw answers are only on GET /v1/admin/heard-from (the last 100; no email). A missing or over-long value does not fail signup.

GET /v1/admin/stats adds `signups`: the last 30 America/New_York days of new owners by normalised client, entry point, ref, and referrer host, plus mint-cap rejections and owners whose last site was removed within 24 hours of creation. A new owner stores that first touch once, including a salted hash of the /24 or /48 and an is_test flag. The raw IP is not stored. Rejected signups (10 per address per hour, 30 per prefix per hour) are counted with the same fields.

Opt-in gallery at /explore. `listed` defaults to false, so no existing site is added. Set it on publish, with PATCH /v1/sites/{slug}, or with the set_listing MCP tool, along with a category, title, description, and up to 5 tags. Password-protected sites cannot be listed. Unlisting removes the site immediately. A new listing is pending until upload screening, a denylist (and the OpenAI moderation API when OPENAI_API_KEY is set), and a quality check all pass. Otherwise it waits on GET /v1/admin/listings?status=pending. Approve and reject are POST /v1/admin/listings/{slug}/approve and /reject, with the admin token. A takedown delists. A new version runs the checks again. Cards are generated images. LISTING_SCREENSHOT_URL is an optional hook for a later screenshot service and is unset. The public index is /explore/index.json.

GET /v1/admin/billing (admin token only) returns a read-only Stripe snapshot: key mode (live or test, from the prefix only), up to 100 subscriptions of any status with price, interval, product name, and customer name or company, the last 100 invoices and charges, and the balance. It never writes to Stripe and returns no customer email.

CORS on every route now also exposes `Mcp-Protocol-Version`, alongside `Mcp-Session-Id`, so browser MCP clients such as claude.ai can read both. /docs explains how to add deed.page as a custom connector in Claude with no authentication.

Expired sites no longer count toward the plan site cap. A TTL expiry drops the slug from the owner index on the next publish, list, or whoami, and on the daily orphan sweep. GET /v1/admin/stats adds `limit_hits`: plan-limit denials by name for 24 hours, 7 days, and 30 days, and how many distinct owners hit a limit in each window. The log stores the internal owner id, the limit name, and a timestamp. It does not store an email or a token.

## 1.9.9

Publishes store a truncated User-Agent, Referer and Origin hosts, optional via, X-Client, and ref. GET /v1/admin/stats adds the top values for 24 hours, 7 days, and 30 days. Snippets send X-Client and a ref query. Unknown slugs and the main 404 link to https://deed.page/?ref=404. The request IP is still not stored.

## 1.9.8

Main-site pages load Vercel Web Analytics and Speed Insights. User-hosted sites are unchanged and stay cookieless. GET /v1/admin/stats returns live-site, owner, publish, and client counts for the admin token. Publishes record channel and user-agent family, not a raw IP.

## 1.9.7

deed.page is a general-audience service. Terms, /abuse, and the docs content rules prohibit sexually explicit or pornographic content, other adult-only content (gambling and age-restricted goods), and content that targets children under 13 or collects their personal data. Those sites are removed on report via /abuse or ops@avatar33.com. Abuse reports accept reason adult.

## 1.9.6

Static files accept a single HTTP Range and answer 206 or 416. set_site_access describes a password hash stored on deed.page. create_webhook and add_domain keep openWorldHint, because they call an outside system.

## 1.9.5

/mcp/chatgpt serves the same MCP tools as /mcp except get_upgrade_link. Quota and plan-limit errors state the limit and mention /pricing, with no checkout URL. Public /support page.

## 1.9.4

get_manifest after a restore returns the live manifest when the version record is not readable yet. A storage failure is 503 storage_unavailable. publish_site, update_site, add_domain, create_webhook, and set_site_access set destructiveHint.

## 1.9.3

A files array of {path, content} or {path, base64} publishes under those paths. Any other array is 400 bad_request.

## 1.9.2

MCP tool descriptions state what each tool does, its inputs, and its outputs. Each tool has annotations.title.

## 1.9.1

Directory readiness: icons, annotations, OpenAI domain verification route
