<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[deed.page]]></title><description><![CDATA[deed.page]]></description><link>https://deedpage.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Thu, 29 Oct 2026 10:53:23 GMT</lastBuildDate><atom:link href="https://deedpage.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Turn a file into a shareable link (PDF, image, CSV, or HTML)]]></title><description><![CDATA[Turning a file into a shareable link means uploading it to a host that serves it at a public https URL, so the person you send it to opens it in a browser…]]></description><link>https://deedpage.hashnode.dev/turn-a-file-into-a-shareable-link-pdf-image-csv-or-html</link><guid isPermaLink="true">https://deedpage.hashnode.dev/turn-a-file-into-a-shareable-link-pdf-image-csv-or-html</guid><dc:creator><![CDATA[Cooper Email]]></dc:creator><pubDate>Wed, 28 Oct 2026 14:05:40 GMT</pubDate><content:encoded><![CDATA[<p>Turning a file into a shareable link means uploading it to a host that serves it at a public https URL, so the person you send it to opens it in a browser instead of downloading an attachment. On deed.page the Free plan holds 25 sites of up to 25 MB each for $0, with no account and no expiry unless you set one (<a href="https://deed.page/pricing">pricing</a>).</p>
<p>Every command below was run against production on October 28, 2026, with a PDF, a PNG, and a CSV. The test sites used a 15-minute expiry and were then deleted.</p>
<h2>How to turn a file into a shareable link</h2>
<ol>
<li><strong>Pick a slug.</strong> It becomes the address: <code>q3-report-file-r7w9</code> turns into <code>https://q3-report-file-r7w9.deed.page/</code>. Use your own suffix; that one is an example.</li>
<li><strong>Upload the file with one request.</strong> The <code>filename=</code> part is the path the file is served at:</li>
</ol>
<pre><code class="language-bash">curl -X PUT "https://deed.page/v1/sites?slug=q3-report-file-r7w9&amp;ref=blog" -F "file=@report.pdf;filename=report.pdf" -H "X-Client: &lt;your agent name&gt;"
</code></pre>
<ol start="3">
<li><strong>Share <code>https://q3-report-file-r7w9.deed.page/report.pdf</code>.</strong> In our test it came back <code>200</code> with <code>content-type: application/pdf</code>, so browsers open it inline. A PNG came back as <code>image/png</code> and a CSV as <code>text/csv</code>.</li>
</ol>
<p>The JSON reply has the site <code>url</code>, a <code>version</code> (<code>v1</code>), and on the first publish a <code>deed_tok_…</code> token. If you open the bare site URL with no <code>index.html</code>, deed.page shows a plain list of the files, so the root link works too.</p>
<p>Use multipart (<code>-F</code>) for anything that is not HTML. A raw <code>curl -T report.pdf</code> body has no filename, so it is not stored as <code>report.pdf</code>. For a single HTML page, <code>curl -T index.html</code> is the shortest form (<a href="https://deed.page/blog/put-html-page-online-from-terminal-with-curl">terminal walkthrough</a>).</p>
<h2>Several files behind one link</h2>
<p>Repeat <code>-F</code> to put more files under the same slug, and add a small <code>index.html</code> if you want a cover page:</p>
<pre><code class="language-bash">curl -X PUT "https://deed.page/v1/sites?slug=q3-report-file-r7w9" -F "file=@index.html;filename=index.html" -F "file=@report.pdf;filename=report.pdf" -F "file=@data.csv;filename=data.csv" -H "Authorization: Bearer $(cat ~/.config/deed-page/token)"
</code></pre>
<p>A single request body is capped at 4 MB. Bigger files go through the plan-upload flow, which keeps the same URL (<a href="https://deed.page/blog/plan-upload-large-sites">large sites</a>). Executables such as <code>.exe</code>, <code>.dmg</code>, and <code>.apk</code> return <code>422 content_rejected</code> and are not stored (<a href="https://deed.page/llms-full.txt">llms-full.txt</a>).</p>
<h2>Update the file without changing the link</h2>
<p>Save the token from the first reply (it is shown once; deed.page keeps only a SHA-256 of it). Send the new file to the same slug with <code>Authorization: Bearer</code> and the link stays the same. In our test the second upload returned <code>"version":"v2"</code> and the link served the new bytes right away. Older versions stay restorable (<a href="https://deed.page/blog/version-history-and-rollback">version history</a>), and <code>DELETE /v1/sites/{slug}</code> takes the link down. <a href="https://deed.page/blog/token-hygiene-for-agents">Token hygiene</a> covers where to keep the token.</p>
<h2>Same thing from an MCP client</h2>
<p>Agents that speak MCP can add <code>https://deed.page/mcp</code> and call <code>publish_site</code>. Text goes in as a string and binary files as base64:</p>
<pre><code class="language-json">{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"publish_site","arguments":{"slug":"q3-report-mcp-r7w9","files":{"report.pdf":{"base64":"JVBERi0xLjQK..."},"index.html":"&lt;!doctype html&gt;&lt;a href=\"report.pdf\"&gt;Q3 report (PDF)&lt;/a&gt;"}}}}
</code></pre>
<p>Replace the truncated base64 with the whole file (<code>base64 -w0 report.pdf</code>). The result has the same <code>url</code> and <code>token</code> fields as the REST reply (<a href="https://deed.page/blog/mcp-publish-walkthrough">MCP walkthrough</a>).</p>
<h2>How deed.page compares with file-to-link sites</h2>
<p>Checked on each site on October 28, 2026:</p>
<ul>
<li><a href="https://quick-share.app/file-to-link">Quick Share File to Link</a>: Google sign-in to upload; a free account keeps up to 3 active files of up to 1 GB, and links expire after up to 7 days.</li>
<li><a href="https://www.uploadtolink.com/">UploadToLink</a>: guests get 1 upload a day, up to 1 MB, with 8-hour links; free signed-in accounts get 3 MB per file and 24-hour retention.</li>
<li><a href="https://reshare.one/">reshare</a>: sign in with Google or email to upload, one file per link up to 50 MB, a permanent link that keeps the same URL across updates, and an MCP connector.</li>
</ul>
<p>Where they are ahead: Quick Share takes files up to 1 GB, and reshare renders Word and PowerPoint files as documents and slides in the browser. deed.page serves the file exactly as uploaded, so a <code>.docx</code> downloads instead of rendering. Where deed.page fits better: the uploader needs no account, the link has no expiry by default, several files can share one address, and scripts or agents can do the whole thing with one HTTP request or one MCP call.</p>
<p>If the same agent also needs an inbox to send that link from, Cooper Email (<a href="https://cooperemail.com">cooperemail.com</a>) is a sister product from Avatar 8 LLC.</p>
<h2>FAQ</h2>
<p><strong>Do I need an account to turn a file into a link on deed.page?</strong>
No. The first upload mints a token in the JSON reply. Keep it to update or delete the link later.</p>
<p><strong>How long does the link last?</strong>
Until you delete it. Add <code>-H "X-Ttl: 86400"</code> only if you want the link to expire after one day.</p>
<p><strong>Can ChatGPT, Claude, Grok, or Cursor make the link for me?</strong>
Yes, if the agent can run a shell command or use an MCP server. Point it at <a href="https://deed.page/llms.txt">llms.txt</a>, or install the skill with <code>npx skills add f-o-x11/deed-page-skill</code> (<a href="https://deed.page/blog/install-the-deed-page-skill">install walkthrough</a>).</p>
]]></content:encoded></item><item><title><![CDATA[Put an HTML page online from the terminal with curl]]></title><description><![CDATA[Putting an HTML page online from the terminal with curl means sending the file as the body of one HTTP request and getting back a public https URL you can…]]></description><link>https://deedpage.hashnode.dev/put-an-html-page-online-from-the-terminal-with-curl</link><guid isPermaLink="true">https://deedpage.hashnode.dev/put-an-html-page-online-from-the-terminal-with-curl</guid><category><![CDATA[Web Development]]></category><category><![CDATA[HTML]]></category><category><![CDATA[curl]]></category><category><![CDATA[ai agents]]></category><dc:creator><![CDATA[Cooper Email]]></dc:creator><pubDate>Tue, 27 Oct 2026 14:05:01 GMT</pubDate><content:encoded><![CDATA[<p>Putting an HTML page online from the terminal with curl means sending the file as the body of one HTTP request and getting back a public https URL you can open in any browser. On deed.page that is one <code>curl -T</code>, and the Free plan holds 25 sites of up to 25 MB each for $0, with no account and no expiry unless you set one (<a href="https://deed.page/pricing">pricing</a>).</p>
<p>Every command below was run against production on October 27, 2026. The test sites used a 15-minute expiry and were then deleted.</p>
<h2>Put an HTML page online from the terminal with curl</h2>
<ol>
<li><strong>Save the page as <code>index.html</code>.</strong> Inline the CSS and JavaScript so the file stands alone. If an assistant wrote it, ask for "one self-contained HTML file".</li>
<li><strong>Publish with one line:</strong></li>
</ol>
<pre><code class="language-bash">curl -T index.html "https://deed.page/v1/sites?slug=terminal-html-k4m2&amp;ref=blog" -H "X-Client: &lt;your agent name&gt;"
</code></pre>
<ol start="3">
<li><strong>Open the <code>url</code> from the JSON reply and share it.</strong></li>
</ol>
<p>Use your own suffix instead of copying <code>terminal-html-k4m2</code> (that shape is an example). A live reply looks like:</p>
<pre><code class="language-json">{"ok":true,"url":"https://terminal-html-k4m2.deed.page/","slug":"terminal-html-k4m2","version":"v1","plan":"free","token":"deed_tok_…","token_note":"Shown once. …"}
</code></pre>
<p><code>curl -T</code> is an HTTP PUT of the file body. PUT and POST on <code>/v1/sites</code> do the same publish. The slug is lowercase letters, digits, and hyphens, up to 63 characters. A name that is taken returns <code>409</code> with a suggestion (<a href="https://deed.page/blog/choosing-a-slug">choosing a slug</a>).</p>
<h2>Keep the token so you can change the page later</h2>
<p>The first reply includes a <code>deed_tok_…</code> token. It is shown once; deed.page stores only its SHA-256. Save it before you lose the shell scrollback:</p>
<pre><code class="language-bash"># TOKEN is the token field from the reply. Nothing is written when it is empty.
if [ -n "$TOKEN" ]; then (umask 077 &amp;&amp; mkdir -p ~/.config/deed-page &amp;&amp; chmod 700 ~/.config/deed-page &amp;&amp; printf '%s' "$TOKEN" &gt; ~/.config/deed-page/token &amp;&amp; chmod 600 ~/.config/deed-page/token); fi
</code></pre>
<p>Update the same address without changing the slug:</p>
<pre><code class="language-bash">curl -T index.html "https://deed.page/v1/sites?slug=terminal-html-k4m2" -H "Authorization: Bearer $(cat ~/.config/deed-page/token)"
</code></pre>
<p>Identical bytes return <code>"unchanged":true</code>. Delete with <code>DELETE /v1/sites/{slug}</code> and the same token. <a href="https://deed.page/blog/token-hygiene-for-agents">Token hygiene for agents</a> covers storing it safely.</p>
<h2>Same publish from an MCP client</h2>
<p>If your agent already speaks MCP, add <code>https://deed.page/mcp</code> (Claude Code: <code>claude mcp add --transport http deed-page https://deed.page/mcp</code>) and call <code>publish_site</code>:</p>
<pre><code class="language-json">{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"publish_site","arguments":{"slug":"terminal-html-k4m2","files":{"index.html":"&lt;!doctype html&gt;&lt;h1&gt;Published from the terminal&lt;/h1&gt;"}}}}
</code></pre>
<p>The tool result has the same <code>url</code> and <code>token</code> fields as the REST reply. The <a href="https://deed.page/blog/mcp-publish-walkthrough">MCP walkthrough</a> covers updates and the other tools.</p>
<h2>More than one file</h2>
<p>For a folder with CSS, images, or data, pack it and send the archive with the same <code>curl -T</code>:</p>
<pre><code class="language-bash">tar -C ./site -czf site.tar.gz .
curl -T site.tar.gz "https://deed.page/v1/sites?slug=terminal-html-k4m2" -H "Authorization: Bearer $(cat ~/.config/deed-page/token)"
</code></pre>
<p><a href="https://deed.page/blog/choose-an-upload-format">Picking an upload format</a> covers tarball, JSON map, multipart, and raw HTML. A single request body is capped at 4 MB; larger sites use the plan-upload flow (<a href="https://deed.page/blog/plan-upload-large-sites">plan upload</a>).</p>
<h2>How this differs from other curl hosts</h2>
<p>Several hosts also accept a one-line curl (checked in search results on October 27, 2026): <a href="https://ship.page/">ship.page</a> (<code>POST /deploy</code>), <a href="https://sharehtml.org/docs">sharehtml.org</a>, <a href="https://hurl.page/docs/deploy">hurl.page</a>, and <a href="https://brewpage.app/free-html-hosting">BrewPage</a>. They are fine when you want a browser drop zone or a short-lived demo. deed.page is built for agents and scripts that need a permanent link from the first request, a Bearer token instead of an emailed claim, and a native MCP server (<a href="https://deed.page/docs">docs</a>, <a href="https://deed.page/llms.txt">llms.txt</a>).</p>
<p>If the same team also needs an agent-owned inbox or a short waitlist page, Cooper Email (<a href="https://cooperemail.com">cooperemail.com</a>) and Perq (<a href="https://perq.sh">perq.sh</a>) are sister products from Avatar 8 LLC.</p>
<h2>FAQ</h2>
<p><strong>Do I need an API key before the first curl?</strong>
No. The first anonymous publish mints the token in the JSON reply. Later updates send <code>Authorization: Bearer</code>.</p>
<p><strong>Does the page expire if I forget to claim it?</strong>
Not by default. Add <code>-H "X-Ttl: 86400"</code> only when you want a one-day page. Otherwise keep the token and leave it up.</p>
<p><strong>Can ChatGPT, Claude, Grok, or Cursor run this for me?</strong>
Yes, if the agent has a shell or the MCP server. Point it at <a href="https://deed.page/llms.txt">llms.txt</a> or install the skill with <code>npx skills add f-o-x11/deed-page-skill</code> (<a href="https://deed.page/blog/install-the-deed-page-skill">install walkthrough</a>).</p>
]]></content:encoded></item><item><title><![CDATA[The free way to put a webpage online without signing up]]></title><description><![CDATA[Putting a webpage online for free without signing up means sending your HTML file to a static host that accepts anonymous uploads and replies with a public…]]></description><link>https://deedpage.hashnode.dev/put-a-webpage-online-free-without-signing-up</link><guid isPermaLink="true">https://deedpage.hashnode.dev/put-a-webpage-online-free-without-signing-up</guid><category><![CDATA[webdev]]></category><category><![CDATA[HTML]]></category><category><![CDATA[hosting]]></category><dc:creator><![CDATA[Cooper Email]]></dc:creator><pubDate>Mon, 26 Oct 2026 13:55:32 GMT</pubDate><content:encoded><![CDATA[<p>Putting a webpage online for free without signing up means sending your HTML file to a static host that accepts anonymous uploads and replies with a public https link anyone can open. On deed.page that is one request, and the Free plan holds 25 sites of up to 25 MB each for $0, with no account, no email code, and no expiry unless you ask for one (<a href="https://deed.page/pricing">pricing</a>).</p>
<p>Every command below was run against production on October 26, 2026. The test sites were published with a 15-minute expiry and then deleted.</p>
<h2>The free way to put a webpage online without signing up</h2>
<ul>
<li><strong>1. Save the page as one HTML file.</strong> Put the CSS in a <code>&lt;style&gt;</code> tag and any JavaScript in a <code>&lt;script&gt;</code> tag, and name it <code>index.html</code>. If the page was written by ChatGPT, Claude, Grok, or another assistant, ask it for "one self-contained HTML file".</li>
<li><strong>2. Send it with one command</strong> (or ask your agent to run it):</li>
</ul>
<pre><code class="language-bash">curl -T index.html "https://deed.page/v1/sites?slug=no-signup-page-&lt;random&gt;&amp;ref=blog" -H "X-Client: &lt;your agent name&gt;"
</code></pre>
<ul>
<li><strong>3. Open the <code>url</code> from the reply and share it.</strong></li>
</ul>
<p>Replace <code>&lt;random&gt;</code> with a few characters of your own, for example <code>no-signup-page-cde958</code>. The reply is JSON:</p>
<pre><code class="language-json">{"ok":true,"url":"https://no-signup-page-cde958.deed.page/","slug":"no-signup-page-cde958","version":"v1","plan":"free","token":"deed_tok_…","token_note":"Shown once. …"}
</code></pre>
<p>That <code>url</code> works right away over HTTPS on any phone or laptop. The slug is the part of the address you pick: lowercase letters, digits, and hyphens, up to 63 characters. A name someone else already owns returns <code>409</code> with a suggestion (<a href="https://deed.page/blog/choosing-a-slug">choosing a slug</a>).</p>
<p>deed.page has no upload form in the browser. It is built for curl and for AI agents, so the person asking can be you in a terminal or an assistant with a shell, such as Claude Code, Codex, Cursor, or Grok Bot.</p>
<h2>Keep the token so you can change the page later</h2>
<p>There is no account, so the first reply carries a <code>deed_tok_…</code> token instead. It is shown once and deed.page stores only its SHA-256, so save it:</p>
<pre><code class="language-bash"># TOKEN is the token field from the reply. Nothing is written when it is empty.
if [ -n "$TOKEN" ]; then (umask 077 &amp;&amp; mkdir -p ~/.config/deed-page &amp;&amp; chmod 700 ~/.config/deed-page &amp;&amp; printf '%s' "$TOKEN" &gt; ~/.config/deed-page/token &amp;&amp; chmod 600 ~/.config/deed-page/token); fi
</code></pre>
<p>To change the page, send the new file to the same slug with the token. The address stays the same and the version number goes up:</p>
<pre><code class="language-bash">curl -T index.html "https://deed.page/v1/sites?slug=no-signup-page-cde958" -H "Authorization: Bearer $(cat ~/.config/deed-page/token)"
</code></pre>
<p>Sending identical bytes again returns <code>"unchanged":true</code>. To take the page down, send <code>DELETE /v1/sites/{slug}</code> with the same token. <a href="https://deed.page/blog/token-hygiene-for-agents">Token hygiene for agents</a> covers storing it safely.</p>
<h2>Put the page online from an MCP client</h2>
<p>Agents that speak MCP can skip curl. Add <code>https://deed.page/mcp</code> as a server (in Claude Code: <code>claude mcp add --transport http deed-page https://deed.page/mcp</code>) and call <code>publish_site</code>:</p>
<pre><code class="language-json">{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"publish_site","arguments":{"slug":"no-signup-page-&lt;random&gt;","files":{"index.html":"&lt;!doctype html&gt;&lt;h1&gt;My page is online&lt;/h1&gt;"}}}}
</code></pre>
<p>The result has the same <code>url</code> and <code>token</code> fields as the REST reply. The <a href="https://deed.page/blog/mcp-publish-walkthrough">MCP walkthrough</a> covers updates and the other tools.</p>
<h2>How long no-signup pages last on other hosts</h2>
<p>Most no-signup hosts keep an anonymous page for a limited time, then ask you to sign in or claim it. These are the defaults each service states on its own page, checked on October 26, 2026:</p>
<table>
<thead>
<tr>
<th>Host</th>
<th>Anonymous page lasts</th>
<th>To keep it longer</th>
</tr>
</thead>
<tbody><tr>
<td><a href="https://beacon.host/guides/host-static-site-no-signup">beacon.host</a></td>
<td>24 hours</td>
<td>Open the claim link and sign in with an emailed code</td>
</tr>
<tr>
<td><a href="https://uploadthefile.com/publish">UploadTheFile</a></td>
<td>48 hours</td>
<td>Sign in free</td>
</tr>
<tr>
<td><a href="https://brewpage.app/">BrewPage</a></td>
<td>15 days by default for API publishes, up to 30 days</td>
<td>Optional profile, up to 90 days</td>
</tr>
<tr>
<td><a href="https://ship.page/temporary-website-hosting">ship.page</a></td>
<td>30 days</td>
<td>Claim with a free login</td>
</tr>
<tr>
<td>deed.page</td>
<td>No expiry unless you set <code>X-Ttl</code></td>
<td>Nothing to do; keep the token</td>
</tr>
</tbody></table>
<p>Several of them have a drag-and-drop page in the browser, which deed.page does not. If you have no terminal and no agent, one of those is the quicker path. If you want the link to stay up without coming back to claim it, deed.page is built for that (<a href="https://deed.page/blog/comparing-deed-page-and-here-now">here.now comparison</a> for one more dated example).</p>
<h2>What the Free plan includes</h2>
<p>Free is $0 for 25 sites, 25 MB per site, 60 publishes per hour, and the last 10 versions of each site (<a href="https://deed.page/docs">docs</a>). Pages are public to anyone with the link and are not listed in the <a href="https://deed.page/explore">Explore gallery</a> unless you opt in. Free pages show a small "Published with deed.page" link in the corner. Hobby ($3/month) and Pro ($15/month) remove it and add a site password.</p>
<h2>FAQ</h2>
<p><strong>Is it really free, with no card and no email?</strong>
Yes. The first upload needs no account, no card, and no email address. A recovery email is optional, and without one a lost token cannot be restored.</p>
<p><strong>Does the page expire?</strong>
Not by default. Add <code>-H "X-Ttl: 86400"</code> (seconds) only if you want it gone after a day, and delete it any time with the token.</p>
<p><strong>Can the page have images or more than one file?</strong>
Yes. Pack the folder as a tarball (<code>tar -C ./site -czf site.tar.gz .</code>) and send it with the same <code>curl -T</code> command. <a href="https://deed.page/blog/choose-an-upload-format">Picking an upload format</a> shows every option.</p>
]]></content:encoded></item><item><title><![CDATA[How to share an AI-generated report as a web page]]></title><description><![CDATA[To share an AI-generated report as a web page, save it as one HTML file and publish it to a static host that returns a public https link.]]></description><link>https://deedpage.hashnode.dev/share-ai-generated-report-as-web-page</link><guid isPermaLink="true">https://deedpage.hashnode.dev/share-ai-generated-report-as-web-page</guid><category><![CDATA[Web Hosting]]></category><category><![CDATA[ai agents]]></category><category><![CDATA[HTML]]></category><dc:creator><![CDATA[Cooper Email]]></dc:creator><pubDate>Sun, 25 Oct 2026 13:55:44 GMT</pubDate><content:encoded><![CDATA[<p>Sharing an AI-generated report as a web page means saving the report as an HTML file and publishing it to a static host that hands back an https link anyone can open in a browser. On deed.page that is one request, and the Free plan holds 25 sites of up to 25 MB each for $0 with no signup (<a href="https://deed.page/pricing">pricing</a>).</p>
<p>Every command below was run against production on October 25, 2026. The test sites were published with a 15-minute expiry and then deleted.</p>
<h2>How to share an AI-generated report as a web page</h2>
<ul>
<li><strong>1. Ask for one self-contained HTML file.</strong> A prompt like "Put this report in a single HTML file with the CSS in a <code>&lt;style&gt;</code> tag and the JavaScript in a <code>&lt;script&gt;</code> tag" keeps the page from pointing at files that only exist on the model's side.</li>
<li><strong>2. Save it as <code>report.html</code>.</strong> Any file name works; a single file is served as the home page.</li>
<li><strong>3. Publish it with one command and send the link.</strong></li>
</ul>
<pre><code class="language-bash">curl -T report.html "https://deed.page/v1/sites?slug=ai-report-&lt;random&gt;&amp;ref=blog" -H "X-Client: &lt;your agent name&gt;"
</code></pre>
<p>Replace <code>&lt;random&gt;</code> with a few random characters (for example <code>ai-report-e58a20</code>). The reply is JSON:</p>
<pre><code class="language-json">{"ok":true,"url":"https://ai-report-e58a20.deed.page/","slug":"ai-report-e58a20","version":"v1","plan":"free","token":"deed_tok_…", "token_note":"Shown once. …"}
</code></pre>
<p><code>url</code> is the link to share. There is no account, no email code, and no claim link, and the site does not expire unless you ask for that with an <code>X-Ttl</code> header (<a href="https://deed.page/docs">docs</a>). The slug rules are strict (lowercase letters, digits, and hyphens), and a name someone else owns returns <code>409</code> with a suggestion; <a href="https://deed.page/blog/choosing-a-slug">Choosing a slug</a> has the details.</p>
<h2>Keep the token so the link stays yours</h2>
<p>The first publish returns a <code>deed_tok_…</code> Bearer token once. deed.page keeps only its SHA-256, so save it right away:</p>
<pre><code class="language-bash">mkdir -p ~/.config/deed-page &amp;&amp; printf '%s' "$TOKEN" &gt; ~/.config/deed-page/token &amp;&amp; chmod 600 ~/.config/deed-page/token
</code></pre>
<p>When next week's report is ready, publish to the same slug with the token. The link stays the same and the site gets a new version:</p>
<pre><code class="language-bash">curl -T report.html "https://deed.page/v1/sites?slug=ai-report-e58a20" -H "Authorization: Bearer $(cat ~/.config/deed-page/token)"
</code></pre>
<p>Sending identical bytes again returns <code>"unchanged":true</code> instead of a new version. Free keeps the last 10 versions, and each one has its own permalink such as <code>https://v1--ai-report-e58a20.deed.page/</code> (<a href="https://deed.page/blog/version-history-and-rollback">rollback guide</a>). <a href="https://deed.page/blog/token-hygiene-for-agents">Token hygiene for agents</a> covers storing it, and <code>POST /v1/tokens/rotate</code> replaces a leaked one (<a href="https://deed.page/docs">docs</a>).</p>
<h2>Reports with charts, images, or data files</h2>
<p>If the report loads a chart image or a JSON file next to it, publish the folder as a tarball. Paths are kept exactly:</p>
<pre><code class="language-bash">tar -C ./report -czf report.tar.gz . &amp;&amp; curl -T report.tar.gz "https://deed.page/v1/sites?slug=ai-report-&lt;random&gt;&amp;ref=blog" -H "X-Client: &lt;your agent name&gt;"
</code></pre>
<p>In our test, <code>data/q3.json</code> inside the tarball was served at <code>https://&lt;slug&gt;.deed.page/data/q3.json</code>. <a href="https://deed.page/blog/choose-an-upload-format">Picking an upload format</a> compares tarballs, JSON maps, multipart, and raw HTML.</p>
<h2>Publish the report from an MCP client</h2>
<p>Agents that speak MCP can skip curl. Add <code>https://deed.page/mcp</code> as a server (in Claude it is a custom connector with no sign-in step), then call <code>publish_site</code>:</p>
<pre><code class="language-json">{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"publish_site","arguments":{"slug":"ai-report-&lt;random&gt;","files":{"index.html":"&lt;!doctype html&gt;&lt;h1&gt;Q3 summary&lt;/h1&gt;&lt;p&gt;Revenue table here.&lt;/p&gt;"}}}}
</code></pre>
<p>The result has the same <code>url</code> and <code>token</code> fields as the REST reply. Claude Code users can add the server with <code>claude mcp add --transport http deed-page https://deed.page/mcp</code>. The <a href="https://deed.page/blog/mcp-publish-walkthrough">MCP walkthrough</a> goes through <code>update_site</code> and the other tools.</p>
<h2>Who can see the report</h2>
<p>Anyone with the link can open it. The site is not listed in the <a href="https://deed.page/explore">Explore gallery</a> unless you opt in (the publish reply shows <code>"listed":false</code>). A password gate is available on Hobby ($3/month) and Pro ($15/month), not on Free, so leave secrets and personal data out of Free reports. Free pages also show a small "Published with deed.page" link in the corner, which the token can turn off with <code>PUT /v1/sites/{slug}/badge</code> (<a href="https://deed.page/docs">docs</a>).</p>
<p>If the report also needs to go out by email, the same team runs <a href="https://cooperemail.com">Cooper Email</a>, which gives agents their own inboxes.</p>
<h2>FAQ</h2>
<p><strong>Do charts and scripts in the report still work?</strong>
Yes. CSS and JavaScript inside the HTML file are served with the page, and libraries loaded from a public CDN load as usual. Files referenced by a relative path must be in the tarball.</p>
<p><strong>How long does the link last?</strong>
It is permanent by default. Add <code>-H "X-Ttl: 86400"</code> (or <code>ttl_seconds</code> over MCP) only if you want it to expire, and delete it any time with <code>DELETE /v1/sites/{slug}</code> and the token.</p>
<p><strong>Can I update the report without changing the link?</strong>
Yes. Publish to the same slug with the token from the first reply. The URL stays the same, and older versions stay available until they fall out of the plan's version limit.</p>
]]></content:encoded></item><item><title><![CDATA[Signed webhooks on deed.page — let an agent hear about publishes, restores, and deletes]]></title><description><![CDATA[Sometimes another system needs to know a deed.page site changed: a second agent that shares the link, a CI job waiting on a custom domain, or a version…]]></description><link>https://deedpage.hashnode.dev/signed-webhooks-for-agents</link><guid isPermaLink="true">https://deedpage.hashnode.dev/signed-webhooks-for-agents</guid><category><![CDATA[webhooks]]></category><category><![CDATA[ai agents]]></category><category><![CDATA[api]]></category><dc:creator><![CDATA[Cooper Email]]></dc:creator><pubDate>Sat, 24 Oct 2026 13:52:03 GMT</pubDate><content:encoded><![CDATA[<p>Sometimes another system needs to know a deed.page site changed: a second agent that shares the link, a CI job waiting on a custom domain, or a version log. deed.page sends a signed JSON <code>POST</code> to a URL you register, on every plan, including Free.</p>
<p>Every response below was captured against production on October 24, 2026. The test site, webhooks, and token were deleted or revoked afterwards.</p>
<h2>Register a webhook</h2>
<pre><code class="language-bash">curl -s -X POST "https://deed.page/v1/webhooks" \
  -H "Authorization: Bearer $DEEDPAGE_TOKEN" \
  -H "content-type: application/json" \
  -d '{"url":"https://hooks.example.com/deed"}'
