Drive Pucker Check from your own code
The one metered task, check, is available over HTTP. It takes fruit_count and fruits - one numbered line per persimmon with its label, shape, feel, colour, skin, days on the counter and note, exactly as the page writes it. It returns one entry per fruit (verdict, claim, evidence for and against, a test and a next step), the one to eat first and a ripening tip, or a polite decline for health, baby and pet questions.
The division of labour first: the page works out its own verdict per fruit by a fixed rule; the model checks the claim and writes the words; the page then re-checks every verdict, the count, the one to eat first (softest ready fruit, then a split skin, then more days, then the lower number) and the word limits. Build fruits the same way (see "Build the input yourself") so the lines match.
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": "...", "status": 402, "details": { ... } } }
The token is minted for this app (the guest call takes {"slug":"pucker-check"}), 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, fruit_count, fruits) 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: checking a tray is metered and needs a personal token. |
| 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 status and the verdicts
Every run sends task: check (the one task is check); a missing or different one is still answered as check. status is declined when any note asks a health or medical question or about feeding the fruit to a baby, a child or a pet. Otherwise each fruit gets a verdict, checked in this order and stopping at the first match: past_it (mould, or a note about mildew, fuzz, sliminess, rot, fizz or a sour, boozy, alcohol, vinegar, bad or off smell); ready (pointed astringent kind that is jelly-soft and deep orange or dark red-orange; squat non-astringent kind that is not hard and not green; kind not known but jelly-soft and deep in colour); unsure (kind not known, firm to soft, not green); not_yet (everything else).
Input (every field is a string)
| Field | Required | What it holds |
|---|---|---|
task | yes | Always check. |
fruit_count | yes | How many persimmons, 1 to 6, as text. |
fruits | yes | One numbered line per fruit, fields separated by | : the label (the person's own words, up to 40 characters, or Fruit N), shape:, feel:, colour:, skin:, days on the counter: (a number or not given) and note: (up to 100 characters, or none), worded exactly as the page writes them. |
retry_note | no | Only on the page's one reformat retry, after an unusable reply. |
Reply (one JSON object)
{"task": "check", "status": "ok", "reason": "",
"fruits": [{"n": 1, "verdict": "ready", "claim": "...", "for": "...", "against": "", "test": "...", "next": "..."}, ...],
"eat_first": 1, "eat_first_why": "...", "tip": "", "closing": "..."}
| Field | What it holds |
|---|---|
status, reason | ok or declined; for declined, one sentence of at most 25 words, fruits empty, eat_first 0 and every other field empty. |
fruits | Exactly fruit_count entries in input order. Each: n, verdict (ready, not_yet, unsure or past_it), claim (the claim checked, at most 16 words), for and against (facts from that fruit's own line, at most 20 words each, empty when none), test (one hands-on check before biting, at most 18 words) and next (what to do now, at most 18 words). |
eat_first, eat_first_why | The n of the softest ready fruit (ties: a split skin, then more days on the counter, then the lower number), or 0 when none is ready; and the deciding facts in at most 20 words. |
tip | A ripening tip of at most 24 words, only when some fruit is not_yet; otherwise empty. |
closing | One line of at most 18 words that agrees with the verdicts. |
The page then checks the reply (PuckerKit.check) against its own verdict for each fruit, the count, the one to eat first and the word limits, and lists any disagreement under "The page's check".
Worked examples
These are the three free samples on the page: the exact input and a stand-in model's reply, unedited. Together they cover every outcome - a counter bowl of four (ready, not yet, ready, unsure), a gift box of three (not yet, ready, past it) and a note with a health question (declined).
A counter bowl of four - ready, not yet, unsure
Input:
{
"task": "check",
"fruit_count": "4",
"fruits": "1. big one by the window | shape: pointed, acorn-shaped - the astringent kind | feel: jelly-soft, like a water balloon | colour: dark red-orange and glossy | skin: wrinkled or bruised | days on the counter: 9 | note: skin looks almost see-through\n2. small pointed one | shape: pointed, acorn-shaped - the astringent kind | feel: firm, like an apple | colour: yellow-orange | skin: smooth | days on the counter: 2 | note: none\n3. flat one from the market | shape: squat, tomato-shaped - the non-astringent kind | feel: firm, like an apple | colour: deep orange | skin: a few dark specks | days on the counter: not given | note: none\n4. the one my neighbour gave me | shape: kind not known | feel: gives a little when pressed | colour: yellow-orange | skin: smooth | days on the counter: 4 | note: none"
}
Reply:
{
"task": "check",
"status": "ok",
"reason": "",
"fruits": [
{
"n": 1,
"verdict": "ready",
"claim": "The big one by the window is ready to eat now.",
"for": "Jelly-soft flesh, dark red-orange colour, wrinkled or bruised and see-through skin, with 9 days on the counter show ripeness.",
"against": "",
"test": "Press the flesh gently to confirm it still feels jelly-soft before eating.",
"next": "Eat it with a spoon since the flesh is jelly-soft."
},
{
"n": 2,
"verdict": "not_yet",
"claim": "The small pointed one is ready to eat now.",
"for": "",
"against": "Its flesh is firm, like an apple, and its colour is yellow-orange.",
"test": "Press the flesh gently to check if it still feels firm.",
"next": "Leave it at room temperature and check again later."
},
{
"n": 3,
"verdict": "ready",
"claim": "The flat one from the market is ready to eat now.",
"for": "It is a non-astringent, tomato-shaped persimmon that feels firm, like an apple, and is deep orange.",
"against": "",
"test": "Press the flesh gently to confirm it still feels firm, like an apple.",
"next": "Slice it and eat since the flesh is firm."
},
{
"n": 4,
"verdict": "unsure",
"claim": "The one labelled \"the one my neighbour gave me\" is ready to eat now.",
"for": "It gives a little when pressed, with 4 days on the counter.",
"against": "Its colour is yellow-orange.",
"test": "Touch a slice to your tongue; if it feels dry and furry, it still needs to wait.",
"next": "Do the tongue-tip test, then eat it or wait based on the result."
}
],
"eat_first": 1,
"eat_first_why": "The big one by the window is jelly-soft, softer than the firm flat one from the market.",
"tip": "Place the small pointed one in a paper bag alongside a ripe banana or apple at room temperature.",
"closing": "Overall your tray has 2 ready, 1 not yet, and 1 unsure persimmon."
}
A gift box of three - not yet, ready, past it
Input:
{
"task": "check",
"fruit_count": "3",
"fruits": "1. Fruit 1 | shape: squat, tomato-shaped - the non-astringent kind | feel: hard as a stone | colour: yellow with green patches | skin: smooth | days on the counter: 1 | note: none\n2. the softest one | shape: kind not known | feel: jelly-soft, like a water balloon | colour: deep orange | skin: split or leaking juice | days on the counter: 6 | note: none\n3. bottom of the box | shape: pointed, acorn-shaped - the astringent kind | feel: jelly-soft, like a water balloon | colour: dark red-orange and glossy | skin: mould, or a sour or boozy smell | days on the counter: not given | note: white fuzz near the stem"
}
Reply:
{
"task": "check",
"status": "ok",
"reason": "",
"fruits": [
{
"n": 1,
"verdict": "not_yet",
"claim": "Fruit 1 is ready to eat now.",
"for": "",
"against": "It is hard as a stone and yellow with green patches.",
"test": "Press it gently to feel if it gives at all.",
"next": "Leave it at room temperature and check again later."
},
{
"n": 2,
"verdict": "ready",
"claim": "The softest one is ready to eat now.",
"for": "It is jelly-soft, deep orange, has split or leaking juice, and 6 days on the counter.",
"against": "",
"test": "Press it once more to confirm the flesh feels fully jelly-soft.",
"next": "Eat it with a spoon since the flesh is jelly-soft."
},
{
"n": 3,
"verdict": "past_it",
"claim": "Bottom of the box is ready to eat now.",
"for": "It is jelly-soft and dark red-orange and glossy.",
"against": "Its skin is mould, or a sour or boozy smell, and there is white fuzz near the stem.",
"test": "Check for a sour or boozy smell and visible mould before discarding.",
"next": "Put it in the compost or bin and never eat it."
}
],
"eat_first": 2,
"eat_first_why": "It is the only fruit that is ready to eat now.",
"tip": "Store Fruit 1 in a paper bag alongside a ripe banana to speed ripening.",
"closing": "One fruit is ready now, one needs more time, and one should be discarded."
}
A health question - declined
Input:
{
"task": "check",
"fruit_count": "2",
"fruits": "1. Fruit 1 | shape: pointed, acorn-shaped - the astringent kind | feel: firm, like an apple | colour: yellow-orange | skin: smooth | days on the counter: not given | note: none\n2. Fruit 2 | shape: squat, tomato-shaped - the non-astringent kind | feel: gives a little when pressed | colour: deep orange | skin: smooth | days on the counter: 3 | note: is this okay for my diabetes?"
}
Reply:
{
"task": "check",
"status": "declined",
"reason": "This page only checks ripeness for you - questions about health conditions belong with a doctor or vet.",
"fruits": [],
"eat_first": 0,
"eat_first_why": "",
"tip": "",
"closing": ""
}
Build the input yourself
Load the engine in node (global.window = global; require("./pucker.js"); require("./tray.js")), then:
const K = PuckerKit, T = PuckerTray;
const built = K.buildInput({ fruits: [
{ label: "big one", kind: "pointed", feel: "jelly", colour: "red", skin: "wrinkled", days: "9", note: "" },
{ label: "", kind: "squat", feel: "hard", colour: "green", skin: "smooth", days: "", note: "" } ] });
built.parsed.rows.map(r => r.verdict); // the page's own verdicts: ["ready", "not_yet"]
built.prescan.eat_first; // 1 - the softest ready fruit
const wire = K.toWire(built.input); // POST this to /estimate and /run
// kind: pointed | squat | unknown feel: hard | firm | gives | soft | jelly
// colour: green | yellow | orange | red skin: smooth | specks | wrinkled | split | mould
// then: const reply = K.normalize(K.parseReply(job.output.output));
// K.check(reply, built).notes // the page's re-check of the reply
// T.svg({ rows: built.parsed.rows, eatFirst: reply.eat_first })
// K.md(built, reply) // the checked tray 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://pucker-check.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://pucker-check.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"]
// pucker-check.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://pucker-check.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://pucker-check.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
}
// PuckerCheck.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 PuckerCheck {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://pucker-check.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://pucker-check.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://pucker-check.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://pucker-check.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":"pucker-check"}' | 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": "pucker-check"})["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: "pucker-check" })).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": "pucker-check"}, 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", "pucker-check"), 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: "pucker-check" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "pucker-check"])["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 = "pucker-check" }))!["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": "check", "fruit_count": "2", "fruits": "1. ...
2. ..."} - 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": "check", "fruit_count": "2", "fruits": "1. ...
2. ..."} - 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: "check", fruit_count, fruits} (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 pucker-check:<task>:<inputHash>:a<attempt> (the one task is check), 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="pucker-check:check:$(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 = "pucker-check:" + 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("status"))
import { createHash } from "node:crypto";
const key = `pucker-check:${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.status);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "pucker-check:" + 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 = "pucker-check:" + 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 = "pucker-check:#{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["status"]].inspect
$key = "pucker-check:" . $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["status"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"pucker-check:{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: pucker-check:check:$(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": "check", "fruit_count": "2", "fruits": "1. ...
2. ..."} - 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": "pucker-check:check:" + 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": `pucker-check:check:${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", "pucker-check:check:"+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", "pucker-check:check:" + 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" => "pucker-check:check:#{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: pucker-check:check:" . 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", $"pucker-check:check:{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);