Drive Patch the Hull from your own code
The one metered task, patch-plan, is available over HTTP. It takes one website's answers - a short label, the PHP version as the hosting panel shows it, hosting, who changes the server, backup, whether a short outage is fine, and whether visitors log in or pay - and returns a patch-night runbook: 3 to 6 steps naming who does each one, what to do if it stalls, how to recover, and a note for the person who changes the server.
The division of labour first: the page decides; the model words it. The page reads the version, picks the verdict against the fixed builds published on 21 October 2026 for CVE-2026-109618 and CVE-2026-109619, works out the target, whose job the server steps are and whether a note is needed, and sends those as facts. The model never changes the verdict. Not professional security 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":"patch-the-hull"}), 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, site, 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: the reasons are metered and need 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 verdicts
Every run sends task: patch-plan (the one task is patch-plan); a missing or different one is still answered as patch-plan. All fields are strings.
| Field | Required | What it holds |
|---|---|---|
task | yes | Always patch-plan. |
site | yes | One line: site: ... | version as given: ... | hosting: ... | who changes the server: ... | backup: ... | a short outage: ... | visitors log in or pay: ... | notes: ..., with "not given" for blanks. |
facts | yes | The page's lines: the verdict id and its reason, the target, the version it read (flagged when it looks like a distribution or panel build), whose job the server steps are, whether a note is needed and for whom, and the published fixed builds. |
retry_note | no | Only on a reformat retry: why the previous reply was unusable. |
The verdict, first match wins: no full version the page can read (blank, unreadable, a line that never existed, a line above 8.5, or 8.2 to 8.5 without its build number) is find-version; a line older than 5.4 is upgrade-line; 8.2 to 8.5 at or past the fixed build (8.2.35, 8.3.36, 8.4.27, 8.5.12) is patched, below it update-now; 7.4, 8.0 and 8.1 (source patches only) are backport-or-upgrade; 5.4 to 7.3 (no fix) are upgrade-line.
The reply
One JSON object, no prose around it:
{"task": "patch-plan", "verdict": "update-now", "headline": "...", "steps": [{"who": "host", "do": "...", "check": "..."}], "escalate": "...", "recover": "...", "note": "...", "closing": "..."}
| Field | What it holds |
|---|---|
verdict | The verdict id from facts, copied exactly. |
headline | One sentence, at most 24 words, on what the verdict means for this site. |
steps[] | 3 to 6 steps in order: who (you, web person or host), do (one action, at most 40 words) and check (how that person knows it is done, at most 20 words). |
escalate, recover | What to do if a step stalls, and if the site misbehaves after the change. |
note | A message of at most 100 words for the person who changes the server; empty when the owner changes it. |
closing | One sentence that agrees with the verdict. |
The page then checks the reply (HullKit.check) - the verdict against its own, the step count, who each step is for, the note, build numbers that are neither published fixed builds nor yours, time or day words you did not give, talk of attacks you did not mention, products you did not name, and contact details or public posting - and lists anything it finds under "The page's check".
Worked examples - all five verdicts
These are the 5 free samples on the page: the exact input the page sends and the reply that replays for free. Together they cover all five verdicts.
Bakery orders - verdict: update now
Input:
{
"task": "patch-plan",
"site": "site: Bakery order site | version as given: 8.3.14 | hosting: Shared hosting (a control panel) | who changes the server: The hosting company | backup: Yes, never tried restoring | a short outage: Needs a quiet slot | visitors log in or pay: Yes | notes: customers pay for cake orders on it",
"facts": "verdict: update-now (8.3.14 is below the fixed build 8.3.36)\ntarget: 8.3.36 or later\nversion read: 8.3.14\nserver steps belong to: host\nnote for the person who changes the server: write one, addressed to the hosting company\npublished fixed builds: 8.2.35, 8.3.36, 8.4.27, 8.5.12; 7.4, 8.0 and 8.1 source patches only; 5.4 to 7.3 no fix"
}
Reply:
{
"task": "patch-plan",
"verdict": "update-now",
"headline": "The Bakery order site is running PHP 8.3.14, which is below the fixed build, so it needs updating to 8.3.36 or later.",
"steps": [
{
"who": "you",
"do": "Tell the hosting company the Bakery order site's PHP needs to move from 8.3.14 to 8.3.36 or later, share the prepared note, and agree a quiet slot for the change.",
"check": "The hosting company has confirmed the target build and a quiet slot."
},
{
"who": "host",
"do": "Update the site's PHP to 8.3.36 or later during the agreed quiet slot.",
"check": "The hosting control panel shows PHP 8.3.36 or later."
},
{
"who": "you",
"do": "After the change, open the main pages and place a test cake order to confirm payment still works.",
"check": "The main pages load and the test order payment goes through."
},
{
"who": "you",
"do": "Tell the hosting company the checks passed, or describe any problems you find instead.",
"check": "The hosting company has your confirmation or problem report."
}
],
"escalate": "If the hosting company does not act or the quiet slot passes without a change, contact them again and ask for a new quiet slot and a status update.",
"recover": "If the site misbehaves after the change, ask the hosting company to restore it from the backup and confirm the PHP version again before trying the update once more.",
"note": "To the hosting company: the Bakery order site is on PHP 8.3.14, which is below the fixed build. Please update it to 8.3.36 or later. Customers pay for cake orders on this site, so please check that ordering and payment still work after the change. A backup already exists; please make sure it stays safe during the update. Please agree a quiet slot for the change with the site owner first, since a short outage is not fine. Thank you.",
"closing": "Moving the Bakery order site to PHP 8.3.36 or later closes the known gap once the hosting company completes the update."
}
Choir newsletter - verdict: backport or move up
Input:
{
"task": "patch-plan",
"site": "site: Choir newsletter site | version as given: 7.4.33 | hosting: Managed hosting | who changes the server: A web person (freelancer or agency) | backup: Yes, and a restore has worked | a short outage: A short outage is fine | visitors log in or pay: No | notes: a freelancer built it and still looks after it",
"facts": "verdict: backport-or-upgrade (line 7.4 got a source patch but no official fixed build)\ntarget: a 7.4 build carrying the backported fix, or a move to 8.2.35, 8.3.36, 8.4.27, 8.5.12 or later\nversion read: 7.4.33\nserver steps belong to: web person\nnote for the person who changes the server: write one, addressed to the web person\npublished fixed builds: 8.2.35, 8.3.36, 8.4.27, 8.5.12; 7.4, 8.0 and 8.1 source patches only; 5.4 to 7.3 no fix"
}
Reply:
{
"task": "patch-plan",
"verdict": "backport-or-upgrade",
"headline": "The Choir newsletter site's PHP line only has a source patch, so it needs a backported build or a move to a fixed build.",
"steps": [
{
"who": "you",
"do": "Send your web person the note below, asking them to choose a backported build of the current PHP line or a move to one of the newer fixed builds.",
"check": "Your web person confirms they got the note and picked one of the two options."
},
{
"who": "web person",
"do": "If moving to a newer line, check the site's own software works with it first; if staying on the current line, prepare the backported build instead.",
"check": "The software check is finished or the backported build is ready."
},
{
"who": "web person",
"do": "Apply the backported build or move the server to the chosen fixed build; a short outage during this step is fine.",
"check": "The PHP build number now matches the option you chose."
},
{
"who": "you",
"do": "Open the site's main pages to make sure they load normally after the change.",
"check": "The main pages open without errors."
}
],
"escalate": "If your web person does not act, contact the hosting company to ask whether they can apply the change instead.",
"recover": "If the site behaves oddly after the change, ask your web person to restore it from the existing backup and confirm the main pages work again.",
"note": "To the web person: our PHP version is 7.4.33, on a line that only has a source-level patch, not an official build. Please either prepare a 7.4 build with the backported fix, or move the site to one of the newer fixed builds: 8.2.35, 8.3.36, 8.4.27, 8.5.12, or later. If you move to a newer line, please check the site's own software works with it first. A short outage during the change is fine. Please confirm once the build is updated.",
"closing": "Choosing a backported build or a move to a newer fixed build will bring the Choir newsletter site onto a supported PHP build."
}
Family photo blog - verdict: move to a supported line
Input:
{
"task": "patch-plan",
"site": "site: Family photo blog | version as given: 5.6.40 | hosting: Our own server or VPS | who changes the server: Me | backup: No backup | a short outage: A short outage is fine | visitors log in or pay: No | notes: I set the server up myself years ago",
"facts": "verdict: upgrade-line (line 5.6 is in the affected range and got no fix)\ntarget: a move to a supported line: 8.2.35, 8.3.36, 8.4.27, 8.5.12 or later\nversion read: 5.6.40\nserver steps belong to: you\nnote for the person who changes the server: none (they change it themselves)\npublished fixed builds: 8.2.35, 8.3.36, 8.4.27, 8.5.12; 7.4, 8.0 and 8.1 source patches only; 5.4 to 7.3 no fix"
}
Reply:
{
"task": "patch-plan",
"verdict": "upgrade-line",
"headline": "The Family photo blog is running PHP 5.6, a line with no fix, so it needs to move to a newer supported line.",
"steps": [
{
"who": "you",
"do": "Make a backup of the site and its files before you change anything on the server.",
"check": "A backup copy of the site exists that you have confirmed."
},
{
"who": "you",
"do": "Check whether the site's own software still works on a newer PHP line, and update it first if it does not.",
"check": "The site's own software is confirmed to work, or has been updated."
},
{
"who": "you",
"do": "Move the server to one of the supported lines - 8.2.35, 8.3.36, 8.4.27, 8.5.12 or later; a short outage during this step is fine.",
"check": "The server reports one of the new build numbers instead of 5.6.40."
},
{
"who": "you",
"do": "Confirm the new PHP build is active on the server after the move.",
"check": "The reported build matches the one you moved to."
},
{
"who": "you",
"do": "Open the main pages of the Family photo blog and check that they load as expected.",
"check": "The main pages open and look the same as before."
}
],
"escalate": "Since you are the one who changes the server, if you get stuck on a step, pause and work out the issue fully before moving on, rather than guessing.",
"recover": "If the Family photo blog does not work properly after the move, stop making further changes and check the site's own software for errors until you find the cause.",
"note": "",
"closing": "Moving the Family photo blog to a newer supported PHP line is the way to address the line with no fix."
}
Allotment society - verdict: fixed build
Input:
{
"task": "patch-plan",
"site": "site: Allotment society site | version as given: 8.4.27 | hosting: Shared hosting (a control panel) | who changes the server: The hosting company | backup: Yes, and a restore has worked | a short outage: A short outage is fine | visitors log in or pay: Yes | notes: members log in to book plots",
"facts": "verdict: patched (8.4.27 is at or past the fixed build 8.4.27)\ntarget: stay on 8.4.27 or later\nversion read: 8.4.27\nserver steps belong to: host\nnote for the person who changes the server: write one, addressed to the hosting company\npublished fixed builds: 8.2.35, 8.3.36, 8.4.27, 8.5.12; 7.4, 8.0 and 8.1 source patches only; 5.4 to 7.3 no fix"
}
Reply:
{
"task": "patch-plan",
"verdict": "patched",
"headline": "The Allotment society site is already on PHP build 8.4.27, a published fixed build, so no update is required.",
"steps": [
{
"who": "you",
"do": "Check the PHP version shown in the hosting control panel for the Allotment society site and confirm it reads build 8.4.27 or later.",
"check": "The control panel shows PHP build 8.4.27 or later."
},
{
"who": "you",
"do": "Check that backups of the Allotment society site continue to be made and that a test restore still works.",
"check": "A backup exists and a test restore has worked."
},
{
"who": "you",
"do": "Send the note below to the hosting company so they know to keep the site on its current fixed build.",
"check": "The note has been sent to the hosting company."
},
{
"who": "host",
"do": "Keep the Allotment society site on PHP build 8.4.27, or move it only to a later published fixed build, during any server work.",
"check": "The hosting company confirms this in writing."
},
{
"who": "you",
"do": "Check php.net's release page for the 8.4 line for any new security releases after build 8.4.27.",
"check": "You have looked at php.net's 8.4 release list."
}
],
"escalate": "If the hosting company does not reply to the note or confirm the build in writing, contact the hosting company again and ask for a written reply.",
"recover": "If anything on the site, such as the page where members log in to book plots, stops working, ask the hosting company to restore the latest backup, which has worked when tested.",
"note": "To the hosting company: the Allotment society site's PHP version has been checked and is already on build 8.4.27, which is a published fixed build, so no update is needed. Please keep the site on build 8.4.27, or move it only to a later published fixed build if any server changes are made. Please confirm this in writing. Thank you.",
"closing": "The Allotment society site is on a fixed PHP build, and following these steps will help keep it that way."
}
Book swap - verdict: find the version
Input:
{
"task": "patch-plan",
"site": "site: Book swap site | version as given: the panel just says 8.x | hosting: Not sure | who changes the server: Not sure | backup: Not sure | a short outage: Not sure | visitors log in or pay: Yes | notes: volunteers took it over from someone else",
"facts": "verdict: find-version (no version number the page could read)\ntarget: the full three-part version number, from the hosting panel or whoever runs the server\nversion read: none\nserver steps belong to: you (first find out who changes the server)\nnote for the person who changes the server: write one, addressed to the hosting company or web person, whichever changes it\npublished fixed builds: 8.2.35, 8.3.36, 8.4.27, 8.5.12; 7.4, 8.0 and 8.1 source patches only; 5.4 to 7.3 no fix"
}
Reply:
{
"task": "patch-plan",
"verdict": "find-version",
"headline": "The Book swap site's PHP version is not yet known well enough to say whether it needs an update.",
"steps": [
{
"who": "you",
"do": "Work out who changes the server for the site, such as the hosting company or someone who helps with it, since that is not sure yet.",
"check": "You know who to contact if a server change turns out to be needed."
},
{
"who": "you",
"do": "Check the hosting control panel's PHP settings page yourself for the full three-part version number, such as 8.x.xx, rather than just the line, 8.x.",
"check": "You see a three-part number, or the page does not show one."
},
{
"who": "you",
"do": "If the panel does not show the full number, send a short note to whoever changes the server asking for it.",
"check": "You have sent the note, or you already have the number."
},
{
"who": "you",
"do": "Find out whether a backup of the site exists, or ask for one to be made, since you are not sure one is in place.",
"check": "You know a backup exists, or one has just been made."
},
{
"who": "you",
"do": "Once you have the full version number, compare it with the published fixed builds (8.2.35, 8.3.36, 8.4.27, 8.5.12) to see whether it is at, above or below one of them.",
"check": "You know whether the version is at, above or below a fixed build."
},
{
"who": "you",
"do": "Once any future change is made, check that the pages where visitors log in or pay still work as before.",
"check": "Those pages load and let visitors log in or pay as usual."
}
],
"escalate": "If whoever changes the server does not reply, contact the hosting company directly to ask about the version and who can make changes to it.",
"recover": "If the site misbehaves after any future change, tell whoever changes the server so they can look into it and undo what they changed if needed.",
"note": "Hello. This note is for whoever changes the server for the Book swap site. I'm trying to find out the PHP version it is running, since the hosting panel only shows a partial number. Could you tell me the full three-part version number, such as 8.x.xx, and let me know whether you are able to make a server change if one turns out to be needed? Please do not send any passwords or login details, just the version number and your answer. Thank you for your help.",
"closing": "Finding the full version number is the first step toward knowing what, if anything, the Book swap site needs next."
}
Build the input yourself
Load the engine in node (global.window = global; require("./hullkit.js")), then:
const K = HullKit;
const form = K.form({ site: "Club booking site", version: "8.2.20", hosting: "shared", updater: "host",
backup: "tested", downtime: "quiet", visitors: "yes", notes: "" });
const built = K.buildInput(form); // built.facts holds the verdict, its reason, the target and whose job the server steps are
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.md(built, reply, K.title(form)) // the runbook as Markdown
The answer keys: hosting is shared, managed, server or unsure; updater is host, webperson, me or unsure; backup is tested, untested, none or unsure; downtime is ok, quiet or unsure; visitors is yes, no or unsure.
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://patch-the-hull.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://patch-the-hull.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"]
// patch-the-hull.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://patch-the-hull.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://patch-the-hull.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
}
// PatchTheHull.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 PatchTheHull {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://patch-the-hull.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://patch-the-hull.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://patch-the-hull.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://patch-the-hull.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":"patch-the-hull"}' | 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": "patch-the-hull"})["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: "patch-the-hull" })).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": "patch-the-hull"}, 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", "patch-the-hull"), 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: "patch-the-hull" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "patch-the-hull"])["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 = "patch-the-hull" }))!["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": "patch-plan", "site": "site: Bakery order site | version as given: 8.3.14 | ...", "facts": "...", ...}
# - 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": "patch-plan", "site": "site: Bakery order site | version as given: 8.3.14 | ...", "facts": "...", ...}
# - 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: "patch-plan", site, 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 patch-the-hull:<task>:<inputHash>:a<attempt> (the one task is patch-plan), 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="patch-the-hull:patch-plan:$(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 = "patch-the-hull:" + 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"), len(reply.get("steps", [])))
import { createHash } from "node:crypto";
const key = `patch-the-hull:${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.steps || []).length);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "patch-the-hull:" + 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 = "patch-the-hull:" + 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 = "patch-the-hull:#{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["steps"] || []).length].inspect
$key = "patch-the-hull:" . $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"], " ", count($reply["steps"] ?? []), "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"patch-the-hull:{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: patch-the-hull:patch-plan:$(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": "patch-plan", "site": "site: Bakery order site | version as given: 8.3.14 | ...", "facts": "...", ...}
# - 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": "patch-the-hull:patch-plan:" + 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": `patch-the-hull:patch-plan:${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", "patch-the-hull:patch-plan:"+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", "patch-the-hull:patch-plan:" + 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" => "patch-the-hull:patch-plan:#{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: patch-the-hull:patch-plan:" . 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", $"patch-the-hull:patch-plan:{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);