# {"ok":true,"id":"whk_…","url":"https://hooks.example.com/deed",
#  "events":["site.published","site.version_restored","site.deleted",
#            "site.taken_down","domain.active","report.received"],
#  "secret":"whsec_…",
#  "secret_note":"Shown once. We store it to sign deliveries and do not return it again."}
</code></pre>
<p>Store <code>secret</code> right away, next to the token. <code>GET /v1/webhooks</code> lists endpoints and the plan limit, never the secret. Omit <code>events</code> to get all six, or pass a subset.</p>
<table>
<thead>
<tr>
<th>Event</th>
<th>Sent when</th>
<th><code>data</code></th>
</tr>
</thead>
<tbody><tr>
<td><code>site.published</code></td>
<td>a publish changed files</td>
<td><code>slug</code>, <code>url</code>, <code>version</code></td>
</tr>
<tr>
<td><code>site.version_restored</code></td>
<td>a restore made a new version</td>
<td><code>slug</code>, <code>url</code>, <code>version</code>, <code>restored_from</code></td>
</tr>
<tr>
<td><code>site.deleted</code></td>
<td>the owner deleted the site</td>
<td><code>slug</code></td>
</tr>
<tr>
<td><code>site.taken_down</code></td>
<td>the operator took the site down</td>
<td><code>slug</code>, <code>reason</code>, <code>scope</code></td>
</tr>
<tr>
<td><code>domain.active</code></td>
<td>a custom hostname went live</td>
<td><code>slug</code>, <code>domain</code>, <code>url</code></td>
</tr>
<tr>
<td><code>report.received</code></td>
<td>someone reported your site</td>
<td><code>slug</code>, <code>report_id</code>, <code>reason</code> (never the reporter's contact)</td>
</tr>
</tbody></table>
<p>An identical republish returns <code>"unchanged": true</code> and sends nothing. In our test, publishing v2 produced one <code>site.published</code> delivery, the same bytes again produced none, and restoring v1 produced <code>site.version_restored</code> with <code>"version":"v3"</code>.</p>
<h2>The same thing over MCP</h2>
<p><code>https://deed.page/mcp</code> has <code>create_webhook</code>, <code>list_webhooks</code>, and <code>delete_webhook</code>. Pass the token as a Bearer header or a <code>token</code> argument.</p>
<pre><code class="language-bash">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":"create_webhook",
       "arguments":{"url":"https://hooks.example.com/deed","events":["site.published","site.deleted"]}}}'
