Drive Fortune Cookie Generator from your own code
Everything the web page does is available over HTTP. The base URL is
https://api.skillsafe.ai/v1/app-api, authentication is a bearer
token, and every response uses the same envelope.
Pick a language once — the choice applies to every sample on the page and is remembered.
The envelope
Every response, success or failure, has this shape:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "UNAUTHORIZED", "message": "..." } }
Read data on success and error.code on failure. Check
the HTTP status too: a 402 carries a well-formed error body.
Authentication, and the header that does not exist
One header: Authorization: Bearer <token>. The token is bound
to this app, which is why nothing else needs to identify it.
There is no X-App-Slug header on any endpoint. If
you have seen one documented, it was wrong. The single call that names the app is
POST /guest, and it names it in the body as
{"slug": "fortune-cookie-generator"}.
Errors
| Status | code | What it means and what to do |
|---|---|---|
| 400 | VALIDATION_ERROR | The body was not acceptable. Note that /estimate does NOT validate - it accepts anything - so this comes from /run. |
| 401 | UNAUTHORIZED | Missing, malformed or expired token. Mint a fresh guest token, or sign the user in again. A cold 401 from /me before any token exists is the correct answer, not a fault. |
| 402 | INSUFFICIENT_CREDITS | The balance is below min_credits. Compare estimate.hold_credits against /me credits BEFORE submitting; a 402 after submit is a failure of your client, not of the user. |
| 404 | NOT_FOUND | Unknown path, or a job id that does not belong to this token. |
| 409 | CONFLICT | An Idempotency-Key replay whose body does not match the original. Reuse a key only for a genuine retry of the same input. |
| 429 | RATE_LIMITED | Back off and retry; never tight-loop. |
| 500 | INTERNAL | Retry once with the SAME Idempotency-Key so the retry cannot double-bill. |
The request body
The same object goes to /estimate, /run and /run-stream. These fields are taken from the app's own buildInput(), not from intent.
| Field | Type | Required | What it is for |
|---|---|---|---|
| count | number | yes | 3, 5 or 7. None of these divides the 13 turns or the 11 angles evenly, which is deliberate: it stops covering the device set from being the cheapest way to satisfy the brief. |
| tone | string | yes | dry | warm | ominous | absurd. |
| tone_pull | string | yes | The instruction for that tone, written from the writer's vantage. |
| tone_avoid | string[] | yes | The specific ways that tone fails; the last entry is usually the neighbouring tone it collapses into. |
| tone_override | string | yes | The ONE house rule this tone is REQUIRED to break. This is what makes the tones structurally different rather than four adjectives. |
| tone_shape | object | yes | Measured target ranges for the batch average: wpl, clause, evaluative, you. Send it - the app measures the reply against these, and grading output against a spec it never received measures the model's defaults instead. |
| mode | string | yes | aphorism | prophecy | advice. |
| mode_pull | string | yes | What kind of sentence to write. |
| mode_forbid | string[] | yes | What that mode may not contain. |
| house_rules | object | yes | H1-H8, the craft defaults, keyed by id. |
| turns | object[] | yes | {id, phrasing} - the joint each sentence is built on. More are sent than there are fortunes to write. |
| angles | object[] | yes | {id, phrasing} - where to aim. count + 2 are sent, so no arrangement of the batch can clear the set. |
| subject | string | no | What to aim the batch at. Omit for an unaimed batch. |
| crowding | object | no | {count, note} - how many corpus lines sit near the subject. The LINES ARE NEVER SENT: showing a writer the sayings it must not reproduce makes reproduction more likely. |
| avoid_openings | string[] | no | Openings already used, two content words deep. The one field in the input that is a hard constraint. |
Guard the shape yourself.
/estimate posts your argument as the request body and validates
nothing. A bare string, a number, null and []
all return ok: true with a well-formed estimate, a correct model
binding and an identical hold_credits. So a passing
estimate proves the model binding and tells you nothing whatever about your
input shape. Assert it is an object before every spend; the client is the only
place this is catchable.
The reply
The model returns labelled lines, not JSON. Three lines per
fortune, a blank line between records, and one NUMBERS: line at the
end:
FORTUNE: The kettle arrives on Thursday, before the beds do.
TURN: dated_arrival
ANGLE: a_place
FORTUNE: Your spare key turns up in the second box, already labelled.
TURN: already_done
ANGLE: the_thing_itself
NUMBERS: 3 14 22 29 41 57
The format is deliberate. Each line completes on its own, so a stream cut anywhere yields every whole record before the cut — no bracket-balancing recovery needed, and a batch truncated after three fortunes is three fortunes rather than nothing.
Never pad a short batch to the count you asked for. Report the shortfall instead. Padding makes a truncated run indistinguishable from a complete one, which is the whole reason the format is recoverable.
1. A tiny client
Two helpers the rest of the page reuses.
# Two shell helpers. Everything below reuses them.
# Keep the token out of your shell history: read it from a file you own.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="YOUR_TOKEN"
ss_get() { # ss_get /me
curl -sS "$BASE$1" -H "Authorization: Bearer $TOKEN"
}
ss_post() { # ss_post /estimate '{"count":5}'
curl -sS -X POST "$BASE$1" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "$2"
}import json
import urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # read it from your own secret store
def call(path, body=None, method=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(
BASE + path, data=data,
method=method or ("POST" if data else "GET"),
headers={"Authorization": "Bearer " + TOKEN,
"Content-Type": "application/json"})
with urllib.request.urlopen(req) as res:
env = json.loads(res.read())
if not env.get("ok", True):
raise RuntimeError(env.get("error"))
return env["data"]
# Note: Cloudflare rejects the default python-urllib user agent against
# api.skillsafe.ai with error code 1010. If you see that, set a normal
# User-Agent header, or use requests, or use curl.const BASE = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN";
async function call(path, body, method) {
const res = await fetch(BASE + path, {
method: method || (body ? "POST" : "GET"),
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json",
},
body: body ? JSON.stringify(body) : undefined,
});
const env = await res.json();
if (!res.ok || env.ok === false) throw new Error(JSON.stringify(env.error));
return env.data;
}package main
import (
"bytes"
"encoding/json"
"errors"
"io"
"net/http"
)
const base = "https://api.skillsafe.ai/v1/app-api"
const token = "YOUR_TOKEN"
type envelope struct {
OK *bool `json:"ok"`
Data json.RawMessage `json:"data"`
Error json.RawMessage `json:"error"`
}
func call(path string, body any, method string) (json.RawMessage, error) {
var rdr io.Reader
if body != nil {
b, _ := json.Marshal(body)
rdr = bytes.NewReader(b)
if method == "" {
method = "POST"
}
}
if method == "" {
method = "GET"
}
req, err := http.NewRequest(method, base+path, rdr)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
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 res.StatusCode >= 400 || (env.OK != nil && !*env.OK) {
return nil, errors.New(string(env.Error))
}
return env.Data, nil
}import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class Ss {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final String TOKEN = "YOUR_TOKEN";
static final HttpClient HTTP = HttpClient.newHttpClient();
static String call(String path, String jsonBody, String method) throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json");
if (jsonBody != null) {
b.method(method == null ? "POST" : method,
HttpRequest.BodyPublishers.ofString(jsonBody));
} else {
b.method(method == null ? "GET" : method, HttpRequest.BodyPublishers.noBody());
}
HttpResponse<String> res = HTTP.send(b.build(),
HttpResponse.BodyHandlers.ofString());
if (res.statusCode() >= 400) throw new RuntimeException(res.body());
return res.body(); // { "ok": true, "data": ... }
}
}require 'net/http'
require 'json'
require 'uri'
BASE = 'https://api.skillsafe.ai/v1/app-api'
TOKEN = 'YOUR_TOKEN'
def call(path, body = nil, method = nil)
uri = URI(BASE + path)
klass = if method == 'GET' || (method.nil? && body.nil?)
Net::HTTP::Get
else
Net::HTTP::Post
end
req = klass.new(uri)
req['Authorization'] = "Bearer #{TOKEN}"
req['Content-Type'] = 'application/json'
req.body = JSON.dump(body) if body
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
env = JSON.parse(res.body)
raise env['error'].to_s if res.code.to_i >= 400 || env['ok'] == false
env['data']
end<?php
const BASE = 'https://api.skillsafe.ai/v1/app-api';
const TOKEN = 'YOUR_TOKEN';
function call(string $path, $body = null, ?string $method = null) {
$ch = curl_init(BASE . $path);
$headers = ['Authorization: Bearer ' . TOKEN, 'Content-Type: application/json'];
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
if ($method !== null) {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
}
$raw = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$env = json_decode($raw, true);
if ($code >= 400 || ($env['ok'] ?? true) === false) {
throw new RuntimeException(json_encode($env['error'] ?? null));
}
return $env['data'];
}using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
static class Ss {
const string Base = "https://api.skillsafe.ai/v1/app-api";
const string Token = "YOUR_TOKEN";
static readonly HttpClient Http = new HttpClient();
public static async Task<JsonElement> Call(string path, object? body = null,
HttpMethod? method = null) {
var m = method ?? (body is null ? HttpMethod.Get : HttpMethod.Post);
using var req = new HttpRequestMessage(m, Base + path);
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token);
if (body is not null) {
req.Content = new StringContent(JsonSerializer.Serialize(body),
Encoding.UTF8, "application/json");
}
using var res = await Http.SendAsync(req);
var raw = await res.Content.ReadAsStringAsync();
var env = JsonDocument.Parse(raw).RootElement;
if (!res.IsSuccessStatusCode) throw new Exception(raw);
return env.GetProperty("data");
}
}2. A token
A guest token needs no authentication and is the one call that names the app — in the body. For a signed-in user, use the SSO flow in the web app, or manage a token by hand on the token page.
# A guest token. This is the ONE call that names the app, and it names it
# in the BODY - there is no X-App-Slug header on any endpoint.
curl -sS -X POST "https://api.skillsafe.ai/v1/app-api/guest" \
-H "Content-Type: application/json" \
-d '{"slug": "fortune-cookie-generator"}'
# 201 Created
# { "ok": true,
# "data": { "token": "...", "guest_id": "...", "expires_at": "..." } }import json, urllib.request
req = urllib.request.Request(
"https://api.skillsafe.ai/v1/app-api/guest",
data=json.dumps({"slug": "fortune-cookie-generator"}).encode(),
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req) as res:
data = json.loads(res.read())["data"]
print(data["token"], data["guest_id"], data["expires_at"])const res = await fetch("https://api.skillsafe.ai/v1/app-api/guest", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ slug: "fortune-cookie-generator" }),
});
const { data } = await res.json();
console.log(data.token, data.guest_id, data.expires_at);body, _ := json.Marshal(map[string]string{"slug": "fortune-cookie-generator"})
res, err := http.Post(
"https://api.skillsafe.ai/v1/app-api/guest",
"application/json", bytes.NewReader(body))
if err != nil {
panic(err)
}
defer res.Body.Close()
var env struct {
Data struct {
Token string `json:"token"`
GuestID string `json:"guest_id"`
ExpiresAt string `json:"expires_at"`
} `json:"data"`
}
json.NewDecoder(res.Body).Decode(&env)
fmt.Println(env.Data.Token, env.Data.GuestID, env.Data.ExpiresAt)HttpRequest req = HttpRequest.newBuilder(
URI.create("https://api.skillsafe.ai/v1/app-api/guest"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{\"slug\":\"fortune-cookie-generator\"}"))
.build();
HttpResponse<String> res = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body()); // data.token, data.guest_id, data.expires_atrequire 'net/http'
require 'json'
uri = URI('https://api.skillsafe.ai/v1/app-api/guest')
req = Net::HTTP::Post.new(uri)
req['Content-Type'] = 'application/json'
req.body = JSON.dump({ slug: 'fortune-cookie-generator' })
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
data = JSON.parse(res.body)['data']
puts data['token'], data['guest_id'], data['expires_at']<?php
$ch = curl_init('https://api.skillsafe.ai/v1/app-api/guest');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['slug' => 'fortune-cookie-generator']));
$data = json_decode(curl_exec($ch), true)['data'];
curl_close($ch);
echo $data['token'], ' ', $data['guest_id'], ' ', $data['expires_at'], "\n";using var http = new HttpClient();
var payload = new StringContent("{\"slug\":\"fortune-cookie-generator\"}",
Encoding.UTF8, "application/json");
using var res = await http.PostAsync(
"https://api.skillsafe.ai/v1/app-api/guest", payload);
var env = JsonDocument.Parse(await res.Content.ReadAsStringAsync()).RootElement;
var data = env.GetProperty("data");
Console.WriteLine(data.GetProperty("token").GetString());3. Who the token belongs to
GET /me returns exactly three fields: subject_type, subject_id and credits. There is no username, no email and no id; the signed-in test is subject_type === "user".
ss_get /me
# { "ok": true,
# "data": { "subject_type": "user", "subject_id": "...", "credits": 41200 } }
#
# Those are the ONLY three fields. There is no username, no email and no id.
# The signed-in test is subject_type == "user"; a guest reads "guest".me = call("/me")
print(me["subject_type"], me["credits"])
# Exactly three fields: subject_type, subject_id, credits.
signed_in = me["subject_type"] == "user"const me = await call("/me");
console.log(me.subject_type, me.credits);
// Exactly three fields: subject_type, subject_id, credits.
const signedIn = me.subject_type === "user";raw, err := call("/me", nil, "GET")
if err != nil {
panic(err)
}
var me struct {
SubjectType string `json:"subject_type"`
SubjectID string `json:"subject_id"`
Credits int64 `json:"credits"`
}
json.Unmarshal(raw, &me)
fmt.Println(me.SubjectType, me.Credits)String body = Ss.call("/me", null, "GET");
System.out.println(body);
// data.subject_type, data.subject_id, data.credits - and nothing else.me = call('/me', nil, 'GET')
puts me['subject_type'], me['credits']
# Exactly three fields: subject_type, subject_id, credits.
signed_in = me['subject_type'] == 'user'<?php
$me = call('/me', null, 'GET');
echo $me['subject_type'], ' ', $me['credits'], "\n";
// Exactly three fields: subject_type, subject_id, credits.
$signedIn = $me['subject_type'] === 'user';var me = await Ss.Call("/me", null, HttpMethod.Get);
Console.WriteLine(me.GetProperty("subject_type").GetString());
Console.WriteLine(me.GetProperty("credits").GetInt64());4. What it will cost
Free, and it creates no job. hold_credits is a reservation priced against the full output cap — a finished run normally charges far less. Show it as reserved, never as the price.
ss_post /estimate '{
"count": 5,
"tone": "ominous",
"tone_pull": "Say an ordinary thing and let it sit one beat too long. ...",
"tone_avoid": ["threat", "anything a reader could mistake for a real warning"],
"tone_override": "H6 ONE-BREATH - ominous REQUIRES a second clause ...",
"tone_shape": { "wpl": [7, 14], "clause": [0.9, 2.0],
"evaluative": [0, 0.5], "you": [0.3, 1.4] },
"mode": "prophecy",
"mode_pull": "Put one event in the reader's future and give it an edge ...",
"mode_forbid": ["an instruction", "a standing general truth"],
"house_rules": { "H1": "SHORT - a fortune runs under fourteen words. ...", "...": "..." },
"turns": [
{ "id": "dated_arrival", "phrasing": "Give an ordinary arrival a date, ..." },
{ "id": "already_done", "phrasing": "Build the sentence like a forecast ..." }
],
"angles": [
{ "id": "a_place", "phrasing": "Anchor it to a room, a road or a doorway ..." },
{ "id": "the_body", "phrasing": "Put it in the body - hands, sleep, ..." }
],
"subject": "moving house",
"crowding": { "count": 12, "note": "Worked, not exhausted. ..." },
"avoid_openings": ["kettle arrives", "spare key"]
}'
# { "ok": true, "data": {
# "hold_credits": 2140, "min_credits": 190,
# "model": "gpt-5.6-terra", "model_alias": "gpt-terra", "markup_bps": 1000 } }
#
# Free. No charge, no job. hold_credits prices the FULL output cap and is a
# RESERVATION, not the price - a finished run normally charges far less.body = {
"count": 5,
"tone": "ominous",
"tone_pull": "Say an ordinary thing and let it sit one beat too long. ...",
"tone_avoid": ["threat", "anything a reader could mistake for a real warning"],
"tone_override": "H6 ONE-BREATH - ominous REQUIRES a second clause ...",
"tone_shape": { "wpl": [7, 14], "clause": [0.9, 2.0],
"evaluative": [0, 0.5], "you": [0.3, 1.4] },
"mode": "prophecy",
"mode_pull": "Put one event in the reader's future and give it an edge ...",
"mode_forbid": ["an instruction", "a standing general truth"],
"house_rules": { "H1": "SHORT - a fortune runs under fourteen words. ...", "...": "..." },
"turns": [
{ "id": "dated_arrival", "phrasing": "Give an ordinary arrival a date, ..." },
{ "id": "already_done", "phrasing": "Build the sentence like a forecast ..." }
],
"angles": [
{ "id": "a_place", "phrasing": "Anchor it to a room, a road or a doorway ..." },
{ "id": "the_body", "phrasing": "Put it in the body - hands, sleep, ..." }
],
"subject": "moving house",
"crowding": { "count": 12, "note": "Worked, not exhausted. ..." },
"avoid_openings": ["kettle arrives", "spare key"]
}
est = call("/estimate", body)
print(est["hold_credits"], est["min_credits"], est["model_alias"])
# WARNING: /estimate posts your argument AS the request body and validates
# NOTHING. A bare string, a number, null and [] all return ok:true with a
# well-formed estimate and the SAME hold_credits. There is no server-side signal
# that your shape is wrong, so assert it yourself before every spend.
assert isinstance(body, dict), "the run input must be an object"const body = {
"count": 5,
"tone": "ominous",
"tone_pull": "Say an ordinary thing and let it sit one beat too long. ...",
"tone_avoid": ["threat", "anything a reader could mistake for a real warning"],
"tone_override": "H6 ONE-BREATH - ominous REQUIRES a second clause ...",
"tone_shape": { "wpl": [7, 14], "clause": [0.9, 2.0],
"evaluative": [0, 0.5], "you": [0.3, 1.4] },
"mode": "prophecy",
"mode_pull": "Put one event in the reader's future and give it an edge ...",
"mode_forbid": ["an instruction", "a standing general truth"],
"house_rules": { "H1": "SHORT - a fortune runs under fourteen words. ...", "...": "..." },
"turns": [
{ "id": "dated_arrival", "phrasing": "Give an ordinary arrival a date, ..." },
{ "id": "already_done", "phrasing": "Build the sentence like a forecast ..." }
],
"angles": [
{ "id": "a_place", "phrasing": "Anchor it to a room, a road or a doorway ..." },
{ "id": "the_body", "phrasing": "Put it in the body - hands, sleep, ..." }
],
"subject": "moving house",
"crowding": { "count": 12, "note": "Worked, not exhausted. ..." },
"avoid_openings": ["kettle arrives", "spare key"]
};
// /estimate validates NOTHING - a bare string returns a plausible estimate with
// an identical hold. Guard the shape yourself; the client is the only place it
// is catchable.
if (body === null || typeof body !== "object" || Array.isArray(body)) {
throw new Error("the run input must be an object");
}
const est = await call("/estimate", body);
console.log(est.hold_credits, est.min_credits, est.model_alias);body := map[string]any{
"count": 5,
"tone": "ominous",
"mode": "prophecy",
// ... the full shape is in the JSON tab
}
raw, err := call("/estimate", body, "")
if err != nil {
panic(err)
}
var est struct {
HoldCredits int64 `json:"hold_credits"`
MinCredits int64 `json:"min_credits"`
Model string `json:"model"`
ModelAlias string `json:"model_alias"`
MarkupBps int `json:"markup_bps"`
}
json.Unmarshal(raw, &est)
fmt.Println(est.HoldCredits, est.ModelAlias)String body = """
{ "count": 5, "tone": "ominous", "mode": "prophecy" }
"""; // the full shape is in the JSON tab
String res = Ss.call("/estimate", body, "POST");
System.out.println(res); // data.hold_credits, data.min_credits, data.model_aliasbody = {
count: 5,
tone: 'ominous',
mode: 'prophecy'
# ... the full shape is in the JSON tab
}
raise 'the run input must be an object' unless body.is_a?(Hash)
est = call('/estimate', body)
puts est['hold_credits'], est['min_credits'], est['model_alias']<?php
$body = [
'count' => 5,
'tone' => 'ominous',
'mode' => 'prophecy',
// ... the full shape is in the JSON tab
];
if (!is_array($body)) {
throw new RuntimeException('the run input must be an object');
}
$est = call('/estimate', $body);
echo $est['hold_credits'], ' ', $est['model_alias'], "\n";var body = new {
count = 5,
tone = "ominous",
mode = "prophecy",
// ... the full shape is in the JSON tab
};
var est = await Ss.Call("/estimate", body);
Console.WriteLine(est.GetProperty("hold_credits").GetInt64());
Console.WriteLine(est.GetProperty("model_alias").GetString());5. Write the fortunes
Send an Idempotency-Key on every run. A network blip or a malformed first reply must never double-bill. If the response carries a job_id, poll GET /jobs/{job_id} until status is succeeded or failed.
# Pass an Idempotency-Key. A network blip must never double-bill.
curl -sS -X POST "$BASE/run" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: fl-7x2k9q-3ma8vb" \
-d "$BODY"
# Either the finished run, or a job to poll:
# { "ok": true, "data": { "job_id": "job_...", "status": "queued" } }
curl -sS "$BASE/jobs/job_..." -H "Authorization: Bearer $TOKEN"
# poll until data.status is "succeeded" or "failed"import time
run = call("/run", body) # add the Idempotency-Key header in production
if "job_id" in run:
while True:
job = call("/jobs/" + run["job_id"])
if job["status"] in ("succeeded", "failed"):
break
time.sleep(1)
out = job.get("output", "")
else:
out = run.get("output", "")
print(out)// The SDK's run() takes the key POSITIONALLY: ss.run(input, idempotencyKey).
const run = await call("/run", body);
let out;
if (run.job_id) {
for (;;) {
const job = await call(`/jobs/${run.job_id}`);
if (job.status === "succeeded" || job.status === "failed") { out = job.output; break; }
await new Promise((r) => setTimeout(r, 1000));
}
} else {
out = run.output;
}
console.log(out);raw, err := call("/run", body, "")
if err != nil {
panic(err)
}
var run struct {
JobID string `json:"job_id"`
Output string `json:"output"`
}
json.Unmarshal(raw, &run)
out := run.Output
for run.JobID != "" && out == "" {
time.Sleep(time.Second)
jraw, _ := call("/jobs/"+run.JobID, nil, "GET")
var job struct {
Status string `json:"status"`
Output string `json:"output"`
}
json.Unmarshal(jraw, &job)
if job.Status == "succeeded" || job.Status == "failed" {
out = job.Output
break
}
}
fmt.Println(out)String run = Ss.call("/run", body, "POST");
System.out.println(run);
// If data.job_id is present, poll GET /jobs/{job_id} on a one-second interval
// until data.status is "succeeded" or "failed", then read data.output.
// Send an Idempotency-Key header on the POST so a retry cannot double-bill.run = call('/run', body)
out = run['output']
if run['job_id']
loop do
job = call("/jobs/#{run['job_id']}", nil, 'GET')
if %w[succeeded failed].include?(job['status'])
out = job['output']
break
end
sleep 1
end
end
puts out<?php
$run = call('/run', $body);
$out = $run['output'] ?? '';
if (isset($run['job_id'])) {
while (true) {
$job = call('/jobs/' . $run['job_id'], null, 'GET');
if (in_array($job['status'], ['succeeded', 'failed'], true)) {
$out = $job['output'] ?? '';
break;
}
sleep(1);
}
}
echo $out, "\n";var run = await Ss.Call("/run", body);
string outText = run.TryGetProperty("output", out var o) ? (o.GetString() ?? "") : "";
if (run.TryGetProperty("job_id", out var jid)) {
while (true) {
var job = await Ss.Call($"/jobs/{jid.GetString()}", null, HttpMethod.Get);
var status = job.GetProperty("status").GetString();
if (status is "succeeded" or "failed") {
outText = job.TryGetProperty("output", out var jo) ? (jo.GetString() ?? "") : "";
break;
}
await Task.Delay(1000);
}
}
Console.WriteLine(outText);6. Streaming, and the wire format nine apps get wrong
POST /run-stream returns server-sent events. Note that
EventSource cannot POST and cannot set an Authorization
header, so you read the response body yourself.
The wire format is event: plus data:, frames
separated by a blank line. There is no {"type":"delta"}
envelope, and a delta's text is at .text. Several published samples
across this platform describe data: {"type":"delta"}; that format
does not exist, and a parser written against it never fires. Event names are
job, delta, done, pending and
error.
event: job
data: {"job_id":"job_...","status":"running"}
event: delta
data: {"text":"FORTUNE: The kettle arrives on Thursday, before the beds do.\n"}
event: delta
data: {"text":"TURN: dated_arrival\n"}
event: done
data: {"output":"FORTUNE: ...","charged_credits":812,"truncated":false}
curl -sS -N -X POST "$BASE/run-stream" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: fl-7x2k9q-3ma8vb" \
-d "$BODY"
# The wire format, exactly. Frames are separated by a BLANK LINE; the event name
# is on an "event:" line and the payload on a "data:" line:
#
# event: delta
# data: {"text":"FORTUNE: The kettle arrives on Thursday, ...\n"}
#
# Event names are: job, delta, done, pending, error.
# A delta payload carries its text at .text - NOT at .delta, and there is no
# {"type":"delta"} envelope. A parser written against that shape never fires.import json
import requests # urllib does not stream cleanly
with requests.post(BASE + "/run-stream", json=body, stream=True, headers={
"Authorization": "Bearer " + TOKEN,
"Content-Type": "application/json",
"Idempotency-Key": "fl-7x2k9q-3ma8vb"}) as res:
acc, event, buf = "", "message", ""
for raw in res.iter_lines(decode_unicode=True):
if raw is None:
continue
if raw == "": # blank line terminates a frame
if buf:
payload = json.loads(buf)
if event == "delta":
acc += payload.get("text", "")
elif event in ("done", "pending"):
print("charged:", payload.get("charged_credits"))
elif event == "error":
raise RuntimeError(payload)
event, buf = "message", ""
continue
if raw.startswith("event:"):
event = raw[6:].strip()
elif raw.startswith("data:"):
buf += raw[5:].strip()
print(acc)// EventSource cannot POST and cannot set Authorization, so read the body.
const res = await fetch(BASE + "/run-stream", {
method: "POST",
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json",
"Idempotency-Key": "fl-7x2k9q-3ma8vb",
},
body: JSON.stringify(body),
});
const reader = res.body.getReader();
const dec = new TextDecoder();
let buffer = "", acc = "";
for (;;) {
const { done, value } = await reader.read();
if (done) break;
buffer += dec.decode(value, { stream: true });
let i;
while ((i = buffer.indexOf("\n\n")) >= 0) { // frames split on a blank line
const frame = buffer.slice(0, i);
buffer = buffer.slice(i + 2);
let event = "message", data = "";
for (const line of frame.split("\n")) {
if (line.startsWith("event:")) event = line.slice(6).trim();
else if (line.startsWith("data:")) data += line.slice(5).trim();
}
if (!data) continue;
const payload = JSON.parse(data);
if (event === "delta") acc += payload.text || ""; // .text, not .delta
else if (event === "done") console.log("charged", payload.charged_credits);
else if (event === "error") throw new Error(JSON.stringify(payload));
}
}
console.log(acc);req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(bodyJSON))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "fl-7x2k9q-3ma8vb")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 0, 64*1024), 1024*1024)
var acc strings.Builder
event, data := "message", ""
for sc.Scan() {
line := sc.Text()
if line == "" { // blank line terminates a frame
if data != "" {
var p struct {
Text string `json:"text"`
ChargedCredits int64 `json:"charged_credits"`
}
json.Unmarshal([]byte(data), &p)
if event == "delta" {
acc.WriteString(p.Text)
}
}
event, data = "message", ""
continue
}
if strings.HasPrefix(line, "event:") {
event = strings.TrimSpace(line[6:])
} else if strings.HasPrefix(line, "data:") {
data += strings.TrimSpace(line[5:])
}
}
fmt.Println(acc.String())HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + "/run-stream"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.header("Idempotency-Key", "fl-7x2k9q-3ma8vb")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<java.io.InputStream> res = HTTP.send(req,
HttpResponse.BodyHandlers.ofInputStream());
var reader = new java.io.BufferedReader(new java.io.InputStreamReader(res.body()));
StringBuilder acc = new StringBuilder();
String event = "message", data = "", line;
while ((line = reader.readLine()) != null) {
if (line.isEmpty()) { // blank line terminates a frame
if (!data.isEmpty() && event.equals("delta")) {
// payload.text carries the chunk - not payload.delta
acc.append(extractText(data));
}
event = "message"; data = "";
continue;
}
if (line.startsWith("event:")) event = line.substring(6).trim();
else if (line.startsWith("data:")) data += line.substring(5).trim();
}
System.out.println(acc);require 'net/http'
require 'json'
uri = URI(BASE + '/run-stream')
req = Net::HTTP::Post.new(uri)
req['Authorization'] = "Bearer #{TOKEN}"
req['Content-Type'] = 'application/json'
req['Idempotency-Key'] = 'fl-7x2k9q-3ma8vb'
req.body = JSON.dump(body)
acc = ''
event = 'message'
data = ''
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(req) do |res|
buffer = ''
res.read_body do |chunk|
buffer += chunk
while (i = buffer.index("\n\n"))
frame = buffer[0...i]
buffer = buffer[(i + 2)..]
event = 'message'
data = ''
frame.split("\n").each do |line|
event = line[6..].strip if line.start_with?('event:')
data += line[5..].strip if line.start_with?('data:')
end
next if data.empty?
payload = JSON.parse(data)
acc += payload['text'].to_s if event == 'delta'
end
end
end
end
puts acc<?php
$ch = curl_init(BASE . '/run-stream');
$acc = '';
$buffer = '';
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . TOKEN,
'Content-Type: application/json',
'Idempotency-Key: fl-7x2k9q-3ma8vb',
]);
curl_setopt($ch, CURLOPT_WRITEFUNCTION,
function ($ch, $chunk) use (&$acc, &$buffer) {
$buffer .= $chunk;
while (($i = strpos($buffer, "\n\n")) !== false) {
$frame = substr($buffer, 0, $i);
$buffer = substr($buffer, $i + 2);
$event = 'message';
$data = '';
foreach (explode("\n", $frame) as $line) {
if (str_starts_with($line, 'event:')) $event = trim(substr($line, 6));
elseif (str_starts_with($line, 'data:')) $data .= trim(substr($line, 5));
}
if ($data === '') continue;
$p = json_decode($data, true);
if ($event === 'delta') $acc .= $p['text'] ?? '';
}
return strlen($chunk);
});
curl_exec($ch);
curl_close($ch);
echo $acc, "\n";using var req = new HttpRequestMessage(HttpMethod.Post, Base + "/run-stream");
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token);
req.Headers.Add("Idempotency-Key", "fl-7x2k9q-3ma8vb");
req.Content = new StringContent(JsonSerializer.Serialize(body),
Encoding.UTF8, "application/json");
using var res = await Http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var stream = await res.Content.ReadAsStreamAsync();
using var reader = new StreamReader(stream);
var acc = new StringBuilder();
string ev = "message", data = "";
while (!reader.EndOfStream) {
var line = await reader.ReadLineAsync();
if (string.IsNullOrEmpty(line)) { // blank line terminates a frame
if (data.Length > 0 && ev == "delta") {
var p = JsonDocument.Parse(data).RootElement;
if (p.TryGetProperty("text", out var t)) acc.Append(t.GetString());
}
ev = "message"; data = "";
continue;
}
if (line.StartsWith("event:")) ev = line[6..].Trim();
else if (line.StartsWith("data:")) data += line[5..].Trim();
}
Console.WriteLine(acc.ToString());7. Reading the reply
A parser in every language. All of them keep whole records and report the shortfall rather than inventing one.
# The reply is LABELLED LINES, so awk is enough and a truncated reply still
# yields every whole record before the cut.
echo "$OUT" | awk -F': ' '/^FORTUNE: /{print $2}'
# The kettle arrives on Thursday, before the beds do.
# Your spare key turns up in the second box, already labelled.
# ...
echo "$OUT" | awk -F': ' '/^NUMBERS: /{print $2}'
# 3 14 22 29 41 57import re
def parse_reply(text, expected=None):
fortunes, numbers, cur = [], [], None
for raw in text.splitlines():
m = re.match(r"^\s*([A-Z][A-Z_]*)\s*:\s*(.*)$", raw)
if not m:
continue
label, value = m.group(1).upper(), m.group(2).strip()
if label == "FORTUNE":
if cur and cur["text"]:
fortunes.append(cur)
cur = {"text": value, "turn": None, "angle": None}
elif label == "TURN" and cur:
cur["turn"] = value
elif label == "ANGLE" and cur:
cur["angle"] = value
elif label == "NUMBERS":
numbers = [int(n) for n in re.findall(r"\d+", value)]
if cur and cur["text"]:
fortunes.append(cur)
# Report the shortfall; NEVER invent a record to reach the expected count,
# or a truncated run becomes indistinguishable from a complete one.
want = expected or len(fortunes)
return {"fortunes": fortunes, "numbers": numbers,
"parsed": len(fortunes), "expected": want,
"partial": 0 < len(fortunes) < want}
print(parse_reply(out, 5))function parseReply(text, expected) {
const LABEL = /^\s*([A-Z][A-Z_]*)\s*:\s*([\s\S]*)$/;
const fortunes = [];
let numbers = [], cur = null;
for (const raw of String(text).split(/\r?\n/)) {
const m = raw.match(LABEL);
if (!m) continue;
const label = m[1].toUpperCase();
const value = m[2].trim();
if (label === "FORTUNE") {
if (cur && cur.text) fortunes.push(cur);
cur = { text: value, turn: null, angle: null };
} else if (label === "TURN" && cur) cur.turn = value;
else if (label === "ANGLE" && cur) cur.angle = value;
else if (label === "NUMBERS") numbers = (value.match(/\d+/g) || []).map(Number);
}
if (cur && cur.text) fortunes.push(cur);
const want = expected || fortunes.length;
return { fortunes, numbers, parsed: fortunes.length, expected: want,
partial: fortunes.length > 0 && fortunes.length < want };
}
console.log(parseReply(out, 5));type Fortune struct {
Text string
Turn string
Angle string
}
func parseReply(text string) ([]Fortune, []int) {
var out []Fortune
var nums []int
var cur *Fortune
re := regexp.MustCompile(`^\s*([A-Z][A-Z_]*)\s*:\s*(.*)$`)
for _, raw := range strings.Split(text, "\n") {
m := re.FindStringSubmatch(strings.TrimRight(raw, "\r"))
if m == nil {
continue
}
label, value := strings.ToUpper(m[1]), strings.TrimSpace(m[2])
switch label {
case "FORTUNE":
if cur != nil && cur.Text != "" {
out = append(out, *cur)
}
cur = &Fortune{Text: value}
case "TURN":
if cur != nil { cur.Turn = value }
case "ANGLE":
if cur != nil { cur.Angle = value }
case "NUMBERS":
for _, d := range regexp.MustCompile(`\d+`).FindAllString(value, -1) {
n, _ := strconv.Atoi(d)
nums = append(nums, n)
}
}
}
if cur != nil && cur.Text != "" {
out = append(out, *cur)
}
return out, nums
}import java.util.*;
import java.util.regex.*;
static List<Map<String, String>> parseReply(String text) {
Pattern label = Pattern.compile("^\\s*([A-Z][A-Z_]*)\\s*:\\s*(.*)$");
List<Map<String, String>> out = new ArrayList<>();
Map<String, String> cur = null;
for (String raw : text.split("\r?\n")) {
Matcher m = label.matcher(raw);
if (!m.matches()) continue;
String k = m.group(1).toUpperCase();
String v = m.group(2).trim();
if (k.equals("FORTUNE")) {
if (cur != null && !cur.get("text").isEmpty()) out.add(cur);
cur = new HashMap<>();
cur.put("text", v);
} else if (cur != null && (k.equals("TURN") || k.equals("ANGLE"))) {
cur.put(k.toLowerCase(), v);
}
}
if (cur != null && !cur.get("text").isEmpty()) out.add(cur);
return out; // a short list is a short batch - do not pad it
}def parse_reply(text, expected = nil)
fortunes = []
numbers = []
cur = nil
text.each_line do |raw|
m = raw.match(/\A\s*([A-Z][A-Z_]*)\s*:\s*(.*)\z/)
next unless m
label = m[1].upcase
value = m[2].strip
case label
when 'FORTUNE'
fortunes << cur if cur && !cur[:text].empty?
cur = { text: value, turn: nil, angle: nil }
when 'TURN' then cur[:turn] = value if cur
when 'ANGLE' then cur[:angle] = value if cur
when 'NUMBERS' then numbers = value.scan(/\d+/).map(&:to_i)
end
end
fortunes << cur if cur && !cur[:text].empty?
want = expected || fortunes.length
{ fortunes: fortunes, numbers: numbers, parsed: fortunes.length,
expected: want, partial: fortunes.length.positive? && fortunes.length < want }
end<?php
function parse_reply(string $text, ?int $expected = null): array {
$fortunes = [];
$numbers = [];
$cur = null;
foreach (preg_split('/\r?\n/', $text) as $raw) {
if (!preg_match('/^\s*([A-Z][A-Z_]*)\s*:\s*(.*)$/', $raw, $m)) continue;
$label = strtoupper($m[1]);
$value = trim($m[2]);
if ($label === 'FORTUNE') {
if ($cur !== null && $cur['text'] !== '') $fortunes[] = $cur;
$cur = ['text' => $value, 'turn' => null, 'angle' => null];
} elseif ($label === 'TURN' && $cur) { $cur['turn'] = $value; }
elseif ($label === 'ANGLE' && $cur) { $cur['angle'] = $value; }
elseif ($label === 'NUMBERS') {
preg_match_all('/\d+/', $value, $d);
$numbers = array_map('intval', $d[0]);
}
}
if ($cur !== null && $cur['text'] !== '') $fortunes[] = $cur;
$want = $expected ?? count($fortunes);
return ['fortunes' => $fortunes, 'numbers' => $numbers,
'parsed' => count($fortunes), 'expected' => $want,
'partial' => count($fortunes) > 0 && count($fortunes) < $want];
}using System.Text.RegularExpressions;
record Fortune(string Text, string? Turn, string? Angle);
static (List<Fortune>, List<int>) ParseReply(string text) {
var label = new Regex(@"^\s*([A-Z][A-Z_]*)\s*:\s*(.*)$");
var outList = new List<Fortune>();
var numbers = new List<int>();
string? cur = null, turn = null, angle = null;
void Flush() {
if (!string.IsNullOrEmpty(cur)) outList.Add(new Fortune(cur!, turn, angle));
cur = turn = angle = null;
}
foreach (var raw in text.Split('\n')) {
var m = label.Match(raw.TrimEnd('\r'));
if (!m.Success) continue;
var k = m.Groups[1].Value.ToUpperInvariant();
var v = m.Groups[2].Value.Trim();
switch (k) {
case "FORTUNE": Flush(); cur = v; break;
case "TURN": turn = v; break;
case "ANGLE": angle = v; break;
case "NUMBERS":
foreach (Match d in Regex.Matches(v, @"\d+"))
numbers.Add(int.Parse(d.Value));
break;
}
}
Flush();
return (outList, numbers); // a short list is a short batch - never padded
}What the app does with the reply, and what you may want to
The web page runs three browser-side checks over every batch before showing it, and you can reproduce any of them from the reply alone:
- Corpus membership. Each line is matched against 474 anonymous traditional proverbs and fixed idioms, across two independent channels. It scores 100% on lines the corpus holds and 4.4% on a held-out set of 160 it does not. It is a corpus-membership test, not an originality test, and a clean result means only that no known match was found.
- Attribution. Eleven grammatical shapes that present a line as somebody else's — a signed name, an invented national proverb, a quoted phrase. The app never produces these and refuses to pass them through.
- Register. Thirteen grammatical shapes the mass-produced slip is built on. This check holds shapes, never specimen text, so it can tell you a sentence is ordinary but never that it was copied.
If you build on this, please keep the distinction the app makes: a corpus hit means the line already exists; a register hit means the line is ordinary. They are different claims and merging them accuses a merely-generic sentence of being a copied one.