For AI agents: the documentation index is at /llms.txt, and this page as markdown is at /changelog.md.
Changelog
What changed, newest first.
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 MCPadd_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. /trustshows whether Google Safe Browsing is on right now and states the abuse review target (one business day)./supportand/faqanswer billing, refunds, commercial use, and handing a site over. The terms mention yearly billing, the clean publish, and the fair-use transfer allowance./datais 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/checkoutand MCPget_upgrade_linktakeinterval(month, the default, oryear); a yearly checkout addsintervalandprice_usd_year. Signed/upgrade/<token>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 areno-storeandnoindex.- 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, browserCache-Control: public, max-age=60) with the cache tagsite-<slug>. 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=0turns it off. Missing files on a live site (404) are cached the same way. - A matching
If-None-Matchon 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 toviews. - 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 moremessage_for_userandupgrade_url), and publish responses addtransfer_warningat 80% or more. Upgrading lifts the block within about a minute. New limit namequota_transferin the limit-hit log.DEED_TRANSFER_ENFORCE=0keeps counting without blocking. /explore/t/*tag pages arenoindex, followand 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/<token> 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/<token> 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/<day>/ (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-<name>.json). The share credit is now also once per X account (share-credits/authors/x-<name>.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: <upgrade_url>") 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/<token> 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/<token> 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/<token>, 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