# structuredContent: {"ok":true,"id":"whk_…","events":["site.published","site.deleted"],"secret":"whsec_…",…}
</code></pre>
<p>Without a token it returns <code>isError: true</code>.</p>
<h2>What a delivery looks like</h2>
<pre><code class="language-http">POST /deed HTTP/1.1
Content-Type: application/json
User-Agent: deed.page-webhooks
Deed-Signature: t=1792848217,v1=fd6836…

{"id":"evt_…","type":"site.published","created":"2026-10-24T13:23:37.000Z",
 "data":{"slug":"rain-log-7f3k","url":"https://rain-log-7f3k.deed.page/","version":"v2"}}
</code></pre>
<p><code>v1</code> is the hex HMAC-SHA256 of <code>&lt;t&gt;.&lt;raw body&gt;</code>, keyed with your <code>whsec_</code> secret. Verify the raw bytes before parsing. Reject a timestamp more than 5 minutes from your clock, either direction; deed.page doesn't check this for you.</p>
<h2>Verify the signature</h2>
<p>Node, no dependencies:</p>
<pre><code class="language-js">import { createServer } from "node:http";
import { createHmac, timingSafeEqual } from "node:crypto";

const SECRET = process.env.DEED_WEBHOOK_SECRET; // the whsec_… value, shown once
const seen = new Set(); // use a real store in production

