# polishmyturd — agent protocol > An AI agent drafts text (email reply, Basecamp comment, ad copy), pushes it > here, and hands the human ONE link. The human edits in the browser and presses > Copy. The agent later pulls the human's final version, diffs it against its own > draft, and learns. Bodies are native markdown / plain text — no HTML, no > rich-text format, ever. ## Do not drive this app with a browser Never use browser automation (Chrome, Playwright, Puppeteer, etc.) against this app to push or fetch a draft — not even the doc edit page. That page is for the human only. Agents talk to the `.json` endpoints below with plain HTTP (curl or equivalent). The human is the only one who ever opens a URL in a browser here. ## The whole flow 0. Before drafting anything, `GET /lessons.json` and read every entry. Treat `voice_pattern` bodies as literal few-shot examples — carry them into your own context as-is, don't just summarize them as facts. Treat `banned_phrase` and `process` bodies as plain rules to follow. (Examples beat written rules for capturing implicit voice/style — that's why `voice_pattern` is stored as before/after pairs, not descriptions.) 1. `POST /docs.json` with `{title, kind, body}` → response includes `url`. PREFERRED: open that URL in the human's browser yourself (`open ` on macOS, `xdg-open` on Linux, `start` on Windows) — don't just print it. `kind` is a display label only, drives no behavior: email, basecamp, ad, landing, blog, or other (default). 2. The human edits at `url` (autosaved) and presses **Copy** or **Send to agent** → doc status becomes `done` (editing alone makes it `edited`). There is no push notification — nothing tells you when this happens unless you go looking. Two ways to find out: a. WATCH FOR IT (preferred — don't make the human come back and tell you): right after step 1, start a background loop that polls `GET /docs/TOKEN.json` every 15-30s and checks `status`. The moment it's no longer `drafted`, that's your signal — stop polling and go to step 4. In Claude Code, use a background/monitoring tool if you have one; otherwise a plain shell loop works everywhere: while true; do doc_status=$(curl -s https://www.polishmyturd.com/docs/TOKEN.json | python3 -c "import json,sys;print(json.load(sys.stdin)['status'])") [ "$doc_status" != "drafted" ] && echo "status is now: $doc_status" && break sleep 20 done Don't name that variable `status` in zsh — it's a reserved read-only variable (`$status` = `$?`) and the loop dies with "read-only variable: status". Use `doc_status` as shown, or `st`. While you're watching, ALSO poll `GET /docs/TOKEN/asks.json` the same way and answer anything unanswered as it appears — don't wait for the doc itself to be done first. See "Ask AI" below. Doug can keep editing after Copy/Send to agent — that's normal, not a bug. `needs_review` re-includes a doc if it was edited again after your last `reviewed: true`, so if you're still around, harvest once, `PATCH reviewed: true`, then check `needs_review` again a minute later before you fully stop watching — don't assume one pass is final. b. Or just check `GET /docs.json?needs_review=1` (step 3) at the start of a later session, in case nobody was watching. 3. `GET /docs.json?needs_review=1` lists docs the human touched that no agent has harvested yet — the catch-all for anything a watcher (2a) missed. 4. For each: `GET /docs/:token.json` → `draft` (agent's original), `final` (human's version), and `feedback` (an optional free-text note the human left — read it, it often says WHY, not just what changed). Diff draft vs. final. Extract lessons and `POST` each one to `/lessons.json` — see below. 5. `PATCH /docs/:token.json` with `{"reviewed": true}` to mark it harvested. ## API reference Auth: possession of a doc URL is the capability — read and write need nothing else. Only `DELETE` (docs and lessons) needs the one shared bearer token, so a leaked URL can't wipe anything: `-H "Authorization: Bearer $PMT_API_TOKEN"`. Get it with `heroku config:get PMT_API_TOKEN -a polishmyturd` (the Heroku CLI is logged in on Doug's machines) or from `$PMT_API_TOKEN` if the shell has it. A missing or wrong token gets a 401. Create: curl -s -X POST https://www.polishmyturd.com/docs.json \ -H "Content-Type: application/json" \ -d '{"title": "Reply to Phaedon", "kind": "email", "body": "Hey Phaedon,\n\nGood catch, thanks. ..."}' => 201 {"token":"abc...","title":"Reply to Phaedon","kind":"email","status":"drafted", "url":"https://www.polishmyturd.com/docs/abc...","draft":"Hey Phaedon,...","final":null, "current":"Hey Phaedon,...","revision":0,"feedback":null,"notes":null, "lint":[{"category":"passive","text":"...","occurrence":1}, ...]} `notes` is yours to write, never Doug's — a place for agent-to-human context ("build this as a button in the Plunk editor") that must NEVER end up in `draft`/`final`/`current`. It renders in its own box, outside the doc, and Copy never touches it — unlike putting a build note inline in the body, which is one Copy away from shipping to every recipient. Set it at create time (`{title, kind, body, notes}`) or later with a plain PATCH `{"notes": "..."}`. `lint` comes back on every create/show/update response — a list of flagged spans in `current` (each with `category` and the exact `text` matched). Categories: `hard` / `very_hard` (readability), `passive` (passive voice), `weakener` (hedge words), `ai_ism` (AI writing tells like the em dash). Before handing the link to Doug, PATCH the same doc's body to fix anything lint flags — don't just hand over flagged text and hope he doesn't notice. Read one (draft + final bodies included): curl -s https://www.polishmyturd.com/docs/TOKEN.json List (all docs, newest first; add ?needs_review=1 for unharvested edits): curl -s "https://www.polishmyturd.com/docs.json?needs_review=1" Revise your own draft (fixing lint, applying feedback, your own second thoughts) — PATCH the SAME doc's body. While the doc is still `drafted` this updates your draft in place; do NOT post a new doc for a revision. Always send back the `revision` you last read — see conflicts, below: curl -s -X PATCH https://www.polishmyturd.com/docs/TOKEN.json \ -H "Content-Type: application/json" -d '{"body": "revised text", "revision": 0}' Rename a doc (title and/or kind, independent of any body edit): curl -s -X PATCH https://www.polishmyturd.com/docs/TOKEN.json \ -H "Content-Type: application/json" -d '{"title": "New title"}' Mark harvested: curl -s -X PATCH https://www.polishmyturd.com/docs/TOKEN.json \ -H "Content-Type: application/json" -d '{"reviewed": true}' Delete an abandoned draft — only works while `status` is still `drafted` and no human has edited it yet (a 422 otherwise: the human's edit is not yours to throw away): curl -s -X DELETE https://www.polishmyturd.com/docs/TOKEN.json \ -H "Authorization: Bearer $PMT_API_TOKEN" Upload an image (multipart, one file per call) and get a hosted URL back. Drop that URL into the markdown body as a normal `![alt](url)` — bodies stay pure markdown, images are never inlined as base64: curl -s -X POST https://www.polishmyturd.com/docs/TOKEN/images.json -F "file=@/local/path.png" => 201 {"url":"https://www.polishmyturd.com/rails/active_storage/blobs/redirect/.../path.png", "filename":"path.png","width":1200,"height":800} `width`/`height` are the image's REAL pixel dimensions — check them before using the image, not after. There is no way to resize an image once it's in the doc: plain markdown (`![alt](url)`) has no width field, full stop — checked directly against the CommonMark spec and against Lexxy's own source, neither has one, and Basecamp's own apps only get around this by storing raw HTML instead of markdown, which this app deliberately does not do. If an image is the wrong size for where it's going (e.g. too big for an email), resize it yourself before uploading — don't upload first and hope. Read all accumulated lessons (do this BEFORE drafting, not after): curl -s https://www.polishmyturd.com/lessons.json => [{"id":1,"category":"voice_pattern","body":"\ndraft: ...\nfinal: ...\n","source_doc_token":"abc...","created_at":"..."}, ...] Add a lesson after harvesting a diff. `category` is one of `banned_phrase`, `voice_pattern`, `process`. For `voice_pattern`, write `body` as a ready-to-use `` block with the actual draft/final excerpts — not a description of the pattern: curl -s -X POST https://www.polishmyturd.com/lessons.json \ -H "Content-Type: application/json" \ -d '{"category": "voice_pattern", "body": "\ndraft: Does it drain my battery?\nfinal: Will this drain my phone'"'"'s battery?\n", "source_doc_token": "abc..."}' curl -s -X POST https://www.polishmyturd.com/lessons.json \ -H "Content-Type: application/json" \ -d '{"category": "process", "body": "Verify every quote against a real source before using it — annotate the verification in the doc."}' Delete a lesson that turned out wrong (a bad example actively teaches the wrong voice, so don't leave it lying around): curl -s -X DELETE https://www.polishmyturd.com/lessons/ID.json \ -H "Authorization: Bearer $PMT_API_TOKEN" ## Ask AI (agent-mediated, not an in-app LLM call) Doug can select text in the editor and ask a question about it — "give me 3 better subject lines," "why is this weak?" There is deliberately no LLM call inside the app for this (see the "no rich-text format, ever" line at the top — same principle: the app stores state, agents supply the intelligence). It's just a row you're expected to answer while you're already watching the doc. List asks for a doc (poll this in your watcher, step 2a): curl -s https://www.polishmyturd.com/docs/TOKEN/asks.json => [{"id":1,"selected_text":"We made mileage tracking automatic (no start button)", "question":"give me 3 better subject lines","answer":null,"answered_at":null,"created_at":"..."}] Answer one — plain text, not markdown (it renders as-is in a small box, not through the doc's markdown pipeline): curl -s -X PATCH https://www.polishmyturd.com/docs/TOKEN/asks/ID.json \ -H "Content-Type: application/json" -d '{"answer": "Try: Your phone tracks it. You don'"'"'t."}' `selected_text` may be empty if Doug clicked Ask without selecting anything — answer from the whole doc's context in that case. Answer promptly; Doug is looking at this waiting for a reply, unlike the main doc-review loop, which he may come back to hours later. ## Semantics and footguns - The URL is the capability: anyone with the token can read and write. No login, no API key. Do not post tokens anywhere public. - `body` is stored verbatim. Send markdown, get markdown back. Escape nothing except normal JSON string escaping (`\n` for newlines). - ONE deliverable = ONE doc, forever. Revising after feedback, lint, or your own second thoughts means PATCHing the SAME doc's body (send `revision`) — never posting a new doc for a revision. A new doc is only for a genuinely different deliverable (a different email, a different landing page). Posting a new doc per revision floods the list with near-duplicates and splits Doug's edits across copies instead of one clean diff. - `draft` is the agent's original; `final` is the human's working copy; `current` is whichever one is authoritative right now (`final` once it exists, else `draft`) — read `current` if you just need the latest text and don't care which author wrote it. - `revision` is a counter on `current` (your draft while `drafted`, Doug's copy once he's edited). If you PATCH `body` (or `html`), send back the `revision` you last read. If someone else saved in between, you get `409` with `{"errors": [...], "doc": {...current state...}}` instead of silently overwriting it — re-fetch, re-apply your change on top, and retry with the new `revision`. Omitting `revision` skips the check entirely, so only do that if you're certain nobody else is touching this doc. - While `status` is `drafted`, a `body` PATCH updates YOUR draft in place — that's the normal way to revise. Once Doug has edited (`edited`/`done`), a `body` PATCH still works but now targets HIS copy — at that point stop patching and talk to him instead; further silent agent edits on top of his work are how you lose track of what he actually changed. - A doc can be `done` (copied) with `final` null — the human shipped the draft unchanged. That is signal too: the draft was good. - `status`: drafted → edited (human changed something) → done (human pressed **Copy** or **Send to agent**). `copied_at` gets set either way — don't assume it means the text was actually pasted somewhere; "Send to agent" is for docs with nowhere to paste (a review, a plan, feedback with no shippable text) and never touches the clipboard. - `needs_review=1` returns edited/done docs that are unreviewed OR were edited again after the last `reviewed: true` — it's edit-aware, not just a one-time flag. Mark reviewed as often as you like; a further edit un-marks it automatically. - Errors return `{"errors": [...]}` with 422; a missing token 404s; a stale `revision` 409s (see above). ## Learning loop convention (for any agent, any session, any machine) After harvesting a diff (and any `feedback` text), `POST` durable lessons to `/lessons.json` — NOT to your own memory, notes, or CLAUDE.md. Nothing you write to your own memory is visible to a different session, a different machine, or a different agent — the whole point of this app is that the lesson lives in one place every future session can pull from, regardless of who or what connects next. An unchanged shipped draft counts as a positive example too — worth a `voice_pattern` lesson describing what it did right.