Drive Melt Pool from your own code
The one metered task, diagnose, is available over HTTP. It takes 1 to 5 jar candles - the page's shelf lines (width, wicks, wax left, usual burn, the full-pool time, whether the jar is too wide and the burns too short or too long, the hours and evenings left above the stop line as an estimate, what was seen and the verdict) and the person's note on each candle - and returns a headline, 1 card per candle (the verdict, a cause for every symptom, the root cause, the fix at that root, the patch to skip and the next burn) and a closing line.
The division of labour first: the page does the arithmetic and a fixed rule decides each verdict; the model walks a fixed list of causes per symptom, reads the notes and writes the fixes. After a run the page checks that every verdict follows the rule, that every symptom has a cause the page's order allows, that each root cause follows from the causes given, that every number in the prose is on the shelf or in the notes, that word limits hold, and that no ids leak into the prose.
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":"melt-pool"}), 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, shelf, notes) 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: a reading is metered and needs a personal sign-in. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
The task field, the input and the verdicts
Every run sends task: diagnose (the one task is diagnose); a missing or different one is still answered as diagnose. Full pool: 1 h per 2.5 cm of width per wick, rounded to the half hour, at least 1 h and at most 4 h; more than 10 cm of width per wick is too wide; a usual burn under the full-pool time is a short burn, over 4 h a long burn. The verdict rule per candle, checked in order: 1.3 cm of wax or less, retire; anything seen, too wide, short burns or long burns, fix-first; otherwise keep-burning.
| Field | Required | What it holds |
|---|---|---|
task | yes | Always diagnose. |
shelf | yes | The page's lines: a house numbers: line, then 1 line per candle such as candle 1 | Fig jar | 8 cm wide | 1 wick | 6 cm of wax left | usual burn 1 h | full pool needs 3 h | too wide: no | short burns: yes | long burns: no | about 42 h and 14 evenings above the stop line (estimate) | seen: tunnel | verdict: fix-first. |
notes | yes | 1 line per candle, candle 1 | text or candle 1 | none. |
retry_note | no | Only on the page's single reformat retry. |
Symptoms are tunnel, soot, big-flame, drowning and mushroom. Each has a fixed list of causes, walked in order, first that holds: tunnel - too-wide, short-burns, draught, unclear; soot and big-flame - long-wick, draught, long-burns, unclear; drowning - short-wick, long-burns, unclear; mushroom - long-burns, long-wick, unclear. Shelf flags decide too-wide, short-burns and long-burns; the note decides the rest.
The reply is one JSON object:
{"task": "diagnose", "headline": "...", "candles": [{"candle": "...", "verdict": "fix-first", "causes": [{"symptom": "tunnel", "cause": "short-burns", "why": "..."}], "root_cause": "short-burns", "root_fix": "...", "skip": "...", "next_burn": "..."}, ...], "closing": "..."}
| Field | What it holds |
|---|---|
headline | How many candles have each verdict and the most common root cause, at most 18 words. |
candles | 1 per candle in shelf order: candle (the name), verdict (copied), causes (1 per symptom, only for fix-first; each with symptom, cause and why, at most 16 words), root_cause (stop-line, none, a cause id or unclear), root_fix (at most 30 words), skip (the patch to skip, at most 16 words, empty for keep-burning) and next_burn (at most 22 words). |
closing | One light next step, at most 20 words. |
The page then checks the reply (MeltKit.check) and lists any disagreement under "The page's check".
Worked examples - every verdict
These are the four free samples on the page: the exact input the page sends and the reply that replays for free.
Mantel of 3, one tunnelling - fix-first, keep-burning and retire
Input:
{
"task": "diagnose",
"shelf": "house numbers: full pool = 1 h per 2.5 cm of width per wick, at least 1 h, at most 4 h | stop line = 1.3 cm of wax left | trim the wick to 6 mm\ncandle 1 | Fig and cedar jar | 8 cm wide | 1 wick | 6 cm of wax left | usual burn 1 h | full pool needs 3 h | too wide: no | short burns: yes | long burns: no | about 42 h and 14 evenings above the stop line (estimate) | seen: tunnel | verdict: fix-first\ncandle 2 | Three-wick tin | 12 cm wide | 3 wicks | 4 cm of wax left | usual burn 2 h | full pool needs 1.5 h | too wide: no | short burns: no | long burns: no | about 18 h and 12 evenings above the stop line (estimate) | seen: none | verdict: keep-burning\ncandle 3 | Old pillar jar | 7 cm wide | 1 wick | 1 cm of wax left | usual burn 3 h | full pool needs 3 h | too wide: no | short burns: no | long burns: no | at or below the stop line | seen: soot | verdict: retire",
"notes": "candle 1 | lit for an hour after work each time, wax stays stuck to the sides\ncandle 2 | melts evenly right to the edge\ncandle 3 | glass has gone black near the top"
}
Reply:
{
"task": "diagnose",
"headline": "1 candle needs fixing, 1 should retire, 1 is burning well; too short a burn is the cause.",
"candles": [
{
"candle": "Fig and cedar jar",
"verdict": "fix-first",
"causes": [
{
"symptom": "tunnel",
"cause": "short-burns",
"why": "Usual burn is 1 h, short of the 3 h full pool needs."
}
],
"root_cause": "short-burns",
"root_fix": "Let it burn for the full 3 h pool time each time, until the wax melts all the way to the edge.",
"skip": "Skip adding more short sessions instead of letting it run the full pool time.",
"next_burn": "Next time, let it burn for the full 3 h pool time before putting it out."
},
{
"candle": "Three-wick tin",
"verdict": "keep-burning",
"causes": [],
"root_cause": "none",
"root_fix": "Nothing here needs fixing; it is burning the way it should.",
"skip": "",
"next_burn": "Next time, light it and let it burn as usual; no changes are needed."
},
{
"candle": "Old pillar jar",
"verdict": "retire",
"causes": [],
"root_cause": "stop-line",
"root_fix": "Stop lighting it; the wax has reached the stop line, so the glass could overheat, and you can clean out the empty jar to use it again.",
"skip": "Skip relighting it to squeeze out whatever wax remains at the bottom.",
"next_burn": "Do not light it again; clean the jar and set it aside."
}
],
"closing": "Next time you light Fig and cedar jar, let it run the full 3 h pool time."
}
By the window, soot - a jar too wide, causes from a note
Input:
{
"task": "diagnose",
"shelf": "house numbers: full pool = 1 h per 2.5 cm of width per wick, at least 1 h, at most 4 h | stop line = 1.3 cm of wax left | trim the wick to 6 mm\ncandle 1 | Big bowl candle | 14 cm wide | 1 wick | 7 cm of wax left | usual burn 4 h | full pool needs 4 h | too wide: yes | short burns: no | long burns: no | about 157 h and 39 evenings above the stop line (estimate) | seen: tunnel | verdict: fix-first\ncandle 2 | Vanilla tumbler | 8 cm wide | 1 wick | 5 cm of wax left | usual burn 3 h | full pool needs 3 h | too wide: no | short burns: no | long burns: no | about 33 h and 11 evenings above the stop line (estimate) | seen: soot, big-flame | verdict: fix-first",
"notes": "candle 1 | deep hole in the middle, thick wall of wax all round\ncandle 2 | sits on the sill by the open window, never trimmed the wick"
}
Reply:
{
"task": "diagnose",
"headline": "2 candles need fixing, and being too wide is the main cause so far.",
"candles": [
{
"candle": "Big bowl candle",
"verdict": "fix-first",
"causes": [
{
"symptom": "tunnel",
"cause": "too-wide",
"why": "Shelf marks it too wide; 14 cm across for 1 wick is a lot to melt."
}
],
"root_cause": "too-wide",
"root_fix": "Its 1 wick can't melt 14 cm in 4 h; a foil collar open at the center can help the edges soften while you watch it, though tunnelling may continue.",
"skip": "Skip letting it run well past 4 h just hoping the edge finally melts.",
"next_burn": "Next time, add a foil collar open in the middle and still put it out by 4 h."
},
{
"candle": "Vanilla tumbler",
"verdict": "fix-first",
"causes": [
{
"symptom": "soot",
"cause": "long-wick",
"why": "The note says the wick is never trimmed, so it stays long."
},
{
"symptom": "big-flame",
"cause": "long-wick",
"why": "It is never trimmed, so the wick burns taller than it should, feeding a bigger flame."
}
],
"root_cause": "long-wick",
"root_fix": "Trim the wick down to 6 mm before each time you light it, so the flame stays a sensible size.",
"skip": "Skip wiping soot off the glass while the wick keeps its current length.",
"next_burn": "Next time, trim the wick to 6 mm before lighting it."
}
],
"closing": "Next time you light Big bowl candle, try a foil collar and stop it by 4 h."
}
Long evenings, drowning wick - long burns and a tie for the root
Input:
{
"task": "diagnose",
"shelf": "house numbers: full pool = 1 h per 2.5 cm of width per wick, at least 1 h, at most 4 h | stop line = 1.3 cm of wax left | trim the wick to 6 mm\ncandle 1 | Amber travel tin | 6 cm wide | 1 wick | 3 cm of wax left | usual burn 5 h | full pool needs 2.5 h | too wide: no | short burns: no | long burns: yes | about 8 h and 3 evenings above the stop line (estimate) | seen: drowning, mushroom | verdict: fix-first\ncandle 2 | Sea salt jar | 9 cm wide | 2 wicks | 8 cm of wax left | usual burn 2 h | full pool needs 2 h | too wide: no | short burns: no | long burns: no | about 38 h and 19 evenings above the stop line (estimate) | seen: none | verdict: keep-burning",
"notes": "candle 1 | I trim the wick right down to the wax before lighting\ncandle 2 | none"
}
Reply:
{
"task": "diagnose",
"headline": "1 candle needs fixing and 1 is burning well; a wick trimmed too short is the cause.",
"candles": [
{
"candle": "Amber travel tin",
"verdict": "fix-first",
"causes": [
{
"symptom": "drowning",
"cause": "short-wick",
"why": "The note says the wick is trimmed right down to the wax before lighting."
},
{
"symptom": "mushroom",
"cause": "long-burns",
"why": "Usual burn is 5 h, well beyond the 4 h house cap."
}
],
"root_cause": "short-wick",
"root_fix": "Stop trimming the wick down to the wax; leaving about 6 mm may help, and if drowning continues, this wick could be too small for a 6 cm jar.",
"skip": "Skip repeatedly relighting a wick that has sunk beneath the wax surface.",
"next_burn": "Next time, trim the wick to about 6 mm, not down to the wax, before lighting."
},
{
"candle": "Sea salt jar",
"verdict": "keep-burning",
"causes": [],
"root_cause": "none",
"root_fix": "There is nothing to fix here; it is burning the way it should.",
"skip": "",
"next_burn": "Next time, light it and let it burn as you have been; nothing needs to change."
}
],
"closing": "Next time you light Amber travel tin, trim the wick to about 6 mm, not down to the wax."
}
New candle, not sure why - an unclear cause, a fix with nothing seen
Input:
{
"task": "diagnose",
"shelf": "house numbers: full pool = 1 h per 2.5 cm of width per wick, at least 1 h, at most 4 h | stop line = 1.3 cm of wax left | trim the wick to 6 mm\ncandle 1 | Birthday gift candle | 10 cm wide | 1 wick | 9 cm of wax left | usual burn 4 h | full pool needs 4 h | too wide: no | short burns: no | long burns: no | about 108 h and 27 evenings above the stop line (estimate) | seen: big-flame | verdict: fix-first\ncandle 2 | Small votive jar | 5 cm wide | 1 wick | 4 cm of wax left | usual burn 1 h | full pool needs 2 h | too wide: no | short burns: yes | long burns: no | about 9 h and 4 evenings above the stop line (estimate) | seen: none | verdict: fix-first",
"notes": "candle 1 | none\ncandle 2 | only lit while cooking dinner"
}
Reply:
{
"task": "diagnose",
"headline": "2 candles need fixing; burning for too short a spell is the main cause so far.",
"candles": [
{
"candle": "Birthday gift candle",
"verdict": "fix-first",
"causes": [
{
"symptom": "big-flame",
"cause": "unclear",
"why": "Nothing on the shelf or in the note yet points to why the flame is large."
}
],
"root_cause": "unclear",
"root_fix": "Hold off on any changes for now; watch closely on the next burn for whatever sign points to what is causing the large flame.",
"skip": "Skip jumping to a fix before you have had a chance to watch it burn again.",
"next_burn": "Next time, notice how long the wick is before lighting, whether air moves near it, and how long it burns."
},
{
"candle": "Small votive jar",
"verdict": "fix-first",
"causes": [],
"root_cause": "short-burns",
"root_fix": "Let Small votive jar run for its full 2 h pool time each lighting, rather than stopping after 1 h.",
"skip": "Skip squeezing in another quick burn at dinner time for the same short stretch.",
"next_burn": "Next time, let it burn for the full 2 h pool time instead of stopping after 1 h."
}
],
"closing": "Next time you light Birthday gift candle, watch closely for what might be causing the large flame."
}
Build the input yourself
Load the engine in node (global.window = global; require("./meltkit.js")), then:
const K = MeltKit;
const built = K.buildInput(K.form({ candles: [
{ name: "Fig jar", width: 8, left: 6, wicks: 1, burn: 1, seen: { tunnel: true }, notes: "wax stays on the sides" },
{ name: "Tin", width: 12, left: 4, wicks: 3, burn: 2, seen: {}, notes: "" }] }));
built.input.shelf; // the page's lines: numbers, flags and the verdict per candle
const wire = K.toWire(built.input); // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// built.facts.list.map(c => c.verdict) // the verdicts the page's rule gives
// K.check(reply, built).notes // the page's re-check of the reply
// K.md(built, reply, "my shelf") // the reading as Markdown
Step 1 — a tiny client helper
One function that sends JSON and unwraps the envelope. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here" # from https://melt-pool.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://melt-pool.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"]
// melt-pool.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://melt-pool.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://melt-pool.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
}
// WhatWasThat.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 WhatWasThat {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://melt-pool.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://melt-pool.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://melt-pool.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://melt-pool.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":"melt-pool"}' | 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": "melt-pool"})["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: "melt-pool" })).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": "melt-pool"}, 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", "melt-pool"), 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: "melt-pool" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "melt-pool"])["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 = "melt-pool" }))!["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": "diagnose", "shelf": "house numbers: full pool = 1 h per 2.5 cm of width per wick, ...\ncandle 1 | Birthday gift candle | 10 cm wide | 1 wick | ... | seen: big-flame | verdict: fix-first\n...", "notes": "candle 1 | none\ncandle 2 | only lit while cooking dinner"}
# - 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": "diagnose", "shelf": "house numbers: full pool = 1 h per 2.5 cm of width per wick, ...\ncandle 1 | Birthday gift candle | 10 cm wide | 1 wick | ... | seen: big-flame | verdict: fix-first\n...", "notes": "candle 1 | none\ncandle 2 | only lit while cooking dinner"}
# - 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: "diagnose", shelf, notes} (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 melt-pool:<task>:<inputHash>:a<attempt> (the one task is diagnose), 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="melt-pool:diagnose:$(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 = "melt-pool:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the reply as one JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("verdict"))
import { createHash } from "node:crypto";
const key = `melt-pool:${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 := "melt-pool:" + 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 = "melt-pool:" + 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 = "melt-pool:#{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 = "melt-pool:" . $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 = $"melt-pool:{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: melt-pool:diagnose:$(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": "diagnose", "shelf": "house numbers: full pool = 1 h per 2.5 cm of width per wick, ...\ncandle 1 | Birthday gift candle | 10 cm wide | 1 wick | ... | seen: big-flame | verdict: fix-first\n...", "notes": "candle 1 | none\ncandle 2 | only lit while cooking dinner"}
# - 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": "melt-pool:diagnose:" + 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": `melt-pool:diagnose:${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", "melt-pool:diagnose:"+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", "melt-pool:diagnose:" + 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" => "melt-pool:diagnose:#{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: melt-pool:diagnose:" . 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", $"melt-pool:diagnose:{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);