Drive Same Pond from your own code
The one metered task, pond, is available over HTTP. It takes a few of someone's own things with their traits, plus what the page measured about them - the clumps, the loners, the closest pair and the pond's shape - and returns a pond keeper's short note on each clump and loner, a line on the closest pair and one closing line.
The division of labour first: the page does all the measuring; the model writes only the notes. Two things are as alike as the traits they share over all the traits either has. A clump is a group in which every pair shares at least half (complete linkage, strongest first, ties to the earlier thing). The pond map is a neighbour-weighted, UMAP-style layout from a fixed start, so the same ticks always draw the same pond. After the reply the page checks every note against the ticks. Your code can do the same with the engine files the page loads (see "Build the input yourself").
Base URL and the envelope
Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response uses one envelope:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }
The token is minted for this app (the guest call takes {"slug":"same-pond"}), 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, things, clumps, loners, closest, shape and the counts) 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 keeper's notes are metered and need a personal sign-in. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
The task field, the input and the rules
Every run sends task: pond (the one task is pond); a missing or different one is still answered as pond. All fields are strings. Build them with PondKit.buildInput rather than by hand - it does the measuring.
| Field | Required | What it holds |
|---|---|---|
task | yes | Always pond. |
kind | no | What the things are, in the person's words. |
things | yes | One line per thing: N. name - trait, trait. Three to ten things. |
clumps | yes | One line per clump (A. 3. the flask; 4. the travel cup - shared by all: has a lid) or none. |
loners | yes | One line per thing that floats alone, with its own traits and its nearest thing (or nearest: none), or none. |
closest | yes | The pair sharing the most and what they share, or none. |
shape | yes | together, clumps or apart - decided by the page. |
thing_count, clump_count, loner_count | yes | Counts as strings, for checking. |
retry_note | no | Only on a reformat retry: why the previous reply was unusable. |
The reply
One JSON object:
{"task": "pond", "shape": "clumps", "clumps": [{"id": "A", "note": "..."}], "loners": [{"n": 1, "note": "..."}], "closest": "...", "keeper": "..."}
| Field | What it holds |
|---|---|
shape | The input's shape, copied. |
clumps[] | One entry per input clump, in order: id and a 12-to-40-word note that quotes the first shared trait and names at least two members. |
loners[] | One entry per loner, in order: n and an 8-to-30-word note that names it and quotes its first trait. |
closest | One sentence on the closest pair, quoting the first trait they share; empty when no pair shares anything. |
keeper | One closing sentence of at most 20 words that agrees with the shape. |
The page reads the reply with ReplyKit.normalize and checks it with ReplyKit.check, which lists, under "The page's check", a missing or extra note, a note that drops its first shared trait, a trait a thing does not have, and any number or barred word the list never had.
Worked examples - every sample
These are the free samples on the page: the exact input the page sends and the reply that replays for free. Together they cover all three shapes - one clump holding everything, clumps with loners (one of which shares nothing), and everything floating alone.
The mug cupboard (clumps and loners)
Input:
{
"task": "pond",
"kind": "mugs in my cupboard",
"things": "1. the chipped blue mug - has a handle, blue, chipped, holiday souvenir\n2. the giant soup mug - has a handle, huge, holds soup\n3. the travel cup - has a lid, leaks a bit, goes in a bag\n4. the flask - has a lid, goes in a bag, keeps tea hot\n5. the tiny espresso cup - tiny, has a handle, holiday souvenir\n6. the jam jar glass - no handle, see-through, was a jam jar\n7. the cow-shaped mug - has a handle, shaped like a cow, holiday souvenir",
"clumps": "A. 3. the travel cup; 4. the flask - shared by all: has a lid, goes in a bag\nB. 5. the tiny espresso cup; 7. the cow-shaped mug - shared by all: has a handle, holiday souvenir",
"loners": "1. the chipped blue mug - own traits: has a handle, blue, chipped, holiday souvenir; nearest: 5. the tiny espresso cup, sharing: has a handle, holiday souvenir\n2. the giant soup mug - own traits: has a handle, huge, holds soup; nearest: 5. the tiny espresso cup, sharing: has a handle\n6. the jam jar glass - own traits: no handle, see-through, was a jam jar; nearest: none (shares no trait with any other thing)",
"closest": "3. the travel cup and 4. the flask - sharing: has a lid, goes in a bag",
"shape": "clumps",
"thing_count": "7",
"clump_count": "2",
"loner_count": "3"
}
Reply:
{
"task": "pond",
"shape": "clumps",
"clumps": [
{
"id": "A",
"note": "Travel cup and flask drift in the same calm patch, each marked 'has a lid', closer to each other than to anything else here."
},
{
"id": "B",
"note": "Tiny espresso cup and cow-shaped mug settle into a patch of their own, both 'has a handle', nearer to each other than to the rest."
}
],
"loners": [
{
"n": 1,
"note": "The chipped blue mug floats alone, 'has a handle', nearest the tiny espresso cup, sharing 'holiday souvenir' with it."
},
{
"n": 2,
"note": "Giant soup mug drifts apart from the rest, 'has a handle', nearest the tiny espresso cup though they only share 'has a handle'."
},
{
"n": 6,
"note": "The jam jar glass floats apart, 'no handle', and shares no trait with anything else in the pond."
}
],
"closest": "The travel cup and the flask share the most of anything in the pond, both marked 'has a lid'.",
"keeper": "Some of these settle into clumps while others keep drifting the pond on their own."
}
Winter hats (one clump)
Input:
{
"task": "pond",
"kind": "my winter hats",
"things": "1. the bobble hat - wool, knitted, keeps ears warm, has a bobble\n2. the striped beanie - wool, knitted, keeps ears warm, striped\n3. the fisherman hat - wool, knitted, keeps ears warm, rolled brim\n4. the cable-knit hat - wool, knitted, keeps ears warm, has a bobble\n5. the earflap hat - wool, knitted, keeps ears warm, has ties",
"clumps": "A. 1. the bobble hat; 2. the striped beanie; 3. the fisherman hat; 4. the cable-knit hat; 5. the earflap hat - shared by all: wool, knitted, keeps ears warm",
"loners": "none",
"closest": "1. the bobble hat and 4. the cable-knit hat - sharing: wool, knitted, keeps ears warm, has a bobble",
"shape": "together",
"thing_count": "5",
"clump_count": "1",
"loner_count": "0"
}
Reply:
{
"task": "pond",
"shape": "together",
"clumps": [
{
"id": "A",
"note": "Bobble hat, striped beanie, fisherman hat, cable-knit hat and earflap hat all float in the same patch of pond, tied together by being 'wool' underneath it all."
}
],
"loners": [],
"closest": "Nothing in the pond sits nearer than bobble hat and cable-knit hat, sharing 'wool' and a few more traits besides.",
"keeper": "Every hat here ended up bobbing in the same stretch of water, none drifting off on its own."
}
The hallway shelf (all apart)
Input:
{
"task": "pond",
"kind": "the hallway shelf",
"things": "1. the spare keys - metal, jangly, on a ring\n2. the dish of coins - metal, round, never counted\n3. the umbrella - folds up, keeps rain off, one broken spoke\n4. the torch - needs batteries, bright, rubber grip\n5. the sunglasses - folds up, scratched, case lost",
"clumps": "none",
"loners": "1. the spare keys - own traits: metal, jangly, on a ring; nearest: 2. the dish of coins, sharing: metal\n2. the dish of coins - own traits: metal, round, never counted; nearest: 1. the spare keys, sharing: metal\n3. the umbrella - own traits: folds up, keeps rain off, one broken spoke; nearest: 5. the sunglasses, sharing: folds up\n4. the torch - own traits: needs batteries, bright, rubber grip; nearest: none (shares no trait with any other thing)\n5. the sunglasses - own traits: folds up, scratched, case lost; nearest: 3. the umbrella, sharing: folds up",
"closest": "1. the spare keys and 2. the dish of coins - sharing: metal",
"shape": "apart",
"thing_count": "5",
"clump_count": "0",
"loner_count": "5"
}
Reply:
{
"task": "pond",
"shape": "apart",
"clumps": [],
"loners": [
{
"n": 1,
"note": "Spare keys carry 'metal' among their traits and drift nearest the dish of coins, though that shared 'metal' leaves them just short of joining up."
},
{
"n": 2,
"note": "The dish of coins also carries 'metal', floating nearest the spare keys, though sharing just 'metal' isn't enough to pull them together."
},
{
"n": 3,
"note": "The umbrella 'folds up' among its traits and drifts nearest the sunglasses, sharing that same 'folds up', yet not enough to count as alike."
},
{
"n": 4,
"note": "The torch carries 'needs batteries' among its traits but shares no trait with anything else in the pond."
},
{
"n": 5,
"note": "The sunglasses also list 'folds up' among their traits, drifting nearest the umbrella, though that shared 'folds up' isn't quite enough to draw them close."
}
],
"closest": "Spare keys and the dish of coins drift closer together than anything else in the pond, bound by that shared 'metal'.",
"keeper": "Across this whole pond, nothing drifts close enough to clump together with anything else."
}
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({ kind: "mugs", lines: "the flask: has a lid, keeps tea hot\nthe travel cup: has a lid, leaks a bit\nthe chipped mug: has a handle" }));
// built.ok is false, with built.note, until there are three things that each have a trait
const wire = K.toWire(built.input); // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output));
// R.check(reply, built).notes // the page's re-check of every note
// K.layout(built.form, built.an) // the pond map, points in the unit square
Form keys: lines (one thing per line, name: trait, trait; up to ten things, sixteen traits in all, eight per thing) and kind (optional).
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://same-pond.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://same-pond.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"]
// same-pond.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://same-pond.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://same-pond.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
}
// SamePond.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 SamePond {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://same-pond.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://same-pond.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://same-pond.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://same-pond.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":"same-pond"}' | 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": "same-pond"})["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: "same-pond" })).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": "same-pond"}, 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", "same-pond"), 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: "same-pond" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "same-pond"])["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 = "same-pond" }))!["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": "pond", "things": "1. the flask - has a lid\n...", "clumps": "...", "loners": "...", "closest": "...", "shape": "clumps"}
# - 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": "pond", "things": "1. the flask - has a lid\n...", "clumps": "...", "loners": "...", "closest": "...", "shape": "clumps"}
# - 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: "pond", things, clumps, loners, closest, shape} (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 same-pond:<task>:<inputHash>:a<attempt> (the one task is pond), 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="same-pond:pond:$(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 = "same-pond:" + 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("shape"), reply.get("keeper"))
import { createHash } from "node:crypto";
const key = `same-pond:${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.shape, reply.keeper);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "same-pond:" + 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 = "same-pond:" + 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 = "same-pond:#{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["shape"], reply["keeper"]].inspect
$key = "same-pond:" . $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["shape"] ?? ""), " ", ($reply["keeper"] ?? ""), "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"same-pond:{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: same-pond:pond:$(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": "pond", "things": "1. the flask - has a lid\n...", "clumps": "...", "loners": "...", "closest": "...", "shape": "clumps"}
# - 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": "same-pond:pond:" + 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": `same-pond:pond:${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", "same-pond:pond:"+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", "same-pond:pond:" + 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" => "same-pond:pond:#{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: same-pond:pond:" . 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", $"same-pond:pond:{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);