function verify(raw, header) {
  const parts = Object.fromEntries(
    (header || "").split(",").map((p) =&gt; p.split("=", 2)),
  );
  const t = Number(parts.t);
  if (!t || !parts.v1) return false;
  if (Math.abs(Date.now() / 1000 - t) &gt; 300) return false; // 5-minute window
  const want = createHmac("sha256", SECRET).update(`${t}.${raw}`).digest();
  const got = Buffer.from(parts.v1, "hex");
  return got.length === want.length &amp;&amp; timingSafeEqual(got, want);
}

createServer((req, res) =&gt; {
  let raw = "";
  req.setEncoding("utf8");
  req.on("data", (c) =&gt; (raw += c));
  req.on("end", () =&gt; {
    if (!verify(raw, req.headers["deed-signature"])) {
      res.writeHead(401).end();
      return;
    }
    const event = JSON.parse(raw);
    res.writeHead(204).end(); // answer fast, work after
    if (seen.has(event.id)) return; // a retry can repeat an id
    seen.add(event.id);
    console.log(event.type, event.data.slug, event.data.version ?? "");
  });
}).listen(8787);
</code></pre>
<p>The same check in Python:</p>
<pre><code class="language-python">import hashlib, hmac, time

def verify(secret: str, raw: bytes, header: str) -&gt; bool:
    parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
    t, v1 = parts.get("t", ""), parts.get("v1", "")
    if not t.isdigit() or not v1 or abs(time.time() - int(t)) &gt; 300:
        return False
    want = hmac.new(secret.encode(), t.encode() + b"." + raw, hashlib.sha256).hexdigest()
    return hmac.compare_digest(want, v1)
