Drive Remembered As from your own code
The one metered task, keepsake, is available over HTTP. It takes one experience split into up to ten moments, each with a feeling from -5 to +5 and a number of minutes, plus the page's numbers - and returns a keepsake reading: the verdict, a headline, what memory is likely to lean on (the peak), what the ending does, what memory mostly leaves out, one small change to the ending and a caption for the snow globe.
The division of labour first: the page counts; the model reads. The page works out the lived average (feelings weighted by minutes), the peak (the most intense moment; a tie goes to the longer, then the later), the end, the remembered score (peak and end averaged), the gap and the verdict, and sends them as facts. The model never changes those numbers. After the reply, the page lists anything that does not match. A thinking aid, not professional advice.
Base URL and the envelope
Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response uses one envelope:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }
The token is minted for this app (the guest call takes {"slug":"remembered-as"}), 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, experience, moments, facts) is missing. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: the keepsake reading is metered and needs a personal sign-in. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
The task field, the input and the rules
Every run sends task: keepsake (the one task is keepsake); a missing or different one is still answered as keepsake. All fields are strings.
| Field | Required | What it holds |
|---|---|---|
task | yes | Always keepsake. |
experience | yes | name | it already happened (or it is still ahead) | note: .... The note is none when empty; a name is up to 70 characters, a note up to 240. |
moments | yes | One moment a line, in order: N. what happened | felt: +3 | minutes: 20. Two to ten lines; feelings are whole numbers from -5 to +5, minutes 1 to 720, a moment up to 80 characters. |
facts | yes | The page's numbers: total minutes, lived average, peak, end, remembered score, gap, verdict, whether the ending rises, dips or is level, whether the end is above or below the lived average, what memory mostly leaves out and, for a harsher verdict, the ending what-if. Build it with MemKit.buildInput. |
retry_note | no | Only on a reformat retry: why the previous reply was unusable. |
The verdict, from the gap (remembered minus lived): sweeter-in-memory at +1.0 or more, harsher-in-memory at -1.0 or less, otherwise true-to-life.
The reply
One JSON object, no prose around it:
{"task": "keepsake", "verdict": "harsher-in-memory", "headline": "...", "peak_note": "...", "end_note": "...", "left_out": "...", "next_time": "...", "caption": "..."}
| Field | What it holds |
|---|---|
verdict | sweeter-in-memory, harsher-in-memory or true-to-life - always the page's. |
headline | One sentence: what the verdict means, with the remembered score and the lived average. |
peak_note | The peak moment and why memory is likely to lean on it. |
end_note | Whether the ending rises, dips or is level, and whether it pulls the memory up or down. |
left_out | The longest moment memory mostly leaves out and the share of the minutes it drops; empty when every moment is the peak or the end. |
next_time | One small, physically sensible change to the ending, using only the moments given - never a date or a time. |
caption | 3 to 8 words for the base of the snow globe, from the input's own words. |
The page reads the reply with ReplyKit.normalize and checks it with ReplyKit.check, which lists, under "The page's check", a verdict that differs from the page's, a headline number that is not one of the page's, a note that does not name the right moment, a change that sets a day or time you did not give, and an overlong caption.
Worked examples - every verdict
These are the free samples on the page: the exact input the page sends and the reply that replays for free. Together they cover every verdict - sweeter in memory, harsher in memory and true to life - an experience that already happened and one still ahead, and a peak that is also the end.
A long flight home - sweeter-in-memory
Input:
{
"task": "keepsake",
"experience": "The long flight home | it already happened | note: none",
"moments": "1. Boarding queue at the gate | felt: -2 | minutes: 35\n2. Middle seat, no sleep | felt: -3 | minutes: 300\n3. A film I had wanted to see | felt: +2 | minutes: 120\n4. Landing over the city lights | felt: +4 | minutes: 15",
"facts": "Total: 470 minutes over 4 moments.\nLived average (each feeling weighted by its minutes): -1.4\nPeak (the most intense moment, either way; ties go to the longer, then the later): moment 4 \"Landing over the city lights\", felt +4, 15 minutes\nEnd (the last moment): moment 4 \"Landing over the city lights\", felt +4, 15 minutes - the peak is also the end\nRemembered score (peak and end averaged; here one moment is both): +4.0\nGap (remembered minus lived): +5.4\nVerdict: sweeter-in-memory (rule: gap +1.0 or more is sweeter-in-memory; -1.0 or less is harsher-in-memory; otherwise true-to-life)\nEnding: rises (moment 3 felt +2, the last felt +4)\nEnd against the lived average: the last moment (+4) is above the lived average (-1.4), so it pulls the memory up.\nMostly left out of memory: 3 moments that are neither peak nor end, 455 minutes of 470 minutes (97%). The longest of them: moment 2 \"Middle seat, no sleep\", 300 minutes, felt -3."
}
Reply:
{
"task": "keepsake",
"verdict": "sweeter-in-memory",
"headline": "Memory is likely to keep this flight kinder than it was lived, with a remembered score of +4.0 against a lived average of -1.4.",
"peak_note": "Landing over the city lights, felt +4, is likely to carry the whole memory of this flight.",
"end_note": "Landing over the city lights rises from the moment before it and pulls the memory up above how the flight was lived overall.",
"left_out": "Memory is likely to leave out \"Middle seat, no sleep,\" which lasted 300 minutes, part of the 97% of the flight that mostly goes unremembered.",
"next_time": "Next time you're on a similar flight, keep the ending just as it is - let landing over the city lights close it out.",
"caption": "Landing over the city lights"
}
A day at the climbing wall - harsher-in-memory
Input:
{
"task": "keepsake",
"experience": "Saturday at the climbing wall | it already happened | note: First time leading a route.",
"moments": "1. Warm-up on the easy wall | felt: +2 | minutes: 20\n2. Bouldering with Sam | felt: +3 | minutes: 60\n3. Leading my first route | felt: +4 | minutes: 15\n4. Locker key stuck, missed the bus | felt: -3 | minutes: 25",
"facts": "Total: 120 minutes over 4 moments.\nLived average (each feeling weighted by its minutes): +1.7\nPeak (the most intense moment, either way; ties go to the longer, then the later): moment 3 \"Leading my first route\", felt +4, 15 minutes\nEnd (the last moment): moment 4 \"Locker key stuck, missed the bus\", felt -3, 25 minutes\nRemembered score (peak and end averaged): +0.5\nGap (remembered minus lived): -1.2\nVerdict: harsher-in-memory (rule: gap +1.0 or more is sweeter-in-memory; -1.0 or less is harsher-in-memory; otherwise true-to-life)\nEnding: dips (moment 3 felt +4, the last felt -3)\nEnd against the lived average: the last moment (-3) is below the lived average (+1.7), so it pulls the memory down.\nEnding what-if: a last moment felt -2 or better would lift it out of harsher-in-memory.\nMostly left out of memory: 2 moments that are neither peak nor end, 80 minutes of 120 minutes (67%). The longest of them: moment 2 \"Bouldering with Sam\", 60 minutes, felt +3."
}
Reply:
{
"task": "keepsake",
"verdict": "harsher-in-memory",
"headline": "Memory is likely to keep this experience harsher than it was lived, with a remembered score of +0.5 against a lived average of +1.7.",
"peak_note": "Leading your first route, which felt +4, is likely to be the moment memory leans on most.",
"end_note": "Your ending, \"Locker key stuck, missed the bus,\" dips from the moment before and pulls the memory down.",
"left_out": "Bouldering with Sam, which lasted 60 minutes, is the longest of the moments memory is likely to leave out, part of the 67% of the time memory mostly drops.",
"next_time": "For a similar one next time, you could close with a short pause to recall leading your first route.",
"caption": "Leading my first route"
}
A market day still ahead - true-to-life
Input:
{
"task": "keepsake",
"experience": "Market and lunch with Jo | it is still ahead | note: none",
"moments": "1. Bus into town | felt: -1 | minutes: 30\n2. Browsing the market stalls | felt: +2 | minutes: 60\n3. Lunch at the noodle bar | felt: +3 | minutes: 45\n4. Queue at the bakery | felt: -1 | minutes: 20\n5. Bus home | felt: -1 | minutes: 30",
"facts": "Total: 185 minutes over 5 moments.\nLived average (each feeling weighted by its minutes): +0.9\nPeak (the most intense moment, either way; ties go to the longer, then the later): moment 3 \"Lunch at the noodle bar\", felt +3, 45 minutes\nEnd (the last moment): moment 5 \"Bus home\", felt -1, 30 minutes\nRemembered score (peak and end averaged): +1.0\nGap (remembered minus lived): +0.1\nVerdict: true-to-life (rule: gap +1.0 or more is sweeter-in-memory; -1.0 or less is harsher-in-memory; otherwise true-to-life)\nEnding: level (moment 4 felt -1, the last felt -1)\nEnd against the lived average: the last moment (-1) is below the lived average (+0.9), so it pulls the memory down.\nMostly left out of memory: 3 moments that are neither peak nor end, 110 minutes of 185 minutes (59%). The longest of them: moment 2 \"Browsing the market stalls\", 60 minutes, felt +2."
}
Reply:
{
"task": "keepsake",
"verdict": "true-to-life",
"headline": "The remembered score of +1.0 sits close to the lived average of +0.9, so memory is likely to keep this experience close to how it feels when it happens.",
"peak_note": "Lunch at the noodle bar, which felt +3, is likely to be the moment memory leans on most.",
"end_note": "Bus home felt -1, the same as the moment before it, so the ending is level, and is likely to pull the memory down, below the lived average.",
"left_out": "Browsing the market stalls (60 minutes) is the longest of the moments memory is likely to leave out, part of the 59% of all the minutes that mostly fade from memory.",
"next_time": "You could move lunch at the noodle bar to come right before bus home.",
"caption": "Lunch at the noodle bar"
}
Build the input yourself
Load the engine in node (global.window = global; require("./memkit.js"); require("./replykit.js")), then:
const K = MemKit, R = ReplyKit;
const built = K.buildInput(K.form({ name: "The long flight home", tense: "past", note: "", moments: [
{ label: "Middle seat, no sleep", feel: -3, mins: 300 },
{ label: "Landing over the city lights", feel: 4, mins: 15 } ] })); // built.ok is false, with built.note, until two moments have minutes
const wire = K.toWire(built.input); // POST this to /estimate and /run; built.analysis holds lived10, rem10, gap10, verdict
// then: const reply = R.normalize(R.parseReply(job.output.output));
// R.check(reply, built).notes // the page's re-check of the reply
// R.md(built, reply, K.title(built.form)) // the moments, the numbers and the reading as Markdown
Moment keys: label, feel (-5 to 5) and mins (1 to 720). Experience keys: name, tense (past or planned), note and moments.
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://remembered-as.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://remembered-as.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"]
// remembered-as.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://remembered-as.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://remembered-as.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
}
// RememberedAs.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 RememberedAs {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://remembered-as.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://remembered-as.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://remembered-as.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://remembered-as.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":"remembered-as"}' | 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": "remembered-as"})["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: "remembered-as" })).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": "remembered-as"}, 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", "remembered-as"), 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: "remembered-as" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "remembered-as"])["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 = "remembered-as" }))!["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": "keepsake", "experience": "The long flight home | it already happened | note: none", "moments": "1. Boarding queue at the gate | felt: -2 | minutes: 35 ...", "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": "keepsake", "experience": "The long flight home | it already happened | note: none", "moments": "1. Boarding queue at the gate | felt: -2 | minutes: 35 ...", "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: "keepsake", experience, moments, 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 remembered-as:<task>:<inputHash>:a<attempt> (the one task is keepsake), 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="remembered-as:keepsake:$(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 = "remembered-as:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the reply as one JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("verdict"), reply.get("caption"))
import { createHash } from "node:crypto";
const key = `remembered-as:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.verdict, reply.caption);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "remembered-as:" + 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 = "remembered-as:" + 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 = "remembered-as:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["verdict"], reply["caption"]].inspect
$key = "remembered-as:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", ($reply["verdict"] ?? ""), " ", ($reply["caption"] ?? ""), "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"remembered-as:{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: remembered-as:keepsake:$(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": "keepsake", "experience": "The long flight home | it already happened | note: none", "moments": "1. Boarding queue at the gate | felt: -2 | minutes: 35 ...", "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": "remembered-as:keepsake:" + 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": `remembered-as:keepsake:${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", "remembered-as:keepsake:"+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", "remembered-as:keepsake:" + 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" => "remembered-as:keepsake:#{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: remembered-as:keepsake:" . 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", $"remembered-as:keepsake:{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);