Driving Confession Generator from code
Base URL https://api.skillsafe.ai/v1/app-api. Every response is a
{"data": …} / {"error": …} envelope. Every request carries
Authorization: Bearer <token>. There is no
X-App-Slug header — the token is scoped to this app.
The run body is the input object itself. Not {"input": {…}}.
Wrapping it returns 200 and quietly hides every field from the model, which is
the most expensive mistake available on this endpoint because nothing fails.
Errors
| Code | Meaning | What to do |
|---|---|---|
UNAUTHORIZED | Missing, malformed or expired token. | Mint a guest token, or sign in for a personal one. |
INSUFFICIENT_CREDITS | Balance below min_credits. | Top up. Call /estimate first — it is free. |
VALIDATION_ERROR | The body is not a valid input object. | Check error.details; usually a missing briefs array. |
RATE_LIMITED | Too many requests. | Back off and retry. Never tight-loop. |
JOB_FAILED | The model run failed. | Retry with the same Idempotency-Key; it will not double-bill. |
The output contract
A single JSON object with a confessions array. Each entry carries
seed (echoed back unchanged from the matching brief), label,
speaker and text. aside is present
only where that brief's second_beat was non-null.
{
"confessions": [
{
"seed": "k3f9x2m1qp:0:8371",
"label": "The kitchen ledger",
"speaker": "nine years on the same floor, never once late",
"text": "I have kept a written count of every time a colleague has taken the last of the milk without replacing it, and the figure is currently four hundred and six."
}
]
}
Bind confessions to coordinates by seed, never by array index. A reply that
drops one confession shifts every later index by one, and an index-bound reader then
attributes every remaining confession to the wrong coordinate.
1. A tiny client
Every call is the same three things: the base URL, a bearer token, and a JSON body. This helper is used by every later step.
# Every call below uses these two.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="YOUR_TOKEN" # see step 2
call() {
curl -sS -X "$1" "$BASE$2" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
${3:+-d "$3"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # see step 2
def call(method, path, body=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")
with urllib.request.urlopen(req) as r:
return json.loads(r.read())
const BASE = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN"; // see step 2
async function call(method, path, body) {
const res = await fetch(BASE + path, {
method,
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json"
},
body: body === undefined ? undefined : JSON.stringify(body)
});
const json = await res.json();
if (json.error) throw new Error(json.error.code + ": " + json.error.message);
return json.data;
}
package main
import (
"bytes"
"encoding/json"
"io"
"net/http"
"os"
)
const base = "https://api.skillsafe.ai/v1/app-api"
var token = os.Getenv("SKILLSAFE_TOKEN") // or a literal "YOUR_TOKEN"
func call(method, path string, body any) (map[string]any, error) {
var buf io.Reader
if body != nil {
b, _ := json.Marshal(body)
buf = bytes.NewReader(b)
}
req, err := http.NewRequest(method, base+path, buf)
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 out map[string]any
return out, json.NewDecoder(res.Body).Decode(&out)
}
import java.net.URI;
import java.net.http.*;
public class Client {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final String TOKEN = "YOUR_TOKEN"; // see step 2
static final HttpClient HTTP = HttpClient.newHttpClient();
static String call(String method, String path, String jsonBody) throws Exception {
HttpRequest.BodyPublisher pub = jsonBody == null
? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(jsonBody);
HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.method(method, pub)
.build();
return HTTP.send(req, HttpResponse.BodyHandlers.ofString()).body();
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # see step 2
def call(method, path, body = nil)
uri = URI(BASE + path)
klass = { "GET" => Net::HTTP::Get, "POST" => Net::HTTP::Post }[method]
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) }
JSON.parse(res.body)
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN"; // see step 2
function call(string $method, string $path, ?array $body = null): array {
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . TOKEN,
"Content-Type: application/json",
],
]);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
$out = curl_exec($ch);
curl_close($ch);
return json_decode($out, true);
}
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
static class Client {
const string Base = "https://api.skillsafe.ai/v1/app-api";
const string Token = "YOUR_TOKEN"; // see step 2
static readonly HttpClient Http = new HttpClient();
public static async Task<JsonDocument> Call(HttpMethod method, string path, object? body = null) {
var req = new HttpRequestMessage(method, Base + path);
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token);
if (body != null)
req.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
var res = await Http.SendAsync(req);
return JsonDocument.Parse(await res.Content.ReadAsStringAsync());
}
}
2. Get a token
A guest token is enough for /me and /estimate. Writing confessions is metered and needs a personal token, which comes from signing in. The easiest way to get either is the token page — it reads the token this browser already holds, with Reveal and Copy buttons, so you never have to open a developer console.
# Mint a guest token for this app (no account needed).
curl -sS -X POST "https://api.skillsafe.ai/v1/app-api/guest" \
-H "Content-Type: application/json" \
-d '{"slug":"confession-generator"}'
# -> {"data":{"token":"...","subject_type":"guest"}}
# Or open https://confession-generator.skillsafe.ai/tokens.html and press "Copy token".
guest = call("POST", "/guest", {"slug": "confession-generator"})
TOKEN = guest["data"]["token"]
# A guest token can call /me and /estimate. Running costs credits and
# needs a personal token: https://confession-generator.skillsafe.ai/tokens.html
const guest = await call("POST", "/guest", { slug: "confession-generator" });
// guest.token is enough for /me and /estimate.
// Running is metered: get a personal token from /tokens.html
guest, err := call("POST", "/guest", map[string]any{"slug": "confession-generator"})
if err != nil {
panic(err)
}
// guest["data"].(map[string]any)["token"] is enough for /me and /estimate.
String guest = call("POST", "/guest", "{\"slug\":\"confession-generator\"}");
// Parse out data.token. Enough for /me and /estimate; running needs a
// personal token from https://confession-generator.skillsafe.ai/tokens.html
guest = call("POST", "/guest", { "slug" => "confession-generator" })
token = guest["data"]["token"]
# Enough for /me and /estimate. Running is metered.
$guest = call("POST", "/guest", ["slug" => "confession-generator"]);
$token = $guest["data"]["token"];
// Enough for /me and /estimate. Running is metered.
var guest = await Client.Call(HttpMethod.Post, "/guest", new { slug = "confession-generator" });
// guest.RootElement.GetProperty("data").GetProperty("token")
// Enough for /me and /estimate. Running is metered.
3. Check the session with /me
Returns exactly three fields: subject_type, subject_id and credits. Note what is not there — there is no email and no name. A signed-in user is one whose subject_type is the literal string "user"; anything else is a guest.
call GET /me
# -> {"data":{"subject_type":"user","subject_id":"usr_...","credits":184320}}
me = call("GET", "/me")["data"]
signed_in = me["subject_type"] == "user"
print(me["credits"], "credits", "(signed in)" if signed_in else "(guest)")
const me = await call("GET", "/me");
const signedIn = me.subject_type === "user";
console.log(me.credits, "credits", signedIn ? "(signed in)" : "(guest)");
me, _ := call("GET", "/me", nil)
data := me["data"].(map[string]any)
signedIn := data["subject_type"] == "user"
fmt.Println(data["credits"], signedIn)
String me = call("GET", "/me", null);
// data.subject_type == "user" -> signed in
// data.credits -> balance
me = call("GET", "/me")["data"]
signed_in = me["subject_type"] == "user"
puts "#{me["credits"]} credits #{signed_in ? "(signed in)" : "(guest)"}"
$me = call("GET", "/me")["data"];
$signedIn = $me["subject_type"] === "user";
echo $me["credits"] . " credits" . ($signedIn ? " (signed in)" : " (guest)");
var me = (await Client.Call(HttpMethod.Get, "/me")).RootElement.GetProperty("data");
bool signedIn = me.GetProperty("subject_type").GetString() == "user";
Console.WriteLine($"{me.GetProperty("credits").GetInt32()} credits, signed in: {signedIn}");
4. Price it with /estimate
Free, and it creates no job. It returns hold_credits (what is reserved, priced against the full output cap), min_credits, and the model binding. The body is the input object itself — there is no input wrapper and no X-App-Slug header. An input wrapper returns 200 and silently hides your fields from the model.
call POST /estimate "$(cat input.json)"
# -> {"data":{"hold_credits":2610,"min_credits":420,
# "model":"gpt-5.6-terra","model_alias":"gpt-terra","markup_bps":1000}}
est = call("POST", "/estimate", build_input())["data"]
assert est["model_alias"] == "gpt-terra"
print("reserves", est["hold_credits"], "credits")
const est = await call("POST", "/estimate", buildInput());
console.log("reserves", est.hold_credits, "credits");
// hold_credits is a HOLD, not the price. What is charged is usually far lower.
est, _ := call("POST", "/estimate", buildInput())
fmt.Println(est["data"].(map[string]any)["hold_credits"])
String est = call("POST", "/estimate", buildInput());
// data.hold_credits is reserved, not charged.
est = call("POST", "/estimate", build_input)["data"]
puts "reserves #{est["hold_credits"]} credits"
$est = call("POST", "/estimate", build_input())["data"];
echo "reserves {$est["hold_credits"]} credits";
var est = (await Client.Call(HttpMethod.Post, "/estimate", BuildInput()))
.RootElement.GetProperty("data");
Console.WriteLine($"reserves {est.GetProperty("hold_credits").GetInt32()} credits");
5. The input object
This is the whole contract. shape is "set" (one confession per brief) or "push" (exactly one brief, written deliberately far from everything in prior). briefs[] carries one drawn coordinate per confession, and each seed must come back unchanged in the matching confession. Every value is prose to write from, never a phrase to place in the output.
# input.json
{
"shape": "set",
"count": 1,
"domain": {
"id": "work",
"pull": "an office, a trade, a shift or a client, and the small politics of being seen to be competent",
"tired": [
"pretending to be busy when the manager walks past"
]
},
"register": {
"id": "petty",
"dial": 2,
"label": "Small and a bit mean",
"pull": "Exactly one person is very slightly worse off, in a way they will never identify, and the confessor knows it.",
"boundary": "The harm stays at the scale of a minor inconvenience."
},
"voice": {
"id": "deadpan",
"label": "Deadpan",
"pull": "State the worst clause at exactly the volume of the dullest one.",
"avoid": "Any word whose job is to tell the reader this is funny.",
"overrides": "May be flatter and less vivid than the house rule on concrete detail would normally push for."
},
"steer": "",
"briefs": [
{
"seed": "k3f9x2m1qp:0:8371",
"coordinate": {
"failing": "keeps_score",
"system": "private_ledger",
"stake": "whole_team",
"stance": "flat_delivery",
"sting": "a_number",
"cadence": "flat_report",
"arena": "in_a_shared_space",
"prop": "a_document",
"telling": "blurted",
"duration": "about_eleven_years",
"beat": "none"
},
"confession": [
{
"axis": "failing",
"label": "What they actually did",
"value": "keeps a private tally of something far too small to be tallied"
},
{
"axis": "system",
"label": "How it survives",
"value": "a written record exists that nobody else knows about"
},
{
"axis": "stake",
"label": "Who is on the wrong end of it",
"value": "a team whose arrangements assume a competence that is not there"
},
{
"axis": "stance",
"label": "How they hold it",
"value": "states it like a meter reading and moves on"
},
{
"axis": "sting",
"label": "The detail that makes it worse",
"value": "an exact figure exists and they know it to the unit"
}
],
"telling": [
{
"axis": "cadence",
"label": "The shape of the telling",
"value": "one declarative sentence, no comic framing, and no second sentence"
},
{
"axis": "arena",
"label": "The social geometry",
"value": "in a kitchen, corridor or stairwell where paths cross unpredictably"
},
{
"axis": "prop",
"label": "The object it hangs on",
"value": "a form, receipt or printed sheet that says something inconvenient"
},
{
"axis": "telling",
"label": "Why it is being said at all",
"value": "said out loud before the decision to say it was made"
},
{
"axis": "duration",
"label": "How long it has run",
"value": "roughly eleven years, and the roughness of that estimate is itself telling"
}
],
"second_beat": null,
"still_near": null
}
]
}
def build_input():
return {
"shape": "set",
"count": 1,
"domain": {
"id": "work",
"pull": "an office, a trade, a shift or a client, and the small politics of being seen to be competent",
"tired": [
"pretending to be busy when the manager walks past"
]
},
"register": {
"id": "petty",
"dial": 2,
"label": "Small and a bit mean",
"pull": "Exactly one person is very slightly worse off, in a way they will never identify, and the confessor knows it.",
"boundary": "The harm stays at the scale of a minor inconvenience."
},
"voice": {
"id": "deadpan",
"label": "Deadpan",
"pull": "State the worst clause at exactly the volume of the dullest one.",
"avoid": "Any word whose job is to tell the reader this is funny.",
"overrides": "May be flatter and less vivid than the house rule on concrete detail would normally push for."
},
"steer": "",
"briefs": [
{
"seed": "k3f9x2m1qp:0:8371",
"coordinate": {
"failing": "keeps_score",
"system": "private_ledger",
"stake": "whole_team",
"stance": "flat_delivery",
"sting": "a_number",
"cadence": "flat_report",
"arena": "in_a_shared_space",
"prop": "a_document",
"telling": "blurted",
"duration": "about_eleven_years",
"beat": "none"
},
"confession": [
{
"axis": "failing",
"label": "What they actually did",
"value": "keeps a private tally of something far too small to be tallied"
},
{
"axis": "system",
"label": "How it survives",
"value": "a written record exists that nobody else knows about"
},
{
"axis": "stake",
"label": "Who is on the wrong end of it",
"value": "a team whose arrangements assume a competence that is not there"
},
{
"axis": "stance",
"label": "How they hold it",
"value": "states it like a meter reading and moves on"
},
{
"axis": "sting",
"label": "The detail that makes it worse",
"value": "an exact figure exists and they know it to the unit"
}
],
"telling": [
{
"axis": "cadence",
"label": "The shape of the telling",
"value": "one declarative sentence, no comic framing, and no second sentence"
},
{
"axis": "arena",
"label": "The social geometry",
"value": "in a kitchen, corridor or stairwell where paths cross unpredictably"
},
{
"axis": "prop",
"label": "The object it hangs on",
"value": "a form, receipt or printed sheet that says something inconvenient"
},
{
"axis": "telling",
"label": "Why it is being said at all",
"value": "said out loud before the decision to say it was made"
},
{
"axis": "duration",
"label": "How long it has run",
"value": "roughly eleven years, and the roughness of that estimate is itself telling"
}
],
"second_beat": None,
"still_near": None
}
]
}
function buildInput() {
return {
"shape": "set",
"count": 1,
"domain": {
"id": "work",
"pull": "an office, a trade, a shift or a client, and the small politics of being seen to be competent",
"tired": [
"pretending to be busy when the manager walks past"
]
},
"register": {
"id": "petty",
"dial": 2,
"label": "Small and a bit mean",
"pull": "Exactly one person is very slightly worse off, in a way they will never identify, and the confessor knows it.",
"boundary": "The harm stays at the scale of a minor inconvenience."
},
"voice": {
"id": "deadpan",
"label": "Deadpan",
"pull": "State the worst clause at exactly the volume of the dullest one.",
"avoid": "Any word whose job is to tell the reader this is funny.",
"overrides": "May be flatter and less vivid than the house rule on concrete detail would normally push for."
},
"steer": "",
"briefs": [
{
"seed": "k3f9x2m1qp:0:8371",
"coordinate": {
"failing": "keeps_score",
"system": "private_ledger",
"stake": "whole_team",
"stance": "flat_delivery",
"sting": "a_number",
"cadence": "flat_report",
"arena": "in_a_shared_space",
"prop": "a_document",
"telling": "blurted",
"duration": "about_eleven_years",
"beat": "none"
},
"confession": [
{
"axis": "failing",
"label": "What they actually did",
"value": "keeps a private tally of something far too small to be tallied"
},
{
"axis": "system",
"label": "How it survives",
"value": "a written record exists that nobody else knows about"
},
{
"axis": "stake",
"label": "Who is on the wrong end of it",
"value": "a team whose arrangements assume a competence that is not there"
},
{
"axis": "stance",
"label": "How they hold it",
"value": "states it like a meter reading and moves on"
},
{
"axis": "sting",
"label": "The detail that makes it worse",
"value": "an exact figure exists and they know it to the unit"
}
],
"telling": [
{
"axis": "cadence",
"label": "The shape of the telling",
"value": "one declarative sentence, no comic framing, and no second sentence"
},
{
"axis": "arena",
"label": "The social geometry",
"value": "in a kitchen, corridor or stairwell where paths cross unpredictably"
},
{
"axis": "prop",
"label": "The object it hangs on",
"value": "a form, receipt or printed sheet that says something inconvenient"
},
{
"axis": "telling",
"label": "Why it is being said at all",
"value": "said out loud before the decision to say it was made"
},
{
"axis": "duration",
"label": "How long it has run",
"value": "roughly eleven years, and the roughness of that estimate is itself telling"
}
],
"second_beat": null,
"still_near": null
}
]
};
}
// Unmarshal the same JSON, or build it with map[string]any.
var raw = []byte(`{
"shape": "set",
"count": 1,
"domain": {
"id": "work",
"pull": "an office, a trade, a shift or a client, and the small politics of being seen to be competent",
"tired": [
"pretending to be busy when the manager walks past"
]
},
"register": {
"id": "petty",
"dial": 2,
"label": "Small and a bit mean",
"pull": "Exactly one person is very slightly worse off, in a way they will never identify, and the confessor knows it.",
"boundary": "The harm stays at the scale of a minor inconvenience."
},
"voice": {
"id": "deadpan",
"label": "Deadpan",
"pull": "State the worst clause at exactly the volume of the dullest one.",
"avoid": "Any word whose job is to tell the reader this is funny.",
"overrides": "May be flatter and less vivid than the house rule on concrete detail would normally push for."
},
"steer": "",
"briefs": [
{
"seed": "k3f9x2m1qp:0:8371",
"coordinate": {
"failing": "keeps_score",
"system": "private_ledger",
"stake": "whole_team",
"stance": "flat_delivery",
"sting": "a_number",
"cadence": "flat_report",
"arena": "in_a_shared_space",
"prop": "a_document",
"telling": "blurted",
"duration": "about_eleven_years",
"beat": "none"
},
"confession": [
{
"axis": "failing",
"label": "What they actually did",
"value": "keeps a private tally of something far too small to be tallied"
},
{
"axis": "system",
"label": "How it survives",
"value": "a written record exists that nobody else knows about"
},
{
"axis": "stake",
"label": "Who is on the wrong end of it",
"value": "a team whose arrangements assume a competence that is not there"
},
{
"axis": "stance",
"label": "How they hold it",
"value": "states it like a meter reading and moves on"
},
{
"axis": "sting",
"label": "The detail that makes it worse",
"value": "an exact figure exists and they know it to the unit"
}
],
"telling": [
{
"axis": "cadence",
"label": "The shape of the telling",
"value": "one declarative sentence, no comic framing, and no second sentence"
},
{
"axis": "arena",
"label": "The social geometry",
"value": "in a kitchen, corridor or stairwell where paths cross unpredictably"
},
{
"axis": "prop",
"label": "The object it hangs on",
"value": "a form, receipt or printed sheet that says something inconvenient"
},
{
"axis": "telling",
"label": "Why it is being said at all",
"value": "said out loud before the decision to say it was made"
},
{
"axis": "duration",
"label": "How long it has run",
"value": "roughly eleven years, and the roughness of that estimate is itself telling"
}
],
"second_beat": null,
"still_near": null
}
]
}`)
func buildInput() map[string]any {
var m map[string]any
_ = json.Unmarshal(raw, &m)
return m
}
static String buildInput() {
return """
{
"shape": "set",
"count": 1,
"domain": {
"id": "work",
"pull": "an office, a trade, a shift or a client, and the small politics of being seen to be competent",
"tired": [
"pretending to be busy when the manager walks past"
]
},
"register": {
"id": "petty",
"dial": 2,
"label": "Small and a bit mean",
"pull": "Exactly one person is very slightly worse off, in a way they will never identify, and the confessor knows it.",
"boundary": "The harm stays at the scale of a minor inconvenience."
},
"voice": {
"id": "deadpan",
"label": "Deadpan",
"pull": "State the worst clause at exactly the volume of the dullest one.",
"avoid": "Any word whose job is to tell the reader this is funny.",
"overrides": "May be flatter and less vivid than the house rule on concrete detail would normally push for."
},
"steer": "",
"briefs": [
{
"seed": "k3f9x2m1qp:0:8371",
"coordinate": {
"failing": "keeps_score",
"system": "private_ledger",
"stake": "whole_team",
"stance": "flat_delivery",
"sting": "a_number",
"cadence": "flat_report",
"arena": "in_a_shared_space",
"prop": "a_document",
"telling": "blurted",
"duration": "about_eleven_years",
"beat": "none"
},
"confession": [
{
"axis": "failing",
"label": "What they actually did",
"value": "keeps a private tally of something far too small to be tallied"
},
{
"axis": "system",
"label": "How it survives",
"value": "a written record exists that nobody else knows about"
},
{
"axis": "stake",
"label": "Who is on the wrong end of it",
"value": "a team whose arrangements assume a competence that is not there"
},
{
"axis": "stance",
"label": "How they hold it",
"value": "states it like a meter reading and moves on"
},
{
"axis": "sting",
"label": "The detail that makes it worse",
"value": "an exact figure exists and they know it to the unit"
}
],
"telling": [
{
"axis": "cadence",
"label": "The shape of the telling",
"value": "one declarative sentence, no comic framing, and no second sentence"
},
{
"axis": "arena",
"label": "The social geometry",
"value": "in a kitchen, corridor or stairwell where paths cross unpredictably"
},
{
"axis": "prop",
"label": "The object it hangs on",
"value": "a form, receipt or printed sheet that says something inconvenient"
},
{
"axis": "telling",
"label": "Why it is being said at all",
"value": "said out loud before the decision to say it was made"
},
{
"axis": "duration",
"label": "How long it has run",
"value": "roughly eleven years, and the roughness of that estimate is itself telling"
}
],
"second_beat": null,
"still_near": null
}
]
}
""";
}
def build_input
JSON.parse(<<~JSON)
{
"shape": "set",
"count": 1,
"domain": {
"id": "work",
"pull": "an office, a trade, a shift or a client, and the small politics of being seen to be competent",
"tired": [
"pretending to be busy when the manager walks past"
]
},
"register": {
"id": "petty",
"dial": 2,
"label": "Small and a bit mean",
"pull": "Exactly one person is very slightly worse off, in a way they will never identify, and the confessor knows it.",
"boundary": "The harm stays at the scale of a minor inconvenience."
},
"voice": {
"id": "deadpan",
"label": "Deadpan",
"pull": "State the worst clause at exactly the volume of the dullest one.",
"avoid": "Any word whose job is to tell the reader this is funny.",
"overrides": "May be flatter and less vivid than the house rule on concrete detail would normally push for."
},
"steer": "",
"briefs": [
{
"seed": "k3f9x2m1qp:0:8371",
"coordinate": {
"failing": "keeps_score",
"system": "private_ledger",
"stake": "whole_team",
"stance": "flat_delivery",
"sting": "a_number",
"cadence": "flat_report",
"arena": "in_a_shared_space",
"prop": "a_document",
"telling": "blurted",
"duration": "about_eleven_years",
"beat": "none"
},
"confession": [
{
"axis": "failing",
"label": "What they actually did",
"value": "keeps a private tally of something far too small to be tallied"
},
{
"axis": "system",
"label": "How it survives",
"value": "a written record exists that nobody else knows about"
},
{
"axis": "stake",
"label": "Who is on the wrong end of it",
"value": "a team whose arrangements assume a competence that is not there"
},
{
"axis": "stance",
"label": "How they hold it",
"value": "states it like a meter reading and moves on"
},
{
"axis": "sting",
"label": "The detail that makes it worse",
"value": "an exact figure exists and they know it to the unit"
}
],
"telling": [
{
"axis": "cadence",
"label": "The shape of the telling",
"value": "one declarative sentence, no comic framing, and no second sentence"
},
{
"axis": "arena",
"label": "The social geometry",
"value": "in a kitchen, corridor or stairwell where paths cross unpredictably"
},
{
"axis": "prop",
"label": "The object it hangs on",
"value": "a form, receipt or printed sheet that says something inconvenient"
},
{
"axis": "telling",
"label": "Why it is being said at all",
"value": "said out loud before the decision to say it was made"
},
{
"axis": "duration",
"label": "How long it has run",
"value": "roughly eleven years, and the roughness of that estimate is itself telling"
}
],
"second_beat": null,
"still_near": null
}
]
}
JSON
end
function build_input(): array {
return json_decode(<<<'JSON'
{
"shape": "set",
"count": 1,
"domain": {
"id": "work",
"pull": "an office, a trade, a shift or a client, and the small politics of being seen to be competent",
"tired": [
"pretending to be busy when the manager walks past"
]
},
"register": {
"id": "petty",
"dial": 2,
"label": "Small and a bit mean",
"pull": "Exactly one person is very slightly worse off, in a way they will never identify, and the confessor knows it.",
"boundary": "The harm stays at the scale of a minor inconvenience."
},
"voice": {
"id": "deadpan",
"label": "Deadpan",
"pull": "State the worst clause at exactly the volume of the dullest one.",
"avoid": "Any word whose job is to tell the reader this is funny.",
"overrides": "May be flatter and less vivid than the house rule on concrete detail would normally push for."
},
"steer": "",
"briefs": [
{
"seed": "k3f9x2m1qp:0:8371",
"coordinate": {
"failing": "keeps_score",
"system": "private_ledger",
"stake": "whole_team",
"stance": "flat_delivery",
"sting": "a_number",
"cadence": "flat_report",
"arena": "in_a_shared_space",
"prop": "a_document",
"telling": "blurted",
"duration": "about_eleven_years",
"beat": "none"
},
"confession": [
{
"axis": "failing",
"label": "What they actually did",
"value": "keeps a private tally of something far too small to be tallied"
},
{
"axis": "system",
"label": "How it survives",
"value": "a written record exists that nobody else knows about"
},
{
"axis": "stake",
"label": "Who is on the wrong end of it",
"value": "a team whose arrangements assume a competence that is not there"
},
{
"axis": "stance",
"label": "How they hold it",
"value": "states it like a meter reading and moves on"
},
{
"axis": "sting",
"label": "The detail that makes it worse",
"value": "an exact figure exists and they know it to the unit"
}
],
"telling": [
{
"axis": "cadence",
"label": "The shape of the telling",
"value": "one declarative sentence, no comic framing, and no second sentence"
},
{
"axis": "arena",
"label": "The social geometry",
"value": "in a kitchen, corridor or stairwell where paths cross unpredictably"
},
{
"axis": "prop",
"label": "The object it hangs on",
"value": "a form, receipt or printed sheet that says something inconvenient"
},
{
"axis": "telling",
"label": "Why it is being said at all",
"value": "said out loud before the decision to say it was made"
},
{
"axis": "duration",
"label": "How long it has run",
"value": "roughly eleven years, and the roughness of that estimate is itself telling"
}
],
"second_beat": null,
"still_near": null
}
]
}
JSON, true);
}
static object BuildInput() => JsonSerializer.Deserialize<object>("""
{
"shape": "set",
"count": 1,
"domain": {
"id": "work",
"pull": "an office, a trade, a shift or a client, and the small politics of being seen to be competent",
"tired": [
"pretending to be busy when the manager walks past"
]
},
"register": {
"id": "petty",
"dial": 2,
"label": "Small and a bit mean",
"pull": "Exactly one person is very slightly worse off, in a way they will never identify, and the confessor knows it.",
"boundary": "The harm stays at the scale of a minor inconvenience."
},
"voice": {
"id": "deadpan",
"label": "Deadpan",
"pull": "State the worst clause at exactly the volume of the dullest one.",
"avoid": "Any word whose job is to tell the reader this is funny.",
"overrides": "May be flatter and less vivid than the house rule on concrete detail would normally push for."
},
"steer": "",
"briefs": [
{
"seed": "k3f9x2m1qp:0:8371",
"coordinate": {
"failing": "keeps_score",
"system": "private_ledger",
"stake": "whole_team",
"stance": "flat_delivery",
"sting": "a_number",
"cadence": "flat_report",
"arena": "in_a_shared_space",
"prop": "a_document",
"telling": "blurted",
"duration": "about_eleven_years",
"beat": "none"
},
"confession": [
{
"axis": "failing",
"label": "What they actually did",
"value": "keeps a private tally of something far too small to be tallied"
},
{
"axis": "system",
"label": "How it survives",
"value": "a written record exists that nobody else knows about"
},
{
"axis": "stake",
"label": "Who is on the wrong end of it",
"value": "a team whose arrangements assume a competence that is not there"
},
{
"axis": "stance",
"label": "How they hold it",
"value": "states it like a meter reading and moves on"
},
{
"axis": "sting",
"label": "The detail that makes it worse",
"value": "an exact figure exists and they know it to the unit"
}
],
"telling": [
{
"axis": "cadence",
"label": "The shape of the telling",
"value": "one declarative sentence, no comic framing, and no second sentence"
},
{
"axis": "arena",
"label": "The social geometry",
"value": "in a kitchen, corridor or stairwell where paths cross unpredictably"
},
{
"axis": "prop",
"label": "The object it hangs on",
"value": "a form, receipt or printed sheet that says something inconvenient"
},
{
"axis": "telling",
"label": "Why it is being said at all",
"value": "said out loud before the decision to say it was made"
},
{
"axis": "duration",
"label": "How long it has run",
"value": "roughly eleven years, and the roughness of that estimate is itself telling"
}
],
"second_beat": null,
"still_near": null
}
]
}
""");
6. Run it, and poll
POST /run returns a job_id immediately. Poll GET /run/{job_id} until status is succeeded or failed. Always send an Idempotency-Key: a content hash of the input plus the shape plus an attempt counter. A retry that reuses the key cannot double-bill.
KEY="confession-generator:set:$(shasum -a 256 input.json | cut -c1-16):a1" JOB=$(curl -sS -X POST "https://api.skillsafe.ai/v1/app-api/run" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $KEY" \ -d @input.json | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["job_id"])') until [ "$(call GET /run/$JOB | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["status"])')" != "running" ]; do sleep 1 done call GET /run/$JOB
import hashlib, time
def run(inp, attempt=1):
key = "%s:%s:%s:a%d" % ("confession-generator", inp["shape"],
hashlib.sha256(json.dumps(inp, sort_keys=True).encode()).hexdigest()[:16], attempt)
req = urllib.request.Request(BASE + "/run",
data=json.dumps(inp).encode(), method="POST")
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Content-Type", "application/json")
req.add_header("Idempotency-Key", key)
with urllib.request.urlopen(req) as r:
job = json.loads(r.read())["data"]["job_id"]
while True:
got = call("GET", "/run/" + job)["data"]
if got["status"] != "running":
return got
time.sleep(1)
async function run(input, attempt = 1) {
const hash = [...JSON.stringify(input)]
.reduce((h, c) => (h * 31 + c.charCodeAt(0)) >>> 0, 7).toString(36);
const key = `confession-generator:${input.shape}:${hash}:a${attempt}`;
const res = await fetch(`${BASE}/run`, {
method: "POST",
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json",
"Idempotency-Key": key
},
body: JSON.stringify(input)
});
const { data } = await res.json();
let job;
do {
await new Promise(r => setTimeout(r, 1000));
job = await call("GET", `/run/${data.job_id}`);
} while (job.status === "running");
return job;
}
func run(input map[string]any, attempt int) (map[string]any, error) {
b, _ := json.Marshal(input)
sum := sha256.Sum256(b)
key := fmt.Sprintf("confession-generator:%v:%x:a%d", input["shape"], sum[:8], attempt)
req, _ := http.NewRequest("POST", base+"/run", bytes.NewReader(b))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", key)
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var started map[string]any
json.NewDecoder(res.Body).Decode(&started)
id := started["data"].(map[string]any)["job_id"].(string)
for {
job, err := call("GET", "/run/"+id, nil)
if err != nil {
return nil, err
}
d := job["data"].(map[string]any)
if d["status"] != "running" {
return d, nil
}
time.Sleep(time.Second)
}
}
static String run(String input, int attempt) throws Exception {
String key = "confession-generator:set:" + Integer.toHexString(input.hashCode()) + ":a" + attempt;
HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + "/run"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.header("Idempotency-Key", key)
.POST(HttpRequest.BodyPublishers.ofString(input))
.build();
String started = HTTP.send(req, HttpResponse.BodyHandlers.ofString()).body();
String jobId = extract(started, "job_id");
String job;
do {
Thread.sleep(1000);
job = call("GET", "/run/" + jobId, null);
} while ("running".equals(extract(job, "status")));
return job;
}
require "digest"
def run(input, attempt = 1)
key = "confession-generator:#{input["shape"]}:" \
"#{Digest::SHA256.hexdigest(JSON.dump(input))[0, 16]}:a#{attempt}"
uri = URI(BASE + "/run")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = key
req.body = JSON.dump(input)
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
job_id = JSON.parse(res.body)["data"]["job_id"]
loop do
job = call("GET", "/run/#{job_id}")["data"]
return job if job["status"] != "running"
sleep 1
end
end
function run(array $input, int $attempt = 1): array {
$key = "confession-generator:{$input["shape"]}:"
. substr(hash("sha256", json_encode($input)), 0, 16) . ":a{$attempt}";
$ch = curl_init(BASE . "/run");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($input),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . TOKEN,
"Content-Type: application/json",
"Idempotency-Key: " . $key,
],
]);
$jobId = json_decode(curl_exec($ch), true)["data"]["job_id"];
curl_close($ch);
do {
sleep(1);
$job = call("GET", "/run/" . $jobId)["data"];
} while ($job["status"] === "running");
return $job;
}
static async Task<JsonElement> Run(object input, int attempt = 1) {
var body = JsonSerializer.Serialize(input);
var hash = Convert.ToHexString(
System.Security.Cryptography.SHA256.HashData(Encoding.UTF8.GetBytes(body)))[..16];
var key = $"confession-generator:set:{hash}:a{attempt}";
var req = new HttpRequestMessage(HttpMethod.Post, "https://api.skillsafe.ai/v1/app-api/run");
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "YOUR_TOKEN");
req.Headers.Add("Idempotency-Key", key);
req.Content = new StringContent(body, Encoding.UTF8, "application/json");
var started = JsonDocument.Parse(await (await Http.SendAsync(req)).Content.ReadAsStringAsync());
var jobId = started.RootElement.GetProperty("data").GetProperty("job_id").GetString();
JsonElement job;
do {
await Task.Delay(1000);
job = (await Client.Call(HttpMethod.Get, $"/run/{jobId}")).RootElement.GetProperty("data");
} while (job.GetProperty("status").GetString() == "running");
return job;
}
7. Stream it with /run-stream
POST /run-stream is the same run, the same body and the same Idempotency-Key, delivered as server-sent events. Frames are separated by a blank line and carry a named event — job, delta, done, and terminally error or pending. Accumulate the text of every delta; that concatenation is the JSON object. A "seed" appearing in it means another confession has started, which is a far more honest progress signal than a character count.
curl -N -X POST "https://api.skillsafe.ai/v1/app-api/run-stream" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-H "Accept: text/event-stream" \
-d @input.json
# event: job
# data: {"job_id":"job_...","status":"running"}
#
# event: delta
# data: {"text":"{\"confessions\":[{\"seed\":\"k3f9"}
#
# event: done
# data: {"job_id":"job_...","status":"succeeded","charged_credits":874,
# "output":{"output":"{\"confessions\":[...]}"}}
req = urllib.request.Request(BASE + "/run-stream",
data=json.dumps(build_input()).encode(), method="POST")
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Content-Type", "application/json")
req.add_header("Idempotency-Key", key)
req.add_header("Accept", "text/event-stream")
raw, name = "", None
with urllib.request.urlopen(req) as stream:
# On an idempotent replay the server may answer with plain JSON.
if "text/event-stream" not in stream.headers.get("content-type", ""):
done = json.loads(stream.read())["data"]
else:
for line in stream:
line = line.decode().rstrip("\n")
if line.startswith("event:"):
name = line[6:].strip()
elif line.startswith("data:"):
payload = json.loads(line[5:].strip())
if name == "delta":
raw += payload["text"]
started = raw.count('"seed"') # confessions begun so far
elif name == "done":
print("charged", payload["charged_credits"])
elif name == "error":
raise RuntimeError(payload["code"] + ": " + payload["message"])
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(buildInput())
});
// An idempotent replay may come back as plain JSON, not an event stream.
if (!(res.headers.get("content-type") || "").includes("text/event-stream")) {
const { data } = await res.json();
return data;
}
const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = "", raw = "";
for (;;) {
const { done, value } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
const frames = buf.split("\n\n");
buf = frames.pop();
for (const frame of frames) {
let name = null, data = null;
for (const line of frame.split("\n")) {
if (line.startsWith("event:")) name = line.slice(6).trim();
else if (line.startsWith("data:")) data = JSON.parse(line.slice(5).trim());
}
if (name === "delta") {
raw += data.text;
const started = (raw.match(/"seed"/g) || []).length;
console.log(`${started} confessions begun`);
} else if (name === "done") {
console.log("charged", data.charged_credits);
} else if (name === "error") {
throw new Error(`${data.code}: ${data.message}`);
}
}
}
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(b))
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)
var raw strings.Builder
var name string
for sc.Scan() {
line := sc.Text()
switch {
case strings.HasPrefix(line, "event:"):
name = strings.TrimSpace(strings.TrimPrefix(line, "event:"))
case strings.HasPrefix(line, "data:"):
var payload map[string]any
json.Unmarshal([]byte(strings.TrimPrefix(line, "data:")), &payload)
if name == "delta" {
raw.WriteString(payload["text"].(string))
} else if name == "done" {
fmt.Println("charged", payload["charged_credits"])
}
}
}
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(buildInput()))
.build();
String[] name = { null };
StringBuilder raw = new StringBuilder();
HTTP.send(req, HttpResponse.BodyHandlers.ofLines()).body().forEach(line -> {
if (line.startsWith("event:")) {
name[0] = line.substring(6).trim();
} else if (line.startsWith("data:")) {
String payload = line.substring(5).trim();
if ("delta".equals(name[0])) raw.append(extract(payload, "text"));
else if ("done".equals(name[0])) System.out.println(extract(payload, "charged_credits"));
}
});
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.dump(build_input)
raw = +""
name = nil
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(req) do |res|
res.read_body do |chunk|
chunk.each_line do |line|
line = line.chomp
if line.start_with?("event:") then name = line[6..].strip
elsif line.start_with?("data:")
payload = JSON.parse(line[5..].strip)
case name
when "delta" then raw << payload["text"]
when "done" then puts "charged #{payload["charged_credits"]}"
when "error" then raise "#{payload["code"]}: #{payload["message"]}"
end
end
end
end
end
end
$raw = ""; $name = null;
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(build_input()),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . TOKEN,
"Content-Type: application/json",
"Idempotency-Key: " . $key,
"Accept: text/event-stream",
],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$raw, &$name) {
foreach (explode("\n", $chunk) as $line) {
$line = rtrim($line);
if (str_starts_with($line, "event:")) {
$name = trim(substr($line, 6));
} elseif (str_starts_with($line, "data:")) {
$payload = json_decode(substr($line, 5), true);
if ($name === "delta") $raw .= $payload["text"];
elseif ($name === "done") echo "charged {$payload["charged_credits"]}";
}
}
return strlen($chunk);
},
]);
curl_exec($ch);
curl_close($ch);
var req = new HttpRequestMessage(HttpMethod.Post,
"https://api.skillsafe.ai/v1/app-api/run-stream");
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "YOUR_TOKEN");
req.Headers.Add("Idempotency-Key", key);
req.Headers.Add("Accept", "text/event-stream");
req.Content = new StringContent(JsonSerializer.Serialize(BuildInput()),
Encoding.UTF8, "application/json");
var res = await Http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());
var raw = new StringBuilder();
string? name = null;
while (await reader.ReadLineAsync() is string line) {
if (line.StartsWith("event:")) {
name = line[6..].Trim();
} else if (line.StartsWith("data:")) {
var payload = JsonDocument.Parse(line[5..].Trim()).RootElement;
if (name == "delta") raw.Append(payload.GetProperty("text").GetString());
else if (name == "done")
Console.WriteLine($"charged {payload.GetProperty("charged_credits").GetInt32()}");
}
}
A note on what the browser does that the API does not
The web app draws its own coordinates, checks the user's steer for distress and for the
content boundary before spending anything, and reconciles the reply against what was
drawn. Over the API you supply briefs yourself, so none of that happens for
you. If you are building on this, the reconciliation logic is plain JavaScript in
reconcile.js and the boundary in
guard.js, both readable and both dependency-free.
Confessions are invented and spoken by people who do not exist. Do not present anything this API returns as a real disclosure by a real person.