</code></pre>
<p>We tested both against deed.page's own signing function: a valid header passed; a changed body, a 10-minute-old timestamp, and the wrong secret failed; a repeated event id was logged once.</p>
<h2>Test it, then read the delivery log</h2>
<pre><code class="language-bash">curl -s -X POST "https://deed.page/v1/webhooks/$HOOK_ID/test" -H "Authorization: Bearer $DEEDPAGE_TOKEN"
# {"ok":true,"id":"whk_…","note":"…","delivery":{"type":"webhook.test","ok":true,"status":204,"attempts":1,"error":null,…}}

curl -s "https://deed.page/v1/webhooks/$HOOK_ID/deliveries" -H "Authorization: Bearer $DEEDPAGE_TOKEN"
</code></pre>
<p>Delivery is best-effort, not a queue. deed.page tries up to 3 times: right away, then after 0.5 seconds, then after 2 seconds. Any 2xx counts as success. Redirects are not followed. Against an endpoint that always returns 500, both the test and the <code>site.deleted</code> event were logged as <code>"ok":false,"status":500,"attempts":3,"error":"HTTP 500"</code>, and then the event is gone. If a missed event matters, answer 2xx fast and reconcile with <code>GET /v1/sites</code> or <code>list_versions</code> on a schedule. The log keeps the 30 most recent deliveries across your webhooks.</p>
<h2>Limits and errors</h2>
<p>Free allows 1 webhook, Hobby 5, and Pro 20 (<a href="https://deed.page/pricing">pricing</a>). Targets must be public <code>http</code> or <code>https</code> on port 80 or 443. Private and local addresses are refused.</p>
<table>
<thead>
<tr>
<th>Status</th>
<th><code>code</code></th>
<th>Cause we saw</th>
</tr>
</thead>
<tbody><tr>
<td>400</td>
<td><code>bad_target</code></td>
<td><code>http://localhost/…</code>, <code>http://10.0.0.5/…</code>, <code>https://169.254.169.254/…</code>, port 8443, or <code>ftp://</code></td>
</tr>
<tr>
<td>400</td>
<td><code>bad_request</code></td>
<td>an unknown event such as <code>site.updated</code></td>
</tr>
<tr>
<td>401</td>
<td><code>unauthorized</code></td>
<td>no Bearer token</td>
</tr>
<tr>
<td>401</td>
<td><code>token_revoked</code></td>
<td>a token that was revoked or rotated</td>
</tr>
<tr>
<td>403</td>
<td><code>suspended</code></td>
<td><code>"This token is disabled."</code></td>
</tr>
<tr>
<td>403</td>
<td><code>webhook_limit</code></td>
<td><code>"Free allows 1 webhook. See /pricing."</code></td>
</tr>
<tr>
<td>404</td>
<td><code>not_found</code></td>
<td>test, deliveries, or delete on an unknown id</td>
</tr>
</tbody></table>
<p>To rotate a secret, delete the webhook (<code>DELETE /v1/webhooks/{id}</code> or <code>delete_webhook</code>) and create it again. The new secret comes back once.</p>
<p>Full reference: <a href="https://deed.page/docs">deed.page/docs</a>. Related: <a href="https://deed.page/blog/version-history-and-rollback">version history and rollback</a> and <a href="https://deed.page/blog/custom-domains-via-api">custom domains via API</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Opt an agent's site into the deed.page Explore gallery — listing over REST and MCP]]></title><description><![CDATA[How an agent adds a deed.page site to the public gallery with one PATCH, a publish header, or the set_listing MCP tool.]]></description><link>https://deedpage.hashnode.dev/opt-an-agent-s-site-into-the-deed-page-explore-gallery-listing-over-rest-and-mcp</link><guid isPermaLink="true">https://deedpage.hashnode.dev/opt-an-agent-s-site-into-the-deed-page-explore-gallery-listing-over-rest-and-mcp</guid><category><![CDATA[AI]]></category><category><![CDATA[agents]]></category><category><![CDATA[webdev]]></category><category><![CDATA[api]]></category><dc:creator><![CDATA[Cooper Email]]></dc:creator><pubDate>Fri, 23 Oct 2026 13:58:59 GMT</pubDate><content:encoded><![CDATA[<p>A deed.page site is public at its own URL, but nothing lists it anywhere unless the owner asks. The public gallery at <a href="https://deed.page/explore">deed.page/explore</a> is opt-in. <code>listed</code> defaults to <code>false</code>, 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.</p>
<p>Every example below was run against production on October 23, 2026. The test sites used <code>X-Ttl: 900</code> and were deleted afterwards.</p>
<h2>What every publish already tells you</h2>
<p>A normal publish response carries the listing state:</p>
<pre><code class="language-bash">curl -s -X PUT "https://deed.page/v1/sites/rain-log-&lt;random&gt;" \
  -H "X-Ttl: 900" -H "content-type: text/html" \
  --data-binary @index.html
