AI and prompts

The choice to make first

Route Cost Needs an AI key? Scope
GET /ai/prompts/suggestions/ free no read
POST /ai/prompts/rewrite/ free no read
POST /ai/suggestions/ one provider call yes write
POST /ai/rewrite/ one provider call yes write

The prompt routes return exactly what we would have sent to the model, with your real posts already interpolated. If the caller is itself an AI agent, it should run that prompt and skip the paid route entirely.

Grounding: the context field

Every route on this page takes an optional context string, up to 4,000 characters. Send it whenever you can.

Without it, the prompt contains a topic string and the account's own post history, and nothing else. Ask for posts about something neither one describes and the model does not stop. It invents the missing specifics and writes them as fact. This is not hypothetical: an account whose history was about an unrelated side project, asked for posts about a launch, produced "Just shipped the first version! Time to see how the App Store review goes" for a product with no app and no review.

context is where the real facts go, and the prompt tells the model to take every specific from there or from the posts, and to invent nothing else:

{"topic": "the launch",
 "context": "Shipped a public REST API (22 routes) and an MCP server on npm on 21 Aug. No mobile app."}

Longer input is clamped rather than rejected, and the response says so in warnings with a parameter_clamped entry. Nothing here is stored; context is an input, not part of the account's history.

GET /ai/prompts/suggestions/

Optional ?topic=shipping in public to steer it, ?count= for how many ideas (1–20, default 5), and ?context= for ground truth.

{"status": "success", "data": {
  "handle": "alice.bsky.social",
  "post_count": 128,
  "system": "...",
  "prompt": "...",
  "combined": "..."
}}

Use system and prompt separately, or combined if you have one input box. The prompt asks for a single JSON object containing post_suggestions (a list of strings) plus seven analysis keys.

POST /ai/prompts/suggestions/

The same route and the same response, with {"topic", "count", "context"} in the body instead of the query string.

Prefer it whenever you are sending context. Ground truth tends to run to paragraphs, and a query string is the wrong place for that: it lands in access logs and proxy caches. It is the same reason the rewrite prompt has always been POST-only.

POST /ai/prompts/rewrite/

{"text": "my rambling draft", "instructions": "punchier, under 300 characters",
 "context": "optional ground truth"}

Returns the same system / prompt / combined trio.

Connectors: grounding a draft in a source instead of typed context

GET /connectors/ lists the sources you attached in Settings -- a GitHub repo (commits and merged PRs from chosen repos) or a website/pasted notes -- each with its own stored instruction saying what to look for:

{"status": "success", "data": {"connectors": [
  {"id": 3, "kind": "github", "name": "kunopilot repo",
   "instruction": "Look for shipped features and bug fixes, ignore refactors.",
   "cursor_at": "2026-09-01T09:00:00Z", "last_fetched_at": "2026-09-03T14:02:11Z",
   "last_error": ""}
]}}

Pass up to 2 of their ids as connectors (a list, or a comma-separated string) on any of the four AI routes on this page, alongside an optional window: today, 7d (default), or since_last. The fetched material grounds the prompt exactly the way context does -- it just comes from a source rather than from what you typed:

{"topic": "this week in public", "connectors": [3], "window": "7d"}

Fetching a connector is free, but does require the account to have some credit balance or its own AI key, and is capped to a limited number of runs per day. A since_last window only advances that connector's cursor when POST /ai/suggestions/ actually runs a generation -- never on either prompt-only route, so previewing what a connector would contribute can never cause a later real run to miss material.

POST /ai/suggestions/

Spends one call on your own key. Optional body {"topic": "...", "count": 10, "context": "..."}.

{"status": "success", "data": {
  "handle": "alice.bsky.social",
  "posts_analyzed": 128,
  "suggestions": ["...", "..."],
  "insights": {"tone_style": {"...": "..."}}
}}

With no post history it returns an empty list and a message telling you to sync first, rather than an error.

GET /ai/suggestions/ and DELETE /ai/suggestions/{id}/

Listing stored suggestions is free and works with a read key. Deleting needs write scope, and only ever touches your own account's suggestions.

POST /ai/rewrite/

{"text": "my rambling draft", "instructions": "punchier"}

Returns {"text": "...", "original": "..."}.

This is POST, not GET, on purpose. The internal endpoint the web app uses puts draft text in the query string, which means post content lands in access logs. The public API does not repeat that.

When there is no AI key

Both paid routes return 403 no_ai_key with a prompt_url pointing at the free equivalent, rather than dead-ending. Any agent worth using should follow it.