commentdraft
A command line tool that triages comments on your own posts into reply, skip, or escalate, drafts the reply from one source document, and sends nothing a person has not approved one keystroke at a time.
Eight platform connection guides written from primary sources, and a publish path with no way to approve in bulk: one keystroke, one reply
The problem
I was building comment handling for paying clients, and the failure that mattered was never a crash. It was a public reply under someone's own post quoting a price that had changed, or answering a question the source material never covered. That lands on the client, in front of their audience, and nothing raises an exception. Sitting on top of it, every platform has rules about replying with software, and the documentation for those rules contradicts itself often enough that reading it properly is part of the work rather than a preliminary to it. What the job needed was a tool that could refuse: refuse to answer what it cannot source, refuse to send what a person has not read, and refuse to claim a reach it does not have.
Architecture and approach
One model call per comment, against a prefix assembled once and kept byte-identical for the whole run so the provider serves it from cache. The prefix carries the operator's voice rules, their worked examples, the output contract, and the entire source document, which the prompt instructs it to treat as the only thing it may state as fact. Every comment comes back as reply, skip, or escalate with a one-line reason the operator can disagree with, and the run writes a CSV and a review page. Publishing is a separate command that shows one comment and one draft and waits for a key. Reading a platform and writing to it are separate config tables holding separate credentials, so the starting posture holds no write credential and cannot post at all. The interesting parts of this design are all places where the obvious answer turned out to be wrong.

Key technical decisions
The approval gate, and the keystrokes that walked straight through it
The first version read a key before each send, and I took that to be the gate. It was not. Five characters pasted into the terminal before the reply had rendered approved five replies nobody had read: the keys were already sitting in the input queue, and the read consumed them in order. A keystroke buffered before the content appears is not consent, it is timing. The gate now discards the terminal's input queue at the moment the prompt becomes readable and reads exactly one byte after that, and a test drives a real pty to prove the pasted case approves nothing. The rest of the property is structural rather than configurable, because a setting that exists is a setting somebody eventually turns on: there is no --yes and no --all, the config vocabulary is a frozen allowlist so a key of any name that could stand in for a keystroke fails the build until somebody writes it down, and an AST test fails if the send is named anywhere outside the one branch a keystroke reaches. YouTube's Developer Policies require express consent before each action, which makes this a compliance requirement on that platform rather than a product opinion.
A safety claim I had to retire, and the grep that proved nothing
For a while the strongest line in the README was that no HTTP client existed anywhere in the package. It was true, it was checkable in one command, and it was about to stop being either the moment a connector shipped. It was also being checked badly: the command ran with --exclude=approve.py --exclude-dir=platforms, which excluded the only two paths a send would ever live in, so it proved the send was not somewhere it had never been. I replaced the claim rather than the connector. What survives a connector is that nothing reaches a platform a person did not read and approve, and that is what is enforced now, by a marker on the single send statement and an AST walk that fails the build if the send is referenced anywhere else, or referenced without being called, which is how an alias would otherwise carry it into a loop. A claim that is about to become false is worse than no claim, because people plan around it.
A contradiction in Meta's documentation, answered with a runtime check
Meta documents one call as both "reply to this comment" and "edit this comment". Both readings are published, they cannot both be right, and the wrong one means every reply silently overwrites the customer's own words. I could not settle it from the documentation and I was not going to find out on somebody's Page, so the connector proves the outcome instead of assuming it: it compares the id the POST returned against the id it posted to, then reads the reply back and confirms it carries the right parent. A read-back that cannot be performed ends the whole run rather than the row, because a write path that edits comments will edit the next one too. The comparison allows for both of Facebook's spellings of a comment id, because the two ways of being wrong are not symmetric: a false positive halts a run over an overwrite that did not happen, which is recoverable, and a false negative destroys comments quietly.
The cheap model that cost twenty-seven times more
I assumed the cheaper published rate meant the cheaper run. In the bake-off it cost about 27 times more per comment than the default, where the sticker prices alone predicted a gap near 8. The difference was the prompt cache. The prefix dominates the bill on every call here, and the default's route billed it at a reduced cached rate on 28 of its 29 calls while neither of the others was served from a cache at all. That measurement is why the prefix is assembled once and kept byte-identical, and why anything that varies per row, a timestamp, a row counter, a shuffled example order, costs every cache hit in the run rather than a little latency. It is also the reason there is no retrieval: while the source document fits the window there is nothing for a retriever to miss, and a retriever that misses is precisely the failure this tool exists to prevent. One run, one example, one gateway, one day, published with the command that reproduces it and the caveat that says to read it narrowly.
Connecting it to a social platform
Reading and replying through a platform's own API is where most of the real work turned out to be, and almost none of it is code. Each of the eight platforms was researched from its own primary sources, with every endpoint, scope string, quota number and policy clause carrying the URL it came from and the date it was read, and each guide states near the top which of its claims were read twice and which cannot be settled without credentials. What follows is the short version: what stands between an operator and a first working call on each one.
The guides document how to connect. The code connects to one of them, Facebook Pages, built and tested against fakes and never run against a live Page. Nobody has made a call on any of the eight, the one with a connector included.
- connector ships
- no permission needed
- someone has to say yes
Nothing to clear. An operator who owns the Page and makes their own app needs no App Review and no Business Verification.
Nothing, for your own professional account at Standard Access. The comments webhook is the exception and needs Advanced Access.
Nothing, for the single-operator path. Anything beyond it needs App Review, which is not reachable until a Business has completed Business Verification.
A 10,000 unit daily quota, so 200 replies a day at the ceiling, 192 if a reply costs 52. An app left in Testing gets refresh tokens that expire in 7 days.
Access is granted rather than taken. The Responsible Builder Policy requires approval before the first call, through a support ticket.
Prior written approval from X before replies written by software are deployed. No published turnaround, no queue position, no appeal, and no self-serve path.
The developer of record must be a company with a matching domain. A solo consultant cannot register.
A two-stage partner review, and above it the Member Data question. The Marketing API Terms prohibit exporting Member Data to a third party and define it to include a member's comment; whether a model gateway is a third party or the operator's own service provider is unsettled, and passing the review would not answer it.
The first four need no permission from anybody. The last four each need a human at the platform to say yes, and three of the four publish no idea of how long that takes.
All eight guides, and what each one costsResults
Published on PyPI under Apache-2.0, with a worked example in the repository, a fictional field guide with its knowledge file, its voice file and a small comment file, so the whole loop runs from a clone. The claims are built to be checked rather than believed: the test suite runs offline with no API key, the review page in the write-up is rendered by the tool from its own run rather than mocked up, and every figure in the documentation resolves to a command a reader can run. Eight platform connection guides carry the primary-source URL behind each endpoint, scope and quota, and the date each was read. What it does not do is stated in the same places: one connector exists, for Facebook Pages, and it has never been run against a live Page.