# {"slug":"rain-log-…","version":"v1","listing":{"listed":false}, "token":"deed_tok_…", ...}
</code></pre>
<p>Keep the <code>token</code> from the first publish (for example as <code>DEEDPAGE_TOKEN</code>). Changing the listing is an owner action.</p>
<h2>Opt in with one PATCH</h2>
<pre><code class="language-bash">curl -s -X PATCH "https://deed.page/v1/sites/rain-log-&lt;random&gt;" \
  -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"]}'
</code></pre>
<pre><code class="language-json">{"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}}
</code></pre>
<p>The rules:</p>
<ul>
<li><code>category</code> is required and must be one of: <code>Apps &amp; tools</code>, <code>Games</code>, <code>Business</code>, <code>Portfolio</code>, <code>Docs &amp; guides</code>, <code>Education</code>, <code>Events</code>, <code>Other</code>.</li>
<li><code>title</code> is required, at most 80 characters. <code>description</code> is required, at most 300.</li>
<li><code>tags</code> is optional, at most 5. Each tag is lowercase letters, digits and hyphens, at most 32 characters.</li>
</ul>
<p>Each of these breaks one rule and returns <code>400 listing_invalid</code> with a message that names the problem:</p>
<pre><code class="language-text">{"listed":true}                       → category must be one of: Apps &amp; 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.
</code></pre>
<p>With no token you get <code>401 unauthorized</code>. With a token that doesn't own the slug you get <code>403 forbidden</code> ("This token does not own that slug.").</p>
<h2>Or set it on the publish itself</h2>
<p>The same fields can travel with the upload as JSON body fields, query parameters (<code>listed</code>, <code>category</code>, <code>tags</code>, <code>title</code>, <code>description</code>), or headers:</p>
<pre><code class="language-bash">curl -s -X PUT "https://deed.page/v1/sites/rain-log-&lt;random&gt;" \
  -H "Authorization: Bearer $DEEDPAGE_TOKEN" \
  -H "content-type: text/html" \
  -H "X-Listed: true" \
  -H "X-Listing-Category: Docs &amp; 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 &amp; guides",...}}
</code></pre>
<h2>Or over MCP</h2>
<p>At <code>https://deed.page/mcp</code>, the <code>set_listing</code> tool takes the same fields. <code>publish_site</code> and <code>update_site</code> accept them too.</p>
<pre><code class="language-bash">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-&lt;random&gt;","listed":true,"category":"Other",
       "title":"Rain gauge log","description":"Daily rainfall readings for one backyard gauge, with a weekly total.",
       "tags":["weather"]}}}'
</code></pre>
<p><code>structuredContent</code> holds the same <code>listing</code> object as the REST reply. Without a token, the tool returns <code>isError: true</code> with <code>"set_listing needs your token."</code></p>
<h2>Why a listing comes back "pending"</h2>
<p>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 <code>pending</code> and waits for an operator. The owner sees the reason in <code>reason</code>. The public site record shows the listing without it. Reasons we triggered in testing:</p>
<ul>
<li><code>title_slug</code>: the title looked like a slug (<code>"rain-gauge-log"</code>) or a hex string. Use words.</li>
<li><code>description_short</code>: the description was under 20 characters (<code>"Rain log."</code>).</li>
<li><code>trivial</code>: the page itself was a stub. We republished <code>&lt;h1&gt;Coming soon&lt;/h1&gt;</code>.</li>
</ul>
<p>That last one matters for agents that publish on a schedule. <strong>Every new version runs the checks again</strong>, even when the publish leaves out the listing fields. Our v2 stub flipped the listing from <code>approved</code> to <code>pending</code> (<code>reason: "trivial"</code>). Republishing the real page as v3 brought it straight back to <code>approved</code>. Test, draft and redirect pages are held for the same reason.</p>
<p>Password-protected sites (Hobby and Pro) cannot be listed. Trying returns <code>400 listing_private</code>.</p>
<h2>Where the listing shows up</h2>
<p>Approved listings appear on <code>/explore</code>, on category pages like <code>/explore/c/docs-and-guides</code>, on tag pages like <code>/explore/t/weather</code>, and in the machine-readable index:</p>
<pre><code class="language-bash">curl -s https://deed.page/explore/index.json | jq '.listings[] | {slug, category_slug, tags, url, card_url}'
</code></pre>
<p>The index is cached (<code>cache-control: public, max-age=180</code>), so a change can take about three minutes to appear there. Each entry has a generated card image at <code>/explore/card/{slug}</code>. There are no screenshots. The gallery and its category pages are in <code>/sitemap.xml</code>, and cards link out with <code>rel="ugc noopener"</code>.</p>
<h2>Taking a site back out</h2>
<pre><code class="language-bash">curl -s -X PATCH "https://deed.page/v1/sites/rain-log-&lt;random&gt;" \
  -H "Authorization: Bearer $DEEDPAGE_TOKEN" \
  -H "content-type: application/json" -d '{"listed":false}'
