Drive Between Hours from your own code
The one metered task, read, is available over HTTP. It takes a description of a place, what kind of place it is, when it is seen, the cues switched on and the page's own notes. It returns the cues it counted, a verdict (deeply_liminal, at_the_right_hour or just_a_place), a planner's reading, up to four signs pointing at the description's own words, the most in-between hour, a way to notice it and a visit note.
The division of labour first: the page owns the kinds of place, the hours, the eight cues, the word match, the verdict rule and the corridor drawing; the model owns which cues the description plainly supports, the planner's reading, the signs and the wording; the page then re-checks the reply.
Base URL and the envelope
Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response uses one envelope:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "...", "status": 402, "details": { ... } } }
The token is minted for this app (the guest call takes {"slug":"between-hours"}), 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, place, kind, when, cues, page_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: a reading is metered and needs a personal token. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
The task field and the verdicts
Every run sends task: read (the one task is read); a missing or different one is still answered as read. The verdict is a rule on the reply's own counted cues, checked in order: just_a_place when neither passing nor crowd is counted; deeply_liminal when one of them is counted AND empty is counted AND at least one of light, repeat, sealed, familiar, hush; otherwise at_the_right_hour.
| Field | Required | What it holds |
|---|---|---|
task | yes | Always read. |
place | yes | The description, in the person's words (up to about 700 characters; longer text keeps its start and end with the middle marked as cut). |
kind | yes | One of stairwell, corridor, waiting, concourse, parking, mall, school, hotel, pool, playground, other. |
when | yes | early, midday, evening, late, offseason or not said. |
cues | yes | The switched-on cue ids, comma-separated, or none: passing, crowd, empty, light, repeat, sealed, familiar, hush. |
page_facts | yes | The page's notes, one key | ... per line: kind, when, ticked, words_suggest (a hint only), busy_words, cue_ids, verdict_rule and page_verdict. |
clip_note | no | What the page cut, if anything. |
retry_note | no | Only on the automatic reformat retry. |
The reply
One JSON object, in output.output of the finished job:
{"task": "read", "title": "...", "cues_counted": ["passing", "empty"],
"verdict": "deeply_liminal | at_the_right_hour | just_a_place", "verdict_reason": "...", "reading": "...",
"signs": [{"cue": "passing", "seen": "..."}], "best_hours": {"when": "early", "why": "..."},
"tip_more": "...", "visit_note": "..."}
| Field | What it holds |
|---|---|
cues_counted | Every switched-on cue plus any the description states plainly (never empty when it mentions crowds, queues or regulars). |
verdict / verdict_reason | The rule above on the counted cues, and one sentence (at most 30 words). |
reading | A planner's reading of what the space is built for against how it is described (at most 45 words). |
signs | Up to four {cue, seen} items, one per counted cue, each pointing at the description's words (at most 20 words). |
best_hours | when (one of the five hours) it most likely feels in-between, and why (at most 25 words). |
tip_more / visit_note / title | One small legal way to notice it (at most 25 words); a courtesy and safety note (at most 25 words); a playful title (at most 8 words). |
The page then checks the reply (HallKit.check): every switched-on cue counted, extra cues backed by the page's word match, the verdict against the rule on the reply's own counted cues, one sign per counted cue, a known hour, word limits, and nothing the description does not say - no added lights, sounds, objects, weather, days or seasons - plus no he/she, no internal field names, no meme names, nothing spooky and no suggestion to go anywhere closed or private.
Worked examples, one per sample
These are the four free samples on the page: the exact input and a stand-in model's reply, unedited. Together they cover all three verdicts - a back stairwell before anyone arrives and a pool just before closing (deeply in-between), a station ticket hall at the evening rush (in-between at the right hour) and a busy neighbourhood cafe (just a place).
A back stairwell at 7am - deeply in-between
Input:
{
"task": "read",
"place": "The back stairwell at my office building, which I take at 7am before anyone else is in. Eight identical landings, each with the same grey fire door and the same painted floor number. The lights are long strip lights that buzz faintly, and every footstep echoes up and down the shaft.",
"kind": "stairwell",
"when": "early",
"cues": "passing, empty, light, repeat, hush",
"page_facts": "kind | a stairwell\nwhen | early morning\nticked | passing, empty, light, repeat, hush - the person's own switches; always count these\nwords_suggest | none - the page's word match on the description and the kind of place, a hint that can be wrong\nbusy_words | no\ncue_ids | passing = built for passing through or waiting; crowd = built for a crowd that is not there; empty = nobody else around; light = flat or buzzing artificial light; repeat = repeating doors, tiles or rows; sealed = no windows or no clock; familiar = feels familiar from childhood; hush = a hum, an echo or a hush\nverdict_rule | check in order and stop at the first that fits: 1) just_a_place when neither passing nor crowd is counted; 2) deeply_liminal when passing or crowd is counted AND empty is counted AND at least one of light, repeat, sealed, familiar, hush is counted; 3) otherwise at_the_right_hour\npage_verdict | deeply_liminal (from the ticked switches alone)"
}
Reply:
{
"task": "read",
"title": "Eight Identical Landings, All to Yourself",
"cues_counted": [
"passing",
"empty",
"light",
"repeat",
"hush"
],
"verdict": "deeply_liminal",
"verdict_reason": "It's built for passing through, with nobody else around this early, and buzzing lights, repeating landings and echoing footsteps make it feel fully in-between.",
"reading": "A stairwell is designed for passing through, not lingering, yet at 7am you walk it entirely alone - every identical landing, buzzing lights, and echoing step amplifying how in-between this passing-through space feels.",
"signs": [
{
"cue": "repeat",
"seen": "Eight identical landings, each with the same grey door and floor number"
},
{
"cue": "hush",
"seen": "Every footstep echoes up and down the stairwell shaft"
},
{
"cue": "light",
"seen": "Long strip lights buzz faintly through the stairwell"
},
{
"cue": "empty",
"seen": "You take this stairwell at 7am before anyone else is in"
}
],
"best_hours": {
"when": "early",
"why": "At 7am, before anyone else arrives, the identical landings and echoing footsteps feel most in-between."
},
"tip_more": "Pause on a landing and listen to how your footsteps echo before you continue up or down.",
"visit_note": "Use the stairwell only when you're meant to be in the building, keep the fire doors clear, and leave if someone asks."
}
An indoor pool just before closing - deeply in-between
Input:
{
"task": "read",
"place": "The indoor pool at the leisure centre during the last lane swim before closing, when I'm usually the only one left. Rows of empty spectator benches, the water perfectly still, no windows at all, and that chlorine smell from school swimming lessons. The only sound is the filter humming.",
"kind": "pool",
"when": "late",
"cues": "crowd, empty, light, sealed, familiar, hush",
"page_facts": "kind | an indoor pool or leisure centre\nwhen | late at night\nticked | crowd, empty, light, sealed, familiar, hush - the person's own switches; always count these\nwords_suggest | repeat - the page's word match on the description and the kind of place, a hint that can be wrong\nbusy_words | no\ncue_ids | passing = built for passing through or waiting; crowd = built for a crowd that is not there; empty = nobody else around; light = flat or buzzing artificial light; repeat = repeating doors, tiles or rows; sealed = no windows or no clock; familiar = feels familiar from childhood; hush = a hum, an echo or a hush\nverdict_rule | check in order and stop at the first that fits: 1) just_a_place when neither passing nor crowd is counted; 2) deeply_liminal when passing or crowd is counted AND empty is counted AND at least one of light, repeat, sealed, familiar, hush is counted; 3) otherwise at_the_right_hour\npage_verdict | deeply_liminal (from the ticked switches alone)"
}
Reply:
{
"task": "read",
"title": "The Last Lap, All to Yourself",
"cues_counted": [
"crowd",
"empty",
"light",
"repeat",
"sealed",
"familiar",
"hush"
],
"verdict": "deeply_liminal",
"verdict_reason": "It's built for a crowd that isn't there, with nobody else around, rows of empty benches, no windows, a familiar smell and only a hum for company.",
"reading": "A pool like this is built for a crowd that isn't there, but during the last swim before closing, you have the benches and the still water entirely to yourself.",
"signs": [
{
"cue": "empty",
"seen": "You say you're usually the only one left for the last lane swim before closing."
},
{
"cue": "sealed",
"seen": "No windows at all, exactly as you described it."
},
{
"cue": "hush",
"seen": "The only sound is the filter humming, you said."
},
{
"cue": "familiar",
"seen": "That chlorine smell takes you back to school swimming lessons."
}
],
"best_hours": {
"when": "late",
"why": "The last lane swim before closing already leaves you alone with rows of empty benches and a humming filter."
},
"tip_more": "Pause by the still water and listen to the filter humming before you leave.",
"visit_note": "Stay only while it's open to you, keep exits and doors clear, and leave right away if asked."
}
A station ticket hall at the evening rush - in-between at the right hour
Input:
{
"task": "read",
"place": "The ticket hall at our main train station around six in the evening. Crowds stream past the long row of ticket machines, the departure boards keep flipping, and there's always a queue at the coffee kiosk. Big glass roof overhead.",
"kind": "concourse",
"when": "evening",
"cues": "passing, repeat",
"page_facts": "kind | a station or terminal concourse\nwhen | evening\nticked | passing, repeat - the person's own switches; always count these\nwords_suggest | none - the page's word match on the description and the kind of place, a hint that can be wrong\nbusy_words | yes - the description mentions crowds or busyness\ncue_ids | passing = built for passing through or waiting; crowd = built for a crowd that is not there; empty = nobody else around; light = flat or buzzing artificial light; repeat = repeating doors, tiles or rows; sealed = no windows or no clock; familiar = feels familiar from childhood; hush = a hum, an echo or a hush\nverdict_rule | check in order and stop at the first that fits: 1) just_a_place when neither passing nor crowd is counted; 2) deeply_liminal when passing or crowd is counted AND empty is counted AND at least one of light, repeat, sealed, familiar, hush is counted; 3) otherwise at_the_right_hour\npage_verdict | at_the_right_hour (from the ticked switches alone)"
}
Reply:
{
"task": "read",
"title": "Rows of Machines, Rivers of People",
"cues_counted": [
"passing",
"repeat"
],
"verdict": "at_the_right_hour",
"verdict_reason": "Built for passing through and lined with repeating machines, it's in-between at the right hour - but the crowds keep it far from deserted, so it's not deeply in-between.",
"reading": "This hall is built to move people through - and right now it's doing exactly that, streams of people cutting past repeating machines, the evening crowd filling it the way it's meant to be used.",
"signs": [
{
"cue": "passing",
"seen": "Crowds stream past the long row of ticket machines - everyone moving through, not stopping."
},
{
"cue": "repeat",
"seen": "The long row of ticket machines repeats again and again along the hall."
}
],
"best_hours": {
"when": "late",
"why": "Late, once the evening crowds thin and the ticket machines and departure boards sit mostly unused."
},
"tip_more": "Pause by the ticket machines a moment and watch the departure board flip while the crowd moves past you.",
"visit_note": "Only linger if the hall is open to the public, keep ticket machines and exits clear, and move on if asked."
}
A busy neighbourhood cafe - just a place
Input:
{
"task": "read",
"place": "Our neighbourhood cafe at lunchtime: regulars chatting at the counter, the radio on, big windows onto the street and the smell of toast. It looks almost exactly like the cafe my grandparents ran when I was small.",
"kind": "other",
"when": "midday",
"cues": "familiar",
"page_facts": "kind | somewhere else\nwhen | middle of the day\nticked | familiar - the person's own switches; always count these\nwords_suggest | none - the page's word match on the description and the kind of place, a hint that can be wrong\nbusy_words | yes - the description mentions crowds or busyness\ncue_ids | passing = built for passing through or waiting; crowd = built for a crowd that is not there; empty = nobody else around; light = flat or buzzing artificial light; repeat = repeating doors, tiles or rows; sealed = no windows or no clock; familiar = feels familiar from childhood; hush = a hum, an echo or a hush\nverdict_rule | check in order and stop at the first that fits: 1) just_a_place when neither passing nor crowd is counted; 2) deeply_liminal when passing or crowd is counted AND empty is counted AND at least one of light, repeat, sealed, familiar, hush is counted; 3) otherwise at_the_right_hour\npage_verdict | just_a_place (from the ticked switches alone)"
}
Reply:
{
"task": "read",
"title": "A Cafe That Remembers Your Grandparents",
"cues_counted": [
"familiar"
],
"verdict": "just_a_place",
"verdict_reason": "This cafe isn't built for moving people through or for a gathering that isn't there - it simply carries a feeling from childhood.",
"reading": "This cafe is built for people to settle in and meet - regulars at the counter, a radio playing, windows looking onto the street - which keeps it rooted in the everyday rather than in-between.",
"signs": [
{
"cue": "familiar",
"seen": "It looks almost exactly like the cafe your grandparents ran when you were small."
}
],
"best_hours": {
"when": "early",
"why": "Before the regulars settle in at lunchtime, the counter and windows may feel far quieter and less busy."
},
"tip_more": "Pause by the window, catch the radio under the chatter and the smell of toast, and notice how ordinary it feels.",
"visit_note": "Stop by only when it's open to you, keep the entrance clear, and leave if asked."
}
Build the input yourself
Load the script in node (global.window = global; require("./hallkit.js")), then:
const K = HallKit;
const built = K.buildInput({ place: "The corridor to the gym changing rooms at 6am: long, carpeted, nobody else about.", kind: "corridor", when: "early", cues: ["passing", "empty", "repeat"] });
built.pre; // { ticked, hinted, suggested, busy, verdict, lamps } - the page's free reading
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 reading
// K.verdictFor(reply.cues_counted) // the verdict rule on the reply's own cues
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://between-hours.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://between-hours.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"]
// between-hours.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://between-hours.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://between-hours.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
}
// BetweenHours.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 BetweenHours {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://between-hours.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://between-hours.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://between-hours.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://between-hours.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":"between-hours"}' | 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": "between-hours"})["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: "between-hours" })).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": "between-hours"}, 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", "between-hours"), 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: "between-hours" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "between-hours"])["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 = "between-hours" }))!["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": "read", "place": "...", "kind": "stairwell", "when": "early", "cues": "passing, empty, light", "page_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": "read", "place": "...", "kind": "stairwell", "when": "early", "cues": "passing, empty, light", "page_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: "read", place, kind, when, cues, page_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 between-hours:<task>:<inputHash>:a<attempt> (the one task is read), 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="between-hours:read:$(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 = "between-hours:" + 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("title"))
import { createHash } from "node:crypto";
const key = `between-hours:${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.title);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "between-hours:" + 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 = "between-hours:" + 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 = "between-hours:#{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["title"]].inspect
$key = "between-hours:" . $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["title"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"between-hours:{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: between-hours:read:$(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": "read", "place": "...", "kind": "stairwell", "when": "early", "cues": "passing, empty, light", "page_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": "between-hours:read:" + 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": `between-hours:read:${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", "between-hours:read:"+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", "between-hours:read:" + 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" => "between-hours:read:#{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: between-hours:read:" . 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", $"between-hours:read:{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);