Your assistant — Claude Code, Codex, Cline, Kimi, or anything that can run a shell command — can publish a document and hand you back a secure, tracked link: password, expiry, per-page analytics, revoke. This page is the whole setup. You can paste this URL straight into your AI and let it install itself; the one step it cannot do for you is creating the token.
/ai-skill-setup.md instead of the HTML — same content, about a tenth of the tokens. First decide which of these you are, then follow only that route:
pitchstation.ai/?share=1, or run a curl you prepare with a $PITCHSTATION_TOKEN placeholder (§10) — never paste their real token into the chat.
Open your assistant in the folder you work in, and paste this. It reads the guide, installs itself, and writes down the defaults so future sessions behave the same.
Once it is set up, this is the everyday sentence — no flags, no jargon:
PitchStation authenticates agents with a Personal Access Token (PAT): a string starting pst_ that acts as you. An AI cannot create one for you — it lives behind your sign-in.
pitchstation.ai/login.html — “Continue with Google” provisions instantly, or use email + password.
pitchstation.ai/tokens.html → New token.
claude-code-macbook, codex-work-laptop, ci-publish. The name is the only way you will know which one to revoke later — “token 3” tells you nothing in six months.
pst_… value immediatelyIt is shown once. Close the page without copying it and there is no recovery — you delete it and create another. That costs nothing.
A .env file beside the skill (Route A, §04) or the MCP host's env block (Route B, §05). Both are covered below.
If you would rather your assistant never sees the string, run this yourself in your own terminal before the AI gets to the .env step. read -rs hides the input and keeps it out of your shell history:
Then tell the AI “the .env is already written, skip that step”. Pasting the token to the agent instead is a reasonable trade-off too — it is revocable in two clicks — but the file above is the tidier habit.
Two ways to connect. They do the same things; they differ in what has to be installed on the machine.
Recommended. Two files and a .env. Needs only python3 (already on every Mac and Linux box). No Node, no MCP host, no restart. Works in Claude Code CLI, the desktop app, and any session with shell access.
A single Node 18+ file registered with an MCP host. Choose this if you already run MCP (Claude Code with other MCP servers, Cline, Continue) and want native tools rather than shell commands.
| Route A — portable | Route B — MCP | |
|---|---|---|
| Needs | python3 only | Node 18+ and an MCP host |
| Install | 2 files + .env | 1 file + host config + restart |
| How the AI calls it | shell: python3 pitchstation.py publish … | tools: pitchstation_publish, _list, _replace, _revoke… |
| Covers | publish, bundles, teams, replace, edit, comments, revoke | all of that plus spaces, invoices, expense packs |
| Works in web chatbots | No — both need a machine that can run commands | No — see §10 for browser-only assistants |
Installing both is fine and quite common: the MCP tools when the host has them, the portable script everywhere else. The skill files know about each other — the portable one defers to the MCP tools when they are present.
Everything lives in one directory. Run this on the machine your assistant works on:
Then the config file beside them — this is the whole configuration surface (see §06):
list printing your shares (or an empty list on a new account) means you are connected. Restart the assistant so it picks up SKILL.md, and it will use the script on its own from then on.
PITCHSTATION_URL at your own deployment — http://localhost:3018 in development. The env var beats the .env file, so you can override it for one command without editing anything.One dependency-free Node file. Download it somewhere stable (not a temp folder — the host launches it by absolute path on every start):
Register it. In Claude Code, one command does it:
Any other MCP host takes the same thing as JSON — use an absolute path, ~ is not expanded here:
Restart the host. Ask your assistant to run pitchstation_list — if it answers, you are connected. Optionally add the matching skill file, which teaches it the conventions the raw tool descriptions do not carry:
ChatGPT cannot use Route A or B: its sandbox has no network access at all, and its browsing tool is GET-only and cannot send an Authorization header. The one way ChatGPT can genuinely publish is a custom GPT with an Action — a one-time, five-minute setup:
ChatGPT → Explore GPTs → Create. Name it something like "PitchStation Publisher".
Configure → Actions → Import from URL → https://www.pitchstation.ai/api/openapi-shares.json. Publish, list, replace, revoke, recipients, and invitations all come with it.
Authentication → API Key → Auth Type Bearer → paste your pst_… token from /tokens.html. The key is stored by the GPT, never shown in chat.
Copy this into the GPT's Instructions box — it carries the house rules the API cannot enforce:
Everyday use is then just: "publish this HTML to PitchStation, link + password", pasted into that GPT. Plain ChatGPT (no Action): it cannot call any API — see §10 for the honest fallback.
There are two layers, and mixing them up is the usual first-run confusion.
Exactly two keys exist. Anything else you have seen in an example is a per-call option, not a config key.
| Key | Default if unset | What it does |
|---|---|---|
PITCHSTATION_URL | https://www.pitchstation.ai | Which deployment to talk to. Change only when self-hosting. |
PITCHSTATION_TOKEN | — (required) | Your pst_ PAT. No default; missing it is a hard error, not a fallback to anonymous. |
Route A reads them from .env next to pitchstation.py; a real environment variable wins over the file. Route B reads them from the MCP host's env block. In every case the key is matched case-insensitively — pitchstation_token and PITCHSTATION_TOKEN both resolve, so a token already exported in your shell under either spelling is picked up as-is.
How a share comes out — who can open it, whether it has a password, when it expires — is decided per call. There is no config file for it, which means if you do not state your defaults, your assistant picks them, and its built-in fallback is not what most people want. Paste this into ~/.claude/CLAUDE.md (or your host's equivalent memory file) and it applies to every session on that machine:
expires_in: a link that dies on its own is one fewer thing to remember to revoke.| Setting | Built-in default | Recommended above |
|---|---|---|
| Access | Route A CLI: account (sign-in required) · MCP: whatever the model chooses | link + password — works for people without accounts |
| Password | none | always, explicit string |
| Expiry | never | 30 days |
| Downloads | off | off — the tracked link stays the record |
| Watermark | off | off, unless the document is sensitive |
| Auto-translate | on for HTML shares (viewer's language picker) | on — but turn it off when the exact wording is the point, e.g. a contract |
Common variations worth naming in your own defaults block instead of re-typing every time: “investor link” = link + password + 30d + no downloads; “team doc” = account mode + your team handle, no expiry; “one-time” = link + password + max-views 1.
Do this once, end to end. It costs nothing and catches a wrong token, a wrong URL, and a misunderstood default in one pass.
On Route B, the same four steps read: pitchstation_list → pitchstation_publish → open in a private window → pitchstation_revoke.
These are the mistakes that actually happen on a first run. They are in §06's defaults block too — repeated here because each one has a cost worth knowing.
| Rule | Why |
|---|---|
| Email ≠ mode | “Share this with bob@x.com” names the reader, not the access mode. Recipient mode emails per-person magic links, which is rarely what was meant. Publish link + password; the user forwards both. |
| Know the password | A password neither the user nor the agent can state is a dead link — the document is published and unreadable. Pass an explicit string; if the server generates one, read it out of the result and say it. |
| Replace, don't re-publish | Re-publishing mints a second URL and splits the analytics across two links, so neither tells the truth about who read what. replace keeps the URL, password, and history. |
| Never resolve an ambiguous team | Two teams can share a handle. A document sent to the wrong team cannot be recalled. The API refuses with a candidate list — surface it and ask. |
| Relay retroactive access | Adding a member to a team grants them everything the team already holds. The API returns the count; say the number out loud — it is correct behaviour and still a surprise. |
| Publishing to a team emails everyone | Not a draft action. Widening an existing share is silent by comparison; publishing is not. |
| 10 MB per payload | Base64 images inside HTML blow through it fast. Strip them or publish as PDF. |
| What you see | What it is |
|---|---|
PITCHSTATION_TOKEN not set | No .env beside pitchstation.py and no PITCHSTATION_TOKEN / pitchstation_token in the environment (any casing counts). Note the .env must sit in the same directory as the script — not the project you are working in. |
| HTTP 401 | Token missing or wrong. Re-copy it — a truncated paste looks identical to a valid one. |
| HTTP 403 “API token revoked” | The token was deleted at /tokens.html. Create a new one and update .env / the MCP env block. |
| HTTP 413 | Over the 10 MB payload cap. Strip embedded base64 images, or publish as PDF. |
HTTP 409 TEAM_AMBIGUOUS | Two of your teams share that handle. Nothing was shared. Retry with @owner/handle. |
400 on --acl / aclUsernames | Allowlists are account-mode only. Drop the flag or switch to --access account. |
| The AI ignores the skill | It only loads at start-up. Restart the assistant. Check the file really is at ~/.claude/skills/<name>/SKILL.md — a nested extra folder is the usual culprit. |
| MCP tools never appear | Relative path in the host config (use an absolute one), Node older than 18, or the host was not restarted. node ~/.pitchstation/pitchstation-mcp-server.js should start and wait, not exit with an error. |
ChatGPT — set up the GPT Action once (§05b) and it publishes natively. Without an Action, plain ChatGPT structurally cannot call any API (no network in its sandbox, no auth headers in its browser tool) — the honest move is a handoff: it saves the HTML as a file and you either drag that into the Share composer, or run the command below with your own token in the environment:
That is the raw-body publish: the file is the body, the controls are query parameters (title, access, expires, password, max_views, watermark, downloads, recipients) — no JSON escaping, so any assistant can prepare it correctly.
An assistant that cannot reach the network itself can still write a page that publishes from your browser. The API answers cross-origin for any request carrying Authorization: Bearer …, so this works from a ChatGPT canvas, an artifact, or an HTML file you open locally:
Why this is safe: the token never leaves your browser except to pitchstation.ai, and cross-origin calls to our API can never carry your session cookies — a page like this can only act with a token you deliberately paste. Revoke any token at /tokens.html if you paste it somewhere you later think better of.
Other browser chatbots — claude.ai, Gemini in a tab — same story: no shell, no filesystem. Use the web app to publish, or run the setup on a machine with a terminal. They can read a share you send them, and review a document that has comments enabled.
Anything that can make an HTTP request — a script, a CI job, an agent framework nobody has heard of — skips all routes and calls the API directly with Authorization: Bearer pst_…. Machine-readable: /llms.txt and the OpenAPI spec at /api/openapi-shares.json; the human reference is /api-docs.html (sign-in required).
Sending a document out for review? That is the other half of the loop and needs no setup on the recipient's side: /ai-review-guide.html teaches their AI to read the document and post anchored comments. Between the two pages, a whole negotiation can run assistant-to-assistant with a human deciding at each end.