Drive Prebuild Desk from your own code
The page does its free work in the browser: it reads a brief against the seven risks of the before-you-build skill
(demand, positioning, monetization, retention, trust, distribution, feature adoption), grades each as backed, named
or silent with line numbers, lists the claims standing in for evidence and the beliefs stated as plans, extracts the
planned build, sets an evidence floor and raises flags PB-01..PB-14. Its two paid lanes are available over HTTP. You
send one JSON object with task set to "review" or "test", and the model
replies with one JSON object. The easiest way to build the facts string is the page itself:
Briefkit.buildInput(lane, Briefkit.analyze(brief, {kind}), {note, assumption, reviewHandoff}) in
briefkit.js, which also runs under Node.
Base URL: https://api.skillsafe.ai/v1/app-api. Every call sends
Content-Type: application/json and, once you have a token,
Authorization: Bearer <token>. Both lanes run on gpt-terra with a 10%
publisher markup. A run needs a signed-in (user) token and is metered: the platform holds
the estimate's hold_credits while the run is in flight and then charges only
what it used (charged_credits). /estimate is free. A plain-text summary is at
/llms.txt.
The response envelope
Every response is {"data": ..., "error": null} on success or {"data": null, "error": {"code", "message"}} on failure.
Error codes
| HTTP | code | meaning |
|---|---|---|
| 400 | VALIDATION_ERROR | The body is not a JSON object, or a field is the wrong type. Every field of the run input is a string. |
| 401 | UNAUTHORIZED | No token, or an expired one: get a new one (step 2). |
| 402 | INSUFFICIENT_CREDITS | The balance is below the run's minimum. Top up at skillsafe.ai/account/billing/. |
| 403 | FORBIDDEN | A guest token tried a metered run: sign in for a personal token. |
| 404 | NOT_FOUND | Unknown job id. |
| 429 | RATE_LIMITED | Back off and retry with the same Idempotency-Key. |
The task field: two lanes
| task | what the model does |
|---|---|
"review" | The pre-mortem. A low, medium or high risk verdict; all seven risks rated on the brief's own evidence with exact quotes; the single main assumption most likely to break the project; the smallest evidence to find first; one next step; what to delay and until when; the evidence that already lowers the risk; and a note separating product risk from engineering difficulty. |
"test" | The test plan for one assumption: the method, who and how many, 2 to 6 steps, a success line and a kill line with numbers, a timebox of six weeks or less, the cost, a ready-to-use asset (an outreach message, an interview script, landing copy, a presale offer), what to build for the test only, and what stays delayed. |
Input fields (all strings)
| field | when | content |
|---|---|---|
task | required | review or test |
brief | required | The brief, verbatim. Past 16,000 characters the page cuts the middle on a line boundary and leaves a [prebuild-desk: ...] marker. |
kind | required | product, feature, landing, internal or agent - the kind the facts were built with; it sets the core risks. |
facts | required | A JSON string: kind, kind_inferred, kind_why, floor (level, why, core), risks (id, status: backed|named|silent|na, signals with line, strength, label, quote), claims, beliefs, scope (items, eng_terms, eng_share), numbers, flags (uid, id, sev, risk, name), summary, notes. It is Briefkit.factsOf(A). |
note | optional | Your constraints, up to 1,500 characters. |
assumption | test only | The assumption to test, up to 1,200 characters. Without it the run uses the review's main assumption, or picks the riskiest one from the scan. |
review_handoff | test only | A JSON string of a review reply's verdict, main_assumption{risk, statement}, evidence_first and delay[{item, until}], for the same brief. |
retry_note | optional | Only on a reformat retry: what was wrong with the previous reply. |
The reply contracts
Both lanes share an envelope: task, kind, headline, questions[], summary.
task: "review"
verdict (low|medium|high), verdict_why, risks[{risk, level: low|medium|high|n/a, evidence[], missing, note}] (seven, in order: demand, positioning, monetization, retention, trust, distribution, adoption), main_assumption{risk, statement, why}, evidence_first{signal, why}, do_next{step, scope}, delay[{item, until}], lowers_risk[{quote, why}], engineering_note.
task: "test"
assumption{risk, statement, source}, method, method_why, audience{who, where, how_many}, steps[], success{metric, threshold}, kill{metric, threshold}, timebox, cost, asset{kind, text}, build_now[], still_delay[].
Worked examples
These are the page's own bundled examples (invented companies), with the input as app.js builds it and the saved model reply the page replays for free.
Review: an invoice-chasing SaaS for freelancers
Input (brief and facts shortened here):
{
"task": "review",
"brief": "# Dunwick - automatic invoice follow-ups for freelancers\n\nEvery freelancer hates chasing late invoices. It is a huge market: there are 70 million freelancers in the US alone, and obviously all of them get paid late at some point. Nobody is doing this well.\n\n## The idea\n\nDunwick connects to your bank account and your invoicing tool, watches for invoices that go past due, and sends polite, escalating reminder emails written by AI in your own tone of voice. When the client pays, Dunwick notices the... (1633 characters in all)",
"kind": "product",
"facts": "(a JSON string; decoded: kind product, floor high, risks named: demand, monetization, retention, trust, distribution; backed: positioning; na: adoption, 5 claims, 1 beliefs, 9 planned items, flags PB-02, PB-09, PB-04, PB-06, PB-07, PB-12, PB-10)",
"note": "We are two people with about ten weeks of savings each."
}
Reply:
{
"task": "review",
"kind": "product",
"verdict": "high",
"verdict_why": "The only behavioural evidence is the founder's own Friday spreadsheet, while the plan is a four-month, nine-part build that outlasts the savings in the note.",
"risks": [
{
"risk": "demand",
"level": "high",
"evidence": [
"I chase invoices manually in a spreadsheet every Friday",
"Every freelancer hates chasing late invoices.",
"obviously all of them get paid late at some point"
],
"missing": "A count of freelancers other than the founder who chase late invoices every week or month and already spend time or money on it.",
"note": "One person's workaround is real but tiny evidence, and the 70 million figure is an unsourced claim, not a count of people with this problem."
},
{
"risk": "positioning",
"level": "high",
"evidence": [
"Nobody is doing this well.",
"automatic invoice follow-ups for freelancers"
],
"missing": "What freelancers use today besides a manual spreadsheet, including the reminder settings inside their invoicing tools, and why they would switch.",
"note": "The brief names no alternative to beat, so it cannot yet say why Dunwick wins against doing nothing or using what freelancers already have."
},
{
"risk": "monetization",
"level": "high",
"evidence": [
"We think freelancers will happily pay $19/month for this"
],
"missing": "Any freelancer, other than the founder, who has paid or pre-paid $19/month, or who pays for a comparable tool today.",
"note": "The price is a belief from the scan, not a
... (shortened here; the page shows it whole)
Test plan: the same brief, carrying the review
Input (with the review handed over):
{
"task": "test",
"brief": "# Dunwick - automatic invoice follow-ups for freelancers\n\nEvery freelancer hates chasing late invoices. It is a huge market: there are 70 million freelancers in the US alone, and obviously all of them get paid late at some point. Nobody is doing this well.\n\n## The idea\n\nDunwick connects to your bank account and your invoicing tool, watches for invoices that go past due, and sends polite, escalating reminder emails written by AI in your own tone of voice. When the client pays, Dunwick notices the... (1633 characters in all)",
"kind": "product",
"facts": "(a JSON string; decoded: kind product, floor high, risks named: demand, monetization, retention, trust, distribution; backed: positioning; na: adoption, 5 claims, 1 beliefs, 9 planned items, flags PB-02, PB-09, PB-04, PB-06, PB-07, PB-12, PB-10)",
"note": "We are two people with about ten weeks of savings each.",
"assumption": "Freelancers other than the founder chase late invoices often enough that they will pay $19/month to have the reminders sent for them.",
"review_handoff": "{\"verdict\":\"high\",\"main_assumption\":{\"risk\":\"demand\",\"statement\":\"Freelancers other than the founder chase late invoices often enough that they will pay $19/month to have the reminders sent for them.\"},\"evidence_first\":\"Freelancers outside the founder's own practice handing over real past-due invoices and paying $19 fo..."
}
Reply:
{
"task": "test",
"kind": "product",
"assumption": {
"risk": "demand",
"statement": "Freelancers other than the founder chase late invoices often enough that they will pay $19/month to have the reminders sent for them.",
"source": "review"
},
"method": "presale",
"method_why": "The brief's only demand evidence is the founder's own Friday spreadsheet and a $19/month belief, so the test needs other freelancers paying real money for a hand-run service before any of the four-month build starts.",
"audience": {
"who": "Freelancers outside the founder's own practice, designers first, who have at least one invoice past due right now",
"where": "The freelancer communities the brief plans to launch in, plus the founder's own network from six years of freelance design work",
"how_many": "30 conversations"
},
"steps": [
"List 40 freelancers you can reach directly in [community or group names] and your own contacts, excluding close friends.",
"Send the outreach message below and book a 15-minute call with everyone who replies.",
"On each call, ask how they chased their last late invoice, then offer the $19 first month of reminders sent for them by hand.",
"For each person who pays, take one real past-due invoice and send the reminders yourself from plain templates, logging each send and payment in a spreadsheet.",
"After 10 working days, count paid presales and handed-over invoices against the thresholds below and decide go or stop."
],
"success": {
"metric": "Freelancers (not the founder) who pay $19 for a first month and hand over at least one real
... (shortened here; the page shows it whole)
Test plan: a waitlist page, with the assumption written by hand
Input:
{
"task": "test",
"brief": "# Fussbowl Suppers - waitlist page\n\nWe want to put up a landing page for Fussbowl Suppers, a weekly meal-plan subscription for parents of picky eaters: every Sunday you get five dinners that the whole family will eat, with a shopping list sorted by aisle.\n\nThe page will have a headline, three benefits, a sample week, and an email sign-up for the waitlist. We will share it in two parenting Facebook groups we belong to (about 18,000 members combined) and on our Instagram (900 followers).\n\nWe are n... (665 characters in all)",
"kind": "landing",
"facts": "(a JSON string; decoded: kind landing, floor medium, risks named: demand, monetization, retention; backed: positioning, distribution; silent: trust; na: adoption, 0 claims, 1 beliefs, 0 planned items, flags PB-02, PB-04, PB-10)",
"note": "No ad budget.",
"assumption": "Parents of picky eaters will pay for a weekly meal plan rather than use free recipe sites."
}
Reply:
{
"task": "test",
"kind": "landing",
"assumption": {
"risk": "monetization",
"statement": "Parents of picky eaters will pay for a weekly meal plan rather than use free recipe sites: at least 5 of every 100 waitlist sign-ups will put down a refundable paid deposit for the first month.",
"source": "given"
},
"method": "presale",
"method_why": "The brief says \"We are not sure yet whether parents would pay\", and a free email sign-up cannot answer that, so the page needs a paid step that only a willing payer will take.",
"audience": {
"who": "Parents in the two parenting Facebook groups and your Instagram followers who have a child they describe as a picky eater",
"where": "the two parenting Facebook groups we belong to and our Instagram, posted organically since there is no ad budget",
"how_many": "a proposed 100 waitlist sign-ups, plus 8 short calls with sign-ups who do not pay"
},
"steps": [
"Build the waitlist page as planned and add a second button after the email sign-up: \"Reserve a founding spot for [deposit amount], fully refundable\" linked to a [payment link].",
"Hand-make one real sample week of five dinners with an aisle-sorted shopping list for the page, so people judge the actual product.",
"Post once in each Facebook group (check each group's self-promotion rules first) and once on Instagram, then a single reminder post 5 days later.",
"Count page visits, email sign-ups and paid deposits each day in one sheet.",
"Message 8 sign-ups who did not pay and ask what they use for dinner planning now and what stopped them from reserving."
... (shortened here; the page shows it whole)
The page re-checks the reply; the API does not
The page holds every reply to account (recon.js): for a review, all seven risks once, every quote in the brief word for word, no low rating without a quote, no flagged claim counted as evidence, the verdict against the ratings and the evidence floor, the main assumption not on a low risk, something delayed, and no market figure the brief does not contain; for a test plan, thresholds with numbers and the kill line below the success line, a timebox of six weeks or less, and nothing built that the review delayed. Over the API you get the model's reply as it is; run the same checks yourself with Recon.reconcile in recon.js, or paste the same brief into the page.
Step 1 — a tiny client helper
One function that sends JSON and unwraps the envelope.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here" # from https://prebuild-desk.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://prebuild-desk.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"]
// prebuild-desk.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://prebuild-desk.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://prebuild-desk.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
}
// PrebuildDesk.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 PrebuildDesk {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://prebuild-desk.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://prebuild-desk.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://prebuild-desk.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://prebuild-desk.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":"prebuild-desk"}' | 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": "prebuild-desk"})["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: "prebuild-desk" })).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": "prebuild-desk"}, 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", "prebuild-desk"), 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: "prebuild-desk" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "prebuild-desk"])["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 = "prebuild-desk" }))!["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 (not wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model reads gpt-5.6-terra and model_alias gpt-terra.
# input.json: the run input object - {"task": "review", "brief": "...", "kind": "product", "facts": "..."}
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": "review", "brief": "...", "kind": "product", "facts": "...", ...}
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, brief, kind, facts, ...}
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 prebuild-desk:<lane>:<inputHash>:a<attempt>. The job's output.output is the reply text; truncated: true means the balance cut it short.
KEY="prebuild-desk:review:$(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 lane's contract
echo "$J" | jq '.data | {charged_credits, truncated}'
import hashlib, time
key = "prebuild-desk:" + 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 lane's JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("verdict"))
import { createHash } from "node:crypto";
const key = `prebuild-desk:${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);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "prebuild-desk:" + 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 = "prebuild-desk:" + 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 = "prebuild-desk:#{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"]].inspect
$key = "prebuild-desk:" . $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"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"prebuild-desk:{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 (test plan)
The same run as Server-Sent Events. test.json is a test-plan input, with review_handoff when it follows a review.
# 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: prebuild-desk:test:$(shasum -a 256 test.json | cut -c1-16):a1" --data @test.json
rev = json.load(open("test.json")) # {"task": "test", "brief": "...", "kind": "...", "facts": "...", "assumption": "...", "review_handoff": "..."}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "prebuild-desk:test:" + 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("test.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": `prebuild-desk:test:${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("test.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", "prebuild-desk:test:"+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("test.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", "prebuild-desk:test:" + 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("test.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "prebuild-desk:test:#{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("test.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: prebuild-desk:test:" . 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("test.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", $"prebuild-desk:test:{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);