Stacktree docs Dashboard

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.

  1. Open claude.ai/settings/connectorsAdd custom connector.
  2. Paste https://api.stacktr.ee/mcp as the Remote MCP server URL.
  3. Leave OAuth Client ID/Secret blank — Stacktree auto-registers via Dynamic Client Registration (RFC 7591).
  4. 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:

Authorization: Bearer stk_live_…API key

Create at app.stacktr.ee/api-keys — or let an agent buy its own over x402 or MPP.

Authorization: Bearer <clerk-session-jwt>session

Clerk session token, for dashboard-originated calls.

Authorization: Bearer <oauth-jwt>OAuth

Access token from /oauth/token, used by custom connectors.

POST/sites

Upload a single HTML/markdown file or a zip. multipart/form-data. Anonymous uploads work — no auth header — and live 24 hours.

filefilerequired

.html / .htm / .md / .zip

public_slugstring

Opt-in public subdomain; authed only.

passwordstring

Basic-auth gate on serve.

expires_in_hoursnumber | "never"

Default: 24h anonymous, never authed (the MCP layer overrides this to 7 days).

burn_after_read"true"

Delete after first view.

agentation"true"

Inject feedback toolbar on serve.

csp_strict"false"

Disable strict CSP (default on).

e2e"true"

Treat upload as ciphertext; the decryption key lives in the URL fragment and is never sent to Stacktree.

pii_checkoff | warn | block

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
}
PUT/sites/:idOrSlug

Replace 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
PATCH/sites/:idOrSlug

Update settings without re-uploading files. JSON body.

passwordstring | null

Set or remove the password gate.

expires_in_hoursnumber | null

null cancels expiry.

allowed_email_domainstring | null

Viewers verify an email on that domain before the page renders.

public_slugstring | null

Claim or release a public subdomain.

agentation · burn_after_read · csp_strictboolean

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

GET/sites

List your sites.

GET/sites/:idOrSlug

One site, with file manifest + absolute preview_url.

GET/raw/:token

Page HTML stripped of head/scripts — clean text for re-feeding into an agent. Honors password gates.

DELETE/sites/:idOrSlug

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

publish_html→ { url, id, expires_at, … }

One call, one private URL.

update_site→ { url, file_count, size_bytes }

Replace in place; the URL survives every revision.

get_site→ { url, settings, … }

Read a published page back.

set_password→ { ok: true }

Password-gate a site.

set_expiry→ { ok: true }

Schedule or cancel auto-delete.

set_email_gate→ { ok: true }

Restrict viewers to an email domain.

set_agentation→ { ok: true }

Toggle the on-page feedback toolbar.

list_sites→ { sites: [...] }

Everything the key owns.

delete_site→ { ok: true }

Hard delete.

link_wallet→ { ok, adopted }

Link the wallet an agent pays from, to claim and manage the pages it published.

list_feedback→ { feedback: [...] }

Read viewer annotations left via the toolbar; unresolved first.

resolve_feedback→ { ok: true }

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 block mode (raw API: warn). Pass pii_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

GET/provision

Lists the accepted payment rails.

POST/provision

402 → pay $1.00 over x402 or MPP → persistent stk_live_ key, no account.

GET/unlock

The à-la-carte catalog: make-permanent $1, custom domain $5/30d, higher limits $5/30d.

POST/unlock

402 → pay → feature entitlement on your key.

POST/pay/sessions

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.

POST/custom-domains
curl -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" }
  }
}
POST/custom-domains/:hostname/verify

After 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:

POST/oauth/register

DCR — rate-limited to 10/IP/hour.

GET/oauth/authorize

Bounces to Clerk-gated consent page on app.stacktr.ee.

POST/oauth/token

Code → access token (HS256 JWT, 30-day TTL).

POST/oauth/revoke

RFC 7009 revocation.

Callback for hosted Claude surfaces: https://claude.ai/api/mcp/auth_callback.

Limits

LimitAnonymousFreeProAgent
Uploads / 24h20 per IP501,000unlimited
Per-site size10 MB25 MB250 MB1 GB
Active sites1 / 24h window5unlimitedunlimited
Files / archive1,0001,0001,0001,000
Default expiry24hnever (API) · 7d (MCP)never (API) · 7d (MCP)never (API) · 7d (MCP)
Custom slug
Unbranded social previews
Custom domains550

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.