Drive Pumpkin Afterlife from your own code
The one metered task, route, is available over HTTP. It takes the household (display coming down or staying up, whether you cook pumpkin, a compost heap, a food-waste caddy) and 1 to 6 pumpkins or gourds as the page's lines - what each one is, its kind, whole or carved, its finish, its condition, the days on show, a note and the page's route - plus the totals, the verdict and the pumpkin to deal with first. It returns, for every pumpkin, why it goes where it goes and the job to do before it goes, then a headline, the first step and a closing.
The division of labour first: a fixed rule on the page gives every route (keep, kitchen, compost, caddy, bin), picks the pumpkin to deal with first and decides the verdict; the model only writes the words. After a run the page checks that the reply copied every one of those and kept to the house rules.
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":"pumpkin-afterlife"}), 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, pile) 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 written routes 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: route (the one task is route); a missing or different one is still answered as route. Each pumpkin's route, first match wins: painted, glitter, spray or glue - bin; the display staying up and the pumpkin whole and firm - keep; you cook pumpkin and it is whole, firm and not an ornamental gourd - kitchen; a compost heap - compost; a food-waste caddy - caddy; otherwise - bin. The pumpkin to deal with first is the leaving one with the highest urgency (mould 3, soft spots 2, frost-hit 2, carved +2, on show 14 days or more +1), the earliest on a tie. The verdict: none leaving - all-staying; any leaving pumpkin carved or with soft spots or mould - clear-today; otherwise - clear-soon.
| Field | Required | What it holds |
|---|---|---|
task | yes | Always route. |
pile | yes | The page's lines: a house rules: line, a household: line, a totals: line (counts, verdict, deal with first), then 1 line per pumpkin such as item 2 | knobbly yellow gourd | kind: ornamental gourd | cut: whole | finish: plain | condition: frost-hit | on show: 10 days | note: went dull and wrinkly after a cold night | route: caddy. |
retry_note | no | Only on the page's single reformat retry. |
The reply is one JSON object:
{"task": "route", "verdict": "clear-today", "headline": "...", "items": [{"item": "1", "route": "compost", "why": "...", "before": "..."}], "first": {"item": "1", "step": "..."}, "closing": "..."}
| Field | What it holds |
|---|---|
verdict | Copied from the totals line. |
headline | The verdict in plain words with how many pumpkins leave and how many stay, at most 16 words. |
items | 1 per pumpkin in list order: item (its number), route (copied), why (the deciding reason, at most 20 words) and before (the job to do before it goes, or to make it last on show, at most 18 words). |
first | item (copied from the totals line, or empty) and step (agreeing with that pumpkin's before). |
closing | One light sentence that agrees with the verdict, at most 20 words. |
The page then checks the reply (PumpKit.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.
Display staying up, every pumpkin firm - all staying
Input:
{
"task": "route",
"pile": "house rules: checked in order - painted, glitter, spray or glue: bin | display kept up, whole and firm: keep | you cook, whole and firm, not an ornamental gourd: kitchen | a compost heap: compost | a food-waste caddy: caddy | otherwise: bin | deal with first: the leaving pumpkin with the highest urgency (mould 3, soft spots 2, frost-hit 2, carved +2, on show 14 days or more +1), the earliest on a tie | verdict: all-staying when none leaves, clear-today when any leaving pumpkin is carved or has soft spots or mould, otherwise clear-soon\nhousehold: display kept up while it lasts | cooks pumpkin: no | compost heap: yes | food-waste caddy: no\ntotals: 3 pumpkins | keep: 3 | kitchen: 0 | compost: 0 | caddy: 0 | bin: 0 | verdict: all-staying | deal with first: none\nitem 1 | big orange pumpkin by the door | kind: carving pumpkin | cut: whole | finish: plain | condition: firm | on show: 12 days | note: stem still green | route: keep\nitem 2 | warty white pumpkin | kind: carving pumpkin | cut: whole | finish: plain | condition: firm | on show: 20 days | note: none | route: keep\nitem 3 | bowl of striped gourds | kind: ornamental gourd | cut: whole | finish: plain | condition: firm | on show: 9 days | note: shiny and hard | route: keep"
}
Reply:
{
"task": "route",
"verdict": "all-staying",
"headline": "Nothing leaves the display - every pumpkin stays on show.",
"items": [
{
"item": "1",
"route": "keep",
"why": "This big orange pumpkin is whole and firm, with a still-green stem, and the display is staying up.",
"before": "Keep an eye on its base for any soft spots while it stays on show."
},
{
"item": "2",
"route": "keep",
"why": "Staying whole and firm, this warty white pumpkin keeps its place while the display stays up.",
"before": "Wipe off any dust and check it is still sitting steady on show."
},
{
"item": "3",
"route": "keep",
"why": "This bowl of striped gourds is whole and firm, shiny and hard, while the display stays up.",
"before": "Give the bowl a shake and check each gourd still feels firm and sound."
}
],
"first": {
"item": "",
"step": ""
},
"closing": "Relax, your pumpkins and gourd bowl get to carry on looking smug on show."
}
A mouldy carved face, a squash for the kitchen and a glitter pumpkin - clear today
Input:
{
"task": "route",
"pile": "house rules: checked in order - painted, glitter, spray or glue: bin | display kept up, whole and firm: keep | you cook, whole and firm, not an ornamental gourd: kitchen | a compost heap: compost | a food-waste caddy: caddy | otherwise: bin | deal with first: the leaving pumpkin with the highest urgency (mould 3, soft spots 2, frost-hit 2, carved +2, on show 14 days or more +1), the earliest on a tie | verdict: all-staying when none leaves, clear-today when any leaving pumpkin is carved or has soft spots or mould, otherwise clear-soon\nhousehold: display coming down now | cooks pumpkin: yes | compost heap: yes | food-waste caddy: no\ntotals: 4 pumpkins | keep: 0 | kitchen: 1 | compost: 2 | caddy: 0 | bin: 1 | verdict: clear-today | deal with first: item 1 (urgency 5)\nitem 1 | carved cat face pumpkin | kind: carving pumpkin | cut: carved | finish: candle-lit (wax inside) | condition: mould | on show: 6 days | note: furry patches inside the lid | route: compost\nitem 2 | butternut squash from the step | kind: eating pumpkin or squash | cut: whole | finish: plain | condition: firm | on show: 15 days | note: skin still glossy | route: kitchen\nitem 3 | glitter pumpkin | kind: carving pumpkin | cut: whole | finish: glitter, spray or glue | condition: firm | on show: 15 days | note: gold spray and sequins | route: bin\nitem 4 | small sugar pumpkin | kind: eating pumpkin or squash | cut: whole | finish: plain | condition: soft spots | on show: 15 days | note: a soft patch near the base | route: compost"
}
Reply:
{
"task": "route",
"verdict": "clear-today",
"headline": "Clear today, 4 pumpkins are leaving and 0 are staying on show.",
"items": [
{
"item": "1",
"route": "compost",
"why": "Carved and showing mould, it can't go to the kitchen or stay on show, and you have a compost heap.",
"before": "Take out the candle and wax, then chop it into chunks so it breaks down."
},
{
"item": "2",
"route": "kitchen",
"why": "Whole and firm, it isn't an ornamental gourd, and you cook pumpkin, so this squash goes to the kitchen.",
"before": "Wash it, then cut it open and use it soon, you could scoop out the seeds first."
},
{
"item": "3",
"route": "bin",
"why": "Covered in glitter, spray or glue, eating or composting it is ruled out, despite being whole and firm.",
"before": "Pick off the sequins and any other removable add-ons before it goes in the bin."
},
{
"item": "4",
"route": "compost",
"why": "Though whole, its soft spots rule out the kitchen and staying on show, and you have a compost heap.",
"before": "Break it into smaller pieces so it composts down well."
}
],
"first": {
"item": "1",
"step": "First, take the candle and wax out of the carved cat face pumpkin, then chop it into chunks."
},
"closing": "With 4 pumpkins on the move today, start with that carved cat face pumpkin's candle and wax."
}
No heap but a caddy, a frost-hit gourd and a painted pumpkin - clear this week
Input:
{
"task": "route",
"pile": "house rules: checked in order - painted, glitter, spray or glue: bin | display kept up, whole and firm: keep | you cook, whole and firm, not an ornamental gourd: kitchen | a compost heap: compost | a food-waste caddy: caddy | otherwise: bin | deal with first: the leaving pumpkin with the highest urgency (mould 3, soft spots 2, frost-hit 2, carved +2, on show 14 days or more +1), the earliest on a tie | verdict: all-staying when none leaves, clear-today when any leaving pumpkin is carved or has soft spots or mould, otherwise clear-soon\nhousehold: display coming down now | cooks pumpkin: yes | compost heap: no | food-waste caddy: yes\ntotals: 3 pumpkins | keep: 0 | kitchen: 1 | compost: 0 | caddy: 1 | bin: 1 | verdict: clear-soon | deal with first: item 2 (urgency 2)\nitem 1 | tall carving pumpkin | kind: carving pumpkin | cut: whole | finish: plain | condition: firm | on show: 10 days | note: none | route: kitchen\nitem 2 | knobbly yellow gourd | kind: ornamental gourd | cut: whole | finish: plain | condition: frost-hit | on show: 10 days | note: went dull and wrinkly after a cold night | route: caddy\nitem 3 | painted pumpkin with a face | kind: carving pumpkin | cut: whole | finish: painted | condition: firm | on show: 16 days | note: acrylic paint all over | route: bin"
}
Reply:
{
"task": "route",
"verdict": "clear-soon",
"headline": "Clear this week - 3 pumpkins leave the display, 0 stay.",
"items": [
{
"item": "1",
"route": "kitchen",
"why": "This tall carving pumpkin is whole and firm, and you cook pumpkin, so it heads to the kitchen.",
"before": "Wash it, cut it open and use it soon - scoop out the seeds as you go."
},
{
"item": "2",
"route": "caddy",
"why": "Whole yet frost-hit, this ornamental gourd skips the kitchen and the show; with no compost heap, caddy it is.",
"before": "Cut it into pieces small enough to fit the caddy."
},
{
"item": "3",
"route": "bin",
"why": "Firm and whole, this carving pumpkin still can't be eaten or composted because it's painted - general bin.",
"before": "Pick off any pins, sequins or add-ons you can remove, then bag it for the bin."
}
],
"first": {
"item": "2",
"step": "First, cut the knobbly yellow gourd into pieces that fit the caddy."
},
"closing": "Start with that frost-hit gourd, then let the rest of your pumpkin round-up carry on through the week."
}
No heap, no caddy, a tie on urgency - clear today, the earliest goes first
Input:
{
"task": "route",
"pile": "house rules: checked in order - painted, glitter, spray or glue: bin | display kept up, whole and firm: keep | you cook, whole and firm, not an ornamental gourd: kitchen | a compost heap: compost | a food-waste caddy: caddy | otherwise: bin | deal with first: the leaving pumpkin with the highest urgency (mould 3, soft spots 2, frost-hit 2, carved +2, on show 14 days or more +1), the earliest on a tie | verdict: all-staying when none leaves, clear-today when any leaving pumpkin is carved or has soft spots or mould, otherwise clear-soon\nhousehold: display coming down now | cooks pumpkin: no | compost heap: no | food-waste caddy: no\ntotals: 3 pumpkins | keep: 0 | kitchen: 0 | compost: 0 | caddy: 0 | bin: 3 | verdict: clear-today | deal with first: item 1 (urgency 4)\nitem 1 | carved lantern with stars | kind: carving pumpkin | cut: carved | finish: candle-lit (wax inside) | condition: soft spots | on show: 5 days | note: lid caving in | route: bin\nitem 2 | carved owl pumpkin | kind: carving pumpkin | cut: carved | finish: plain | condition: soft spots | on show: 5 days | note: edges shrivelled | route: bin\nitem 3 | mini pumpkin on the windowsill | kind: carving pumpkin | cut: whole | finish: plain | condition: firm | on show: 20 days | note: none | route: bin"
}
Reply:
{
"task": "route",
"verdict": "clear-today",
"headline": "Clear today: 3 pumpkins leave, 0 stay on show.",
"items": [
{
"item": "1",
"route": "bin",
"why": "It's carved with soft spots, the display is coming down, and you have no compost heap or food-waste caddy.",
"before": "Take out the candle and wax, then bag it."
},
{
"item": "2",
"route": "bin",
"why": "Carved with soft spots, it can't stay on show or go to kitchen, with no compost heap or food-waste caddy.",
"before": "Bag it up for the general bin."
},
{
"item": "3",
"route": "bin",
"why": "Whole and firm, but the display is coming down, you don't cook, and there's no compost heap or food-waste caddy.",
"before": "Pop it into a bag for the general bin before it goes out."
}
],
"first": {
"item": "1",
"step": "Take the candle and wax out of the carved lantern with stars, then bag it."
},
"closing": "Start with that carved lantern - today's the day these pumpkins head out the door."
}
Build the input yourself
Load the engine in node (global.window = global; require("./pumpkit.js")), then:
const K = PumpKit;
const built = K.buildInput(K.form({ house: { display: "down", cook: "yes", heap: "no", caddy: "yes" }, items: [
{ text: "big round pumpkin", kind: "carving", cut: "whole", finish: "plain", cond: "firm", days: 9, note: "" },
{ text: "carved bat lantern", kind: "carving", cut: "carved", finish: "candle", cond: "soft", days: 4, note: "edges curling" }] }));
built.input.pile; // the page's lines: rules, household, totals and one line per pumpkin
const wire = K.toWire(built.input); // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// built.facts.verdict // the verdict the page's rule gives
// K.check(reply, built).notes // the page's re-check of the reply
// K.md(built, reply, "my display") // the routes 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://pumpkin-afterlife.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://pumpkin-afterlife.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"]
// pumpkin-afterlife.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://pumpkin-afterlife.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://pumpkin-afterlife.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://pumpkin-afterlife.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://pumpkin-afterlife.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://pumpkin-afterlife.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://pumpkin-afterlife.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":"pumpkin-afterlife"}' | 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": "pumpkin-afterlife"})["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: "pumpkin-afterlife" })).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": "pumpkin-afterlife"}, 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", "pumpkin-afterlife"), 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: "pumpkin-afterlife" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "pumpkin-afterlife"])["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 = "pumpkin-afterlife" }))!["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": "route", "pile": "house rules: checked in order - painted, glitter, spray or glue: bin ...
household: display coming down now | cooks pumpkin: yes | compost heap: yes | food-waste caddy: no
totals: 4 pumpkins | keep: 0 | kitchen: 1 | compost: 2 | caddy: 0 | bin: 1 | verdict: clear-today | deal with first: item 1 (urgency 5)
item 1 | carved cat face pumpkin | kind: carving pumpkin | cut: carved | ... | route: compost
..."}
# - 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": "route", "pile": "house rules: checked in order - painted, glitter, spray or glue: bin ...
household: display coming down now | cooks pumpkin: yes | compost heap: yes | food-waste caddy: no
totals: 4 pumpkins | keep: 0 | kitchen: 1 | compost: 2 | caddy: 0 | bin: 1 | verdict: clear-today | deal with first: item 1 (urgency 5)
item 1 | carved cat face pumpkin | kind: carving pumpkin | cut: carved | ... | route: compost
..."}
# - 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: "route", pile} (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 task and the input: a retry with the same key never bills twice. The page uses pumpkin-afterlife:<task>:<inputHash>:a<attempt> (the one task is route), 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="pumpkin-afterlife:route:$(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 = "pumpkin-afterlife:" + 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 = `pumpkin-afterlife:${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 := "pumpkin-afterlife:" + 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 = "pumpkin-afterlife:" + 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 = "pumpkin-afterlife:#{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 = "pumpkin-afterlife:" . $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 = $"pumpkin-afterlife:{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: pumpkin-afterlife:route:$(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": "route", "pile": "house rules: checked in order - painted, glitter, spray or glue: bin ...
household: display coming down now | cooks pumpkin: yes | compost heap: yes | food-waste caddy: no
totals: 4 pumpkins | keep: 0 | kitchen: 1 | compost: 2 | caddy: 0 | bin: 1 | verdict: clear-today | deal with first: item 1 (urgency 5)
item 1 | carved cat face pumpkin | kind: carving pumpkin | cut: carved | ... | route: compost
..."}
# - 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": "pumpkin-afterlife:route:" + 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": `pumpkin-afterlife:route:${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", "pumpkin-afterlife:route:"+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", "pumpkin-afterlife:route:" + 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" => "pumpkin-afterlife:route:#{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: pumpkin-afterlife:route:" . 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", $"pumpkin-afterlife:route:{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);