Drive Writing Prompt Generator from your own code
Everything the web app does goes through one public surface. The base URL is
https://api.skillsafe.ai/v1/app-api, every request carries
Authorization: Bearer <token> and
Content-Type: application/json, and every response is a JSON envelope:
{"ok":true,"data":{…}} on success,
{"ok":false,"error":{"code":"…","message":"…"}} on failure.
Read error.code, not the HTTP status alone.
Get a token on the token page — it reads the one this
browser already holds, so you never need the developer console. A guest token is enough
for /me and /estimate; writing a prompt is metered and needs a
personal token.
Errors
| Code | Meaning | What to do |
|---|---|---|
UNAUTHORIZED | Missing, expired or wrong-app token. | Mint a guest token or sign in again. A cold 401 from /me before any token exists is normal, not a defect. |
INSUFFICIENT_CREDITS | Balance below min_credits. | Top up. Call /estimate first — it is free and tells you the hold. |
VALIDATION_ERROR | The input object failed validation. | Check error.details. The run body is the input object itself — do not wrap it in {"input": …}. |
RATE_LIMITED | Too many requests. | Back off and retry. Do not tight-loop. |
JOB_FAILED | The run started and did not complete. | Retry with the same Idempotency-Key so a partial charge is not doubled. |
The input object
Writing Prompt Generator is a single-contract app. There is no task field and no lane
router; one contract handles both a first draw and a second run pushed away from it,
distinguished by shape.
| Field | Type | Required | What it is |
|---|---|---|---|
shape | string | yes | "draft" or "push". |
brief | object | yes | The drawn coordinate. See below. |
brief.coordinate | object | yes | Nine axis ids. Echoed back as coordinate_used. |
brief.engine | array | yes | {axis,label,value} for the situation elements. Each must be present in the finished prompt. |
brief.surface | array | yes | Same shape, for how and where it is told. |
brief.left_open | array | yes | Elements the model is told about and must not state anywhere in the reply. An element here is not repeated in engine or surface. |
brief.pushed_off | array | yes | Corrections the sampler made within this draw. Informational; may be empty. |
brief.still_near_archetype | string or null | yes | A tired premise this coordinate is still within reach of, or null. |
genre, genre_pull, tired_in_genre | string, string, array | yes | Genre id, what the genre does to a situation, and the moves already worn out inside it. |
form, length_note | string, string | yes | flash, short, novella or novel, and what that length asks of a premise. |
tone | string | yes | A register, or "any". |
steer, steer_clipped | string, boolean | yes | The writer's own words about what they want it near. Often empty. |
boundary_acts | array | yes | The acts the app refuses. |
prior, moved_by, avoid_note | object, array, string | push only | The prompt just read, the engine elements that changed relative to it, and the writer's own note about what not to repeat. |
The output contract
One JSON object, no fence, no prose around it. Keys: title,
prompt, who, want, against,
constraint, cost, pressure,
withheld (array), openings (array of
{frame,line}), complications (array), avoid
(array), why_it_holds, form_note,
coordinate_used (object), and on a push also
moved_from.
Note the deliberate absence: the contract carries no length guidance for any field. A per-field sentence count writes the cadence as well as the contract, and every prompt then arrives the same size regardless of what it is about.
Step 1 — a client helper
Four calls, one envelope, one auth header. Everything below assumes this helper exists. Never hard-code a live token into a file you commit.
# Every call carries the same two headers. Keep the token out of your shell history:
# read it from a file, or paste it into a variable in an interactive shell only.
TOKEN=$(cat ~/.writing-prompt-generator-token)
BASE="https://api.skillsafe.ai/v1/app-api"
api() { # api <method> <path> [json-body]
if [ -n "$3" ]; then
curl -s -X "$1" "$BASE$2" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "$3"
else
curl -s "$BASE$2" -H "Authorization: Bearer $TOKEN"
fi
}
import json, time, urllib.request
TOKEN = "YOUR_TOKEN"
BASE = "https://api.skillsafe.ai/v1/app-api"
def api(method, path, body=None, extra_headers=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(BASE + path, data=data, method=method)
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Content-Type", "application/json")
for k, v in (extra_headers or {}).items():
req.add_header(k, v)
with urllib.request.urlopen(req, timeout=120) as r:
env = json.loads(r.read())
if not env.get("ok"):
raise RuntimeError(env["error"]["code"] + ": " + env["error"]["message"])
return env["data"]
const TOKEN = "YOUR_TOKEN";
const BASE = "https://api.skillsafe.ai/v1/app-api";
async function api(method, path, body, extraHeaders) {
const res = await fetch(BASE + path, {
method,
headers: {
"Authorization": "Bearer " + TOKEN,
"Content-Type": "application/json",
...(extraHeaders || {})
},
body: body === undefined ? undefined : JSON.stringify(body)
});
const env = await res.json();
if (!env.ok) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import (
"bytes"
"encoding/json"
"errors"
"io"
"net/http"
)
const token = "YOUR_TOKEN"
const base = "https://api.skillsafe.ai/v1/app-api"
type envelope struct {
OK bool `json:"ok"`
Data json.RawMessage `json:"data"`
Error *struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
func api(method, path string, body any, extra map[string]string) (json.RawMessage, error) {
var rdr io.Reader
if body != nil {
b, _ := json.Marshal(body)
rdr = bytes.NewReader(b)
}
req, _ := http.NewRequest(method, base+path, rdr)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
for k, v := range extra {
req.Header.Set(k, v)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var env envelope
if err := json.NewDecoder(res.Body).Decode(&env); err != nil {
return nil, err
}
if !env.OK {
return nil, errors.New(env.Error.Code + ": " + env.Error.Message)
}
return env.Data, nil
}
import java.net.URI;
import java.net.http.*;
import java.util.Map;
public class WritingPromptGenerator {
static final String TOKEN = "YOUR_TOKEN";
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final HttpClient CLIENT = HttpClient.newHttpClient();
static String api(String method, String path, String jsonBody, Map<String, String> extra)
throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json");
extra.forEach(b::header);
b.method(method, jsonBody == null
? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(jsonBody));
HttpResponse<String> res = CLIENT.send(b.build(), HttpResponse.BodyHandlers.ofString());
// Parse with the JSON library of your choice and read env.ok before env.data.
return res.body();
}
}
require "json"
require "net/http"
require "uri"
TOKEN = "YOUR_TOKEN"
BASE = "https://api.skillsafe.ai/v1/app-api"
def api(method, path, body = nil, extra = {})
uri = URI(BASE + path)
klass = { "GET" => Net::HTTP::Get, "POST" => Net::HTTP::Post }.fetch(method)
req = klass.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
extra.each { |k, v| req[k] = v }
req.body = JSON.generate(body) unless body.nil?
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
env = JSON.parse(res.body)
raise "#{env['error']['code']}: #{env['error']['message']}" unless env["ok"]
env["data"]
end
<?php
const TOKEN = "YOUR_TOKEN";
const BASE = "https://api.skillsafe.ai/v1/app-api";
function api(string $method, string $path, $body = null, array $extra = []) {
$headers = array_merge([
"Authorization: Bearer " . TOKEN,
"Content-Type: application/json",
], $extra);
$ch = curl_init(BASE . $path);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
$env = json_decode(curl_exec($ch), true);
curl_close($ch);
if (empty($env["ok"])) {
throw new RuntimeException($env["error"]["code"] . ": " . $env["error"]["message"]);
}
return $env["data"];
}
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
const string Token = "YOUR_TOKEN";
const string Base = "https://api.skillsafe.ai/v1/app-api";
var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Token);
async Task<JsonElement> Api(HttpMethod method, string path, object? body = null,
IDictionary<string, string>? extra = null)
{
var req = new HttpRequestMessage(method, Base + path);
if (body is not null)
req.Content = new StringContent(JsonSerializer.Serialize(body),
Encoding.UTF8, "application/json");
if (extra is not null)
foreach (var kv in extra) req.Headers.Add(kv.Key, kv.Value);
var res = await http.SendAsync(req);
var env = JsonDocument.Parse(await res.Content.ReadAsStringAsync()).RootElement;
if (!env.GetProperty("ok").GetBoolean())
{
var e = env.GetProperty("error");
throw new Exception($"{e.GetProperty("code")}: {e.GetProperty("message")}");
}
return env.GetProperty("data");
}
Step 2 — who you are
/me returns exactly three fields: subject_type,
subject_id and credits. There is no email and no display name
— the signed-in test is subject_type === "user".
api GET /me
# {"ok":true,"data":{"subject_type":"user","subject_id":"usr_...","credits":48210}}
# Three fields only. There is no email and no name.
# The signed-in test is subject_type == "user"; a guest token returns "guest".
me = api("GET", "/me")
print(me["subject_type"], me["credits"])
# /me returns exactly three fields: subject_type, subject_id, credits.
signed_in = me["subject_type"] == "user"
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
// Exactly three fields come back. Do not look for an email or a display name.
const signedIn = me.subject_type === "user";
raw, err := api("GET", "/me", nil, nil)
if err != nil {
panic(err)
}
var me struct {
SubjectType string `json:"subject_type"`
SubjectID string `json:"subject_id"`
Credits int `json:"credits"`
}
json.Unmarshal(raw, &me)
signedIn := me.SubjectType == "user"
String body = api("GET", "/me", null, Map.of());
// data has exactly three keys: subject_type, subject_id, credits.
// boolean signedIn = "user".equals(subjectType);
System.out.println(body);
me = api("GET", "/me")
puts "#{me['subject_type']} #{me['credits']}"
signed_in = me["subject_type"] == "user"
<?php
$me = api("GET", "/me");
printf("%s %d\n", $me["subject_type"], $me["credits"]);
$signedIn = $me["subject_type"] === "user";
var me = await Api(HttpMethod.Get, "/me");
Console.WriteLine($"{me.GetProperty("subject_type")} {me.GetProperty("credits")}");
bool signedIn = me.GetProperty("subject_type").GetString() == "user";
Step 3 — build the brief
The coordinate is drawn client-side; there is no endpoint that draws one for you. Mirror
the sampler in /draw.js, or hand over a coordinate you chose. This is the
complete input object for a draft:
# The coordinate is built client-side. There is no server endpoint that draws one, # so an API client either mirrors the sampler or hands over a coordinate directly. # Save the object below as brief.json and reuse it: cat brief.json | jq -c . > /tmp/pw-input.json api POST /estimate "$(cat /tmp/pw-input.json)" # Field notes: # brief.engine the situation. Each entry MUST be present in the finished prompt. # brief.surface how and where it is told. # brief.left_open the model is told these and must NOT state them. # coordinate machine ids, echoed back as coordinate_used for reconciliation.
INPUT = '{\n "shape": "draft",\n "brief": {\n "seed": "any-string-you-like",\n "coordinate": {\n "stance": "silent_witness",\n "want": "name_on_record",\n "against": "the_document",\n "constraint": "room_or_record",\n "pivot": "price_already_paid",\n "pressure": "inspection",\n "withheld": "narrator_honest",\n "frame": "documents",\n "arena": "stopped_work"\n },\n "engine": [\n {"axis": "stance", "label": "Whose story it is",\n "value": "the person who saw it and said nothing at the time"},\n {"axis": "want", "label": "What they want",\n "value": "to get a name back onto a document"},\n {"axis": "against", "label": "What stands in the way",\n "value": "a document that says otherwise and is believed"},\n {"axis": "constraint", "label": "The constraint that forces a choice",\n "value": "they can be right in the room or right on the record, not both"}\n ],\n "surface": [\n {"axis": "pressure", "label": "What makes it now",\n "value": "an inspection, audit or visit already on the calendar"},\n {"axis": "frame", "label": "How it is told",\n "value": "through documents, and through their gaps"},\n {"axis": "arena", "label": "Where it happens",\n "value": "a place of work that has stopped working"}\n ],\n "left_open": [\n {"axis": "pivot", "label": "The turn available",\n "value": "the price was paid by someone who never mentioned it"},\n {"axis": "withheld", "label": "Left for the writer",\n "value": "whether the protagonist is telling the truth"}\n ],\n "pushed_off": [],\n "still_near_archetype": null\n },\n "genre": "literary",\n "genre_pull": "interiority and consequence; the event is small and the pressure is moral",\n "tired_in_genre": ["a death in the family unlocking a memory",\n "the marriage revealed at a dinner"],\n "form": "short",\n "form_note": "one situation carried through a change of position; a subplot would crowd it",\n "tone": "unsparing",\n "steer": "",\n "steer_clipped": false,\n "boundary_acts": ["supplying the procedure for causing harm", "producing sexual content"]\n}'
import json
payload = json.loads(INPUT)
# The body IS the input object. Do not wrap it in {"input": ...} - that returns 200
# while the model never sees the brief.
est = api("POST", "/estimate", payload)
const payload = {
"shape": "draft",
"brief": {
"seed": "any-string-you-like",
"coordinate": {
"stance": "silent_witness",
"want": "name_on_record",
"against": "the_document",
"constraint": "room_or_record",
"pivot": "price_already_paid",
"pressure": "inspection",
"withheld": "narrator_honest",
"frame": "documents",
"arena": "stopped_work"
},
"engine": [
{"axis": "stance", "label": "Whose story it is",
"value": "the person who saw it and said nothing at the time"},
{"axis": "want", "label": "What they want",
"value": "to get a name back onto a document"},
{"axis": "against", "label": "What stands in the way",
"value": "a document that says otherwise and is believed"},
{"axis": "constraint", "label": "The constraint that forces a choice",
"value": "they can be right in the room or right on the record, not both"}
],
"surface": [
{"axis": "pressure", "label": "What makes it now",
"value": "an inspection, audit or visit already on the calendar"},
{"axis": "frame", "label": "How it is told",
"value": "through documents, and through their gaps"},
{"axis": "arena", "label": "Where it happens",
"value": "a place of work that has stopped working"}
],
"left_open": [
{"axis": "pivot", "label": "The turn available",
"value": "the price was paid by someone who never mentioned it"},
{"axis": "withheld", "label": "Left for the writer",
"value": "whether the protagonist is telling the truth"}
],
"pushed_off": [],
"still_near_archetype": null
},
"genre": "literary",
"genre_pull": "interiority and consequence; the event is small and the pressure is moral",
"tired_in_genre": ["a death in the family unlocking a memory",
"the marriage revealed at a dinner"],
"form": "short",
"form_note": "one situation carried through a change of position; a subplot would crowd it",
"tone": "unsparing",
"steer": "",
"steer_clipped": false,
"boundary_acts": ["supplying the procedure for causing harm", "producing sexual content"]
};
// The body IS the input object. An {input: ...} wrapper returns 200 and silently
// hides the brief from the model.
const est = await api("POST", "/estimate", payload);
// Build the same object as a map[string]any, or model it as a struct.
// The important part is that the request body IS the input object - no wrapper.
payload := map[string]any{
"shape": "draft",
"brief": map[string]any{
"seed": "any-string-you-like",
"coordinate": map[string]string{
"stance": "silent_witness", "want": "name_on_record",
"against": "the_document", "constraint": "room_or_record",
"pivot": "price_already_paid", "pressure": "inspection",
"withheld": "narrator_honest", "frame": "documents",
"arena": "stopped_work",
},
"engine": []map[string]string{
{"axis": "constraint", "label": "The constraint that forces a choice",
"value": "they can be right in the room or right on the record, not both"},
},
"surface": []map[string]string{},
"left_open": []map[string]string{},
"pushed_off": []any{},
},
"genre": "literary",
"form": "short",
"tone": "unsparing",
}
// Compose the JSON with your library of choice. The shape that matters:
// { "shape": "draft"|"push",
// "brief": { "seed", "coordinate", "engine"[], "surface"[],
// "left_open"[], "pushed_off"[], "still_near_archetype" },
// "genre", "genre_pull", "tired_in_genre"[], "form", "form_note",
// "tone", "steer", "steer_clipped", "boundary_acts"[] }
//
// The request body IS this object. Wrapping it in {"input": ...} returns 200 and
// hides the brief from the model - the worst kind of failure, because it looks fine.
String payload = readFile("brief.json");
require "json"
payload = JSON.parse(File.read("brief.json"))
# The body IS the input object. No {"input" => ...} wrapper.
est = api("POST", "/estimate", payload)
<?php
$payload = json_decode(file_get_contents("brief.json"), true);
// The body IS the input object. An ["input" => ...] wrapper returns 200 and hides
// the brief from the model.
$est = api("POST", "/estimate", $payload);
var payload = JsonSerializer.Deserialize<JsonElement>(
File.ReadAllText("brief.json"));
// The body IS the input object - no wrapper object around it.
var est = await Api(HttpMethod.Post, "/estimate", payload);
Step 4 — estimate, and guard your own shape
/estimate is free and starts no job. It returns model,
model_alias, markup_bps, hold_credits and
min_credits.
One thing about this endpoint is worth more than the rest of this page.
It performs no validation of the request body. A bare JSON string, a
null and an empty array all return ok:true with a correct
model binding and an identical hold_credits. So the usual
“the estimate came back with the right model, therefore the wiring is right”
check proves the model binding and nothing at all about your input shape. The only place
a malformed body is ever caught is in your own code, before the call.
api POST /estimate "$(cat brief.json)"
# {"ok":true,"data":{"model":"gpt-5.6-terra","model_alias":"gpt-terra",
# "markup_bps":1000,"hold_credits":4820,"min_credits":600}}
# Estimate is FREE and starts no job. Two things to know:
# 1. hold_credits is a HOLD priced at the full output cap, not the price.
# What is charged is usually far lower and comes back on the run.
# 2. The endpoint does NO validation of the body. A bare string, a null and an
# empty array all return ok:true with the same hold and a correct model
# binding. A successful estimate therefore proves the MODEL is right; it
# proves nothing about your input shape. Validate the object yourself.
est = api("POST", "/estimate", payload)
assert est["model_alias"] == "gpt-terra"
print(est["hold_credits"], "held;", est["min_credits"], "minimum")
def must_be_object(inp):
"""/estimate accepts anything and returns a plausible number for it.
A bare string, None and [] all come back ok:true with an identical hold.
So the only place a malformed body is ever caught is here."""
if not isinstance(inp, dict):
raise TypeError("input must be an object, got " + type(inp).__name__)
if "shape" not in inp or not isinstance(inp.get("brief"), dict):
raise TypeError("input is missing shape or brief")
return inp
est = api("POST", "/estimate", must_be_object(payload))
function mustBeObject(input) {
// /estimate posts its argument AS the body and validates nothing: a string,
// a null and an [] all return ok:true with the same hold_credits and a correct
// model binding. The model-binding assertion proves nothing about the shape,
// so guard it on this side of the wire.
if (!input || typeof input !== "object" || Array.isArray(input)) {
throw new TypeError("input must be a plain object");
}
if (!input.shape || typeof input.brief !== "object") {
throw new TypeError("input is missing shape or brief");
}
return input;
}
const est = await api("POST", "/estimate", mustBeObject(payload));
console.log(est.model, est.model_alias, est.markup_bps, est.hold_credits);
raw, err := api("POST", "/estimate", payload, nil)
if err != nil {
panic(err)
}
var est struct {
Model string `json:"model"`
ModelAlias string `json:"model_alias"`
MarkupBps int `json:"markup_bps"`
HoldCredits int `json:"hold_credits"`
MinCredits int `json:"min_credits"`
}
json.Unmarshal(raw, &est)
// est.ModelAlias == "gpt-terra", est.Model == "gpt-5.6-terra", est.MarkupBps == 1000.
// Note: the endpoint validates nothing. Marshalling a wrong shape still succeeds.
String est = api("POST", "/estimate", payload, Map.of());
// data: model, model_alias, hold_credits, min_credits, markup_bps
//
// /estimate performs no validation on the body. Assert the shape of your own
// object before you send it; the response cannot tell you it was wrong.
System.out.println(est);
raise TypeError, "input must be a Hash" unless payload.is_a?(Hash)
raise TypeError, "missing shape/brief" unless payload["shape"] && payload["brief"].is_a?(Hash)
est = api("POST", "/estimate", payload)
puts "#{est['model_alias']} holds #{est['hold_credits']}"
<?php
if (!is_array($payload) || !isset($payload["shape"]) || !is_array($payload["brief"] ?? null)) {
throw new InvalidArgumentException("input must be an object with shape and brief");
}
$est = api("POST", "/estimate", $payload);
echo $est["model_alias"], " holds ", $est["hold_credits"], "\n";
var est = await Api(HttpMethod.Post, "/estimate", payload);
Console.WriteLine(est.GetProperty("model_alias").GetString()); // gpt-terra
Console.WriteLine(est.GetProperty("hold_credits").GetInt32());
// /estimate does not validate the body. A wrong shape returns a plausible number.
Step 5 — run and poll
POST /run returns a job_id; poll GET /jobs/{job_id}
until status is succeeded or failed. The model's
text is at data.output.output, the real cost at
data.charged_credits — usually far below the hold, which prices the
full output cap.
KEY="writing-prompt-generator:draft:$(shasum -a 256 brief.json | cut -c1-16):a1" JOB=$(curl -s -X POST "$BASE/run" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $KEY" \ -d "$(cat brief.json)" | jq -r .data.job_id) # Poll until terminal. Reuse the SAME Idempotency-Key on any retry so a network # blip cannot bill twice. while :; do J=$(api GET "/jobs/$JOB") ST=$(echo "$J" | jq -r .data.status) [ "$ST" = "succeeded" ] || [ "$ST" = "failed" ] && break sleep 1 done echo "$J" | jq -r .data.output.output
import hashlib, time
key = "writing-prompt-generator:draft:" + hashlib.sha256(
json.dumps(payload, sort_keys=True).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", payload, {"Idempotency-Key": key})
job_id = job["job_id"]
while True:
j = api("GET", "/jobs/" + job_id)
if j["status"] in ("succeeded", "failed"):
break
time.sleep(1)
raw = j["output"]["output"] # the model's text
charged = j.get("charged_credits") # what was actually billed
truncated = j.get("truncated") # True when the balance capped the output
const key = "writing-prompt-generator:draft:" + hash(JSON.stringify(payload)) + ":a1";
const job = await api("POST", "/run", payload, { "Idempotency-Key": key });
let j;
for (;;) {
j = await api("GET", "/jobs/" + job.job_id);
if (j.status === "succeeded" || j.status === "failed") break;
await new Promise(r => setTimeout(r, 1000));
}
const raw = j.output.output;
// j.truncated === true means the balance capped the output cap. Surface a
// "top up to continue" affordance rather than presenting a clipped prompt.
key := "writing-prompt-generator:draft:" + shortHash(payload) + ":a1"
raw, err := api("POST", "/run", payload, map[string]string{"Idempotency-Key": key})
if err != nil {
panic(err)
}
var started struct {
JobID string `json:"job_id"`
}
json.Unmarshal(raw, &started)
for {
jraw, err := api("GET", "/jobs/"+started.JobID, nil, nil)
if err != nil {
panic(err)
}
var job struct {
Status string `json:"status"`
Truncated bool `json:"truncated"`
ChargedCredits int `json:"charged_credits"`
Output struct {
Output string `json:"output"`
} `json:"output"`
}
json.Unmarshal(jraw, &job)
if job.Status == "succeeded" || job.Status == "failed" {
break
}
time.Sleep(time.Second)
}
String key = "writing-prompt-generator:draft:" + shortHash(payload) + ":a1";
String started = api("POST", "/run", payload, Map.of("Idempotency-Key", key));
// read job_id, then poll GET /jobs/{job_id} until status is succeeded or failed.
// The model text is at data.output.output; data.charged_credits is the real cost.
//
// On ANY retry, resend the same Idempotency-Key. A retry with a fresh key is a
// second billable run.
require "digest"
key = "writing-prompt-generator:draft:" + Digest::SHA256.hexdigest(JSON.generate(payload))[0, 16] + ":a1"
job = api("POST", "/run", payload, { "Idempotency-Key" => key })
j = nil
loop do
j = api("GET", "/jobs/#{job['job_id']}")
break if %w[succeeded failed].include?(j["status"])
sleep 1
end
raw = j.dig("output", "output")
<?php
$key = "writing-prompt-generator:draft:" . substr(hash("sha256", json_encode($payload)), 0, 16) . ":a1";
$job = api("POST", "/run", $payload, ["Idempotency-Key: $key"]);
do {
sleep(1);
$j = api("GET", "/jobs/" . $job["job_id"]);
} while (!in_array($j["status"], ["succeeded", "failed"], true));
$raw = $j["output"]["output"];
var key = $"writing-prompt-generator:draft:{ShortHash(payload)}:a1";
var started = await Api(HttpMethod.Post, "/run", payload,
new Dictionary<string, string> { ["Idempotency-Key"] = key });
var jobId = started.GetProperty("job_id").GetString();
JsonElement job;
while (true)
{
job = await Api(HttpMethod.Get, $"/jobs/{jobId}");
var st = job.GetProperty("status").GetString();
if (st is "succeeded" or "failed") break;
await Task.Delay(1000);
}
var raw = job.GetProperty("output").GetProperty("output").GetString();
Step 6 — or stream it
POST /run-stream is the same call over SSE. Events are job,
delta, done and error. Accumulate every
delta; if the stream ends early, parse what arrived rather than discarding
it — a reply cut off mid-array still contains most of a usable prompt.
curl -N -X POST "$BASE/run-stream" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $KEY" \ -H "Accept: text/event-stream" \ -d "$(cat brief.json)" # Events: job (accepted), delta (text chunks), done (terminal, carries # charged_credits and truncated), error. # Accumulate every delta. If the stream ends early, parse what you have - # a reply cut off mid-array still contains most of a usable prompt.
import requests
with requests.post(BASE + "/run-stream", json=payload, stream=True, timeout=300,
headers={"Authorization": "Bearer " + TOKEN,
"Content-Type": "application/json",
"Idempotency-Key": key,
"Accept": "text/event-stream"}) as r:
raw = ""
for line in r.iter_lines(decode_unicode=True):
if not line or not line.startswith("data:"):
continue
evt = json.loads(line[5:].strip())
if evt.get("type") == "delta":
raw += evt.get("text", "")
elif evt.get("type") == "done":
charged = evt.get("charged_credits")
truncated = evt.get("truncated")
const res = await fetch(BASE + "/run-stream", {
method: "POST",
headers: {
"Authorization": "Bearer " + TOKEN,
"Content-Type": "application/json",
"Idempotency-Key": key,
"Accept": "text/event-stream"
},
body: JSON.stringify(payload)
});
const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = "", raw = "";
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
const lines = buf.split("\n");
buf = lines.pop();
for (const line of lines) {
if (!line.startsWith("data:")) continue;
const evt = JSON.parse(line.slice(5).trim());
if (evt.type === "delta") raw += evt.text || "";
if (evt.type === "done") console.log("charged", evt.charged_credits);
}
}
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(bodyBytes))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", key)
req.Header.Set("Accept", "text/event-stream")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 1<<20), 1<<20)
var raw string
for sc.Scan() {
line := sc.Text()
if !strings.HasPrefix(line, "data:") {
continue
}
var evt struct {
Type string `json:"type"`
Text string `json:"text"`
}
json.Unmarshal([]byte(strings.TrimSpace(line[5:])), &evt)
if evt.Type == "delta" {
raw += evt.Text
}
}
HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + "/run-stream"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.header("Idempotency-Key", key)
.header("Accept", "text/event-stream")
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
StringBuilder raw = new StringBuilder();
CLIENT.send(req, HttpResponse.BodyHandlers.ofLines()).body()
.filter(l -> l.startsWith("data:"))
.forEach(l -> {
// parse the event; append evt.text when evt.type is "delta"
raw.append(extractDeltaText(l.substring(5).trim()));
});
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = key
req["Accept"] = "text/event-stream"
req.body = JSON.generate(payload)
raw = +""
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
http.request(req) do |res|
res.read_body do |chunk|
chunk.each_line do |line|
next unless line.start_with?("data:")
evt = JSON.parse(line[5..].strip) rescue next
raw << evt["text"].to_s if evt["type"] == "delta"
end
end
end
end
<?php
$ch = curl_init(BASE . "/run-stream");
$raw = "";
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . TOKEN,
"Content-Type: application/json",
"Idempotency-Key: $key",
"Accept: text/event-stream",
],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$raw) {
foreach (explode("\n", $chunk) as $line) {
if (str_starts_with($line, "data:")) {
$evt = json_decode(trim(substr($line, 5)), true);
if (($evt["type"] ?? "") === "delta") { $raw .= $evt["text"] ?? ""; }
}
}
return strlen($chunk);
},
]);
curl_exec($ch);
curl_close($ch);
var req = new HttpRequestMessage(HttpMethod.Post, Base + "/run-stream");
req.Content = new StringContent(JsonSerializer.Serialize(payload),
Encoding.UTF8, "application/json");
req.Headers.Add("Idempotency-Key", key);
req.Headers.Add("Accept", "text/event-stream");
using var res = await http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var sr = new StreamReader(await res.Content.ReadAsStreamAsync());
var raw = new StringBuilder();
while (await sr.ReadLineAsync() is { } line)
{
if (!line.StartsWith("data:")) continue;
var evt = JsonDocument.Parse(line[5..].Trim()).RootElement;
if (evt.GetProperty("type").GetString() == "delta")
raw.Append(evt.GetProperty("text").GetString());
}
Step 7 — parse it, and check it
The reply is one JSON object, occasionally inside a fence. Strip the fence, take
everything from the first {, and parse.
Then run the check that matters: does constraint actually exclude
anything? A constraint that yields to effort, cleverness or waiting is a
difficulty, and a difficulty belongs in against. This is the single
highest-value assertion against a prompt generator's output, and it is structural
rather than a matter of taste.
# The reply is one JSON object. Strip a code fence if one arrived, then check it.
echo "$RAW" | sed -e 's/^```json//' -e 's/^```//' -e 's/```$//' | jq '{
has_all_keys: (["title","prompt","who","want","against","constraint","cost",
"pressure","withheld","openings","complications","avoid",
"why_it_holds","form_note"] - (. | keys)) == [],
constraint_excludes: (.constraint | test("not both|either.*or|at the (cost|expense) of|cannot"; "i")),
openings_distinct: ((.openings | map(.line[0:20]) | unique | length) == (.openings | length)),
coordinate_echoed: .coordinate_used
}'
import re
def parse_result(raw):
m = re.search(r"```(?:json)?\s*([\s\S]*?)```", raw)
s = (m.group(1) if m else raw).strip()
s = s[s.index("{"):]
return json.loads(s)
r = parse_result(raw)
REQUIRED = ["title", "prompt", "who", "want", "against", "constraint"]
missing = [k for k in REQUIRED if not r.get(k)]
assert not missing, "renderer requires " + ", ".join(missing)
# The one check worth running on every reply: does the constraint EXCLUDE anything?
# A difficulty that yields to effort is not a constraint.
EXCLUDES = re.compile(
r"not both|cannot (be|have|do|keep|both)|either\b.{0,80}\bor\b"
r"|at the (cost|expense|price) of|means (losing|giving up|breaking)|in exchange for",
re.I)
if not EXCLUDES.search(r["constraint"]):
print("warning: the constraint does not exclude anything")
# And: is the coordinate you sent the coordinate it wrote to?
assert r["coordinate_used"] == payload["brief"]["coordinate"]
function parseResult(raw) {
const fence = raw.match(/```(?:json)?\s*([\s\S]*?)```/i);
let s = (fence ? fence[1] : raw).trim();
s = s.slice(s.indexOf("{"));
return JSON.parse(s);
}
const r = parseResult(raw);
const EXCLUDES = /not both|cannot (be|have|do|keep|both)|either\b[\s\S]{0,80}\bor\b|at the (cost|expense|price) of|means (losing|giving up|breaking)|in exchange for/i;
if (!EXCLUDES.test(r.constraint)) {
console.warn("the constraint does not exclude anything - it is a difficulty, not a constraint");
}
// Openings must be different ways in, not one sentence three times.
const heads = new Set(r.openings.map(o => o.line.split(/\s+/).slice(0, 3).join(" ")));
if (heads.size !== r.openings.length) console.warn("openings are too close together");
s := raw
if i := strings.Index(s, "{"); i >= 0 {
s = s[i:]
}
var r struct {
Title string `json:"title"`
Prompt string `json:"prompt"`
Constraint string `json:"constraint"`
Openings []struct {
Frame string `json:"frame"`
Line string `json:"line"`
} `json:"openings"`
CoordinateUsed map[string]string `json:"coordinate_used"`
}
if err := json.Unmarshal([]byte(s), &r); err != nil {
panic(err)
}
excludes := regexp.MustCompile(`(?i)not both|cannot (be|have|do|keep|both)|at the (cost|expense) of`)
if !excludes.MatchString(r.Constraint) {
log.Println("warning: the constraint does not exclude anything")
}
// Strip a fence if present, take everything from the first '{', parse.
// Required for the render path: title, prompt, who, want, against, constraint.
//
// The check worth keeping: constraint must EXCLUDE something. Match against
// not both | cannot (be|have|do|keep|both) | either .. or
// | at the (cost|expense|price) of | means (losing|giving up|breaking)
// A constraint that yields to effort is a difficulty and belongs in `against`.
//
// Also assert coordinate_used equals the coordinate you sent: that is how you
// know the prompt was written to your brief rather than around it.
s = raw[/```(?:json)?\s*([\s\S]*?)```/m, 1] || raw
r = JSON.parse(s[s.index("{")..])
%w[title prompt who want against constraint].each do |k|
raise "missing #{k}" if r[k].to_s.empty?
end
excludes = /not both|cannot (be|have|do|keep|both)|either\b.{0,80}\bor\b|at the (cost|expense|price) of/i
warn "constraint does not exclude anything" unless r["constraint"] =~ excludes
raise "coordinate drifted" unless r["coordinate_used"] == payload["brief"]["coordinate"]
<?php
if (preg_match("/```(?:json)?\s*([\s\S]*?)```/i", $raw, $m)) { $raw = $m[1]; }
$r = json_decode(substr($raw, strpos($raw, "{")), true);
foreach (["title", "prompt", "who", "want", "against", "constraint"] as $k) {
if (empty($r[$k])) { throw new RuntimeException("missing $k"); }
}
$excludes = "/not both|cannot (be|have|do|keep|both)|at the (cost|expense|price) of/i";
if (!preg_match($excludes, $r["constraint"])) {
error_log("the constraint does not exclude anything");
}
var text = raw.ToString();
var start = text.IndexOf('{');
var r = JsonDocument.Parse(text[start..]).RootElement;
foreach (var k in new[] { "title", "prompt", "who", "want", "against", "constraint" })
if (!r.TryGetProperty(k, out _)) throw new Exception($"missing {k}");
var constraint = r.GetProperty("constraint").GetString() ?? "";
var excludes = new Regex(@"not both|cannot (be|have|do|keep|both)|at the (cost|expense|price) of",
RegexOptions.IgnoreCase);
if (!excludes.IsMatch(constraint))
Console.Error.WriteLine("the constraint does not exclude anything");
The coordinate vocabulary
The nine axes and every option id are in /axes.js, served from this origin
and readable without a token. An API client can mirror the sampler from
/draw.js or supply its own coordinate; the ids in
brief.coordinate are the only part the reconciler compares against.
| Axis | Band | What it fixes |
|---|---|---|
stance | engine | Whose story it is, by position relative to what has already happened. |
want | engine | What they are after, specific enough to be refused. |
against | engine | The obstacle class, with its own reasons. |
constraint | engine | The exclusive choice. Every option is shaped “A or B, not both”. |
pivot | engine | The turn available, including an option for no turn at all. |
pressure | surface | The clock, including “nothing is forcing it”. |
withheld | surface | What the prompt deliberately does not supply. |
frame | surface | Narrative distance and shape. |
arena | surface | Setting as pressure rather than decoration. |
Distance between two coordinates is measured on the engine band only, and is family-aware: two options in the same family count as most of the way to identical. That is what stops a swapped setting from registering as variety.
Rate limits and etiquette
/estimateis free and starts no job. It is still a request — debounce it.- Reuse one
Idempotency-Keyacross every retry of the same logical run. - On
RATE_LIMITED, back off. Do not tight-loop. truncated: truemeans the balance capped the output. Say so rather than presenting a clipped prompt as whole.