Drive Thousandfold from your own code
The one metered task, scale, is available over HTTP. It takes one ordinary thing - what it is, what it is for, a note, the figures as they are now (an amount, how many people take part, how long one go takes) and the page's five extremes - and returns a stress test: for each extreme whether the thing snaps, bends or holds, what gives way first, what survives and what it shows about the normal-sized version, plus a headline, a verdict, the core truth and one thing to keep.
The division of labour first: the page does the arithmetic; the model does the judging. The page writes every extreme's figure itself (2 pizzas -> 2,000 pizzas, 90 minutes -> about 63 days) and sends them in extremes; the model may only use those numbers. After the reply, the page checks that all five extremes came back, that the verdict matches the count of snaps and that no number appears the page did not write. A thought experiment, not professional 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": "..." } }
The token is minted for this app (the guest call takes {"slug":"thousandfold"}), 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, thing, purpose, normal, extremes) 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: the stress test is metered and needs a personal sign-in. |
| 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, the input and the rules
Every run sends task: scale (the one task is scale); a missing or different one is still answered as scale. All fields are strings.
| Field | Required | What it holds |
|---|---|---|
task | yes | Always scale. |
thing | yes | What the thing is, up to 120 characters. |
purpose | yes | What it is for, up to 160 characters. Every extreme is judged against it, part by part. |
note | no | How it usually goes, up to 280 characters; none when empty. |
normal | yes | The figures as they are now: amount: 2 pizzas | people: 4 people | one go takes: 90 minutes. |
extremes | yes | Five lines in this order - bigger, smaller, crowd, instant, forever - each id | what changes | now -> pushed to. Build it with ScaleKit.buildInput so the figures match the page's. |
retry_note | no | Only on a reformat retry: why the previous reply was unusable. |
In each extreme only one figure changes: bigger and smaller change the amount (same people, same time per go), crowd multiplies the people in one shared go with the amount growing so each keeps the usual share, instant and forever change how long one go lasts. The strains are checked in order against each part of the purpose: snaps if a part is no longer true, bends if every part is still true only after rearranging, holds otherwise. The verdict follows from the count of snaps: none sturdy, one or two cracks, three or more made-to-measure.
The reply
One JSON object, no prose around it:
{"task": "scale", "headline": "...", "verdict": "cracks", "extremes": [{"id": "bigger", "strain": "bends", "breaks": "...", "survives": "...", "shows": "..."}], "core_truth": "...", "keep": "..."}
| Field | What it holds |
|---|---|
headline | One sentence: how many of the five extremes it snaps at and what the verdict means. |
verdict | sturdy, cracks or made-to-measure. |
extremes[].id | bigger, smaller, crowd, instant or forever, five entries in that order. |
extremes[].strain | snaps, bends or holds. |
extremes[].breaks | What gives way first at this extreme. |
extremes[].survives | What still works, even when it snaps. |
extremes[].shows | What the extreme reveals about the normal-sized version. |
core_truth | What the thing depends on, drawn from the extremes that snapped. |
keep | One small thing to protect at its normal size; never a date, a time or a number. |
The page reads the reply with ReplyKit.normalize and checks it with ReplyKit.check, which lists, under "The page's check", a missing or doubled extreme, a verdict that does not match the count of snaps, a headline count that disagrees, a day or time you did not give and any number the page never wrote.
Worked examples - every verdict
These are the free samples on the page: the exact input the page sends and the reply that replays for free. Together they cover every verdict - sturdy, cracks and made-to-measure - and every strain.
Friday pizza night - Made to measure
Strains, in order: snaps, holds, snaps, snaps, snaps.
Input:
{
"task": "scale",
"thing": "Friday pizza night",
"purpose": "a relaxed end to the week, cooked and eaten together",
"note": "We make the dough in the afternoon and everyone tops their own.",
"normal": "amount: 2 pizzas | people: 4 people | one go (one evening) takes: 90 minutes",
"extremes": "bigger | a thousand times the amount, same people, same time per go | 2 pizzas -> 2,000 pizzas\nsmaller | a thousandth of the amount, same people, same time per go | 2 pizzas -> 1/500 of a pizza\ncrowd | a thousand times the people in one shared go, the amount growing so each keeps the usual share | 4 people -> 4,000 people, each with the usual share\ninstant | one go takes a thousandth of the time | 90 minutes -> 5.4 seconds\nforever | one go takes a thousand times as long | 90 minutes -> about 63 days"
}
Reply:
{
"task": "scale",
"headline": "Friday pizza night snaps at four of the five extremes, meaning its normal size - the amount, the group and the time - is part of what makes it work.",
"verdict": "made-to-measure",
"extremes": [
{
"id": "bigger",
"strain": "snaps",
"breaks": "With the same 4 people and the same 90 minutes, there's no way to top, bake and eat 2,000 pizzas together, so eaten together stops being true.",
"survives": "It's still the same four people gathering on the same evening to close out the week.",
"shows": "It shows the normal 2 pizzas are already sized to what four people can cook and eat together in one sitting, not a number that could simply be multiplied up."
},
{
"id": "smaller",
"strain": "holds",
"breaks": "Nothing that matters gives way - the only change is that 1/500 of a pizza is too little for everyone to top their own portion, so the sliver gets shared round instead.",
"survives": "Dough still gets made in the afternoon, and it's still cooked and eaten together by the same four people.",
"shows": "It shows the relaxed, together feel of the evening doesn't depend on there being much pizza at all."
},
{
"id": "crowd",
"strain": "snaps",
"breaks": "Sharing one go with 4,000 people turns a relaxed evening into a large organised occasion, so relaxed stops being true.",
"survives": "Everyone still gets the usual share, and the pizzas are still cooked and eaten together in the one shared go.",
"shows": "It shows the relaxed feel of the normal evening depends on the small number of people sharing it, not on how much pizza there is."
},
{
"id": "instant",
"strain": "snaps",
"breaks": "Finished in 5.4 seconds, the evening has no time left to feel relaxed, so relaxed stops being true.",
"survives": "The pizzas are still cooked and eaten together, and the week still closes on the same evening.",
"shows": "It shows the relaxed feel of the normal 90 minutes comes from the time the evening is allowed to take, not from the pizza itself."
},
{
"id": "forever",
"strain": "snaps",
"breaks": "Stretched to about 63 days as one unbroken go, it keeps spanning the very week it was meant to close, so an end to the week stops being true.",
"survives": "The same four people and the same 2 pizzas carry on through it, and it's still cooked and eaten together.",
"shows": "It shows that calling it an end to the week depends on the evening staying inside one evening, not spreading across weeks."
}
],
"core_truth": "What really holds Friday pizza night together is scale on three fronts at once - a small group, a modest amount and a single bounded evening - and pushing any one of those far enough is what makes the relaxed, together, week-closing nature of it give way first.",
"keep": "Worth keeping pizza night to one evening where everyone cooks and eats together, rather than letting the amount, the group or the time stretch past what that evening can hold."
}
The spare-button jar - Cracks at the edges
Strains, in order: bends, snaps, bends, holds, snaps.
Input:
{
"task": "scale",
"thing": "The spare-button jar",
"purpose": "finding a matching button quickly when one falls off",
"note": "Spare buttons from new shirts and old coats all go in one jam jar.",
"normal": "amount: 1 jar | people: 1 person | one go (one search for a button) takes: 2 minutes",
"extremes": "bigger | a thousand times the amount, same people, same time per go | 1 jar -> 1,000 jars\nsmaller | a thousandth of the amount, same people, same time per go | 1 jar -> 1/1,000 of a jar\ncrowd | a thousand times the people in one shared go, the amount growing so each keeps the usual share | 1 person -> 1,000 people, each with the usual share\ninstant | one go takes a thousandth of the time | 2 minutes -> 0.12 seconds\nforever | one go takes a thousand times as long | 2 minutes -> about 33 hours"
}
Reply:
{
"task": "scale",
"headline": "It snaps at two of the five extremes - where the spare stock gets too thin to hold a match and where the search drags on far past quick - but holds or adapts at the rest.",
"verdict": "cracks",
"extremes": [
{
"id": "bigger",
"strain": "bends",
"breaks": "Spread across 1,000 jars, the buttons need sorting and labelling by type instead of just being tipped out and scanned by eye.",
"survives": "A matching button can still be tracked down, just by going to the right jar instead of searching one small heap.",
"shows": "The single jar works because its whole stock is small enough to take in at a glance."
},
{
"id": "smaller",
"strain": "snaps",
"breaks": "A thousandth of a jar leaves so few spare buttons that there may be nothing left that actually matches the one that fell off.",
"survives": "Whatever few buttons remain can still be checked over in moments.",
"shows": "The jar's usefulness depends on holding enough variety of spares, not just on having a container at all."
},
{
"id": "crowd",
"strain": "bends",
"breaks": "With 1,000 people all reaching for one shared stock at once, the buttons need splitting into several reachable spots or people have to take turns.",
"survives": "Each person can still find a button that matches theirs out of the shared stock.",
"shows": "The one-jar system works because normally only one person needs it at a time."
},
{
"id": "instant",
"strain": "holds",
"breaks": "Nothing that matters gives way - the only change is that the search is over before it would normally even start.",
"survives": "The matching button is still found, just sooner.",
"shows": "The point of the jar is a fast fix, so a faster search only suits that purpose better."
},
{
"id": "forever",
"strain": "snaps",
"breaks": "Stretched to about 33 hours, the search is nowhere near quick by any reasonable measure.",
"survives": "A matching button is still sitting there in the jar, waiting to be found eventually.",
"shows": "The two-minute search only works because it stays short - stretch it out and the quick-fix purpose disappears."
}
],
"core_truth": "What the jar depends on is keeping both ends modest at once: enough variety of saved buttons to make a match likely, and a search short enough to still count as quick, since letting either one stretch too far undoes the whole arrangement.",
"keep": "Keep adding any new spare buttons to the same jar so there's always a reasonable spread to search through."
}
The shared trip album - Sturdy all the way
Strains, in order: bends, holds, bends, holds, holds.
Input:
{
"task": "scale",
"thing": "Our shared trip photo album",
"purpose": "keeping everyone's trip photos in one place to look back on",
"note": "After a trip, each of us adds our own photos to the one album.",
"normal": "amount: 10,000 photos | people: 5 people | one go (adding your photos after a trip) takes: 10 minutes",
"extremes": "bigger | a thousand times the amount, same people, same time per go | 10,000 photos -> 10,000,000 photos\nsmaller | a thousandth of the amount, same people, same time per go | 10,000 photos -> 10 photos\ncrowd | a thousand times the people in one shared go, the amount growing so each keeps the usual share | 5 people -> 5,000 people, each with the usual share\ninstant | one go takes a thousandth of the time | 10 minutes -> 0.6 seconds\nforever | one go takes a thousand times as long | 10 minutes -> about 6.9 days"
}
Reply:
{
"task": "scale",
"headline": "Zero of the five extremes make it snap, so the shared trip photo album keeps everyone's photos in one place to look back on at every extreme tried.",
"verdict": "sturdy",
"extremes": [
{
"id": "bigger",
"strain": "bends",
"breaks": "Finding any one trip's photos among 10,000,000 in the one album would need labels or folders by trip, since nothing sorts them on its own.",
"survives": "Adding your own photos still only takes the usual 10 minutes, and everything still lands in that same one shared album.",
"shows": "It shows the normal album already leans on a bit of order to stay something you can look back on, not just a pile."
},
{
"id": "smaller",
"strain": "holds",
"breaks": "Nothing that matters gives way - with only 10 photos in the whole album, there is simply a lot less to look back on.",
"survives": "The one shared album still holds everyone's trip photos together, just as easy to look back on as ever.",
"shows": "It shows the one-album idea for keeping trip photos together works just as well whether there is a lot to keep or almost nothing."
},
{
"id": "crowd",
"strain": "bends",
"breaks": "With 5,000 people all adding their usual share into the one shared album at once, telling whose photos are whose would need tagging or grouping by person.",
"survives": "Every one of those people's photos still ends up kept together in that same one shared album.",
"shows": "It shows the normal version stays simple partly because there are few enough people that nobody needs to label who added what."
},
{
"id": "instant",
"strain": "holds",
"breaks": "Nothing that matters gives way - adding your photos in 0.6 seconds just stops feeling like an upload and more like the photos simply appearing.",
"survives": "Your trip photos still end up safely added to the one shared album, ready to look back on whenever.",
"shows": "It shows the purpose was never about how long adding takes, only that the photos end up kept together afterwards."
},
{
"id": "forever",
"strain": "holds",
"breaks": "Nothing that matters gives way - adding your photos over about 6.9 days just turns a quick task into one long continuous stretch of it.",
"survives": "The photos still all end up gathered into the one shared album once that stretch is done, ready to look back on.",
"shows": "It shows the normal 10-minute add is really just a brief task, with nothing about keeping or looking back on the photos that needs it to be quick."
}
],
"core_truth": "What never breaks is the one place holding everyone's trip photos and the chance to look back on them; what strains is only how easy that looking-back stays once there is a lot more to sift through or a lot more people adding to it.",
"keep": "Keep loosely grouping or labelling photos by trip as they are added, so the shared album stays easy to look back on."
}
Build the input yourself
Load the engine in node (global.window = global; require("./scalekit.js"); require("./replykit.js")), then:
const K = ScaleKit, R = ReplyKit;
const built = K.buildInput(K.form({ thing: "Friday pizza night", purpose: "a relaxed end to the week, cooked and eaten together",
note: "", amount: "2", unit: "pizza", people: "4", time: "90", time_unit: "minutes" })); // built.ok is false, with built.note, until every figure is valid
const wire = K.toWire(built.input); // POST this to /estimate and /run; built.analysis.rows holds the five figures
// then: const reply = R.normalize(R.parseReply(job.output.output));
// R.check(reply, built).notes // the page's re-check of the reply
// R.md(built, reply, K.title(built.form)) // the thing, the figures and the read as Markdown
Form keys: thing, purpose, note, amount (a number above zero), unit (a singular noun), people (a whole number, 1 or more), time (a number above zero) and time_unit (seconds, minutes, hours, days or weeks).
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://thousandfold.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://thousandfold.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"]
// thousandfold.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://thousandfold.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://thousandfold.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
}
// Thousandfold.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 Thousandfold {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://thousandfold.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://thousandfold.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://thousandfold.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://thousandfold.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":"thousandfold"}' | 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": "thousandfold"})["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: "thousandfold" })).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": "thousandfold"}, 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", "thousandfold"), 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: "thousandfold" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "thousandfold"])["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 = "thousandfold" }))!["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": "scale", "thing": "Friday pizza night", "purpose": "...", "note": "...", "normal": "...", "extremes": "..."}
# - 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": "scale", "thing": "Friday pizza night", "purpose": "...", "note": "...", "normal": "...", "extremes": "..."}
# - 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: "scale", thing, purpose, note, normal, extremes} (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 thousandfold:<task>:<inputHash>:a<attempt> (the one task is scale), 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="thousandfold:scale:$(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 = "thousandfold:" + 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("verdict"), [x.get("strain") for x in reply.get("extremes", [])])
import { createHash } from "node:crypto";
const key = `thousandfold:${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.verdict, (reply.extremes || []).map(function (x) { return x.strain; }));
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "thousandfold:" + 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 = "thousandfold:" + 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 = "thousandfold:#{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["verdict"], (reply["extremes"] || []).map { |x| x["strain"] }].inspect
$key = "thousandfold:" . $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["verdict"] ?? ""), " ", count($reply["extremes"] ?? []), "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"thousandfold:{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: thousandfold:scale:$(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": "scale", "thing": "Friday pizza night", "purpose": "...", "note": "...", "normal": "...", "extremes": "..."}
# - 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": "thousandfold:scale:" + 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": `thousandfold:scale:${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", "thousandfold:scale:"+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", "thousandfold:scale:" + 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" => "thousandfold:scale:#{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: thousandfold:scale:" . 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", $"thousandfold:scale:{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);