Drive Paddling Below from your own code
The one metered task, blueprint, is available over HTTP. It takes one everyday service as three to twelve steps in order, each in one of four lanes - you, in front of you, out of sight, what it runs on - with its minutes and marks for waiting, going wrong and (below the waterline) your guesses, plus the page's numbers. It returns a blueprint reading: the verdict, a headline, the longest stretch out of sight, what went wrong, the longest wait, one fix for whoever runs the service and one question for you to ask next time.
The division of labour first: the page counts; the model reads. The page works out the total minutes, the waiting share, the waterline crossings, the longest stretch out of sight (the run of steps below the waterline with the most minutes; a tie goes to the earlier), the snags, the verdict, which step the fix is for and where the question is asked, and sends them as facts. The model never changes those numbers. After the reply, the page lists anything that does not match. A thinking aid, not professional advice.
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":"paddling-below"}), 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, service, steps, 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 blueprint reading is metered and needs a personal sign-in. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
The task field, the input and the rules
Every run sends task: blueprint (the one task is blueprint); a missing or different one is still answered as blueprint. All fields are strings.
| Field | Required | What it holds |
|---|---|---|
task | yes | Always blueprint. |
service | yes | name | note: .... The note is none when empty; a name is up to 70 characters, a note up to 240. |
steps | yes | One step a line, in order: N. what happened | lane: you | minutes: 5 | you were waiting: yes | went wrong: no, and below the waterline (out of sight, what it runs on) also | seen or told or | your guess. Three to twelve lines; minutes 0 to 240; a step up to 80 characters. |
facts | yes | The page's numbers: total, lane counts and guesses, waiting minutes and share, longest wait, waterline crossings, longest stretch out of sight, snags, verdict, the fix step and the question step. Build it with PondKit.buildInput. |
retry_note | no | Only on a reformat retry: why the previous reply was unusable. |
The verdict, checked in this order: snag-below when a step out of sight or in what it runs on went wrong; else snag-in-view when a step of yours or in front of you went wrong; else long-paddle when waiting took half the minutes or more; else smooth-glide.
The reply
One JSON object, no prose around it:
{"task": "blueprint", "verdict": "snag-below", "headline": "...", "out_of_sight": "...", "snag_note": "...", "wait_note": "...", "fix_for_them": "...", "ask_next_time": "..."}
| Field | What it holds |
|---|---|
verdict | snag-below, snag-in-view, long-paddle or smooth-glide - always the page's. |
headline | One sentence: what the verdict means, with the total minutes and the waiting share. |
out_of_sight | The longest stretch out of sight, its minutes and how many steps below the waterline are guesses; empty when every step was in view. |
snag_note | The first step that went wrong, in its own words and lane, with no cause added; empty when nothing went wrong. |
wait_note | The longest wait with its minutes and the waiting share; empty when no waiting is marked. |
fix_for_them | One small change for whoever runs the service, at the step the page names - never a number of minutes or anything the steps do not mention. |
ask_next_time | One quoted question for you to ask next time, at the step the page names, about your first guess. |
The page reads the reply with ReplyKit.normalize and checks it with ReplyKit.check, which lists, under "The page's check", a verdict that differs from the page's, a percentage that is not the waiting share, a note that does not name the right step, a missing or unexpected note, and a day or time you did not give.
Worked examples - every verdict
These are the free samples on the page: the exact input the page sends and the reply that replays for free. Together they cover every verdict - snagged below, snagged in view, long paddle and smooth glide.
Collecting a parcel - snag-below
Input:
{
"task": "blueprint",
"service": "Collecting a parcel from the depot | note: none",
"steps": "1. Queued at the collection counter | lane: you | minutes: 15 | you were waiting: yes | went wrong: no\n2. Showed the card and my ID | lane: you | minutes: 2 | you were waiting: no | went wrong: no\n3. Clerk scanned the card | lane: in front of you | minutes: 1 | you were waiting: no | went wrong: no\n4. Clerk went into the back to look | lane: out of sight | minutes: 12 | you were waiting: yes | went wrong: no | seen or told\n5. Parcel not on the shelf the scanner showed | lane: out of sight | minutes: 8 | you were waiting: yes | went wrong: yes | seen or told\n6. Shelf locations kept in the depot system | lane: what it runs on | minutes: 0 | you were waiting: no | went wrong: no | your guess\n7. Clerk handed over the parcel | lane: in front of you | minutes: 2 | you were waiting: no | went wrong: no",
"facts": "Total: 40 minutes over 7 steps.\nLanes: you 2, in front of you 2, out of sight 2, what it runs on 1. Your guesses below the waterline (whole service): 1 of 3.\nWaiting: 35 minutes of 40 minutes (88%), at steps 1, 4, 5. Longest wait: step 1 \"Queued at the collection counter\", 15 minutes.\nWaterline crossings: 2 (out of your sight 1 time, back into view 1 time).\nLongest stretch out of sight: steps 4 to 6 (\"Clerk went into the back to look\" to \"Shelf locations kept in the depot system\"), 20 minutes, 3 steps.\nSnags: 1 - step 5 \"Parcel not on the shelf the scanner showed\" (out of sight).\nVerdict: snag-below (checked in order: a snag out of sight or in what it runs on is snag-below; else a snag in your lane or in front of you is snag-in-view; else waiting of half the minutes or more is long-paddle; else smooth-glide)\nFix for whoever runs it: step 5 \"Parcel not on the shelf the scanner showed\" - the first snag out of sight.\nQuestion for you to ask: about step 6 \"Shelf locations kept in the depot system\" (your first guess), asked at step 3 \"Clerk scanned the card\"."
}
Reply:
{
"task": "blueprint",
"verdict": "snag-below",
"headline": "Over 40 minutes, something went wrong out of your sight, with waiting taking up 88% of that time.",
"out_of_sight": "The longest stretch out of sight ran from the clerk going into the back to look to your guess that shelf locations are kept in the depot system, lasting 20 minutes, and only 1 of the 3 steps below the waterline in the whole service is your guess.",
"snag_note": "The snag was that the parcel was not on the shelf the scanner showed, out of sight.",
"wait_note": "The longest wait was spent queued at the collection counter, 15 minutes, and waiting took up 88% of the time overall.",
"fix_for_them": "At the step where the parcel was not on the shelf the scanner showed, whoever runs it could add a habit of checking that shelf a second time.",
"ask_next_time": "Next time, as the clerk scans the card, you could check your guess that shelf locations are kept in the depot system by asking, \"Are shelf locations kept in the depot system?\""
}
A walk-in haircut - snag-in-view
Input:
{
"task": "blueprint",
"service": "A walk-in haircut | note: First visit to this barber.",
"steps": "1. Gave my name at the desk | lane: you | minutes: 1 | you were waiting: no | went wrong: no\n2. Receptionist wrote it on the walk-in list | lane: in front of you | minutes: 1 | you were waiting: no | went wrong: no\n3. Walk-in list kept on a clipboard | lane: what it runs on | minutes: 0 | you were waiting: no | went wrong: no | seen or told\n4. Waited on the sofa | lane: you | minutes: 25 | you were waiting: yes | went wrong: no\n5. Barbers took the next name in turn | lane: out of sight | minutes: 0 | you were waiting: no | went wrong: no | your guess\n6. Barber called the wrong name | lane: in front of you | minutes: 2 | you were waiting: no | went wrong: yes\n7. Haircut and a quick wash | lane: in front of you | minutes: 30 | you were waiting: no | went wrong: no\n8. Paid at the desk | lane: you | minutes: 3 | you were waiting: no | went wrong: no",
"facts": "Total: 62 minutes over 8 steps.\nLanes: you 3, in front of you 3, out of sight 1, what it runs on 1. Your guesses below the waterline (whole service): 1 of 2.\nWaiting: 25 minutes of 62 minutes (40%), at step 4. Longest wait: step 4 \"Waited on the sofa\", 25 minutes.\nWaterline crossings: 4 (out of your sight 2 times, back into view 2 times).\nLongest stretch out of sight: step 3 \"Walk-in list kept on a clipboard\", 0 minutes, 1 step.\nSnags: 1 - step 6 \"Barber called the wrong name\" (in front of you).\nVerdict: snag-in-view (checked in order: a snag out of sight or in what it runs on is snag-below; else a snag in your lane or in front of you is snag-in-view; else waiting of half the minutes or more is long-paddle; else smooth-glide)\nFix for whoever runs it: step 6 \"Barber called the wrong name\" - the first snag in view.\nQuestion for you to ask: about step 5 \"Barbers took the next name in turn\" (your first guess), asked at step 2 \"Receptionist wrote it on the walk-in list\"."
}
Reply:
{
"task": "blueprint",
"verdict": "snag-in-view",
"headline": "Across 62 minutes, something went wrong in a step where you could see it, with waiting making up 40% of the total time.",
"out_of_sight": "The longest stretch out of sight is the walk-in list kept on a clipboard, which took no minutes you counted, and 1 of the 2 steps below the waterline in the whole service is your guess.",
"snag_note": "In front of you, the barber called the wrong name.",
"wait_note": "The longest wait was when you waited on the sofa for 25 minutes, which was 40% of the total time.",
"fix_for_them": "For the barber, a small habit of checking the name against the walk-in list just before calling it out - at the step where the barber called the wrong name - could be worth trying.",
"ask_next_time": "Next time, when the receptionist writes your name on the walk-in list, you could check your guess by asking, \"Do barbers take the next name in turn?\""
}
A puncture repair - long-paddle
Input:
{
"task": "blueprint",
"service": "Puncture repair at the bike shop | note: none",
"steps": "1. Wheeled the bike to the counter | lane: you | minutes: 2 | you were waiting: no | went wrong: no\n2. Mechanic wrote a repair ticket | lane: in front of you | minutes: 3 | you were waiting: no | went wrong: no\n3. Bike waited in the repair queue | lane: out of sight | minutes: 40 | you were waiting: yes | went wrong: no | your guess\n4. Repair tickets pinned on a workshop board | lane: what it runs on | minutes: 0 | you were waiting: no | went wrong: no | your guess\n5. Inner tube swapped in the workshop | lane: out of sight | minutes: 15 | you were waiting: yes | went wrong: no | your guess\n6. Phone call to say it was ready | lane: in front of you | minutes: 1 | you were waiting: no | went wrong: no\n7. Paid and rode off | lane: you | minutes: 4 | you were waiting: no | went wrong: no",
"facts": "Total: 65 minutes over 7 steps.\nLanes: you 2, in front of you 2, out of sight 2, what it runs on 1. Your guesses below the waterline (whole service): 3 of 3.\nWaiting: 55 minutes of 65 minutes (85%), at steps 3, 5. Longest wait: step 3 \"Bike waited in the repair queue\", 40 minutes.\nWaterline crossings: 2 (out of your sight 1 time, back into view 1 time).\nLongest stretch out of sight: steps 3 to 5 (\"Bike waited in the repair queue\" to \"Inner tube swapped in the workshop\"), 55 minutes, 3 steps.\nSnags: none.\nVerdict: long-paddle (checked in order: a snag out of sight or in what it runs on is snag-below; else a snag in your lane or in front of you is snag-in-view; else waiting of half the minutes or more is long-paddle; else smooth-glide)\nFix for whoever runs it: step 3 \"Bike waited in the repair queue\" - the longest wait.\nQuestion for you to ask: about step 3 \"Bike waited in the repair queue\" (your first guess), asked at step 2 \"Mechanic wrote a repair ticket\"."
}
Reply:
{
"task": "blueprint",
"verdict": "long-paddle",
"headline": "Nothing went wrong across 65 minutes of this service, but waiting took up 85% of the time.",
"out_of_sight": "The longest stretch out of sight runs from your guess that the bike waited in the repair queue to your guess that the inner tube was swapped in the workshop, lasting 55 minutes, and 3 of the 3 steps below the waterline in this whole service are your guesses.",
"snag_note": "",
"wait_note": "The longest wait is your guess that the bike waited in the repair queue for 40 minutes, part of the 85% of the whole time spent waiting.",
"fix_for_them": "Whoever runs it could find a way to let you know what is happening, or roughly how long it will take, during what you guessed was the bike waiting in the repair queue.",
"ask_next_time": "Next time, as the mechanic writes the repair ticket, you could ask, \"Does the bike wait in a repair queue before the repair starts?\""
}
Renewing a library card - smooth-glide
Input:
{
"task": "blueprint",
"service": "Renewing my library card | note: The old card had expired.",
"steps": "1. Handed the old card over at the desk | lane: you | minutes: 1 | you were waiting: no | went wrong: no\n2. Librarian checked my address | lane: in front of you | minutes: 2 | you were waiting: no | went wrong: no\n3. Account renewed in the library system | lane: what it runs on | minutes: 1 | you were waiting: no | went wrong: no | your guess\n4. New card printed behind the desk | lane: out of sight | minutes: 2 | you were waiting: yes | went wrong: no | seen or told\n5. Signed the back of the new card | lane: you | minutes: 1 | you were waiting: no | went wrong: no",
"facts": "Total: 7 minutes over 5 steps.\nLanes: you 2, in front of you 1, out of sight 1, what it runs on 1. Your guesses below the waterline (whole service): 1 of 2.\nWaiting: 2 minutes of 7 minutes (29%), at step 4. Longest wait: step 4 \"New card printed behind the desk\", 2 minutes.\nWaterline crossings: 2 (out of your sight 1 time, back into view 1 time).\nLongest stretch out of sight: steps 3 to 4 (\"Account renewed in the library system\" to \"New card printed behind the desk\"), 3 minutes, 2 steps.\nSnags: none.\nVerdict: smooth-glide (checked in order: a snag out of sight or in what it runs on is snag-below; else a snag in your lane or in front of you is snag-in-view; else waiting of half the minutes or more is long-paddle; else smooth-glide)\nFix for whoever runs it: step 4 \"New card printed behind the desk\" - the longest wait.\nQuestion for you to ask: about step 3 \"Account renewed in the library system\" (your first guess), asked at step 2 \"Librarian checked my address\"."
}
Reply:
{
"task": "blueprint",
"verdict": "smooth-glide",
"headline": "This service went without any snags, taking 7 minutes in all, with 29% of that time spent waiting - well under half.",
"out_of_sight": "The longest stretch out of sight ran from your guess that the account was renewed in the library system to the new card being printed behind the desk, taking 3 minutes, and of the 2 steps below the waterline in the whole service, 1 is your guess.",
"snag_note": "",
"wait_note": "The longest wait was while the new card was printed behind the desk, which took 2 minutes, part of the 29% of the service spent waiting.",
"fix_for_them": "While the new card is printed behind the desk, whoever runs it could let you know roughly how long that wait will be.",
"ask_next_time": "Next time, as the librarian checks your address, you could ask, \"Has the account already been renewed in the library system?\""
}
Build the input yourself
Load the engine in node (global.window = global; require("./pondkit.js"); require("./replykit.js")), then:
const K = PondKit, R = ReplyKit;
const built = K.buildInput(K.form({ name: "Getting a spare key cut", note: "", steps: [
{ what: "Handed over the key", lane: "you", mins: 1 },
{ what: "Key cut on the machine", lane: "front", mins: 4, wait: true },
{ what: "Blank keys kept in a drawer", lane: "base", mins: 0, guess: true } ] })); // built.ok is false, with built.note, until the steps are ready
const wire = K.toWire(built.input); // POST this to /estimate and /run; built.analysis holds total, waitPct, stretch, snags, verdict
// then: const reply = R.normalize(R.parseReply(job.output.output));
// R.check(reply, built).notes // the page's re-check of the reply
// R.md(built, reply, K.title(built.form)) // the steps, the numbers and the reading as Markdown
Step keys: what, lane (you, front, back or base), mins (0 to 240), and the booleans wait, snag and guess (a guess counts only below the waterline). Service keys: name, note and steps.
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://paddling-below.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://paddling-below.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"]
// paddling-below.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://paddling-below.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://paddling-below.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
}
// PaddlingBelow.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 PaddlingBelow {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://paddling-below.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://paddling-below.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://paddling-below.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://paddling-below.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":"paddling-below"}' | 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": "paddling-below"})["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: "paddling-below" })).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": "paddling-below"}, 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", "paddling-below"), 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: "paddling-below" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "paddling-below"])["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 = "paddling-below" }))!["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": "blueprint", "service": "Collecting a parcel from the depot | note: none", "steps": "1. Queued at the collection counter | lane: you | minutes: 15 | you were waiting: yes | went wrong: no ...", "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": "blueprint", "service": "Collecting a parcel from the depot | note: none", "steps": "1. Queued at the collection counter | lane: you | minutes: 15 | you were waiting: yes | went wrong: no ...", "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: "blueprint", service, steps, 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 paddling-below:<task>:<inputHash>:a<attempt> (the one task is blueprint), 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="paddling-below:blueprint:$(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 = "paddling-below:" + 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("ask_next_time"))
import { createHash } from "node:crypto";
const key = `paddling-below:${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.ask_next_time);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "paddling-below:" + 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 = "paddling-below:" + 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 = "paddling-below:#{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["ask_next_time"]].inspect
$key = "paddling-below:" . $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"] ?? ""), " ", ($reply["ask_next_time"] ?? ""), "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"paddling-below:{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: paddling-below:blueprint:$(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": "blueprint", "service": "Collecting a parcel from the depot | note: none", "steps": "1. Queued at the collection counter | lane: you | minutes: 15 | you were waiting: yes | went wrong: no ...", "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": "paddling-below:blueprint:" + 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": `paddling-below:blueprint:${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", "paddling-below:blueprint:"+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", "paddling-below:blueprint:" + 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" => "paddling-below:blueprint:#{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: paddling-below:blueprint:" . 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", $"paddling-below:blueprint:{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);