Documentation
The publish primitive for agent-made HTML.
private by default MCP-native replace-in-place
Handing this to a coding agent? Give it the OpenAPI spec and go: api.stacktr.ee/openapi.json for the HTTP API, or agents.stacktr.ee/openapi.json for the pay-per-publish front-door. MCP server: https://api.stacktr.ee/mcp. Machine-readable index: stacktr.ee/llms.txt.
For a guided walkthrough with your actual API key inlined into snippets, open app.stacktr.ee/connect.
Connect an agent
npx stacktree-install — recommended
One command wires every agent at once — Claude Code, Cursor, Codex, OpenCode, Amp — and drops the stacktree-publish skill in for Claude:
npx stacktree-install
It signs you in via a one-time code at app.stacktr.ee/connect/cli and mints an API key automatically; pass an existing key as an argument (npx stacktree-install stk_live_…) to skip sign-in.
Claude.ai — custom connector
No CLI, no API key copy-paste.
- Open claude.ai/settings/connectors → Add custom connector.
- Paste
https://api.stacktr.ee/mcpas the Remote MCP server URL. - Leave OAuth Client ID/Secret blank — Stacktree auto-registers via Dynamic Client Registration (RFC 7591).
- Click Add → Claude.ai redirects you to Stacktree to sign in and approve. Tools are then available in any conversation.
Claude Code · Codex — CLI
One-line install. Identical syntax between the two:
claude mcp add stacktree -- npx -y stacktree-mcp
codex mcp add stacktree -- npx -y stacktree-mcp
Both expose the same 12 tools (see MCP server). Set STACKTREE_API_KEY in your shell — generate one at app.stacktr.ee/api-keys.
Prefer a skill? Installs SKILL.md + helper script into your agent's skills directory:
npx skills@latest add stevysmith/stacktree-skill
export STACKTREE_API_KEY=stk_live_...
Source: github.com/stevysmith/stacktree-skill · the whole collection at stacktr.ee/skills
MCP config file — Cursor / Claude Desktop / Windsurf / Zed
{
"mcpServers": {
"stacktree": {
"command": "npx",
"args": ["-y", "stacktree-mcp"],
"env": { "STACKTREE_API_KEY": "stk_live_..." }
}
}
}
Drop into ~/.cursor/mcp.json, ~/Library/Application Support/Claude/claude_desktop_config.json, ~/.codeium/windsurf/mcp_config.json, or the context_servers key in Zed's settings.
Slack
Add the Slack app (one approval; installing mints the workspace its own free identity, no Stacktree account needed). Then ⋮ → Host on Stacktree on any message with an .html or .md file — channels or DMs — posts a private link back into the conversation. Re-uploading the same filename republishes to the same URL. /stacktree link migrates the workspace's sites into a dashboard account. Details: stacktr.ee/slack.
HTTP API
Auth
Three methods, all resolve to the same user context:
Create at app.stacktr.ee/api-keys — or let an agent buy its own over x402 or MPP.
Clerk session token, for dashboard-originated calls.
Access token from /oauth/token, used by custom connectors.
/sitesUpload a single HTML/markdown file or a zip. multipart/form-data. Anonymous uploads work — no auth header — and live 24 hours.
.html / .htm / .md / .zip
Opt-in public subdomain; authed only.
Basic-auth gate on serve.
Default: 24h anonymous, never authed (the MCP layer overrides this to 7 days).
Delete after first view.
Inject feedback toolbar on serve.
Disable strict CSP (default on).
Treat upload as ciphertext; the decryption key lives in the URL fragment and is never sent to Stacktree.
Default warn (MCP layer overrides to block).
curl -F file=@page.html \
-F password=hunter2 \
-F expires_in_hours=72 \
-H "Authorization: Bearer stk_live_..." \
https://api.stacktr.ee/sites
{
"id": "…",
"url": "https://stacktr.ee/p/abc123…/",
"visibility": "unlisted",
"expires_at": 1781234567,
"file_count": 1,
"size_bytes": 1234,
"has_password": true,
"agentation": false
}
/sites/:idOrSlugReplace a site's files in place — the URL never changes. Same multipart fields as POST. Authed only; 401 if not the owner. E2E-encrypted sites must be replaced with e2e=true uploads (no silent downgrade to plaintext).
curl -X PUT -F file=@new.html \
-H "Authorization: Bearer stk_live_..." \
https://api.stacktr.ee/sites/my-deck
/sites/:idOrSlugUpdate settings without re-uploading files. JSON body.
Set or remove the password gate.
null cancels expiry.
Viewers verify an email on that domain before the page renders.
Claim or release a public subdomain.
Toggle the serve-time behaviors documented under POST.
curl -X PATCH \
-H "Authorization: Bearer stk_live_..." \
-H "Content-Type: application/json" \
-d '{"agentation": true, "expires_in_hours": null}' \
https://api.stacktr.ee/sites/my-deck
List · fetch · raw · delete
List your sites.
One site, with file manifest + absolute preview_url.
Page HTML stripped of head/scripts — clean text for re-feeding into an agent. Honors password gates.
Hard delete: removes R2 objects and metadata.
Share tokens
POST https://api.stacktr.ee/sites/:idOrSlug/share-tokens { "label": "alice", "max_uses": 5, "expires_in_hours": 168 }
GET https://api.stacktr.ee/sites/:idOrSlug/share-tokens
DELETE https://api.stacktr.ee/share-tokens/:tokenId
Returns a URL with ?t=… appended. Bypasses the password gate when valid; revocable per-token; optional max-use counter and expiry.
Feedback
GET https://api.stacktr.ee/sites/:idOrSlug/feedback
POST https://api.stacktr.ee/feedback/:id/resolve { "note": "fixed the header spacing" }
DELETE https://api.stacktr.ee/feedback/:id
Viewer annotations left via the on-page Agentation toolbar (enable with agentation on upload, PATCH, or set_agentation). Each item carries the comment plus the annotated element, selected text, intent, and severity. Unresolved first. The loop: read feedback → fix the page with update_site (same URL) → resolve.
Reactions & engagement
GET https://api.stacktr.ee/sites/:idOrSlug/reactions { counts, total, reactors, messages }
GET https://api.stacktr.ee/sites/:idOrSlug/engagement { sessions, median_active_seconds, avg_scroll, read_to_end_pct, buckets }
How a page landed, read by the owner. Reactions: turn on the on-page reaction bar per site (dashboard Settings) and viewers react with an emoji or leave a short private note, no account. Engagement (Pro): aggregate, non-PII read metrics — typical time on page, scroll depth, read-to-end rate, and a 10-bucket attention heatmap of dwell by page depth. No recording, no session replay. Both feed the dashboard activity bell and the optional daily email digest.
API keys
POST https://api.stacktr.ee/api-keys { "label": "claude desktop" }
GET https://api.stacktr.ee/api-keys
DELETE https://api.stacktr.ee/api-keys/:id
MCP server
Streamable HTTP MCP server at https://api.stacktr.ee/mcp (spec 2025-11-25). Connect from any MCP host that supports OAuth-based remote servers — see Connect an agent.
Tools
One call, one private URL.
Replace in place; the URL survives every revision.
Read a published page back.
Password-gate a site.
Schedule or cancel auto-delete.
Restrict viewers to an email domain.
Toggle the on-page feedback toolbar.
Everything the key owns.
Hard delete.
Link the wallet an agent pays from, to claim and manage the pages it published.
Read viewer annotations left via the toolbar; unresolved first.
Mark an item addressed after fixing the page, with an optional note.
Privacy-first MCP defaults
Agents act autonomously without a human reviewing every flag. The MCP layer applies tighter defaults than the raw API:
- 7-day expiry (raw API: never for authed). Pass
expires_in_hours: "never"for permanence. - PII scan in
blockmode (raw API:warn). Passpii_check: "warn"to publish despite detected sensitive data. - Unlisted token URL, strict CSP,
X-Robots-Tag: noai— same defaults as the raw API.
WebMCP (in-browser)
The dashboard registers the same verbs on document.modelContext where the browser supports it (Chrome origin trial), so an in-browser agent helping a signed-in human can call them without an API key. How and why.
Want the same pattern in your own app? The palette and WebMCP registration are built on agentk, our open-source cmdk extension: define tools once as JSON Schema, humans get generated forms, agents get the schemas.
Agent payments
An agent pays with no human and no account, two ways: per publish at the front-door, or with a persistent key bought once. Both accept x402 (USDC on Base or Solana) and MPP (USDC.e on Tempo), so the agent pays with whatever it holds, gasless for the payer. Machine-readable version: x402.md.
Front-door: pay per publish (no key)
The simplest path, when there is no key and no human to make one. POST your HTML to agents.stacktr.ee/api/publish, get a 402, pay $0.50 over x402 (Base or Solana) or MPP (Tempo), and the page publishes to a private link with the URL in the response. No provisioning step. The endpoint is listed on x402scan and mppscan; its spec is at agents.stacktr.ee/openapi.json.
Persistent key: provision once, then pay as you go
Lists the accepted payment rails.
402 → pay $1.00 over x402 or MPP → persistent stk_live_ key, no account.
The à-la-carte catalog: make-permanent $1, custom domain $5/30d, higher limits $5/30d.
402 → pay → feature entitlement on your key.
No wallet? Returns a pay link + terminal QR; a human pays by card in two taps. Poll GET /pay/sessions/:code/poll.
Paying above the price in a pay session (up to $20) leaves a prepaid balance on the key that later paid actions draw from silently. Balances never expire and are refundable on request.
Claim what an agent published
The wallet that pays at the front-door is recorded with each page, so it doubles as a claim ticket. A human can link that wallet from the dashboard (generate a code, the agent signs it), or the agent can self-link with the link_wallet MCP tool. Every page the wallet published then becomes owned and manageable.
Custom domains
Pro+ feature (or the custom_domain x402 unlock). Bring your own hostname (docs.acme.com et al), point a CNAME at our Cloudflare for SaaS fallback origin, prove ownership via a TXT record, and traffic to that hostname serves your site over HTTPS.
/custom-domainscurl -X POST https://api.stacktr.ee/custom-domains \
-H "Authorization: Bearer stk_live_..." \
-H "Content-Type: application/json" \
-d '{"hostname":"docs.acme.com","site_id":"abc123"}'
Response includes a verify_token and the two DNS records you need to add:
{
"hostname": "docs.acme.com",
"site_id": "abc123",
"verified": false,
"instructions": {
"cname": { "name": "docs.acme.com", "value": "proxy.stacktr.ee", "type": "CNAME" },
"txt": { "name": "_stacktree-verify.docs.acme.com", "value": "verify_", "type": "TXT" }
}
}
/custom-domains/:hostname/verifyAfter adding the DNS records, call verify. We DNS-lookup the TXT record; on match we register the hostname with CF for SaaS and SSL provisioning begins (~60 s).
curl -X POST https://api.stacktr.ee/custom-domains/docs.acme.com/verify \
-H "Authorization: Bearer stk_live_..."
Gotcha — DNS-only CNAME. If your DNS is on Cloudflare, the CNAME must be set to DNS only (grey cloud), not Proxied (orange). A proxied CNAME makes Cloudflare claim the hostname for your own zone and Stacktree's SaaS routing never sees the SNI.
Re-bind or delete
PATCH https://api.stacktr.ee/custom-domains/:hostname # { "site_id": "..." } — re-bind
DELETE https://api.stacktr.ee/custom-domains/:hostname # unregister + drop row
List your domains with GET https://api.stacktr.ee/custom-domains. Unverified rows are auto-pruned after 7 days.
OAuth (custom connector authors)
For MCP host implementers — if you're using a maintained client (Claude.ai, Cursor, etc.) skip this section.
Discovery
GET https://api.stacktr.ee/.well-known/oauth-authorization-server
GET https://api.stacktr.ee/.well-known/oauth-protected-resource
Both return standard RFC 8414 / RFC 9728 metadata documents.
Flow
OAuth 2.1 with PKCE (S256 required) and Dynamic Client Registration (RFC 7591). Endpoints:
DCR — rate-limited to 10/IP/hour.
Bounces to Clerk-gated consent page on app.stacktr.ee.
Code → access token (HS256 JWT, 30-day TTL).
RFC 7009 revocation.
Callback for hosted Claude surfaces: https://claude.ai/api/mcp/auth_callback.
Limits
| Limit | Anonymous | Free | Pro | Agent |
|---|---|---|---|---|
| Uploads / 24h | 20 per IP | 50 | 1,000 | unlimited |
| Per-site size | 10 MB | 25 MB | 250 MB | 1 GB |
| Active sites | 1 / 24h window | 5 | unlimited | unlimited |
| Files / archive | 1,000 | 1,000 | 1,000 | 1,000 |
| Default expiry | 24h | never (API) · 7d (MCP) | never (API) · 7d (MCP) | never (API) · 7d (MCP) |
| Custom slug | — | ✓ | ✓ | ✓ |
| Unbranded social previews | — | — | ✓ | ✓ |
| Custom domains | — | — | 5 | 50 |
DCR rate limit: 10 client registrations / IP / hour. Sites auto-purge from R2 + D1 within 1 hour of expiry.
No plan? The higher_limits unlock ($5 / 30 days over x402) lifts a free identity to Agent-tier limits — 1 GB per site, unlimited daily publishes, no active-site cap.