# {"ok":true,"slug":"rain-log-…","listing":{"listed":false}}
</code></pre>
<p>Unlisting removes the slug from the gallery in the same request. Deleting the site (<code>DELETE /v1/sites/{slug}</code>) removes it as well, and its card returns 404. A TTL site that expires drops out too.</p>
<h2>A good default for agents</h2>
<p>Leave <code>listed</code> 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 <code>listing.status</code> in the response. If it says <code>pending</code>, the <code>reason</code> field tells you what to fix.</p>
<p>The full field reference is in the <a href="https://deed.page/docs">docs</a> under "Gallery". The <a href="https://github.com/f-o-x11/deed-page-skill">deed.page agent skill</a> covers <code>set_listing</code> as well.</p>
]]></content:encoded></item><item><title><![CDATA[Put an agent's site on your own domain — deed.page custom domains over REST and MCP]]></title><description><![CDATA[Every deed.page site already has a permanent address at Sometimes a person wants the page on their own name instead, like docs.example.com for project docs…]]></description><link>https://deedpage.hashnode.dev/put-an-agent-s-site-on-your-own-domain-deed-page-custom-domains-over-rest-and-mcp</link><guid isPermaLink="true">https://deedpage.hashnode.dev/put-an-agent-s-site-on-your-own-domain-deed-page-custom-domains-over-rest-and-mcp</guid><dc:creator><![CDATA[Cooper Email]]></dc:creator><pubDate>Thu, 22 Oct 2026 14:04:12 GMT</pubDate><content:encoded><![CDATA[<p>Every deed.page site already has a permanent address at <code>https://{slug}.deed.page/</code>. Sometimes a person wants the page on their own name instead, like <code>docs.example.com</code> for project docs an agent keeps current, or <code>example.com</code> for a landing page. deed.page lets the agent attach that hostname with an API call. Nobody has to open a dashboard. The agent gets back the exact DNS records to create, and then polls until the domain is live.</p>
<p>Every response below was captured against production on October 22, 2026. The test site used <code>X-Ttl: 900</code>, the test hostname was removed, and the site was deleted afterwards.</p>
<h2>Limits per plan</h2>
<table>
<thead>
<tr>
<th></th>
<th>Free</th>
<th>Hobby ($3/mo)</th>
<th>Pro ($15/mo)</th>
</tr>
</thead>
<tbody><tr>
<td>Custom hostnames per token</td>
<td>1</td>
<td>3</td>
<td>20</td>
</tr>
</tbody></table>
<p>Domain calls (add, status, remove) are also capped at 60 an hour per token and 60 an hour per network. The cap is there to stop polling loops, and normal use stays well under it.</p>
<h2>Attach a hostname</h2>
<p>You need a site the token owns. Then one <code>POST</code> with the hostname as JSON attaches it:</p>
<pre><code class="language-bash">curl -sS -X POST "https://deed.page/v1/sites/$SLUG/domains" \
  -H "Authorization: Bearer $DEEDPAGE_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"domain":"docs.example.com"}'
</code></pre>
<p>The answer is <code>201</code> with the records to create (values shortened here):</p>
<pre><code class="language-json">{"ok":true,"domain":"docs.example.com","slug":"my-docs","apex":false,
 "status":"pending_dns",
 "status_reason":"Create the CNAME and every TXT record. Then check status again.",
 "dns_records":[
   {"type":"CNAME","name":"docs","host":"docs.example.com",
    "value":"9f5677dd5e4f32ed.vercel-dns-016.com","purpose":"routing",
    "note":"Create this CNAME. Do not also create an A record for this name."},
   {"type":"TXT","name":"_vercel","host":"_vercel.example.com",
    "value":"vc-domain-verify=docs.example.com,9b71...","purpose":"verification",
    "note":"Ownership verification required by the hosting project. Create this record exactly."}],
 "url":"https://docs.example.com/"}
</code></pre>
<p>Treat <code>dns_records</code> as the source of truth and don't hard-code values. <code>name</code> is relative to the zone, so it is ready to paste into most DNS panels, and <code>host</code> is the fully qualified name. The routing target is specific to each hostname. Here is what the list holds:</p>
<ul>
<li><strong>A subdomain</strong> (<code>docs.example.com</code>) gets one <code>CNAME</code>.</li>
<li><strong>An apex</strong> (<code>example.com</code>, <code>"apex":true</code>) gets an <code>A</code> record and an <code>ALIAS</code> record. Create one of the two, not both. Use <code>ALIAS</code> (sometimes called <code>ANAME</code>) if your DNS host supports it.</li>
<li><strong>TXT records</strong> prove ownership. The hosting layer sometimes asks for its own <code>_vercel</code> TXT record. When it doesn't, deed.page asks for a <code>_deed-page.&lt;hostname&gt;</code> TXT record with a <code>deed-verify=...</code> value instead. Create every TXT record in the list.</li>
</ul>
<p>Calling the same <code>POST</code> again for the same slug and hostname is safe. It returns <code>200</code> with a fresh status instead of creating a duplicate.</p>
<h2>Poll until active</h2>
<p>After the records exist, check status. This call also re-runs verification, re-reads the DNS config, and checks the certificate:</p>
<pre><code class="language-bash">curl -sS "https://deed.page/v1/sites/$SLUG/domains/docs.example.com" \
  -H "Authorization: Bearer $DEEDPAGE_TOKEN"
</code></pre>
<p><code>status</code> moves through these values:</p>
<ul>
<li><code>pending_dns</code>: routing records are missing or wrong. <code>status_reason</code> says which ones to create.</li>
<li><code>verifying</code>: DNS is in place, but ownership verification hasn't finished.</li>
<li><code>issuing_cert</code>: ownership is confirmed, and the TLS certificate isn't ready yet.</li>
<li><code>active</code>: <code>"Serving this site."</code> The hostname now serves the same site as <code>https://{slug}.deed.page/</code>.</li>
<li><code>error</code>: a hard failure, with the reason in <code>status_reason</code>.</li>
</ul>
<p>DNS can take anywhere from minutes to hours to propagate. Poll every few minutes, not every second, so the agent stays under the 60-calls-an-hour cap. An agent with a <a href="https://deed.page/docs">webhook</a> registered gets a <code>domain.active</code> event when the hostname goes live, so it doesn't need to poll at all.</p>
<p>To see every hostname on a site, along with your quota, call <code>GET /v1/sites/$SLUG/domains</code>:</p>
<pre><code class="language-json">{"ok":true,"domains":[{"domain":"docs.example.com","status":"pending_dns",...}],
 "domain_limit":1,"domains_used":1}
</code></pre>
<h2>Remove a hostname</h2>
<pre><code class="language-bash">curl -sS -X DELETE "https://deed.page/v1/sites/$SLUG/domains/docs.example.com" \
  -H "Authorization: Bearer $DEEDPAGE_TOKEN"
</code></pre>
<p>That call returned <code>{"ok":true,"removed":"docs.example.com","slug":"my-docs"}</code>. Requests to that host stop serving the site, and the slot frees up. Repeating the call returns <code>404 not_found</code>. Deleting the whole site also detaches every hostname on it. You can delete the DNS records afterwards.</p>
<h2>The same thing over MCP</h2>
<p>The MCP server at <code>https://deed.page/mcp</code> has four domain tools: <code>add_domain</code>, <code>domain_status</code>, <code>list_domains</code>, and <code>remove_domain</code>. They return the same JSON as REST, in <code>structuredContent</code>:</p>
<pre><code class="language-bash">curl -sS 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":"add_domain",
       "arguments":{"slug":"'"$SLUG"'","domain":"docs.example.com"}}}'
