Drive With Compliments from your own code
The one metered task, leave-cards, is available over HTTP. It takes 1 to 6 small everyday pinches, each with a name, an optional line on what happened, and four answers: whose it was, how much they would miss it, how you will make it good, and whether it was the last one. It returns a calling card, a reason and a mend for every pinch.
The division of labour first: the page decides; the model words it. The page adds up each pinch's honour points (0-5) against a three-article code, picks its verdict, its weakest article, its mend, its card opening and the finest pinch, and sends those as facts. The model never changes a verdict; it writes the cards and explains. The signature on each card is added by the page and never sent.
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":"with-compliments"}), 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, pinches, facts) is missing. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: the reasons are metered and need a personal sign-in. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
The task field, the input and the verdicts
Every run sends task: leave-cards (the one task is leave-cards); a missing or different one is still answered as leave-cards. All fields are strings.
| Field | Required | What it holds |
|---|---|---|
task | yes | Always leave-cards. |
pinches | yes | One numbered line per pinch (1 to 6): name | what happened: ... | whose it was: ... | how much they would miss it: ... | how you will make it good: ... | was it the last one: ..., with "not given" for a blank note. |
facts | yes | The page's lines: per pinch its honour points, its verdict (with an off-limits note when it applies), its weakest article, the words its card opens with and its mend; then the count of each verdict and the finest pinch. |
retry_note | no | Only on a reformat retry: why the previous reply was unusable. |
The articles score: take only a trifle (Barely 2, A little 1, A lot 0), make it good (Replace it or Give it back 2, Make it up another way 1, Nothing yet 0), never take the last one (No 1, Yes 0). The verdict, first match wins: off-limits (money, valuables, documents, medicine, anything from a shop or a stranger), or A lot with Nothing yet, or honour 0-1 is return-it (return it); honour 4-5 is gentleman (gentleman thief); anything else is rogue (charming rogue).
The reply
One JSON object, no prose around it:
{"task": "leave-cards", "headline": "...", "pinches": [{"name": "...", "verdict": "rogue", "card": "...", "why": "...", "mend": "..."}], "closing": "..."}
| Field | What it holds |
|---|---|
headline | At most 22 words summing up the tray; counts match facts. |
pinches[] | One per input pinch, same order and exact names: verdict copied from facts, card (15 to 40 words, first person, opening with the words in facts, promising only the plan you chose, unsigned), why (at most 50 words, from that pinch's own answers) and mend (one plain step, at most 35 words). |
closing | One sentence starting with the finest pinch (or saying no pinch kept the code well enough). |
The page then checks the reply (PinchKit.check) - each verdict against its own, every pinch present, each card's opening, promises on a Nothing yet plan, time, day or weather words you did not give, contact details or public posting, and named books, films or characters - and lists anything it finds under "The page's check".
Worked examples - all three verdicts
These are the 3 free samples on the page: the exact input the page sends and the reply that replays for free. Together they cover all three verdicts.
Flat share - verdicts: gentleman thief, charming rogue
Input:
{
"task": "leave-cards",
"pinches": "1. The last biscuit | what happened: not given | whose it was: A housemate | how much they would miss it: Barely | how you will make it good: Replace it | was it the last one: Yes\n2. Borrowed phone charger | what happened: it is still plugged in by my bed | whose it was: A housemate | how much they would miss it: A little | how you will make it good: Nothing yet | was it the last one: No\n3. The good blanket | what happened: it has been on my sofa for a while | whose it was: A housemate | how much they would miss it: A lot | how you will make it good: Give it back | was it the last one: No",
"facts": "1. The last biscuit: honour 4 of 5; verdict gentleman; weakest article: Never take the last one; card opens: Dear housemate; mend: nothing to mend\n2. Borrowed phone charger: honour 2 of 5; verdict rogue; weakest article: Make it good; card opens: Dear housemate; mend: change the plan from Nothing yet to Replace it or Give it back (whichever fits the thing), landing in gentleman\n3. The good blanket: honour 3 of 5; verdict rogue; weakest article: Take only a trifle; card opens: Dear housemate; mend: the plan is already Give it back; no change of plan lifts it, so it stays rogue\ncounts: return-it 0, gentleman 1, rogue 2 (of 3 pinches)\nfinest pinch: The last biscuit"
}
Reply:
{
"task": "leave-cards",
"headline": "Three pinches earn one gentleman thief card and two charming rogue cards.",
"pinches": [
{
"name": "The last biscuit",
"verdict": "gentleman",
"card": "Dear housemate, I'll own up to taking the last biscuit, with a wink for my cheek. I will replace it to make it right.",
"why": "The article \"Never take the last one\" cost you points, since you answered yes to taking the last one. Still, you only called it barely missed and you mean to replace it, so the card comes from a gentleman thief.",
"mend": "You don't need another step here, since your card already sets this right."
},
{
"name": "Borrowed phone charger",
"verdict": "rogue",
"card": "Dear housemate, I'll come clean, I borrowed your phone charger and it is still plugged in by my bed. I don't have a plan yet, so tell me what would put this right.",
"why": "The article \"Make it good\" cost you points, since your plan was nothing yet. You still said the housemate would miss it only a little, keeping you a charming rogue.",
"mend": "Since the charger is still by your bed, you could give it back to make this right."
},
{
"name": "The good blanket",
"verdict": "rogue",
"card": "Dear housemate, I confess, your good blanket has been on my sofa for a while. With a playful grin, I will give it back to you.",
"why": "The article \"Take only a trifle\" cost you points, since you said the housemate would miss it a lot. You also said you will give it back, keeping you a charming rogue rather than needing to return it.",
"mend": "Your plan to give it back already does what it can, so the rest is simply owning up in the card."
}
],
"closing": "The last biscuit is the finest pinch here, ahead of the others."
}
At work - verdicts: return it, gentleman thief, charming rogue
Input:
{
"task": "leave-cards",
"pinches": "1. Stapler from the next desk | what happened: it has quietly moved onto my desk | whose it was: A colleague | how much they would miss it: Barely | how you will make it good: Give it back | was it the last one: No\n2. Oat milk in the office fridge | what happened: I used the last of it in my tea | whose it was: Everyone (it was shared) | how much they would miss it: A little | how you will make it good: Make it up another way | was it the last one: No\n3. Coins from the tea fund | what happened: I took them for the vending machine | whose it was: Everyone (it was shared) | how much they would miss it: Barely | how you will make it good: Replace it | was it the last one: No",
"facts": "1. Stapler from the next desk: honour 5 of 5; verdict gentleman; weakest article: none; card opens: Dear colleague; mend: nothing to mend\n2. Oat milk in the office fridge: honour 3 of 5; verdict rogue; weakest article: Take only a trifle; card opens: Dear all; mend: change the plan from Make it up another way to Replace it or Give it back (whichever fits the thing), landing in gentleman\n3. Coins from the tea fund: honour 5 of 5; verdict return-it (off-limits: money, valuables, documents, medicine, a shop or a stranger); weakest article: none; card opens: Dear all; mend: hand it back or replace it - it is off-limits, so no plan makes it a pinch\ncounts: return-it 1, gentleman 1, rogue 1 (of 3 pinches)\nfinest pinch: Stapler from the next desk"
}
Reply:
{
"task": "leave-cards",
"headline": "Cards leave the tray: one thing goes back, one plays the gentleman thief, one confesses like a charming rogue.",
"pinches": [
{
"name": "Stapler from the next desk",
"verdict": "gentleman",
"card": "Dear colleague, I confess I took your stapler, which has quietly moved onto my desk, and with a small bow, I will give it back.",
"why": "The whole code holds together here: the colleague would barely miss it, and you plan to give it back.",
"mend": "Here, the card itself is enough, so there is nothing more for you to do."
},
{
"name": "Oat milk in the office fridge",
"verdict": "rogue",
"card": "Dear all, I admit I used the last of the oat milk in my tea, and with a small tip of my hat, I promise to make it up to you.",
"why": "The weakest article, \"take only a trifle,\" cost points because everyone would miss the oat milk a little, even though you plan to make it up another way.",
"mend": "Because the oat milk is used up, you could simply replace it instead of making it up another way."
},
{
"name": "Coins from the tea fund",
"verdict": "return-it",
"card": "Dear all, I took coins from the tea fund for the vending machine, and I am sorry. The replacement goes with this card.",
"why": "Coins are money, so the code never covers them, even though everyone would barely miss them and you plan to replace it.",
"mend": "Since the coins went into the vending machine, the plain step for you is to replace them in the tea fund."
}
],
"closing": "Stapler from the next desk is the finest pinch on the tray."
}
Family visit - verdicts: return it, gentleman thief, charming rogue
Input:
{
"task": "leave-cards",
"pinches": "1. The green hoodie | what happened: I wore it home after the visit | whose it was: A family member | how much they would miss it: A little | how you will make it good: Give it back | was it the last one: No\n2. The TV remote | what happened: it has been in my bag since I left | whose it was: A family member | how much they would miss it: A lot | how you will make it good: Nothing yet | was it the last one: No\n3. The last slice of cake | what happened: not given | whose it was: A friend | how much they would miss it: A lot | how you will make it good: Replace it | was it the last one: Yes\n4. Two clues of the crossword | what happened: I filled them in while it was left on the table | whose it was: A friend | how much they would miss it: Barely | how you will make it good: Make it up another way | was it the last one: No",
"facts": "1. The green hoodie: honour 4 of 5; verdict gentleman; weakest article: Take only a trifle; card opens: Dear family; mend: nothing to mend\n2. The TV remote: honour 1 of 5; verdict return-it; weakest article: Take only a trifle; card opens: Dear family; mend: change the plan from Nothing yet to Replace it or Give it back (whichever fits the thing), landing in rogue\n3. The last slice of cake: honour 2 of 5; verdict rogue; weakest article: Take only a trifle; card opens: Dear friend; mend: the plan is already Replace it; no change of plan lifts it, so it stays rogue\n4. Two clues of the crossword: honour 4 of 5; verdict gentleman; weakest article: Make it good; card opens: Dear friend; mend: nothing to mend\ncounts: return-it 1, gentleman 2, rogue 1 (of 4 pinches)\nfinest pinch: The green hoodie"
}
Reply:
{
"task": "leave-cards",
"headline": "Two cards here are gentleman thief, one is a charming rogue, and one thing has to go back.",
"pinches": [
{
"name": "The green hoodie",
"verdict": "gentleman",
"card": "Dear family, I took the green hoodie and wore it home after the visit. With a small bow, I will give it back to you.",
"why": "The \"take only a trifle\" article cost you points because your family would only miss the hoodie a little, but you answered give it back and no to it being the last one, which kept this a gentleman thief.",
"mend": "Here, no further mend is needed, because the card you leave is enough to make this right."
},
{
"name": "The TV remote",
"verdict": "return-it",
"card": "Dear family, I am sorry, I took the TV remote, and I am returning it to you.",
"why": "The \"take only a trifle\" article cost you points because your family would miss the remote a lot, and answering nothing yet alongside that is why this has to go back.",
"mend": "The remote is still around, so you can mend this by giving it back rather than leaving it unresolved."
},
{
"name": "The last slice of cake",
"verdict": "rogue",
"card": "Dear friend, I confess, I took the last slice of cake. With a small bow, I will replace it for you.",
"why": "The \"take only a trifle\" article cost you points because your friend would miss the cake a lot, and taking the last one too left little room, even though you answered replace it, so this stays a charming rogue.",
"mend": "Your plan to replace it does what it can, so let the card own up to the rest."
},
{
"name": "Two clues of the crossword",
"verdict": "gentleman",
"card": "Dear friend, I confess, I filled in two clues of the crossword while it was left on the table. With a tip of the hat, I will make it up to you.",
"why": "The \"make it good\" article cost you points because you answered make it up another way, though your friend would barely miss the clues and it was not the last one, so this stays a gentleman thief.",
"mend": "Nothing more needs mending here, because leaving the card sets things right for you."
}
],
"closing": "The green hoodie is the finest pinch on the tray, simply the one that stood highest on the list."
}
Build the input yourself
Load the engine in node (global.window = global; require("./pinchkit.js")), then:
const K = PinchKit;
const form = K.form({ alias: "", pinches: [
{ name: "The last biscuit", note: "", owner: "housemate", missed: "barely", plan: "replace", last: "yes" },
{ name: "Borrowed umbrella", note: "it is still by my door", owner: "colleague", missed: "little", plan: "none", last: "no" }] });
const built = K.buildInput(form); // built.facts holds each pinch's honour, verdict, weakest article and mend
const wire = K.toWire(built.input); // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// K.check(reply, built).notes // the page's re-check of the reply
// K.md(built, reply, K.title(form), K.signature(form)) // the tray as Markdown
The answer keys: owner is housemate, colleague, family, friend or shared; missed is barely, little or lot; plan is replace, return, other or none; last is yes or no.
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://with-compliments.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://with-compliments.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"]
// with-compliments.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://with-compliments.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://with-compliments.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
}
// WithCompliments.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 WithCompliments {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://with-compliments.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://with-compliments.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://with-compliments.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://with-compliments.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":"with-compliments"}' | 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": "with-compliments"})["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: "with-compliments" })).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": "with-compliments"}, 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", "with-compliments"), 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: "with-compliments" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "with-compliments"])["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 = "with-compliments" }))!["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": "leave-cards", "pinches": "1. The last biscuit | what happened: ... | ...", "facts": "...", ...}
# - see the field table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}'
inp = json.load(open("input.json")) # {"task": "leave-cards", "pinches": "1. The last biscuit | what happened: ... | ...", "facts": "...", ...}
# - see the field table
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task: "leave-cards", pinches, facts} (see the field table)
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
Step 5 — POST /run, then poll the job
Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses with-compliments:<task>:<inputHash>:a<attempt> (the one task is leave-cards), 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="with-compliments:leave-cards:$(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 = "with-compliments:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the reply as one JSON object
print(j["charged_credits"], j.get("truncated"), len(reply.get("pinches", [])))
import { createHash } from "node:crypto";
const key = `with-compliments:${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.pinches || []).length);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "with-compliments:" + 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 = "with-compliments:" + 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 = "with-compliments:#{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["pinches"] || []).length].inspect
$key = "with-compliments:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", count($reply["pinches"] ?? []), "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"with-compliments:{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: with-compliments:leave-cards:$(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": "leave-cards", "pinches": "1. The last biscuit | what happened: ... | ...", "facts": "...", ...}
# - see the field table
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "with-compliments:leave-cards:" + 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": `with-compliments:leave-cards:${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", "with-compliments:leave-cards:"+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", "with-compliments:leave-cards:" + 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" => "with-compliments:leave-cards:#{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: with-compliments:leave-cards:" . 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", $"with-compliments:leave-cards:{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);