<!-- Markdown rendering of ai-skill-setup.html for AI agents.
     The HTML page is the same content with diagrams and styling;
     this view exists so an agent spends its context on the instructions. -->
# Connect your AI to _PitchStation_

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.

**If you are the AI reading this:** fetch `/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:
 · **You can run shell commands** (Claude Code, Codex CLI, Cline…) → §02 → §03 → (§04 or §05) → §06 → §07 in order. Do not skip §06.
 · **You are ChatGPT** → your sandbox has **no network** and your browser tool cannot send auth headers — do not attempt to call the API yourself, and do not loop retrying. Publishing works only through a GPT Action (§05b), or hand off per the next line.
 · **You can do neither** → produce the finished HTML as a downloadable file and give the user two options: drag it into the Share composer at `pitchstation.ai/?share=1`, or run a curl you prepare with a `$PITCHSTATION_TOKEN` placeholder (§10) — never paste their real token into the chat.
 Anything on this page that is a placeholder is written like <THIS> — ask the user, never invent it.

## §01 Hand it to your AI

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.

**Setup prompt**

```
Set yourself up to publish documents to PitchStation.

1. Read https://www.pitchstation.ai/ai-skill-setup.md — that is the setup guide,
   written for you. Follow it in order and do not improvise around it.
2. Install the portable skill (Route A, §04) unless I already run an MCP host —
   ask me if you are unsure which.
3. I will give you the Personal Access Token when you ask for it. Do not guess a
   token, do not skip the .env step, and do not print the token back to me.
4. Apply the default configuration in §06 — write it into my CLAUDE.md so it
   survives this session.
5. Verify with the `list` command in §07, then tell me in two lines what you can
   now do and where my token ended up.
```

Once it is set up, this is the everyday sentence — no flags, no jargon:

**Everyday use**

```
Publish <the file, or "the report you just wrote"> to PitchStation
and give me the link and the password.
```

🔐 **Nothing publishes silently.** Every share is created under your account, appears on your [Links](/shares.html) page, and can be revoked from there. If your assistant publishes something you did not want public, revoke it and the URL dies immediately.

## §02 Get your token — the one step only you can do

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.

**Sign in, or create a free account**

[pitchstation.ai/login.html](/login.html) — “Continue with Google” provisions instantly, or use email + password.

**Open the token page**

[pitchstation.ai/tokens.html](/tokens.html) → **New token**.

**Name it after the machine and the agent**

`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.

**Copy the `pst_…` value immediately**

It is shown **once**. Close the page without copying it and there is no recovery — you delete it and create another. That costs nothing.

**Put it where the agent reads it**

A `.env` file beside the skill (Route A, §04) or the MCP host's env block (Route B, §05). Both are covered below.

⚠️ **A PAT is not scoped — it can do anything you can do**, on your account, under your name. Treat it exactly like a password:

- Never commit it. Never put it in a document, a deck, or a share.

- Never paste it into a public chat window or a bug report.

- Leaked or unsure? Delete it at [/tokens.html](/tokens.html) and make a new one. Nothing else breaks — your existing links keep working; you just re-configure the one agent.

### Handing the token over without it landing in a transcript

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:

**You run this — the token stays off-screen**

```
mkdir -p ~/.claude/skills/share-to-pitchstation
read -rs -p "Paste your PitchStation token: " T && \
printf 'PITCHSTATION_URL=https://www.pitchstation.ai\nPITCHSTATION_TOKEN=%s\n' "$T" \
  > ~/.claude/skills/share-to-pitchstation/.env && \
chmod 600 ~/.claude/skills/share-to-pitchstation/.env && unset T && echo "
.env written"
```

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.

## §03 Pick a route

Two ways to connect. They do the same things; they differ in what has to be installed on the machine.

**Route A — portable skill**

**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.

**Route B — MCP server**

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.

## §04 Route A — the portable skill

Everything lives in one directory. Run this on the machine your assistant works on:

**Install — Route A**

```
mkdir -p ~/.claude/skills/share-to-pitchstation
cd ~/.claude/skills/share-to-pitchstation
curl -fsSL https://www.pitchstation.ai/downloads/share-to-pitchstation-SKILL.md -o SKILL.md
curl -fsSL https://www.pitchstation.ai/downloads/pitchstation.py     -o pitchstation.py
```

Then the config file beside them — this is the whole configuration surface (see §06):

**~/.claude/skills/share-to-pitchstation/.env**

```
PITCHSTATION_URL=https://www.pitchstation.ai
PITCHSTATION_TOKEN=pst_your_token_here
```

**Lock it down & check**

```
chmod 600 ~/.claude/skills/share-to-pitchstation/.env
python3 ~/.claude/skills/share-to-pitchstation/pitchstation.py list
```

`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.

📁 **Self-hosting?** Point `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.

## §05 Route B — the MCP server

One dependency-free Node file. Download it somewhere stable (not a temp folder — the host launches it by absolute path on every start):

**Install — Route B**

```
mkdir -p ~/.pitchstation
curl -fsSL https://www.pitchstation.ai/downloads/pitchstation-mcp-server.js \
  -o ~/.pitchstation/pitchstation-mcp-server.js
```

Register it. In Claude Code, one command does it:

**Register with Claude Code**

```
claude mcp add pitchstation \
  --env PITCHSTATION_URL=https://www.pitchstation.ai \
  --env PITCHSTATION_TOKEN=pst_your_token_here \
  -- node ~/.pitchstation/pitchstation-mcp-server.js
```

Any other MCP host takes the same thing as JSON — use an **absolute** path, `~` is not expanded here:

**MCP host config**

```
{
  "mcpServers": {
    "pitchstation": {
      "command": "node",
      "args": ["/absolute/path/to/pitchstation-mcp-server.js"],
      "env": {
        "PITCHSTATION_URL": "https://www.pitchstation.ai",
        "PITCHSTATION_TOKEN": "pst_your_token_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:

**Optional — the MCP skill file**

```
mkdir -p ~/.claude/skills/publish-to-pitchstation
curl -fsSL https://www.pitchstation.ai/downloads/publish-to-pitchstation-SKILL.md \
  -o ~/.claude/skills/publish-to-pitchstation/SKILL.md
```

## §05 bRoute C — ChatGPT (GPT Actions)

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:

**1 · Create the GPT**

ChatGPT → Explore GPTs → **Create**. Name it something like "PitchStation Publisher".

**2 · Import the Action**

Configure → Actions → **Import from URL** → `https://www.pitchstation.ai/api/openapi-shares.json`. Publish, list, replace, revoke, recipients, and invitations all come with it.

**3 · Set authentication**

Authentication → **API Key** → Auth Type **Bearer** → paste your `pst_…` token from [/tokens.html](/tokens.html). The key is stored by the GPT, never shown in chat.

**4 · Paste the instructions**

Copy this into the GPT's Instructions box — it carries the house rules the API cannot enforce:

**GPT instructions**

```
You publish documents to PitchStation via the imported Action and return
tracked /s/ links.

- Default to access:"account". For external audiences use access:"link" with
  expires_in (e.g. "10d") and password:true, and give the user BOTH the full
  url and the password from the response.
- To UPDATE a published document, call replaceShareContent on its slug —
  NEVER publish a second copy; that mints a new URL and splits analytics.
- aclUsernames only with access:"account"; recipients only with
  access:"recipient" (each address gets its own magic link, emailed
  automatically).
- Never reveal the API key. Never invent slugs — list shares to find one.
- If an upload is over ~10 MB the API returns 413: strip embedded base64
  images first, or publish as PDF.
```

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.

## §06 Default configuration

There are two layers, and mixing them up is the usual first-run confusion.

### Layer 1 — connection settings (a file)

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.

### Layer 2 — publishing defaults (instructions, not a file)

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:

**Paste into ~/.claude/CLAUDE.md**

```
## PitchStation — publishing defaults

If the audience is ME ("share to me"), MY TEAM, or named registered
users: access account (+ teams), NO password — signing in is the gate.
Otherwise — outside audience or none named — every share uses:
- access: link
- password: an explicit memorable string you choose, shaped <topic>-<word>-<MMDD>
- expires_in / --expires: 30d
- downloads: off, watermark: off
- Always report back: the FULL url including ?k=..., the password, and the expiry.

Rules, in order of how often they get broken:
1. An email address in my request does NOT mean recipient mode. Publish a
   link + password and give me both to forward. Use recipient mode only if I
   say "magic link" or "email it to them directly".
2. Never hand me a share whose password you do not know. Pass an explicit
   string; if you ever let the server generate one, read it out of the result
   and tell me — a password neither of us has means a dead link.
3. Updating something already shared: use replace (same URL, same password,
   same analytics). Never publish a second copy. Revoke only when the link
   should genuinely die.
4. Ambiguous team name: show me the candidates and stop. Never pick one.
5. Ask before publishing anything I did not explicitly ask you to publish.
```

⚠️ **Why default to a password.** A link-mode share with no password is readable by anyone who ever receives the URL — forwarded, pasted into a group chat, quoted in an email thread. The password costs one extra line in the message you send and turns “anyone with the URL” into “the person I gave it to”. Its natural pair is `expires_in`: a link that dies on its own is one fewer thing to remember to revoke.

### The defaults you are overriding

| 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.

## §07 Verify it actually works

Do this once, end to end. It costs nothing and catches a wrong token, a wrong URL, and a misunderstood default in one pass.

**Smoke test — Route A**

```
cd ~/.claude/skills/share-to-pitchstation
echo '<h1>Hello from PitchStation</h1>' > /tmp/ps-test.html

# 1. Connection: prints your shares, or an empty list. Anything else is §09.
python3 pitchstation.py list

# 2. Publish with the house defaults:
python3 pitchstation.py publish /tmp/ps-test.html \
  --title "Setup test" --access link --password "setup-test-0101" --expires 24h

# 3. Open the printed URL in a private window: it must ASK FOR THE PASSWORD.
#    If it opens straight to the document, the password did not apply — stop
#    and re-read the command before you publish anything real.

# 4. Clean up:
python3 pitchstation.py revoke <slug-from-step-2>
```

On Route B, the same four steps read: `pitchstation_list` → `pitchstation_publish` → open in a private window → `pitchstation_revoke`.

✅ **Step 3 is the one that matters.** Everything else fails loudly. A share that quietly published without the protection you asked for is the only failure mode that looks like success — check it once, on a document that does not matter.

## §08 House rules for the AI

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. |

## §09 Troubleshooting

| 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](/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//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. |

## §10 Other assistants, and no assistant at all

**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](/?share=1), or run the command below with your own token in the environment:

**Publish any HTML file — one command**

```
curl -X POST "https://www.pitchstation.ai/api/share?title=My+Report&access=link&expires=10d&password=auto" \
  -H "Authorization: Bearer $PITCHSTATION_TOKEN" \
  -H "Content-Type: text/html" --data-binary @report.html
```

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.

### Route D — the publish page (works from a ChatGPT canvas)

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:

**Ask any assistant for this**

```
Write a single self-contained HTML page that publishes the document below to
PitchStation from my browser:
- a password-type input for my PitchStation token (never hardcode a token),
- a Publish button that POSTs to https://www.pitchstation.ai/api/share with
  header "Authorization: Bearer <token>" and JSON body
  { htmlContent, title, access: "account" },
- show the returned url as a clickable link, and clear the token field after.
```

**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](/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](/llms.txt) and the OpenAPI spec at [/api/openapi-shares.json](/api/openapi-shares.json); the human reference is [/api-docs.html](/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](/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.
