Drive Seen or Said from your own code
The one metered task, beat-check, is available over HTTP. It takes 1 documentary (its name in your words and how much of it you saw) and up to 6 beats you remember, each with how the film showed it and the page's class, and returns what each beat actually shows, the leap taken on trust and a plain question to check it, plus the beat to check first.
The division of labour first: you pick how each beat was shown, a fixed rule gives the film its verdict and the page picks the beat to check first; the model explains them. After a run the page checks that the verdict and every class match, that the check-first line starts with the page's beat, that every number comes from the beats and that no ids leak into the prose.
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":"seen-or-said"}), 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, film, watched, beats, 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 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 classes
Every run sends task: beat-check (the one task is beat-check); a missing or different one is still answered as beat-check. Each beat's class is how it was shown: seen (the camera was there), found (old footage, a photo or a document), said (an interview), told (a narrator or on-screen text), staged (acted out or reconstructed), cut (2 shots placed side by side) or unclear. The verdict, first match wins: seen and found make at least half the beats on-record; staged and cut at least half rebuilt; said and told at least half on-their-word; otherwise mixed. Check first: the beat furthest from what the camera saw (unclear, cut, staged, told, said, found, seen), the earlier row on a tie.
| Field | Required | What it holds |
|---|---|---|
task | yes | Always beat-check. |
film | yes | The documentary in the person's words, up to 60 characters. |
watched | yes | watched it all, watched part of it or saw a clip or trailer only. |
beats | yes | 1 line per beat: beat 1 | the film says: ... | shown as: the camera was there as it happened | page class: seen. |
facts | yes | The page's lines: film verdict:, check first:, firmest:, classes:. |
The reply
job.output.output is a JSON string:
{"task": "beat-check", "verdict": "on-record", "headline": "...", "beats": [{"beat": 1, "known": "seen", "shows": "...", "leap": "...", "ask": "...?"}, ...], "check_first": "...", "closing": "..."}
| Field | What it holds |
|---|---|
verdict | The page's verdict id, copied. |
headline | What the verdict rests on, at most 16 words. |
beats | 1 entry per beat, in order: beat, known (copied from the page), shows (at most 25 words), leap (at most 25 words) and ask (a question, at most 20 words). |
check_first | The beat to check first and why, ending with the firmest beat, at most 40 words. |
closing | 1 light sentence, at most 20 words. |
The page then checks the reply (BeatKit.check) and lists any disagreement under "The page's check".
Worked examples - every verdict
These are the 4 free samples on the page: the exact input the page sends and the reply that replays for free.
City fox family - seen, told, found, said; mostly on the record
Input:
{
"task": "beat-check",
"film": "a nature film about a city fox family",
"watched": "watched it all",
"beats": "beat 1 | the film says: the vixen moved her cubs to a new den under a shed | shown as: the camera was there as it happened | page class: seen\nbeat 2 | the film says: the cubs were born in early spring | shown as: a narrator or on-screen text said it | page class: told\nbeat 3 | the film says: a neighbour's old photos show foxes in the same garden years ago | shown as: old footage, a photo or a document | page class: found\nbeat 4 | the film says: city foxes are bolder than country foxes | shown as: someone said it in an interview | page class: said",
"facts": "film verdict: on-record (seen or found 2 of 4 beats)\ncheck first: beat 2 (told, the furthest from what the camera saw)\nfirmest: beat 1 (seen)\nclasses: seen 1, found 1, said 1, told 1, staged 0, cut 0, unclear 0"
}
Reply:
{
"task": "beat-check",
"verdict": "on-record",
"headline": "This film rests on the record for 2 of 4 beats you listed.",
"beats": [
{
"beat": 1,
"known": "seen",
"shows": "You get footage of the vixen moving her cubs under the shed into the new den as it happens.",
"leap": "The camera does not show what led to the move or what happened to the den after the shot ends.",
"ask": "What happened just before or after the vixen moved her cubs to the den?"
},
{
"beat": 2,
"known": "told",
"shows": "You are told in words on screen that the cubs were born in early spring.",
"leap": "You still rely on the narrator's word for this detail, since no footage confirms the timing shown here.",
"ask": "How could you check that the cubs were born in early spring?"
},
{
"beat": 3,
"known": "found",
"shows": "You see old photos kept by a neighbour, showing foxes in the same garden years ago.",
"leap": "The photos alone do not say exactly when they were taken or who took them.",
"ask": "Could you find out when the neighbour took those photos of the garden?"
},
{
"beat": 4,
"known": "said",
"shows": "You hear someone say, in an interview, that city foxes are bolder than country foxes.",
"leap": "This rests on that person's own word, without footage comparing city and country foxes directly.",
"ask": "What did the person comparing city and country foxes base the claim on?"
}
],
"check_first": "Beat 2 is worth checking first since it rests on what is told rather than on what a camera recorded, while beat 1 keeps the most direct link to the screen.",
"closing": "Maybe take another look at beat 2 next time you watch this fox family film."
}
Painting found in an attic - staged, staged, said, cut, seen; mostly rebuilt
Input:
{
"task": "beat-check",
"film": "the lost painting film",
"watched": "watched part of it",
"beats": "beat 1 | the film says: the painting hung in the hallway for decades without anyone noticing | shown as: acted out or reconstructed | page class: staged\nbeat 2 | the film says: the grandmother bought it at a flea market | shown as: acted out or reconstructed | page class: staged\nbeat 3 | the film says: an expert says the brushwork matches the painter | shown as: someone said it in an interview | page class: said\nbeat 4 | the film says: after the auction the family looks upset, as if they lost money | shown as: suggested by putting 2 shots side by side | page class: cut\nbeat 5 | the film says: the restorer cleans the canvas in her studio | shown as: the camera was there as it happened | page class: seen",
"facts": "film verdict: rebuilt (staged or cut 3 of 5 beats)\ncheck first: beat 4 (cut, the furthest from what the camera saw)\nfirmest: beat 5 (seen)\nclasses: seen 1, found 0, said 1, told 0, staged 2, cut 1, unclear 0"
}
Reply:
{
"task": "beat-check",
"verdict": "rebuilt",
"headline": "This film rests on rebuilt scenes for 3 of 5 beats you listed.",
"beats": [
{
"beat": 1,
"known": "staged",
"shows": "You watch a reenacted scene showing the painting hanging unnoticed in the hallway for decades.",
"leap": "This scene was staged afterward to stand in for the moment, not filmed as it happened.",
"ask": "What would show how long the painting hung in the hallway before anyone noticed?"
},
{
"beat": 2,
"known": "staged",
"shows": "You see a reenacted scene of the grandmother buying it at a flea market.",
"leap": "This recreates the purchase after the fact, rather than catching it happening at the market.",
"ask": "What would confirm that the grandmother bought it at that flea market?"
},
{
"beat": 3,
"known": "said",
"shows": "You hear an expert say in an interview that the brushwork matches the painter.",
"leap": "This rests on the expert's own word, without other proof shown for the brushwork claim.",
"ask": "What would help confirm that the brushwork matches the painter?"
},
{
"beat": 4,
"known": "cut",
"shows": "You see 2 shots placed together - the auction, then the family looking upset.",
"leap": "The edit links the family's look to the auction, without showing what caused it or any money lost.",
"ask": "What happened between the auction and the family looking upset?"
},
{
"beat": 5,
"known": "seen",
"shows": "You watch the restorer clean the canvas in her studio as it happens on camera.",
"leap": "The camera does not show what happens to the canvas before or after this moment in the studio.",
"ask": "What might the camera have missed just before or after cleaning the canvas?"
}
],
"check_first": "Beat 4 is worth checking first since it rests on an edited link rather than on what a camera recorded, while beat 5 keeps the most direct link to the screen.",
"closing": "Maybe watch beat 4 again sometime to see how the auction scene is cut together."
}
Island post office - told, said, said, found; mostly on their word
Input:
{
"task": "beat-check",
"film": "island post office documentary",
"watched": "watched it all",
"beats": "beat 1 | the film says: it is the oldest post office still open in the country | shown as: a narrator or on-screen text said it | page class: told\nbeat 2 | the film says: letters once came by rowing boat | shown as: someone said it in an interview | page class: said\nbeat 3 | the film says: the post office keeps the island together | shown as: someone said it in an interview | page class: said\nbeat 4 | the film says: a 1950s photo shows a queue outside | shown as: old footage, a photo or a document | page class: found",
"facts": "film verdict: on-their-word (said or told 3 of 4 beats)\ncheck first: beat 1 (told, the furthest from what the camera saw)\nfirmest: beat 4 (found)\nclasses: seen 0, found 1, said 2, told 1, staged 0, cut 0, unclear 0"
}
Reply:
{
"task": "beat-check",
"verdict": "on-their-word",
"headline": "This film rests on their word for 3 of 4 beats you listed.",
"beats": [
{
"beat": 1,
"known": "told",
"shows": "You are told on screen that it is the oldest post office still open in the country.",
"leap": "You still rely on the narrator's word for this claim, without seeing proof it is the oldest.",
"ask": "How could you check whether it is the oldest post office still open in the country?"
},
{
"beat": 2,
"known": "said",
"shows": "You hear someone say in an interview that letters once came by rowing boat.",
"leap": "This rests on that person's own word, without any record showing letters arriving by rowing boat.",
"ask": "What would help confirm that letters once came by rowing boat?"
},
{
"beat": 3,
"known": "said",
"shows": "You hear someone say in an interview that the post office keeps the island together.",
"leap": "This rests on that person's own word about what the post office means for the island.",
"ask": "What else could show that the post office keeps the island together?"
},
{
"beat": 4,
"known": "found",
"shows": "You see an old photo from the 1950s showing a queue outside.",
"leap": "The photo alone does not say why the queue was there or who took the picture.",
"ask": "What would explain why the queue was outside in that 1950s photo?"
}
],
"check_first": "Beat 1 is worth checking first since it rests on what is told rather than on what a camera recorded, while beat 4 keeps the most direct link to the screen.",
"closing": "Maybe take another look at beat 1 next time you watch the island documentary."
}
Lighthouse keeper's diary - told, staged, found, cut, unclear; a mix
Input:
{
"task": "beat-check",
"film": "the lighthouse diary film",
"watched": "watched it all",
"beats": "beat 1 | the film says: the keeper wrote in the diary every night for 30 years | shown as: a narrator or on-screen text said it | page class: told\nbeat 2 | the film says: a storm smashed the lamp room | shown as: acted out or reconstructed | page class: staged\nbeat 3 | the film says: the last diary page is shown up close | shown as: old footage, a photo or a document | page class: found\nbeat 4 | the film says: the keeper was lonely - the diary cuts to an empty sea | shown as: suggested by putting 2 shots side by side | page class: cut\nbeat 5 | the film says: the diary ended up in a museum | shown as: not sure how it was shown | page class: unclear",
"facts": "film verdict: mixed (no group reaches half of 5 beats)\ncheck first: beat 5 (unclear, how it was shown is not known yet)\nfirmest: beat 3 (found)\nclasses: seen 0, found 1, said 0, told 1, staged 1, cut 1, unclear 1"
}
Reply:
{
"task": "beat-check",
"verdict": "mixed",
"headline": "No kind of evidence carries this film on its own, so it reads as a mix.",
"beats": [
{
"beat": 1,
"known": "told",
"shows": "You are told on screen that the keeper wrote in the diary every night for 30 years.",
"leap": "You still rely on the narrator's word for this span, since no footage covers 30 years of writing.",
"ask": "How could you check that the keeper wrote in the diary for 30 years?"
},
{
"beat": 2,
"known": "staged",
"shows": "You watch a reenacted scene of a storm smashing the lamp room.",
"leap": "This recreates the storm after the fact, rather than catching the lamp room damage as it happened.",
"ask": "What would confirm that a storm smashed the lamp room?"
},
{
"beat": 3,
"known": "found",
"shows": "You see the last diary page shown up close, as a record on screen.",
"leap": "The page alone does not say when it was written or by whom.",
"ask": "What would show when this last diary page was written?"
},
{
"beat": 4,
"known": "cut",
"shows": "You see 2 shots placed together - the diary, then a shot of an empty sea.",
"leap": "The edit links the empty sea to the keeper being lonely, without showing that feeling directly.",
"ask": "What would show why the diary cuts to an empty sea here?"
},
{
"beat": 5,
"known": "unclear",
"shows": "How the diary ending up in the museum was put on screen is not yet clear.",
"leap": "Since how it was shown is not known yet, how much this covers cannot be judged.",
"ask": "What would show how the diary ended up in the museum?"
}
],
"check_first": "Beat 5 is worth checking first since how it was shown is not known yet, while beat 3 keeps the most direct link to the screen.",
"closing": "Maybe take another look at beat 5 next time you watch the lighthouse diary film."
}
Build the input yourself
Load the engine in node (global.window = global; require("./beatkit.js")), then:
const K = BeatKit;
const built = K.buildInput(K.form({ film: "a nature film about a city fox family", watched: "all",
beats: [{ text: "the vixen moved her cubs to a new den", how: "seen" }, { text: "the cubs were born in early spring", how: "told" }] }));
built.input.facts; // the page's lines: verdict, check first, firmest, class counts
const wire = K.toWire(built.input); // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// built.facts.verdict // the verdict the page's rule gives
// K.check(reply, built).notes // the page's re-check of the reply
// K.md(built, reply, "my film") // the check as Markdown
Step 1 — a tiny client helper
One function that sends JSON and unwraps the envelope. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here" # from https://seen-or-said.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://seen-or-said.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"]
// seen-or-said.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://seen-or-said.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://seen-or-said.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// WhatWasThat.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class WhatWasThat {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://seen-or-said.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://seen-or-said.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://seen-or-said.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://seen-or-said.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":"seen-or-said"}' | 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": "seen-or-said"})["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: "seen-or-said" })).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": "seen-or-said"}, 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", "seen-or-said"), 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: "seen-or-said" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "seen-or-said"])["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 = "seen-or-said" }))!["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": "beat-check", "film": "a nature film about a city fox family", "watched": "watched it all", "beats": "beat 1 | the film says: ... | shown as: ... | page class: seen", "facts": "film verdict: on-record ..."}
# - 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": "beat-check", "film": "a nature film about a city fox family", "watched": "watched it all", "beats": "beat 1 | the film says: ... | shown as: ... | page class: seen", "facts": "film verdict: on-record ..."}
# - 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: "beat-check", film, watched, beats, 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 seen-or-said:<task>:<inputHash>:a<attempt> (the one task is beat-check), 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="seen-or-said:beat-check:$(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 = "seen-or-said:" + 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("check_first"))
import { createHash } from "node:crypto";
const key = `seen-or-said:${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.check_first);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "seen-or-said:" + 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 = "seen-or-said:" + 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 = "seen-or-said:#{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["check_first"]].inspect
$key = "seen-or-said:" . $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"], " ", json_encode($reply["check_first"]), "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"seen-or-said:{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: seen-or-said:beat-check:$(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": "beat-check", "film": "a nature film about a city fox family", "watched": "watched it all", "beats": "beat 1 | the film says: ... | shown as: ... | page class: seen", "facts": "film verdict: on-record ..."}
# - 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": "seen-or-said:beat-check:" + 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": `seen-or-said:beat-check:${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", "seen-or-said:beat-check:"+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", "seen-or-said:beat-check:" + 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" => "seen-or-said:beat-check:#{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: seen-or-said:beat-check:" . 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", $"seen-or-said:beat-check:{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);