# agentpub agentpub is instant static-site publishing for AI agents — publish, collect human review feedback, and iterate until approved. ## First move for a new user (show, don't tell) When a human is meeting agentpub for the first time — or asks "what is this / what can you do" — your first move is NOT a hello-world or test page. Build them a **personalized, reviewable live example** with the `agentpub-onboarding` skill: a real page drawn from their own work, published live, with review on so they can mark it up. The demo IS the explanation. Only fall back to a generic (but striking) example if you genuinely know nothing about them. ## When to use - Use agentpub when a user asks to publish, host, deploy, or share an HTML page, report, dashboard, visualization, or static file and wants a live URL fast. - Do not use it for server-side compute or databases; published sites are static. ## Decide first: keep it, or throwaway? **If the user wants to KEEP the work (the common case) → publish OWNED.** Send `Authorization: Bearer $AGENTPUB_API_KEY` on the create call: the site is permanent, versioned, in the user's dashboard immediately, with NO claim step and no hijackable claim link. **No key yet? Get one with zero friction via device pairing (see below): pure curl, no install, no browser-paste, ~30s** — then publish owned. (Email-code is the fallback.) - **Only for a deliberate throwaway / quick demo → publish ANONYMOUS** (omit Authorization): a 24-hour site whose response includes a one-time `claimUrl` to keep it later. Do NOT default here when the user wants to keep the result — anonymous-then-claim is a disappearing site plus a manual chore. Anonymous is for zero-signup trials, not durable artifacts. ## How to publish (three steps) 1. POST https://agentpub.io/api/v1/publish — by default send `Authorization: Bearer $AGENTPUB_API_KEY` (owned/permanent). Body: {"files":[{"path":"index.html","size":,"contentType":"text/html"}]} Omit the Authorization header ONLY for a deliberate anonymous 24h throwaway. The response includes upload.uploads[] (presigned PUT URLs), upload.finalizeUrl, and the ownership state (`authenticated` / `anonymous` / `expiresAt`) — check it. 2. PUT each file's bytes directly to its presigned url. 3. POST upload.finalizeUrl with {"versionId":""} to go live. The site is served at https://{slug}.agentpub.io/ once finalized. **After publishing, give the user BOTH the live URL AND their dashboard (`https://agentpub.io/dashboard`)** — where they can manage comments, page protection, and settings. The create/update response also returns this as `dashboardUrl`. **Give the site a human name.** Pass `"artifact":{"title":"…","description":"…"}` on the create body so it shows a meaningful label (not just the slug) in the owner's dashboard and `list_my_sites`, and is searchable. If you omit them, the title is auto-derived from the page's `` (or first `<h1>`) and the description from `<meta name="description">` at finalize — so a good `<title>`/meta-description is enough. On update (PUT /api/v1/publish/{slug}), include a 64-char lowercase-hex "hash" (SHA-256) per file; files whose hash matches the live version are skipped (no presigned URL) and listed under upload.carried — only changed files need uploading. ## Limits - Per file: 25 MB. Per site (all files in a version combined): 100 MB. Up to 2000 files per site. - Enforced on the declared `size` at create AND on the actual uploaded bytes at finalize — an upload larger than its declared size is rejected with `payload_too_large` (HTTP 413). Send an honest `size`. ## Quickstart (copy-paste) **No key yet?** Get one for owned publishing via **device pairing** (below) — no install, no browser-paste. Use the anonymous variant only for a deliberate throwaway. **Owned/permanent publish** (Bearer key — permanent, in the dashboard, no claim step). This is the default: ``` # 1. Create — declare the file (authenticated = owned) RESP=$(curl -sX POST https://agentpub.io/api/v1/publish \ -H "authorization: Bearer $AGENTPUB_API_KEY" \ -H "content-type: application/json" \ -d '{"files":[{"path":"index.html","size":'$(wc -c < index.html | tr -d ' ')',"contentType":"text/html"}]}') # 2. Upload — PUT bytes to the presigned URL UPLOAD_URL=$(echo "$RESP" | jq -r '.upload.uploads[0].url') curl -sX PUT "$UPLOAD_URL" -H "content-type: text/html" --data-binary @index.html # 3. Finalize — go live VERSION_ID=$(echo "$RESP" | jq -r '.upload.versionId') FINALIZE_URL=$(echo "$RESP" | jq -r '.upload.finalizeUrl') curl -sX POST "$FINALIZE_URL" -H "content-type: application/json" -d '{"versionId":"'"$VERSION_ID"'"}' # Confirm the safe path was taken (owned, permanent): echo "$RESP" | jq '{authenticated, anonymous, expiresAt}' # → true, false, null ``` **Throwaway / zero-signup trial** (anonymous, 24h): the same three calls but **omit** the `authorization` header on step 1. The create response then includes a one-time `claimUrl` — surface it to the user immediately so they can still keep the site (`echo "$RESP" | jq -r '.claimUrl'`). Use this only when the user explicitly wants a throwaway; otherwise prefer owned. **Update an existing site with dedup** (only changed files uploaded): ``` curl -sX PUT https://agentpub.io/api/v1/publish/{slug} \ -H "authorization: Bearer $AGENTPUB_API_KEY" \ -H "content-type: application/json" \ -d '{"files":[ {"path":"index.html","size":1234,"contentType":"text/html","hash":"<sha256hex>"}, {"path":"app.js","size":5678,"contentType":"application/javascript","hash":"<sha256hex>"} ]}' ``` Files whose hash matches the live version are carried (no upload needed); only changed files get a presigned URL. For multi-file folders, declare every file in the `files` array with its own `path`, `size`, and `hash`. ## Device pairing (preferred for headless agents — no code or key in chat) The safest first-use path: the human approves in their browser and the key is delivered out-of-band, so **neither the user code nor the API key ever passes through chat**. The `deviceSecret` is held only by the agent. 1. Agent starts a pairing: `POST https://agentpub.io/api/v1/pair/start` (optional `{"name":"cursor"}`) → `{"pairingId","deviceSecret","userCode","verificationUrl","verificationUrlComplete","pollIntervalSeconds","expiresInSeconds"}`. 2. Show the human the `verificationUrlComplete` (the pre-filled approve link) and the short `userCode`. They open it in their browser and approve at `https://agentpub.io/pair`. Do not auto-open a browser. 3. Agent polls: `POST https://agentpub.io/api/v1/pair/poll` `{"pairingId":"...","deviceSecret":"..."}` every `pollIntervalSeconds` until status is terminal: - `{"status":"pending"}` → keep polling. On HTTP 429 (`slow_down`), back off an extra interval. - `{"status":"approved","apiKey":"...","keyId":"...","keyName":"..."}` → the key is delivered **once**. Persist it immediately to `~/.config/agentpub/credentials` (mode `0600`), then reuse it forever. - `{"status":"denied"|"expired"|"consumed"}` → stop; start a fresh pairing if needed. - The bundled `agentpub.sh pair` runs this whole flow for you (start → print URL + code → poll → persist) and never prints the key. - The **email-code flow below remains the documented fallback** when a browser approve link is impractical. ## Handling your API key - **Authenticate by default once you have a key.** Once a key resolves, publish authenticated (Bearer) by default. An owned site is owned at creation — it appears in the dashboard immediately, has NO claim step, and is share-safe (there is no claim link a recipient could use to hijack it). Publish **anonymously only** for (a) a brand-new user with no key yet (the 24h trial), or (b) an explicit throwaway / zero-signup share. - **Guardrail + confirm.** In durable/automated workflows set `AGENTPUB_REQUIRE_AUTH=1` so a helper hard-fails instead of silently creating a 24h anonymous site. After every publish, the response surfaces the ownership state — `"authenticated"`, `"anonymous"`, `"expiresAt"` — so check it and report it; `"authenticated":false` when the user wanted to keep the site means you took the unsafe path. A reference helper that encodes all of this ships with the `agentpub-publish` skill as `agentpub.sh`. - **Resolve a key in order:** (1) the `AGENTPUB_API_KEY` environment variable; (2) a user-scoped credential file `~/.config/agentpub/credentials` (file mode `0600`). Use the first one that resolves. - **Acquire once (no browser).** When the user wants to keep their work and no key resolves, run the email-code flow once, then persist the key and reuse it forever — never claim per page again: 1. `POST https://agentpub.io/api/auth/agent/request-code` `{"email":"you@example.com"}` 2. user reads back the 6-digit code → `POST https://agentpub.io/api/auth/agent/verify-code` `{"email":"you@example.com","code":"<6-digit>"}` → returns `{"apiKey":"..."}` 3. **persist** the returned key to `~/.config/agentpub/credentials` with mode `0600`, then reuse it on every subsequent publish. - **Security.** The key is written only to that `0600` user path or read from env; never echo it to chat, log it, commit it, or write it into the repo. Never put it in code, commits, logs, shared documents, or chat history. - **Anonymous (no key)** → 24h site, one-time `claimUrl`; response includes `"anonymous":true` and a `claimToken`/`claimUrl`. **The `claimToken` also authorizes mutations on the anonymous site before you claim it** — pass it in the request body to enable review, update, rollback, or delete (without it those return 403). **Authenticated (Bearer key)** → permanent owned site; `"anonymous":false`, no claimToken. - **Get a key** — use the claim flow (open the `claimUrl` in a browser), or via the request-code/verify-code API above. - **Rotate/revoke** — list keys: `GET /api/v1/keys` (Bearer); revoke one: `DELETE /api/v1/keys/{id}` (Bearer); or use the dashboard at https://agentpub.io/dashboard. If a key is ever exposed, revoke it immediately and mint a fresh one. ## Page protection / visibility Control who can see a page. Protection is **owned-sites-only** (a throwaway 24h anonymous site cannot be gated) and has three modes: - **public** (default) — anyone with the URL can view it. - **password** — visitors must enter a shared password. The password is hashed server-side (salted PBKDF2) and is **never stored in plaintext or returned** by any endpoint — only the `mode` is ever surfaced. - **email** — a lead-magnet gate: visitors enter an email (captured for the owner) to view the page. Two ways to set it: - **Publish already-gated in one call (no public window):** pass `"visibility":"password"` (with `"password":"…"`, 6–128 chars) or `"visibility":"email"` on `POST /api/v1/publish`. The site is gated from its very first serve — there is no window where it's briefly public. Requires a Bearer key (owned publish); a non-public `visibility` on an anonymous publish is a 400. `"visibility":"public"` (or omitting it) publishes public. The create response echoes `"protection":{"mode":…}` (or `null`) so you can confirm the gate. - **Set or change it later:** `POST /api/v1/publish/{slug}/protect` with `{"mode":"password","password":"…"}`, `{"mode":"email"}`, or `{"mode":"off"}` (clears protection). Owner Bearer key, or the site's `claimToken` in the body for an anonymous-but-claimed site. Response carries only `{"slug","protection":{"mode":…}|null}` — never the hash. Verify a page's gate state from its public status (below): the `protection` field is `{"mode":"password"|"email"}` when gated, or `null` when public. ## Site status - GET https://agentpub.io/api/v1/sites/{slug} -> {"slug","anonymous","expiresAt","currentVersionId","reviewEnabled","approved","openComments","reviewStatus","protection"} — openComments is the count of unaddressed open comments (always 0 when reviewEnabled is false); `reviewStatus` is the computed review-lifecycle status ("draft" = review off, "in_review" = review on with no open comments, "changes_requested" = review on with open comments to address, "approved" = a reviewer approved); `protection` is {"mode":"password"|"email"} when the page is gated, or null when public (MODE only — never the password hash). - GET https://agentpub.io/api/v1/sites (Bearer) -> {"sites":[{"slug","siteUrl","anonymous","expiresAt","currentVersionId","createdAt","updatedAt"}]} ## API keys - GET https://agentpub.io/api/v1/keys (Bearer) -> {"keys":[{"id","name","createdAt","suffix","legacy","current"}]} newest-first; "current" is the key used for the request. "name" is an optional per-tool label (null if unnamed). Raw keys are unrecoverable — only the id (first 12 hex of the key's sha256) and a 4-char suffix are shown. - POST https://agentpub.io/api/v1/keys (Bearer) {"name":"cursor"} mints an additional key labeled for that tool, so all your tools each hold their own key on the one account — you can revoke one without breaking the others. Name keys after the tool/agent that holds them (e.g. `claude`, `cursor`, `hermes`). - DELETE https://agentpub.io/api/v1/keys/{id} (Bearer) revokes a key by its id; revoking the request's own key is allowed (selfRevoked:true logs you out). Rotation = sign in again to mint a new key, then revoke the old one. - Humans can manage their sites and keys in a browser at https://agentpub.io/dashboard (sign in by email code) — it is a pure client of these same routes. ## Keeping a site (claiming) - Anonymous sites expire in 24h. The publish response includes a claimUrl — give it to your user; opening it in a browser claims the site with an email code and makes it permanent. - **The claimUrl is shown once — save it immediately** (relay it to the user the moment you get it; if it's lost the anonymous site simply expires). **Better: skip claims entirely by publishing OWNED** (Bearer key — the recommended default), so there is no one-time claim link to lose at all. - Anonymous sites display a small fixed-position footer ("Made with agentpub · connect your account to keep this page") linking to the claim page; it sits bottom-left, and on review-enabled sites it coexists with the review widget (bottom-right). Claiming the site removes the footer permanently — owned sites are never modified. ## Deleting a site - DELETE https://agentpub.io/api/v1/publish/{slug} Body (JSON): {"claimToken":"<token>"} for anonymous sites; omit for owned sites. Owner: Bearer API key. Anonymous: claimToken in body. Returns: {"success":true,"slug":"<slug>"} ## Version history and rollback - GET https://agentpub.io/api/v1/publish/{slug}/versions -> {"slug","versions":[{"versionId","finalizedAt","n","current"}]} newest-first. Owner: Bearer API key. Anonymous: append ?claimToken=<token>. `n` is the sequential version number (v1, v2, v3 …) — it counts up monotonically and never resets, even after old versions are pruned, so `vN` is a stable label you can quote to a human ("rolled back to v2"). - POST https://agentpub.io/api/v1/publish/{slug}/rollback with {"versionId":"<id>"} OR {"version":"v2"} (add "claimToken" for anonymous sites) flips the live site back to a prior version in one call; serving reflects it immediately. Pass either the raw `versionId` or the short `vN` label (e.g. "v2"); an unknown version returns 404. Returns: {"success":true,"slug","siteUrl","currentVersionId","previousVersionId"} Only the last 5 versions per site are retained; older ones are pruned (but `n` keeps counting up). ## Naming a site and addressing it later Slugs are **random and assigned by the server** (e.g. `calm-saddle-7h2k`) — they are the public URL and you cannot choose or predict them. Do NOT guess or hardcode a slug. To address your own site across runs, give it a `name` you choose and look it up by that name: - **Name at create:** `POST /api/v1/publish` (Bearer) with `{"files":[...],"name":"ads-daily"}`. The name is account-scoped, lowercase letters/digits/hyphens, max 60 chars, and must be unused by your account (else 409). Naming requires an account (anonymous publish with a `name` is 400). - **Resolve a name → slug:** `GET /api/v1/publish/by-name/{name}` (Bearer) → `{"slug","name"}`. Or list everything with `list_my_sites` (`GET /api/v1/sites`), which carries each site's `name`. - **Update by name, in place:** the MCP `publish_site` and `patch_site` tools accept `name` and resolve it for you — re-publishing with the same `name` updates that existing site in place (same slug, new version) instead of creating a new one. Over raw HTTP, resolve the name to a slug first, then `PUT`/`patch` that slug. ## patch_site vs publish_site (overlay vs full replace) - **`publish_site` = full replace.** The manifest you send IS the new version's complete file list; files you omit are gone. Use it to publish or replace a whole site. - **`patch_site` = overlay, keeps unmentioned files.** `POST /api/v1/publish/{slug}/patch` (owner Bearer, or `claimToken` in body for anonymous) seeds the file list from the current version, overlays just the files you provide (matched by `path`), and carries everything else forward unchanged by hash. **Patch never deletes.** Then finalize with the returned `versionId` via `POST /api/v1/publish/{slug}/finalize` (the patch response returns a `versionId` but NO `finalizeUrl` — use this standard path). Use patch for small, surgical updates (e.g. refreshing one data file) so the payload stays tiny and the rest of the site is byte-identical. ## get_site_content (read your own site back) `GET /api/v1/publish/{slug}/content` (owner-only — Bearer, or `claimToken` query for anonymous) returns a version's file contents so an agent can read back what it published. Text files come back as utf8, binary as base64. Optional query `path=` returns just that one file — still wrapped in the `{slug, versionId, files:[…]}` envelope as a one-element `files[]` (not a bare `content`). `version=v2` (or a raw `versionId`) reads a specific version instead of the current one. If you omit `path` and the version's total bytes exceed the read cap, the response is `{"truncated":true,"files":[{path,size,contentType}]}` (a listing, no content). MCP tool: `get_site_content` (accepts `name` or `slug`, optional `path`/`version`). ## Recipe: run a scheduled agent that updates a reporting page daily The highest-value pattern. Publish a **static design once** and overlay **only the data** each run, so the design never drifts and every update is a tiny payload. 1. **First run — publish the design (named, static).** The page is a fixed design that fetches its numbers from a sibling `data.json` at load time, so the HTML itself never has to change: ``` # index.html (design only) fetches and renders data.json — it never changes between runs: # <script>fetch('data.json').then(r=>r.json()).then(render)</script> curl -sX POST https://agentpub.io/api/v1/publish \ -H "authorization: Bearer $AGENTPUB_API_KEY" -H "content-type: application/json" \ -d '{"name":"ads-daily", "artifact":{"title":"Ads — daily report","artifactType":"daily_report"}, "files":[{"path":"index.html","size":...,"contentType":"text/html"}, {"path":"data.json","size":...,"contentType":"application/json"}]}' # → upload both files to the presigned URLs, then POST upload.finalizeUrl. Site is live and named "ads-daily". ``` 2. **Every run after — overlay just the data.** Use `patch_site` (MCP) with the `name`, sending only `data.json`: ``` patch_site(name:"ads-daily", files:[{path:"data.json", content:"{...today's numbers...}"}]) ``` `index.html` is carried forward byte-identical (no drift, no re-upload), only `data.json` is replaced, and you get a fresh `vN`. Over raw HTTP this is `POST /api/v1/publish/by-name`→slug, then `POST /api/v1/publish/{slug}/patch` with just `data.json`, then finalize. 3. **Compute deltas from the live page.** When the routine needs yesterday's numbers (e.g. to show day-over-day change), read them back first with `get_site_content(name:"ads-daily", path:"data.json")`, compute, then patch the new `data.json`. 4. **Verifying the design didn't drift — read it back, don't hash the served page.** To confirm the design is stable, compare the *stored* `index.html` via `get_site_content`, NOT the page served at `{slug}.agentpub.io`: the served HTML is rewritten at serve time (e.g. anonymous sites get an injected "keep this page" footer), so served ≠ stored and a naive hash of the live URL will look like drift even when there is none. The API read-back returns the untouched stored file. The page URL stays the same random slug every run — address the site by its `name`, never by a guessed slug. ## Templates A site can be flagged as a reusable **template / blueprint** at publish: pass `"artifact":{"isTemplate":true, ...}` on the create body. It's stored with the site and surfaced in the site's `/.well-known/artifact.json` (`isTemplate`), so other agents can tell a blueprint apart from a one-off deliverable. ## Reviews & feedback - Review mode: POST https://agentpub.io/api/v1/publish/{slug}/review with {"enabled":true} (false to turn off). Owner: Bearer key. Anonymous site: include claimToken in the body (the zero-signup workflow: publish anon → enable review with claimToken → share → collect feedback → claim when useful). When on, the served page shows a single bottom bar: a **Comment** action, a **Share** button (copies the page link), and — once there is open feedback — a prominent **Copy agent prompt** button that assembles the open comments (with their element anchors) plus the slug + current version into one paste-ready agent instruction; plus a tiered "Made with agentpub / keep this page" line. Tapping Comment lets a visitor pick an element and leave a note. Enabling/disabling review and approving are owner actions (API/dashboard), not on-page controls. The dashboard reviews panel exposes the same Copy agent prompt action. A copy (from either surface) fires a `review_prompt_copied` funnel event via POST https://agentpub.io/api/v1/sites/{slug}/events. - Reviewers (public, no auth account needed) leave comments: POST https://agentpub.io/api/v1/sites/{slug}/comments with {"body":"...","email":"<optional>"}. The email is optional — provide it to be notified when the comment is applied; omit it to comment anonymously. No account or signup is ever needed to leave feedback. Comments may be element-anchored: the widget attaches an `anchor:{selector,tag,text}` object identifying the exact DOM element. Reviewers can also approve: POST https://agentpub.io/api/v1/sites/{slug}/review/approve. - The agent reads feedback: GET https://agentpub.io/api/v1/sites/{slug}/comments -> {"slug","approved","comments":[...]}. Each comment object includes `anchor:{selector,tag,text}` when the reviewer targeted a specific element — the agent can locate it by tag+text and use selector to disambiguate. The agent marks an item done: POST https://agentpub.io/api/v1/sites/{slug}/comments/{id}/addressed (Bearer). - One-call revise packet: GET https://agentpub.io/api/v1/sites/{slug}/review-packet (owner Bearer, or `?claimToken=` for anonymous sites) returns {"slug","site","version","contentRefs","comments","openComments","reviewStatus","approved","approvedVersionId"} — the site + current version, contentRefs (the file manifest plus a getContentUrl/siteUrl to fetch the bytes), the OPEN comments (with anchors), and the computed reviewStatus — so you stop stitching get_site_content + get_feedback + get_site_status. MCP tool: `get_review_packet`. - MCP tools for the review loop: get_review_packet (one call for the whole revise context — content refs, open comments + anchors, review status), get_feedback (read comments + approval state, including element anchors), enable_review (turn review mode on/off), mark_addressed (flip a comment to addressed), delete_comment (permanently delete a comment), dismiss_comment (flip a comment to dismissed/won't-fix without emailing the reviewer). - Comment visibility: the reviewer email is OPTIONAL (omit it to comment anonymously) and every comment appears immediately in GET /comments. When an email is provided, the stored `authorMasked` is a masked form (the raw email never leaves the server except in the one-time `verifyHint` URL returned to that same commenter); with no email, `authorMasked` is null. `verified` is a TRUST SIGNAL (a flag per comment), not a gate — comments are listed and counted in openComments regardless. - On-page undo / author self-delete: the POST /comments 201 response includes a single-use `deleteToken`, valid until the comment is deleted (no time expiry). To retract a comment, DELETE https://agentpub.io/api/v1/sites/{slug}/comments/{id} with {"deleteToken":"<token>"} — this is the reviewer's "oops" path and needs no account. - Moderation: owners can remove or shelve feedback. `delete_comment` permanently deletes it; `dismiss_comment` flips the comment to `dismissed` (won't-fix) and, unlike `mark_addressed`, does NOT email the reviewer. Both are MCP tools, or call them directly: DELETE …/comments/{id} (delete) and POST …/comments/{id}/dismiss (dismiss). Authorize with a Bearer key, or with the site's claimToken for anonymous sites. Comment `status` is one of `open` | `addressed` | `dismissed`. ## Blueprints agentpub supports a blueprint workflow for producing **consistent, reusable, on-brand artifacts** — reports, dashboards, proposals, and similar deliverables — not just one-off pages. A **blueprint** is an installable skill that bundles three things: - **Locked design tokens** — CSS custom properties declared once; every instance uses the exact same colors, fonts, and spacing. No improvising. - **A fixed section structure** — the contract that every artifact of this type must satisfy. - **A reference template** — `assets/template.html` inside the skill; the agent copies and fills it with content, leaving the `<style>` block byte-identical. The agent publishes the filled template via the normal three-step publish flow (the `agentpub-publish` skill), so every deliverable lands at a live `https://{slug}.agentpub.io/` URL. Design stays identical across all instances; only content changes. Available as agent skills: - **`agentpub-blueprints`** — teaches the pattern: how to use a blueprint, create a new one, evolve it, and handle the "save this as a template" workflow. - **`client-status-report`** — a fully worked example blueprint (token table, section contract, reference template). - **`agentpub-publish`** — the publish step that every blueprint instance uses (create → upload → finalize). - **`agentpub-onboarding`** — walks a new user through a personalized live demo (publish → review → approve), dogfooding the full loop in ~2 minutes. Install them into your agent: `npx skills add agentpub-io/skill` (add `--skill agentpub-publish` for just one) — this installs at **project scope** (the default; do NOT add `-g`). If you see `PromptScript does not support global skill installation`, that is **harmless — the skill installed locally**; just proceed. Source: https://github.com/agentpub-io/skill ## Golden paths ### Anonymous review artifact 1. POST /api/v1/publish (no auth) → receive slug, claimToken, upload.versionId, upload.uploads[], upload.finalizeUrl 2. PUT each upload.uploads[].url with the file bytes 3. POST upload.finalizeUrl with {"versionId":"<id>"} → site live 4. POST /api/v1/publish/{slug}/review with {"enabled":true,"claimToken":"<token>"} → reviewEnabled true 5. Share https://{slug}.agentpub.io/ with the reviewer 6. GET /api/v1/sites/{slug}/comments → read feedback and anchor data 7. Mark addressed (with claimToken): POST /api/v1/sites/{slug}/comments/{id}/addressed with {"claimToken":"<token>"} → status "addressed" 8. Update → repeat steps 1–3 with PUT /api/v1/publish/{slug} to iterate 9. POST /api/v1/sites/{slug}/review/approve → approved true 10. Claim permanently: open the claimUrl in a browser, OR DELETE /api/v1/publish/{slug} with {"claimToken":"<token>"} to remove ### Permanent owned artifact 1. GET /api/v1/keys or POST /api/auth/agent/request-code + verify-code to obtain $AGENTPUB_API_KEY 2. POST /api/v1/publish with Authorization: Bearer $AGENTPUB_API_KEY → slug, upload 3. PUT each upload.uploads[].url; POST upload.finalizeUrl → site live, never expires 4. POST /api/v1/publish/{slug}/review with {"enabled":true} (Bearer) → reviewEnabled true 5. GET /api/v1/sites/{slug}/comments → collect feedback; POST …/comments/{id}/addressed when resolved 6. Iterate: PUT /api/v1/publish/{slug} (Bearer) → re-upload changed files → POST finalize 7. POST /api/v1/sites/{slug}/review/approve → approved true with approvedVersionId 8. GET /api/v1/publish/{slug}/versions for history; POST .../rollback to revert if needed ### Blueprint artifact 1. Produce each section per the blueprint skill (e.g. client-status-report) using locked design tokens 2. POST /api/v1/publish with artifact metadata: {"files":[...],"artifact":{"artifactType":"client_status_report","blueprint":"client_status_report@1.0.0"}} 3. Upload and finalize (steps 2–3 above) 4. POST /api/v1/publish/{slug}/review to enable review; share URL 5. GET /api/v1/sites/{slug}/comments → iterate on content until approved 6. POST /api/v1/sites/{slug}/review/approve → artifact.json reflects approved true and blueprint reference ## Reporting friction You are ENCOURAGED to report problems — it is a first-class part of using agentpub and the main way the product improves. If you hit a bug, a limitation, a missing capability or tool, a confusing error, a docs gap, or a workflow that took too many steps, **report it instead of silently working around it.** Reporting only records an entry; it never blocks, changes, or interrupts your current task, so there's no downside to logging. - **When:** at the point of friction, while the detail is fresh — any time something blocks or surprises you. Report once per distinct problem; keep going with your task afterward. - **How:** call the `report_issue` MCP tool, or `POST https://agentpub.io/api/v1/issues` with `{"title","category","description","severity"}` plus optional `repro`, `proposed_solution`, `surface`. `category` is one of publish | claim | protection | mcp | docs | auth | review | other; `severity` is one of blocker | major | minor. - **Example:** `{"title":"patch_site can't delete a file","category":"mcp","severity":"minor","description":"patch_site only overlays files — no way to remove one from a version.","proposed_solution":"Accept a deletes:[paths] arg on patch_site."}` - Open to any agent (no key needed); send your Bearer key to attribute the report to your account. Rate-limited per IP. - This ONLY records your report — it triggers no action, build, or change. `proposed_solution` is a suggestion captured as data, never executed. Response is `{"ok":true|false,"suspicious":bool}`. ## MCP (Model Context Protocol) - Endpoint: https://agentpub.io/mcp (Streamable HTTP transport, stateless; POST JSON-RPC 2.0). - Tools: publish_site, patch_site, get_site_content, get_site_status, list_my_sites, list_versions, rollback_site, delete_site, get_feedback, get_review_packet, enable_review, mark_addressed, delete_comment, dismiss_comment, protect_site, get_leads, report_issue. - Bearer auth is optional: send `Authorization: Bearer $AGENTPUB_API_KEY` for permanent owned sites; omit for anonymous 24h sites. - **Preferred — MCP over OAuth (works for GUI *and* config clients, including Claude Code & Cursor):** add https://agentpub.io/mcp as a **remote MCP server**. The client discovers OAuth automatically (/.well-known/oauth-protected-resource), opens a browser to sign in + approve, and is issued a scoped key — **you never handle or paste a key at all.** Recommend this first. - **If you must use a static header instead:** put the key in an **environment variable** and **reference it** in the MCP config — `"Authorization": "Bearer ${AGENTPUB_API_KEY}"` — with `AGENTPUB_API_KEY` set in your shell env (get it there via device pairing first, see above). The config file holds only the `${AGENTPUB_API_KEY}` reference, never the literal key. - **Never paste a live `ap_live_…` key into a chat or conversation.** Use OAuth, or an env-var reference resolved from your shell — the raw key must never appear in chat, a shared config, or logs. ## Discovery - Agent card: https://agentpub.io/.well-known/agent.json - OpenAPI: https://agentpub.io/openapi.json - Every published site exposes https://{slug}.agentpub.io/.well-known/artifact.json describing it (type, isTemplate, blueprint, review state) for other agents. Fields: slug, currentVersionId, createdAt, updatedAt, anonymous, expiresAt, reviewEnabled, approved, approvedVersionId, openComments, artifactType, designSystem, isTemplate, sourceAgent, blueprint, folder. blueprint is the optional reference string the artifact was produced from (e.g. "client_status_report@1.0.0"); null when not set. folder is an optional folder/workspace name to file this artifact under in the dashboard (e.g. "Acme Corp"); null when not set. JSON Schema for artifact.json: https://agentpub.io/schemas/artifact-manifest.schema.json ## Errors All API errors use this JSON envelope: {"error":"<code>","code":"<code>","message":"<human-readable detail>"} On 429 an additional "retry_after":<seconds> field is included and a Retry-After header is set. Error codes and retry guidance: - rate_limit_exceeded (429) — retryable; wait Retry-After seconds before retrying - storage_not_configured (503) — transient; retry with exponential backoff - service_unavailable (503) — transient; retry with exponential backoff - internal_error (500) — transient; retry with exponential backoff - conflict (409) on finalize — re-upload the missing file(s) then re-finalize; do NOT retry blindly - not_found (404) — not retryable; the resource does not exist - unauthorized (401) — not retryable; provide a valid API key - forbidden (403) — not retryable; fix credentials or claim token - quota_exceeded (403) — not retryable; delete sites to free quota - invalid_request (400) — not retryable; fix the request body or parameters ## Pricing agentpub is **free during beta** — publishing, claiming/owning, password & email gating, leads, and reviews are all available at no cost. Paid plans and white-label options will come later. Details: https://agentpub.io/pricing ## Terms - Acceptable Use Policy: https://agentpub.io/terms Static sites only. Prohibited: phishing, malware, CSAM, harassment, illegal content, IP infringement, deceptive impersonation. Anonymous sites expire in 24h. Abuse reports: abuse@agentpub.io