</code></pre>
<p>In a chat client that already has deed.page connected (see the <a href="https://deed.page/blog/mcp-publish-walkthrough">MCP walkthrough</a>), the agent just calls <code>add_domain</code>, shows the person the records, and calls <code>domain_status</code> later. If the client can't send headers, pass the token as a <code>token</code> argument. Without any token, the tools return <code>isError: true</code> with <code>unauthorized</code>.</p>
<h2>Errors an agent should handle</h2>
<table>
<thead>
<tr>
<th>Status</th>
<th><code>code</code></th>
<th>When</th>
</tr>
</thead>
<tbody><tr>
<td>400</td>
<td><code>domain_invalid</code></td>
<td>Not a bare hostname. <code>https://docs.example.com/x</code> is rejected: send no scheme, path, port, or wildcard.</td>
</tr>
<tr>
<td>400</td>
<td><code>domain_reserved</code></td>
<td><code>deed.page</code> or any subdomain of it. The site already lives at <code>{slug}.deed.page</code>.</td>
</tr>
<tr>
<td>401</td>
<td><code>unauthorized</code></td>
<td>No Bearer token.</td>
</tr>
<tr>
<td>403</td>
<td><code>forbidden</code></td>
<td>The token doesn't own that slug.</td>
</tr>
<tr>
<td>403</td>
<td><code>quota_domains</code></td>
<td>The plan's hostname limit is used up. The message names the upgrade path.</td>
</tr>
<tr>
<td>409</td>
<td><code>domain_taken</code></td>
<td>Another site already has that hostname.</td>
</tr>
<tr>
<td>429</td>
<td><code>rate_limited</code></td>
<td>Over 60 domain calls an hour. Wait <code>retry_after</code> seconds.</td>
</tr>
</tbody></table>
<p>On Free, a second hostname returns <code>quota_domains</code> with <code>"Free allows 1 custom domain. Hobby ($3/mo) includes 3 custom domains."</code> plus the checkout call. <code>POST /api/v1/billing/checkout</code> with <code>{"plan":"hobby"}</code>, or the MCP tool <code>get_upgrade_link</code>, returns a Stripe Checkout link for a person to pay. Upgrading keeps the same token, sites, and domains. Plans are listed at <a href="https://deed.page/pricing">deed.page/pricing</a>.</p>
<h2>A short checklist for agents</h2>
<ol>
<li>Publish the site and keep the token.</li>
<li><code>add_domain</code>, then show the person <code>dns_records</code> exactly as returned.</li>
<li>Poll <code>domain_status</code> every few minutes, or wait for the <code>domain.active</code> webhook.</li>
<li>Report <code>https://&lt;hostname&gt;/</code> only once <code>status</code> is <code>active</code>. Until then, the <code>{slug}.deed.page</code> URL keeps working.</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[Install the deed.page agent skill in Claude Code, Codex, Cursor, and Hermes]]></title><description><![CDATA[There are two ways to give an agent deed.page. The MCP server at adds publishing as tools. The deed.page skill is a single SKILL.md file that tells the…]]></description><link>https://deedpage.hashnode.dev/install-the-deed-page-skill</link><guid isPermaLink="true">https://deedpage.hashnode.dev/install-the-deed-page-skill</guid><dc:creator><![CDATA[Cooper Email]]></dc:creator><pubDate>Wed, 21 Oct 2026 17:11:10 GMT</pubDate><content:encoded><![CDATA[<p>There are two ways to give an agent deed.page. The MCP server at <code>https://deed.page/mcp</code> adds publishing as tools. The deed.page skill is a single <code>SKILL.md</code> file that tells the agent which <code>curl</code> command to run when someone says "publish this", "host this report", or "give me a link". The skill needs no connector setup. It runs in any agent that can read skill files and has a shell.</p>
<p>The skill is MIT-licensed and public at <a href="https://github.com/f-o-x11/deed-page-skill">github.com/f-o-x11/deed-page-skill</a>. Every command below was run on October 21, 2026, against production. The test sites were deleted afterwards.</p>
<h2>Install with one command</h2>
<p>The <a href="https://www.skills.sh/f-o-x11/deed-page-skill">skills.sh</a> installer reads the repo and copies the skill into the folder your agent loads skills from. Run it from your project directory:</p>
<pre><code>npx skills add f-o-x11/deed-page-skill -a claude-code
</code></pre>
<p>That writes <code>./.claude/skills/deed-page/SKILL.md</code>. Choose a different agent with <code>-a</code>:</p>
<pre><code>npx skills add f-o-x11/deed-page-skill -a cursor      # ./.agents/skills/deed-page
npx skills add f-o-x11/deed-page-skill -a codex -g    # ~/.agents/skills/deed-page
</code></pre>
<p><code>-g</code> installs it for every project instead of just the current one. <code>-a hermes-agent</code> targets Hermes Agent. Leave out <code>-a</code> and the installer asks which agents you use. Before it copies anything, the installer prints a security summary for the skill. The file it installs is the same one that lives in the repo.</p>
<h2>Install without npx</h2>
<p>If you would rather not run an installer, the skill is just one file. Copying it by hand works the same way:</p>
<pre><code>git clone --depth 1 https://github.com/f-o-x11/deed-page-skill /tmp/deed-page-skill
mkdir -p ~/.claude/skills &amp;&amp; cp -r /tmp/deed-page-skill/skills/deed-page ~/.claude/skills/
</code></pre>
<p>deed.page also serves a longer version of the skill at <code>https://deed.page/skill.md</code>. It covers the full API, including versions, merge uploads, and domains. The install command for it is listed in <code>https://deed.page/.well-known/skills/index.json</code>:</p>
<pre><code>mkdir -p ~/.claude/skills/deed-page &amp;&amp; curl -fsSL https://deed.page/skill.md -o ~/.claude/skills/deed-page/SKILL.md
</code></pre>
<p>Pick one of the two. The repo version is short and covers publishing. The served version is the full reference.</p>
<h2>What the agent runs</h2>
<p>Once the skill is installed, asking the agent to "put this report online" makes it pick a slug with a random suffix and send one request:</p>
<pre><code>SLUG="report-$(head -c4 /dev/urandom | od -An -tx1 | tr -d ' \n')"
curl -sS -T report.html "https://deed.page/v1/sites?slug=$SLUG&amp;ref=skill-gh" \
  -H "X-Client: Claude Code"
</code></pre>
<p>The response holds the live URL and, on that first publish only, a token:</p>
<pre><code>{"ok":true,"url":"https://report-4fcfd126.deed.page/","slug":"report-4fcfd126",
 "version":"v1","permanent":true,"plan":"free","token":"deed_tok_..."}
</code></pre>
<p>The skill tells the agent to answer with <code>url</code> and keep the token in <code>DEEDPAGE_TOKEN</code> for the session. deed.page stores only a SHA-256 of that token, so it is the one key that can update or delete the site. <code>?ref=skill-gh</code> records that the publish came from the skill and does not change the result. <code>X-Client</code> names the agent.</p>
<p>For a folder, the skill sends a tarball with the same token:</p>
<pre><code>tar -C ./site -czf /tmp/site.tar.gz .
curl -sS -T /tmp/site.tar.gz "https://deed.page/v1/sites?slug=$SLUG&amp;ref=skill-gh" \
  -H "X-Client: Claude Code" -H "Authorization: Bearer $DEEDPAGE_TOKEN"
</code></pre>
<p>That returned <code>"version":"v2"</code> with both files live. When the user asks for a delete, the agent sends <code>curl -sS -X DELETE "https://deed.page/v1/sites/$SLUG" -H "Authorization: Bearer $DEEDPAGE_TOKEN"</code>, which returns <code>{"ok":true,"deleted":"&lt;slug&gt;"}</code>.</p>
<h2>The same token works over MCP</h2>
<p>Skill and MCP aren't an either/or choice. The skill's token also works on the MCP server, so an agent can publish with <code>curl</code> and later manage that site through tools. Here is a raw MCP call that publishes v3 of the same slug:</p>
<pre><code>curl -sS 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":3,"method":"tools/call","params":{"name":"publish_site",
       "arguments":{"slug":"'"$SLUG"'","files":{"index.html":"&lt;h1&gt;v3 via MCP&lt;/h1&gt;"}}}}'
</code></pre>
<p>The result came back with <code>"version": "v3"</code>, and the site served the new page. <code>tools/list</code> currently returns 24 tools. Besides publishing, they cover versions and restore, custom domains, analytics, passwords, webhooks, token rotation, and <code>set_listing</code> for the opt-in gallery at <a href="https://deed.page/explore">deed.page/explore</a>. Every publish response carries <code>"listing":{"listed":false}</code> until the agent opts in with <code>listed: true</code> plus a category, title, and description.</p>
<h2>Skill or MCP?</h2>
<ul>
<li><strong>Use the skill</strong> when the agent has a shell and you want zero setup: no connector to register and no config file. It also fits agents that support skills but not remote MCP servers.</li>
<li><strong>Use MCP</strong> when the agent has no shell, as with many chat clients, or when you want typed tools for versions, domains, and analytics without the agent having to compose <code>curl</code> calls. The <a href="https://deed.page/blog/mcp-publish-walkthrough">MCP walkthrough</a> shows how to connect it.</li>
<li><strong>Use both</strong> with one token. Publishing works the same way over either path.</li>
</ul>
<h2>Limits</h2>
<p>The skill uses the same Free plan as any other client: 25 sites, 25 MB per site, 60 publishes an hour, and 4 MB per request. There's no signup. Free sites are public, so the skill tells the agent never to publish secrets. If your agent also needs an email address of its own, Cooper Email (<a href="https://cooperemail.com">cooperemail.com</a>) comes from the same team at Avatar 8 LLC.</p>
<hr />
<p><em>Originally published on <a href="https://deed.page/blog/install-the-deed-page-skill">deed.page</a>.</em></p>
]]></content:encoded></item></channel></rss>