Drive Dumb Luck? from your own code
The one metered task, ticket, is available over HTTP. It takes one everyday call - whose it was (me, friend, contestant or someone), a tone (wry, warm or deadpan), the call itself, one to six notes numbered from 1 on what was known or done before it, how it turned out and whether it went well or badly - with the page's facts. It returns a status (ok or declined), and for ok a plain label for the call, exactly four factors in a fixed order - information, time, options, odds - each scored 0 to 3 with the note behind it quoted word for word and a short reading, then the total out of 12, sound or shaky, the verdict, the weakest factor, a reason, the part that was luck, one habit for next time and a closing line.
The division of labour first: the page numbers the notes; the model scores the factors and writes the words; the page adds the total again, works out the verdict and the weakest factor itself, prints the ticket and re-checks the reply - four factors in order, quotes word for word from their notes, a score of 1 exactly when no note speaks to a factor, the sum, the verdict and the tie order. Calls about harm, health, crime or money at stake are declined. A thinking toy for fun, not professional, financial, medical or legal advice.
Base URL and the envelope
Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response uses one envelope:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "...", "status": 402, "details": { ... } } }
The token is minted for this app (the guest call takes {"slug":"dumb-luck"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or a required field (task, who, tone, call, notes, outcome, went, page_facts) is missing. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: scoring a call is metered and needs a personal token. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
The task field and the status
Every run sends task: ticket (the one task is ticket); a missing or different one is still answered as ticket. The status field holds one of two ids, checked in this order: declined when the call or the outcome is about someone hurt, ill, injured or killed, medical care or a health choice; a crime, a weapon, drugs or drink; or money staked or risked (betting, gambling, a lottery, trading, investing, a loan or a debt) - a prize on a show or a price paid is fine; otherwise ok.
For ok, the verdict field holds one of four ids, set by the arithmetic and nothing else: good-call (sound, went well), dumb-luck (shaky, went well), bad-beat (sound, went badly) and own-goal (shaky, went badly). A call is sound when its four scores add up to 7 of 12 or more.
Input (every field is a string)
| Field | Required | What it holds |
|---|---|---|
task | yes | Always ticket. |
who | yes | Whose call it was: me, friend (a friend or relative), contestant (a contestant on a show) or someone. |
tone | yes | wry, warm or deadpan. |
call | yes | The call, as typed, up to 90 characters (cut on a word past that). |
notes | yes | 1 to 6 lines as n | note, numbered from 1: what was known, checked, weighed or done before the call. Bullets stripped, repeats dropped, each up to 140 characters. |
outcome | yes | How it turned out, up to 160 characters. |
went | yes | well or badly, as the person judged it. |
page_facts | yes | One key | ... per line: note_count, harm_words, money_words, capitalised_words and ticket_rule. |
clip_note | no | What the page left out, if anything. |
retry_note | no | Only on the page's one reformat retry, after an unusable reply. |
Reply (one JSON object)
{"task": "ticket", "status": "ok", "reason": "", "call_label": "took the train instead of driving",
"factors": [{"factor": "information", "score": 3, "note": 1, "note_quote": "...", "reading": "..."},
{"factor": "time", "score": 1, "note": 0, "note_quote": "", "reading": "..."},
{"factor": "options", ...}, {"factor": "odds", ...}],
"decision_total": 10, "decision": "sound", "went": "well", "verdict": "good-call", "verdict_reason": "...",
"luck_part": "...", "weakest": "time", "next_time": "...", "closing": "..."}
| Field | What it holds |
|---|---|
status, reason | The status by the order above; for declined, at most 30 words saying why (empty for ok). |
call_label | 2 to 8 lowercase words naming the call, starting with a verb (empty when declined). |
factors | Exactly 4, factor in the order information, time, options, odds ([] when declined). Each has score (0 to 3: 3 clearly done, 2 partly, 0 skipped or against the evidence, and 1 only when no note speaks to it), note (the note number behind the score, or 0 for a score of 1), note_quote (1 to 8 words copied from that note, or "") and reading (at most 25 words). |
decision_total, decision | The sum of the four scores (0 to 12) and sound at 7 or more, else shaky. |
went, verdict | The input's went, copied, and the verdict id it gives with the decision. |
weakest | The lowest-scoring factor; a tie goes to the first of odds, information, options, time. |
verdict_reason, luck_part, next_time | At most 30, 25 and 25 words: why the verdict (naming a note), the part of the outcome nobody could know at the time, and one habit for the weakest factor, offered to whoever made the call. |
closing | A light line of at most 16 words. |
The page then checks the reply (LuckKit.check): four factors in order, each score 0 to 3, quotes word for word from the note they point at, a score of 1 exactly when no note is named, the total equal to the sum, the decision and verdict that the total and the outcome give, the weakest factor by score and tie order, no he or she, no internal field names in prose, no name-like word repeated from the input, and the word limits. The ticket always follows the scores. A declined reply should carry no factors; a scored call that the page's harm or money words flag is pointed out.
Worked examples
These are the five free samples on the page: the exact input and a stand-in model's reply, unedited. Together they cover every outcome - the train to a wedding (good call), a holiday flat booked from one photo (dumb luck), a contestant's round-table vote (bad beat), a brother's repaint before a flat viewing (own goal), and a bet on a cup final, which is money at stake, so it is declined.
The train to the wedding - good call
Input:
{
"task": "ticket",
"who": "me",
"tone": "warm",
"call": "took the train to the wedding instead of driving",
"notes": "1 | checked the roadworks map before deciding\n2 | compared the train times with the drive\n3 | the map showed roadworks on both lanes of the motorway",
"outcome": "arrived with time to spare while the motorway queue barely moved",
"went": "well",
"page_facts": "note_count | 3\nharm_words | none\nmoney_words | none\ncapitalised_words | none\nticket_rule | 4 factors in order information, time, options, odds; each scored 0-3; note 0 means score 1; total = the sum; sound at 7 or more; verdict from sound or shaky and went"
}
Reply:
{
"task": "ticket",
"status": "ok",
"reason": "",
"call_label": "took the train over driving to the wedding",
"factors": [
{
"factor": "information",
"score": 3,
"note": 1,
"note_quote": "checked the roadworks map before deciding",
"reading": "Before deciding, you checked the roadworks map, so the fact that mattered was actually looked up and used."
},
{
"factor": "time",
"score": 1,
"note": 0,
"note_quote": "",
"reading": "There is no note about time taken to think here, so it scores 1."
},
{
"factor": "options",
"score": 3,
"note": 2,
"note_quote": "compared the train times with the drive",
"reading": "You compared the train times with the drive, so another option was actually weighed before deciding."
},
{
"factor": "odds",
"score": 3,
"note": 3,
"note_quote": "roadworks on both lanes of the motorway",
"reading": "The map showed roadworks on both lanes, so going with the train matched the clear evidence here."
}
],
"decision_total": 10,
"decision": "sound",
"went": "well",
"verdict": "good-call",
"verdict_reason": "The total was sound and the outcome went well, fitting a good call, with note 2 weighing another option and note 3 matching the clear evidence.",
"luck_part": "Nothing here looks down to chance, since the roadworks slowing the motorway were already known before the call.",
"weakest": "time",
"next_time": "Next time, you could give yourself a short pause to think the choice over before deciding.",
"closing": "A good call that matched a good outcome, nicely done."
}
A flat booked from one photo - dumb luck
Input:
{
"task": "ticket",
"who": "me",
"tone": "wry",
"call": "booked a holiday flat from the first photo I saw",
"notes": "1 | only looked at the first photo\n2 | booked it two minutes after finding it\n3 | didn't bother reading the reviews",
"outcome": "the flat was quiet, bright and right by the beach",
"went": "well",
"page_facts": "note_count | 3\nharm_words | none\nmoney_words | none\ncapitalised_words | none\nticket_rule | 4 factors in order information, time, options, odds; each scored 0-3; note 0 means score 1; total = the sum; sound at 7 or more; verdict from sound or shaky and went"
}
Reply:
{
"task": "ticket",
"status": "ok",
"reason": "",
"call_label": "booked a holiday flat from the first photo",
"factors": [
{
"factor": "information",
"score": 0,
"note": 3,
"note_quote": "didn't bother reading the reviews",
"reading": "Reviews were sitting right there, and they went unread, so the information factor lands at the bottom of the scale."
},
{
"factor": "time",
"score": 0,
"note": 2,
"note_quote": "two minutes after finding it",
"reading": "Finding it and booking it took two minutes, with no mention of anything forcing that pace, so the time factor sits at the bottom."
},
{
"factor": "options",
"score": 1,
"note": 0,
"note_quote": "",
"reading": "No note speaks to weighing other options, so the options factor scores 1."
},
{
"factor": "odds",
"score": 1,
"note": 0,
"note_quote": "",
"reading": "No note speaks to how the odds looked beforehand, so the odds factor scores 1."
}
],
"decision_total": 2,
"decision": "shaky",
"went": "well",
"verdict": "dumb-luck",
"verdict_reason": "The total lands low because the rush in note 2 and the skipped checks keep scores down, yet the outcome went well, so this reads as dumb luck.",
"luck_part": "Nobody could know in advance that the flat would turn out quiet, bright and right by the beach.",
"weakest": "information",
"next_time": "Next time, try skimming a few more photos and reviews before you book, just to see what else is there.",
"closing": "A shaky call, but the beach cooperated this time."
}
The round-table vote - bad beat
Input:
{
"task": "ticket",
"who": "contestant",
"tone": "deadpan",
"call": "voted out the quiet player at the round table",
"notes": "1 | asked the quiet player twice about the missing key and got no answer\n2 | three other players had seen the quiet player by the locked room\n3 | talked it over with the group for an hour before the vote\n4 | the loud player's name came up too",
"outcome": "the quiet player was loyal all along and the team lost the round's prize",
"went": "badly",
"page_facts": "note_count | 4\nharm_words | none\nmoney_words | none\ncapitalised_words | none\nticket_rule | 4 factors in order information, time, options, odds; each scored 0-3; note 0 means score 1; total = the sum; sound at 7 or more; verdict from sound or shaky and went"
}
Reply:
{
"task": "ticket",
"status": "ok",
"reason": "",
"call_label": "voted out the quiet player",
"factors": [
{
"factor": "information",
"score": 3,
"note": 1,
"note_quote": "asked the quiet player twice about the missing",
"reading": "The quiet player was asked twice about the missing key, which counts as checking before the vote."
},
{
"factor": "time",
"score": 3,
"note": 3,
"note_quote": "it over with the group for an hour",
"reading": "They talked it over with the group for an hour before voting, showing the time was used well."
},
{
"factor": "options",
"score": 2,
"note": 4,
"note_quote": "the loud player's name came up too",
"reading": "The loud player's name came up too, but nothing shows it was weighed against the quiet player."
},
{
"factor": "odds",
"score": 3,
"note": 2,
"note_quote": "seen the quiet player by the locked room",
"reading": "Three other players had seen the quiet player by the locked room, matching where the vote landed."
}
],
"decision_total": 11,
"decision": "sound",
"went": "badly",
"verdict": "bad-beat",
"verdict_reason": "With a total of 11, helped by the evidence in note 2, this call reads as sound; the outcome alone makes it a bad beat.",
"luck_part": "Nobody could know the quiet player was loyal all along, which cost the team the round's prize.",
"weakest": "options",
"next_time": "Before the next vote, the contestant might weigh other names, like the loud player, alongside the quiet player.",
"closing": "A solid call, sunk by luck no one could have seen coming."
}
The late repaint - own goal
Input:
{
"task": "ticket",
"who": "friend",
"tone": "wry",
"call": "my brother repainted the hallway before the flat viewing",
"notes": "1 | my brother never tested the colour on the wall first\n2 | decided to do it on the spot after dinner\n3 | the landlord's email said not to change any colours",
"outcome": "the paint was still wet at the viewing and the landlord asked for the old colour back",
"went": "badly",
"page_facts": "note_count | 3\nharm_words | none\nmoney_words | none\ncapitalised_words | none\nticket_rule | 4 factors in order information, time, options, odds; each scored 0-3; note 0 means score 1; total = the sum; sound at 7 or more; verdict from sound or shaky and went"
}
Reply:
{
"task": "ticket",
"status": "ok",
"reason": "",
"call_label": "repainted the hallway before the flat viewing",
"factors": [
{
"factor": "information",
"score": 0,
"note": 1,
"note_quote": "never tested the colour on the wall first",
"reading": "No note shows the colour tested on the wall before painting, so this factor scores at the bottom."
},
{
"factor": "time",
"score": 0,
"note": 2,
"note_quote": "decided to do it on the spot",
"reading": "The choice was made on the spot after dinner, with no planning ahead shown, so time scores at the bottom."
},
{
"factor": "options",
"score": 1,
"note": 0,
"note_quote": "",
"reading": "No note here shows another option being weighed, so under the rules this factor scores 1."
},
{
"factor": "odds",
"score": 0,
"note": 3,
"note_quote": "said not to change any colours",
"reading": "The landlord's email said not to change any colours, so going ahead sits against that clear instruction."
}
],
"decision_total": 1,
"decision": "shaky",
"went": "badly",
"verdict": "own-goal",
"verdict_reason": "The total stayed low for a shaky decision, and the outcome went badly, giving an own-goal, as note 2 shows a decision made on the spot.",
"luck_part": "Nothing in the outcome looks like chance: the untested colour and the known rule line up plainly with what happened.",
"weakest": "odds",
"next_time": "Next time, your brother could check a planned change against the landlord's rules before acting on it.",
"closing": "An own-goal, plainly: still-wet paint and a request to bring back the old colour."
}
A bet on the cup final - declined
Input:
{
"task": "ticket",
"who": "me",
"tone": "wry",
"call": "bet a month's rent on the cup final",
"notes": "1 | the team had won five in a row\n2 | a mate said it was a sure thing",
"outcome": "they lost in extra time",
"went": "badly",
"page_facts": "note_count | 2\nharm_words | none\nmoney_words | bet\ncapitalised_words | none\nticket_rule | 4 factors in order information, time, options, odds; each scored 0-3; note 0 means score 1; total = the sum; sound at 7 or more; verdict from sound or shaky and went"
}
Reply:
{
"task": "ticket",
"status": "declined",
"reason": "This scores everyday, low-stakes calls, not ones touching on harm, health, crime or risked money. Feel free to share a different one instead.",
"call_label": "",
"factors": [],
"decision_total": 0,
"decision": "",
"went": "",
"verdict": "",
"verdict_reason": "",
"luck_part": "",
"weakest": "",
"next_time": "",
"closing": "No hard feelings - this ticket just was not the right fit."
}
Build the input yourself
Load the engine in node (global.window = global; require("./luckkit.js"); require("./ticket.js")), then:
const K = LuckKit, D = LuckDraw;
const built = K.buildInput({ who: "me", tone: "wry", call: "took the train to the wedding instead of driving",
notes: "checked the roadworks map before deciding
compared the train times with the drive",
outcome: "arrived with time to spare", went: "well" });
built.parsed.notes; // ["checked the roadworks map before deciding", ...]
built.prescan.money; // [] - betting or debt words in the call, notes or outcome
const wire = K.toWire(built.input); // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// K.check(reply, built).notes // the page's re-check of the reply
// K.ticket(K.scoresOf(reply.factors), "well") // total, decision, verdict and weakest, worked by the page
// D.svg({ label: reply.call_label, scores: K.scoresOf(reply.factors), went: "well" }, { reveal: true }) // the ticket as SVG
// K.md(built, reply) // the scored ticket as Markdown
Step 1 — a tiny client helper
One function that sends JSON and unwraps the envelope. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here" # from https://dumb-luck.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://dumb-luck.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// dumb-luck.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://dumb-luck.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "paste-your-token-here" // from https://dumb-luck.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// DumbLuck.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class DumbLuck {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://dumb-luck.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://dumb-luck.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "paste-your-token-here"; // from https://dumb-luck.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "paste-your-token-here"; // from https://dumb-luck.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
Step 2 — a token
A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"dumb-luck"}' | jq -r '.data.token')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "dumb-luck"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "dumb-luck" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "dumb-luck"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "dumb-luck"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "dumb-luck" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "dumb-luck"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "dumb-luck" }))!["token"]!.GetValue<string>();
Step 3 — GET /me
Who the token is (subject_type: user or guest) and the credit balance.
api GET /me | jq '.data | {subject_type, credits}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["credits"]}");
Step 4 — POST /estimate
Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.
# input.json: the run input object - {"task": "ticket", "who": "me", "tone": "wry", "call": "...", "notes": "1 | ...\n2 | ...", "outcome": "...", "went": "well", "page_facts": "note_count | 2\n..."} - see the field table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}'
inp = json.load(open("input.json")) # {"task": "ticket", "who": "me", "tone": "wry", "call": "...", "notes": "1 | ...\n2 | ...", "outcome": "...", "went": "well", "page_facts": "note_count | 2\n..."} - see the field table
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task: "ticket", who, tone, call, notes, outcome, went, page_facts} (see the field table)
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
Step 5 — POST /run, then poll the job
Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses dumb-luck:<task>:<inputHash>:a<attempt> (the one task is ticket), and its one reformat retry (with retry_note added) is attempt 2, a new key and a second run. The job's output.output is the reply text; truncated: true means the balance cut it short.
KEY="dumb-luck:ticket:$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}'
import hashlib, time
key = "dumb-luck:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the reply as one JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("status"))
import { createHash } from "node:crypto";
const key = `dumb-luck:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.status);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "dumb-luck:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
fmt.Println(j["charged_credits"], j["truncated"], j["output"].(map[string]any)["output"])
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "dumb-luck:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply);
require "digest"
key = "dumb-luck:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["status"]].inspect
$key = "dumb-luck:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", $reply["status"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"dumb-luck:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>());
Console.WriteLine($"{j["charged_credits"]} {reply}");
Step 6 — POST /run-stream
The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.
# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: dumb-luck:ticket:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json
rev = json.load(open("input.json")) # the same run input as input.json: {"task": "ticket", "who": "me", "tone": "wry", "call": "...", "notes": "1 | ...\n2 | ...", "outcome": "...", "went": "well", "page_facts": "note_count | 2\n..."} - see the field table
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "dumb-luck:ticket:" + hashlib.sha256(json.dumps(rev).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const rev = JSON.parse(readFileSync("input.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `dumb-luck:ticket:${createHash("sha256").update(JSON.stringify(rev)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("input.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "dumb-luck:ticket:"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String rev = java.nio.file.Files.readString(java.nio.file.Path.of("input.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "dumb-luck:ticket:" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "dumb-luck:ticket:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$rev = file_get_contents("input.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: dumb-luck:ticket:" . substr(hash("sha256", $rev), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var rev = File.ReadAllText("input.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"dumb-luck:ticket:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);