Drive First and Last from your own code
The one metered task, order, is available over HTTP. It takes what a short list is for and who will hear or see it, plus the list itself - three to ten numbered lines - and returns which lines matter most for that goal, whether the order may change, and an order that puts what matters in the strong spots at the start and the end, with a reason per move, an opening line to say first and a recap to say last.
The division of labour first: the page draws the washing line and works out the strong spots and the placement; the model judges which lines matter, whether the order can move, and words the reasons, opener and recap. The page reads the list (bullets and numbering stripped, a * at either end of a line stars it), works out the strong spots of a list that long and the order they fill in, and sends both. After the reply it places the lines itself from the reply's key lines - whatever order the reply wrote - and re-checks every part against the list. Your code can do the same with the engine files the page loads (see "Build the input yourself").
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":"first-and-last"}), 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, goal, items, item_count, starred, order_hint, strong_spots, fill_order, pick_count) 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 placement run 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 rules
Every run sends task: order (the one task is order); a missing or different one is still answered as order. All fields are strings.
| Field | Required | What it holds |
|---|---|---|
task | yes | Always order. |
goal | yes | What the list is for and who will hear or see it, in the person's words: 8 to 160 characters. |
items | yes | Numbered lines N. the item, three to ten, at most 80 characters each, in the order the person wrote them. Build it with ListKit.buildInput, which strips bullets and numbering and reads the stars. |
item_count | yes | How many lines items has, as a string. |
starred | yes | The line numbers the person starred as must-remember, comma-separated (1, 6), or none. At most as many as the list has strong spots. |
order_hint | yes | fixed (keep my order), free (the order can change) or unsure (let the run decide). |
strong_spots | yes | The strong spots of a list this long, as positions (see the table below), e.g. 1, 2, 6, 7. |
fill_order | yes | The order those spots fill in: first, last, second, second-to-last, e.g. 1, 7, 2, 6. |
pick_count | yes | How many lines to pick when nothing is starred: 2 for five lines or fewer, else 3. |
retry_note | no | Only on a reformat retry: why the previous reply was unusable. |
The strong spots by list length (the first and last places, where things stick; the middle is where they get lost):
| Lines | strong_spots | fill_order | pick_count |
|---|---|---|---|
| 3 | 1, 3 | 1, 3 | 2 |
| 4 | 1, 4 | 1, 4 | 2 |
| 5 | 1, 2, 5 | 1, 5, 2 | 2 |
| 6 | 1, 2, 5, 6 | 1, 6, 2, 5 | 3 |
| 7 | 1, 2, 6, 7 | 1, 7, 2, 6 | 3 |
| 8 | 1, 2, 7, 8 | 1, 8, 2, 7 | 3 |
| 9 | 1, 2, 8, 9 | 1, 9, 2, 8 | 3 |
| 10 | 1, 2, 9, 10 | 1, 10, 2, 9 | 3 |
The reply takes one verdict: bookend when the order must stay (steps, a time order, or order_hint is fixed) - the lines keep their order and the opener and recap carry what matters; keep when every key line already sits in a strong spot; else reorder - the key lines go into the strong spots in fill order and every other line keeps its relative order in the middle.
The reply
One JSON object:
{"task": "order", "verdict": "reorder", "key": [3, 7, 1], "order": [3, 1, 2, 4, 5, 6, 7], "moves": [{"n": 3, "why": "..."}], "drop": [{"n": 5, "why": "..."}], "opener": "...", "recap": "...", "note": "..."}
| Field | What it holds |
|---|---|
verdict | bookend, keep or reorder. |
key | The line numbers that matter most for the goal, most important first: the starred lines when there are any, else pick_count of them. |
order | The reply's own order, every line number once. The page does not trust it: it recomputes the placement. |
moves[] | For reorder: {n, why}, one short reason per line that moves; empty otherwise. |
drop[] | At most two {n, why}: lines that could be left out. Never a key or starred line. |
opener | A line to say first that names what matters, in the list's own words. |
recap | A line to say last that repeats what matters. |
note | One sentence on why the verdict fits. |
The page reads the reply with ReplyKit.normalize, places the lines with ReplyKit.orderFor(reply, count, fixed) (the placement rule over the key lines for reorder; the person's own order for bookend, keep, an unreadable verdict, or whenever order_hint is fixed), and checks it with ReplyKit.check(reply, built), which lists, under "The page's check", a missing verdict, key lines the list does not have, starred lines left out of what matters, a keep with a key line in the middle, a fixed order the reply did not keep, a reply order that is not the placement rule's, moves without a reason, a drop of a starred or key line, a missing opener or recap or one that does not name the key lines, and any number or internal field name the list never gives.
Worked examples - every sample
These are the free samples on the page: the exact input the page sends, the reply that replays for free, and what the page does with it - the order its line shows and its own check. Together they cover the verdicts reorder, keep, bookend.
A doorstep handover to a flat-sitter
Input:
{
"task": "order",
"goal": "Telling the neighbour who is minding our flat for the weekend what to do, in a quick chat on the doorstep",
"items": "1. water the basil on the windowsill\n2. the post goes on the hall table\n3. the cat gets one tablet hidden in her breakfast\n4. the Wi-Fi password is on the fridge\n5. we got new curtains in the bedroom\n6. help yourself to anything in the biscuit tin\n7. double-lock the front door when you leave",
"item_count": "7",
"starred": "none",
"order_hint": "unsure",
"strong_spots": "1, 2, 6, 7",
"fill_order": "1, 7, 2, 6",
"pick_count": "3"
}
Reply:
{
"task": "order",
"verdict": "reorder",
"key": [
3,
7,
1
],
"order": [
3,
1,
2,
4,
5,
6,
7
],
"moves": [
{
"n": 3,
"why": "The cat's tablet is hidden in her breakfast, an easy detail to miss during a quick chat."
},
{
"n": 1,
"why": "The basil on the windowsill needs watering, something easy to forget over the weekend."
},
{
"n": 2,
"why": "This one shifts to make space and is mentioned in the middle of the chat instead."
}
],
"drop": [
{
"n": 5,
"why": "The new curtains are background news and do not help with any task this weekend."
}
],
"opener": "Three things matter most: the cat's tablet hidden in her breakfast, double-locking the front door when you leave, and watering the basil on the windowsill.",
"recap": "To finish, the cat needs her tablet in breakfast, the front door gets double-locked, and the basil wants watering.",
"note": "The cat's tablet and the basil both moved to the start, and the front door stays at the end."
}
The page's line (ReplyKit.orderFor gives [3, 1, 2, 4, 5, 6, 7]):
- the cat gets one tablet hidden in her breakfast - strong spot
- water the basil on the windowsill - strong spot
- the post goes on the hall table
- the Wi-Fi password is on the fridge
- we got new curtains in the bedroom
- help yourself to anything in the biscuit tin - strong spot
- double-lock the front door when you leave - strong spot
The page's check: kept.
No notes.
A sign for a car-boot stall
Input:
{
"task": "order",
"goal": "A chalkboard sign at our car-boot stall listing what we sell, read by people walking past",
"items": "1. vinyl records\n2. children's books\n3. kitchen odds and ends\n4. board games\n5. lamps\n6. camping gear",
"item_count": "6",
"starred": "1, 6",
"order_hint": "free",
"strong_spots": "1, 2, 5, 6",
"fill_order": "1, 6, 2, 5",
"pick_count": "3"
}
Reply:
{
"task": "order",
"verdict": "keep",
"key": [
1,
6
],
"order": [
1,
2,
3,
4,
5,
6
],
"moves": [],
"drop": [],
"opener": "Two things matter most on this sign: vinyl records and camping gear.",
"recap": "Vinyl records and camping gear remain the two things that matter most here.",
"note": "This order stays because vinyl records and camping gear sit at the start and the end of your sign."
}
The page's line (ReplyKit.orderFor gives [1, 2, 3, 4, 5, 6]):
- vinyl records (starred) - strong spot
- children's books - strong spot
- kitchen odds and ends
- board games
- lamps - strong spot
- camping gear (starred) - strong spot
The page's check: kept.
No notes.
Restarting the router over the phone
Input:
{
"task": "order",
"goal": "Talking my dad through restarting the home Wi-Fi router over the phone",
"items": "1. unplug the router at the wall\n2. wait a full minute before plugging it back in\n3. plug it back in\n4. wait until the light on the front goes steady green\n5. then try the internet on the laptop again",
"item_count": "5",
"starred": "none",
"order_hint": "unsure",
"strong_spots": "1, 2, 5",
"fill_order": "1, 5, 2",
"pick_count": "2"
}
Reply:
{
"task": "order",
"verdict": "bookend",
"key": [
2,
4
],
"order": [
1,
2,
3,
4,
5
],
"moves": [],
"drop": [],
"opener": "Two points matter most: wait a full minute before plugging the router back in, and wait until the light on the front goes steady green.",
"recap": "Leave the router unplugged for the full minute, then check that the light on the front turns steady green.",
"note": "Each step depends on the step before, so the order stays, and the opener and recap carry what matters most to the start and the end for you."
}
The page's line (ReplyKit.orderFor gives [1, 2, 3, 4, 5]):
- unplug the router at the wall - strong spot
- wait a full minute before plugging it back in - strong spot
- plug it back in
- wait until the light on the front goes steady green
- then try the internet on the laptop again - strong spot
The page's check: kept.
No notes.
Build the input yourself
Load the engine in node (global.window = global; require("./listkit.js"); require("./replykit.js")), then:
const K = ListKit, R = ReplyKit;
const built = K.buildInput(K.form({ goal: "Telling the flat-sitter what to do this weekend", items: "water the basil\n* feed the cat\ndouble-lock the door", order: "unsure" })); // built.ok is false, with built.note, until there is a goal and three lines
const wire = K.toWire(built.input); // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output));
// R.orderFor(reply, built.n, wire.order_hint === "fixed") // the line numbers in the page's order
// R.check(reply, built).notes // the page's re-check of every part
Form keys: goal (what the list is for and who hears or sees it), items (one per line, three to ten, 80 characters each; a * at either end stars a line) and order (unsure, free or fixed).
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://first-and-last.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://first-and-last.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"]
// first-and-last.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://first-and-last.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://first-and-last.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
}
// FirstAndLast.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 FirstAndLast {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://first-and-last.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://first-and-last.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://first-and-last.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://first-and-last.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":"first-and-last"}' | 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": "first-and-last"})["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: "first-and-last" })).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": "first-and-last"}, 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", "first-and-last"), 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: "first-and-last" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "first-and-last"])["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 = "first-and-last" }))!["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": "order", "goal": "Telling the flat-sitter what to do this weekend", "items": "1. water the basil\n2. feed the cat\n3. double-lock the door", "item_count": "3", "starred": "none", "order_hint": "unsure", "strong_spots": "1, 3", "fill_order": "1, 3", "pick_count": "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": "order", "goal": "Telling the flat-sitter what to do this weekend", "items": "1. water the basil\n2. feed the cat\n3. double-lock the door", "item_count": "3", "starred": "none", "order_hint": "unsure", "strong_spots": "1, 3", "fill_order": "1, 3", "pick_count": "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: "order", goal, items, item_count, starred, order_hint, strong_spots, fill_order, pick_count} (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 first-and-last:<task>:<inputHash>:a<attempt> (the one task is order), 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="first-and-last:order:$(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 = "first-and-last:" + 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"), reply.get("order"))
import { createHash } from "node:crypto";
const key = `first-and-last:${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, reply.order);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "first-and-last:" + 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 = "first-and-last:" + 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 = "first-and-last:#{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"], reply["order"]].inspect
$key = "first-and-last:" . $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"] ?? ""), " ", implode(",", $reply["order"] ?? []), "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"first-and-last:{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: first-and-last:order:$(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": "order", "goal": "Telling the flat-sitter what to do this weekend", "items": "1. water the basil\n2. feed the cat\n3. double-lock the door", "item_count": "3", "starred": "none", "order_hint": "unsure", "strong_spots": "1, 3", "fill_order": "1, 3", "pick_count": "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": "first-and-last:order:" + 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": `first-and-last:order:${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", "first-and-last:order:"+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", "first-and-last:order:" + 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" => "first-and-last:order:#{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: first-and-last:order:" . 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", $"first-and-last:order:{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);