← Thousandfold
API

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.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or a required field (task, thing, purpose, normal, extremes) is missing.
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: the stress test is metered and needs a personal sign-in.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. 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.

FieldRequiredWhat it holds
taskyesAlways scale.
thingyesWhat the thing is, up to 120 characters.
purposeyesWhat it is for, up to 160 characters. Every extreme is judged against it, part by part.
notenoHow it usually goes, up to 280 characters; none when empty.
normalyesThe figures as they are now: amount: 2 pizzas | people: 4 people | one go takes: 90 minutes.
extremesyesFive 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_notenoOnly 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": "..."}
FieldWhat it holds
headlineOne sentence: how many of the five extremes it snaps at and what the verdict means.
verdictsturdy, cracks or made-to-measure.
extremes[].idbigger, smaller, crowd, instant or forever, five entries in that order.
extremes[].strainsnaps, bends or holds.
extremes[].breaksWhat gives way first at this extreme.
extremes[].survivesWhat still works, even when it snaps.
extremes[].showsWhat the extreme reveals about the normal-sized version.
core_truthWhat the thing depends on, drawn from the extremes that snapped.
keepOne 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"}
}

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')

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}' 

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}' 

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}' 

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