Drive Chrome & Grime from your own code
The one metered task, read-moments, is available over HTTP. It takes 1 to 6 ordinary moments, each with a name, an optional line on what happened, and an answer (No, A little or Clearly) for each of six cues - three for chrome (high tech) and three for grime (low life). It returns a plain-English reading per moment, the flip that would move it into or out of full neon, and a short sign lettered from the moment's own words.
The division of labour first: the page decides; the model explains. The page adds up each moment's chrome (0-6) and grime (0-6), picks its verdict, its strongest cue, its flip and the brightest sign before the run, and sends them in facts. The model never changes them, and the page re-checks the reply against them.
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":"chrome-and-grime"}), 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, 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 reasons 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 verdicts
Every run sends task: read-moments (the one task is read-moments); a missing or different one is still answered as read-moments. All fields are strings.
| Field | Required | What it holds |
|---|---|---|
task | yes | Always read-moments. |
moments | yes | One numbered line per moment (1 to 6): name | what happened: ... | A screen or scanner was in the middle of it: ... | A system made the call: ... | Tech rode on your body: ... | Something was broken or makeshift: ... | You worked round the rules: ... | It was cheap, crowded or after dark: ..., each answer No, A little or Clearly. |
facts | yes | The page's lines: per moment its chrome and grime, its verdict, its strongest cue and its flip (cues moved from one answer to another, and the verdict it would land in); then the verdict counts and the brightest sign. |
retry_note | no | Only on the page's one reformat retry. |
Each answer is worth No 0, A little 1, Clearly 2. The verdict, first match wins: chrome 3+ and grime 3+ is neon (full neon); chrome 3+ is gloss (chrome gloss); grime 3+ is alley (back alley); otherwise unlit. The strongest cue is the highest-scoring cue (ties to the earlier cue); the brightest sign is the full-neon moment with the most points in all (ties to the moment higher on the list), or none.
The reply
{"task": "read-moments", "headline": "...", "moments": [{"name": "...", "verdict": "gloss", "reading": "...", "flip": "...", "sign": "..."}], "closing": "..."}
| Field | What it holds |
|---|---|
headline | At most 22 words summing up the street; counts match facts. |
moments[] | One per input moment, same order and exact names: verdict copied from facts, reading (at most 50 words, from that moment's own answers, naming its strongest cue), flip (one "If ... would" sentence for the page's flip, ending with the verdict it lands in) and sign (two to five words from the moment's own nouns). |
closing | One sentence naming the brightest sign (or saying no moment reached full neon). |
The page then checks the reply (GrimeKit.check) - each verdict against its own, every moment present, a flip and a sign for each, time, season or weather words you did not give, contact details, dark warnings, and named books, films or games - and lists anything it finds under "The page's check".
Worked examples - all four verdicts
These are the 3 free samples on the page: the exact input the page sends and the reply that replays for free. Together they cover all four verdicts.
The commute - verdicts: full neon, chrome gloss, back alley, unlit street
Input:
{
"task": "read-moments",
"moments": "1. Tapped through the ticket gate with my phone | what happened: the gate beeped twice before it opened | A screen or scanner was in the middle of it: Clearly | A system made the call: A little | Tech rode on your body: No | Something was broken or makeshift: No | You worked round the rules: No | It was cheap, crowded or after dark: A little\n2. Fixed my bike chain with a hair tie | what happened: it held for the rest of the ride | A screen or scanner was in the middle of it: No | A system made the call: No | Tech rode on your body: No | Something was broken or makeshift: Clearly | You worked round the rules: A little | It was cheap, crowded or after dark: No\n3. Late bus with earbuds in | what happened: the timetable screen was blank so I asked the driver | A screen or scanner was in the middle of it: A little | A system made the call: No | Tech rode on your body: Clearly | Something was broken or makeshift: A little | You worked round the rules: Clearly | It was cheap, crowded or after dark: Clearly\n4. Walked past the bakery | what happened: not given | A screen or scanner was in the middle of it: No | A system made the call: No | Tech rode on your body: No | Something was broken or makeshift: No | You worked round the rules: No | It was cheap, crowded or after dark: No",
"facts": "1. Tapped through the ticket gate with my phone: chrome 3 of 6; grime 1 of 6; verdict gloss; strongest cue: A screen or scanner was in the middle of it; into neon: It was cheap, crowded or after dark from A little to Clearly and Something was broken or makeshift from No to A little (2 points more), landing in neon\n2. Fixed my bike chain with a hair tie: chrome 0 of 6; grime 3 of 6; verdict alley; strongest cue: Something was broken or makeshift; into neon: A screen or scanner was in the middle of it from No to Clearly and A system made the call from No to A little (3 points more), landing in neon\n3. Late bus with earbuds in: chrome 3 of 6; grime 5 of 6; verdict neon; strongest cue: Tech rode on your body; out of neon: A screen or scanner was in the middle of it from A little to No (1 point fewer), landing in alley\n4. Walked past the bakery: chrome 0 of 6; grime 0 of 6; verdict unlit; strongest cue: none; into neon: A screen or scanner was in the middle of it from No to Clearly and A system made the call from No to A little and Something was broken or makeshift from No to Clearly and You worked round the rules from No to A little (6 points more), landing in neon\ncounts: neon 1, gloss 1, alley 1, unlit 1 (of 4 moments)\nbrightest sign: Late bus with earbuds in"
}
Reply:
{
"task": "read-moments",
"headline": "This street shows one full neon moment, one chrome gloss, one back alley and one unlit, each its own shade.",
"moments": [
{
"name": "Tapped through the ticket gate with my phone",
"verdict": "gloss",
"reading": "You answered that a screen or scanner was clearly in the middle of it, the gate beeping before it opened, and that it was only a little cheap, crowded or after dark, so the chrome shines while the grime stays faint for chrome gloss.",
"flip": "If it had been clearly cheap, crowded or after dark, and something had been a little broken or makeshift, this would land in full neon.",
"sign": "Ticket gate phone"
},
{
"name": "Fixed my bike chain with a hair tie",
"verdict": "alley",
"reading": "You answered that something was clearly broken or makeshift, and it held for the rest of the ride, while a screen or scanner was no part of it, so the grit carries this one to back alley.",
"flip": "If a screen or scanner had clearly been in the middle of it, and a system had made the call a little, this moment would land in full neon.",
"sign": "Bike chain hair tie"
},
{
"name": "Late bus with earbuds in",
"verdict": "neon",
"reading": "You answered that tech clearly rode on your body, and that you clearly worked round the rules by asking the driver once the timetable screen was blank, so both sides came through strong for full neon.",
"flip": "If a screen or scanner had not been in the middle of it at all, this moment would land in back alley.",
"sign": "Bus and earbuds"
},
{
"name": "Walked past the bakery",
"verdict": "unlit",
"reading": "You answered that a screen or scanner was no part of it, and that it was not cheap, crowded or after dark either, so this moment kept both sides quiet and stayed unlit.",
"flip": "If a screen or scanner had been clearly in the middle of it, a system had slightly made the call, something had been clearly broken or makeshift, and you had slightly worked round the rules, this would be full neon.",
"sign": "Bakery walk"
}
],
"closing": "Late bus with earbuds in lights up the brightest on this street."
}
Errands - verdicts: full neon, chrome gloss, unlit street
Input:
{
"task": "read-moments",
"moments": "1. Self-checkout flagged my apples | what happened: an assistant came over and cleared it | A screen or scanner was in the middle of it: Clearly | A system made the call: Clearly | Tech rode on your body: No | Something was broken or makeshift: No | You worked round the rules: A little | It was cheap, crowded or after dark: No\n2. Paid for noodles with my watch | what happened: the card reader at the stall was taped together | A screen or scanner was in the middle of it: Clearly | A system made the call: No | Tech rode on your body: Clearly | Something was broken or makeshift: Clearly | You worked round the rules: No | It was cheap, crowded or after dark: Clearly\n3. Posted a parcel at the counter | what happened: the clerk weighed it by hand | A screen or scanner was in the middle of it: No | A system made the call: No | Tech rode on your body: No | Something was broken or makeshift: No | You worked round the rules: No | It was cheap, crowded or after dark: A little",
"facts": "1. Self-checkout flagged my apples: chrome 4 of 6; grime 1 of 6; verdict gloss; strongest cue: A screen or scanner was in the middle of it; into neon: You worked round the rules from A little to Clearly and Something was broken or makeshift from No to A little (2 points more), landing in neon\n2. Paid for noodles with my watch: chrome 4 of 6; grime 4 of 6; verdict neon; strongest cue: A screen or scanner was in the middle of it; out of neon: A screen or scanner was in the middle of it from Clearly to No (2 points fewer), landing in alley\n3. Posted a parcel at the counter: chrome 0 of 6; grime 1 of 6; verdict unlit; strongest cue: It was cheap, crowded or after dark; into neon: A screen or scanner was in the middle of it from No to Clearly and A system made the call from No to A little and It was cheap, crowded or after dark from A little to Clearly and Something was broken or makeshift from No to A little (5 points more), landing in neon\ncounts: neon 1, gloss 1, alley 0, unlit 1 (of 3 moments)\nbrightest sign: Paid for noodles with my watch"
}
Reply:
{
"task": "read-moments",
"headline": "This handful of moments shows full neon, chrome gloss and unlit, each striking its own balance.",
"moments": [
{
"name": "Self-checkout flagged my apples",
"verdict": "gloss",
"reading": "You get chrome gloss here: a screen or scanner was clearly in the middle of it, and something being broken or makeshift was a no, so the tech side leads while the scrappy side stays quiet.",
"flip": "If you had clearly worked round the rules and something had been a little broken or makeshift, this moment would land in full neon.",
"sign": "Self-checkout apples assistant"
},
{
"name": "Paid for noodles with my watch",
"verdict": "neon",
"reading": "You answered clearly that a screen or scanner was in the middle of it, and the taped-together card reader at the stall shows something was clearly broken or makeshift, so both the tech and the scrappy side shine here in full neon.",
"flip": "If a screen or scanner had not been at all in the middle of it, this moment would land in back alley.",
"sign": "Noodles watch card reader"
},
{
"name": "Posted a parcel at the counter",
"verdict": "unlit",
"reading": "This one stays unlit: a screen or scanner was not in the middle of it, and the only touch on the scrappy side was it being a little cheap, crowded or after dark, too little to tip the balance.",
"flip": "If a screen or scanner had clearly scanned it, a system had decided a little, it had clearly been cheap, crowded or after dark, and something had been a little broken or makeshift, this would land in full neon.",
"sign": "Parcel at the counter"
}
],
"closing": "Paid for noodles with my watch shines brightest among them."
}
A night out - verdicts: full neon, chrome gloss, back alley
Input:
{
"task": "read-moments",
"moments": "1. Queue for the club | what happened: a bouncer scanned my ticket on a cracked phone | A screen or scanner was in the middle of it: Clearly | A system made the call: A little | Tech rode on your body: No | Something was broken or makeshift: Clearly | You worked round the rules: No | It was cheap, crowded or after dark: Clearly\n2. Kebab van after midnight | what happened: cash only, so I hunted for the one cash machine that worked | A screen or scanner was in the middle of it: No | A system made the call: No | Tech rode on your body: No | Something was broken or makeshift: A little | You worked round the rules: Clearly | It was cheap, crowded or after dark: Clearly\n3. Ride home booked on an app | what happened: the app picked the route and the price | A screen or scanner was in the middle of it: Clearly | A system made the call: Clearly | Tech rode on your body: No | Something was broken or makeshift: No | You worked round the rules: No | It was cheap, crowded or after dark: A little",
"facts": "1. Queue for the club: chrome 3 of 6; grime 4 of 6; verdict neon; strongest cue: A screen or scanner was in the middle of it; out of neon: A system made the call from A little to No (1 point fewer), landing in alley\n2. Kebab van after midnight: chrome 0 of 6; grime 5 of 6; verdict alley; strongest cue: You worked round the rules; into neon: A screen or scanner was in the middle of it from No to Clearly and A system made the call from No to A little (3 points more), landing in neon\n3. Ride home booked on an app: chrome 4 of 6; grime 1 of 6; verdict gloss; strongest cue: A screen or scanner was in the middle of it; into neon: It was cheap, crowded or after dark from A little to Clearly and Something was broken or makeshift from No to A little (2 points more), landing in neon\ncounts: neon 1, gloss 1, alley 1, unlit 0 (of 3 moments)\nbrightest sign: Queue for the club"
}
Reply:
{
"task": "read-moments",
"headline": "This street flickers between full neon, chrome gloss and back alley scenes.",
"moments": [
{
"name": "Queue for the club",
"verdict": "neon",
"reading": "Here it's full neon: you answered clearly that a screen or scanner was in the middle of it, as the bouncer scanned your ticket, and you answered clearly that something was broken or makeshift, with that cracked phone keeping things scrappy too.",
"flip": "If a system had not made the call at all, this would land in back alley.",
"sign": "Club queue ticket"
},
{
"name": "Kebab van after midnight",
"verdict": "alley",
"reading": "Here it's back alley: you answered clearly that you worked round the rules, hunting down a cash machine when the van was cash only, and you answered no to any screen or scanner being part of it, so no chrome crept in.",
"flip": "If a screen or scanner had clearly been in the middle of it, and a system had made the call a little, this would land in full neon.",
"sign": "Midnight kebab van"
},
{
"name": "Ride home booked on an app",
"verdict": "gloss",
"reading": "Here it's chrome gloss: you answered clearly that a screen or scanner was in the middle of it, and that a system made the call, as the app picked your route and price, while it was only a little cheap, crowded or after dark, keeping the grime faint.",
"flip": "If it had clearly been cheap, crowded or after dark, and something had been a little broken or makeshift, this would land in full neon.",
"sign": "Ride home app"
}
],
"closing": "Queue for the club shines brightest along this street."
}
Build the input yourself
Load the engine in node (global.window = global; require("./grimekit.js")), then:
const K = GrimeKit;
const form = K.form({ moments: [
{ name: "Charged my phone at a cafe", note: "the socket was held in with tape", screen: "some", system: "no", worn: "no", patched: "yes", workaround: "no", street: "some" },
{ name: "Lift with a touch screen", note: "", screen: "yes", system: "some", worn: "no", patched: "no", workaround: "no", street: "no" }] });
const built = K.buildInput(form); // built.facts holds each moment's chrome, grime, verdict, strongest cue and flip
const wire = K.toWire(built.input); // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// K.check(reply, built).notes // the page's re-check of the reply
// K.md(built, reply, K.title(form)) // the street as Markdown
Each cue answer is "no", "some" (A little) or "yes" (Clearly); the cue keys are screen, system, worn, patched, workaround and street.
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://chrome-and-grime.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://chrome-and-grime.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"]
// chrome-and-grime.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://chrome-and-grime.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://chrome-and-grime.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
}
// ChromeAndGrime.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 ChromeAndGrime {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://chrome-and-grime.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://chrome-and-grime.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://chrome-and-grime.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://chrome-and-grime.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":"chrome-and-grime"}' | 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": "chrome-and-grime"})["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: "chrome-and-grime" })).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": "chrome-and-grime"}, 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", "chrome-and-grime"), 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: "chrome-and-grime" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "chrome-and-grime"])["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 = "chrome-and-grime" }))!["token"]!.GetValue<string>();
Step 3 — GET /me
Who the token is (subject_type: user or guest) and the credit balance.
api GET /me | jq '.data | {subject_type, credits}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["credits"]}");
Step 4 — POST /estimate
Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.
# input.json: the run input object - {"task": "read-moments", "moments": "1. Paid for noodles with my watch | what happened: ... | ...", "facts": "...", ...}
# - see the field table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}'
inp = json.load(open("input.json")) # {"task": "read-moments", "moments": "1. Paid for noodles with my watch | what happened: ... | ...", "facts": "...", ...}
# - see the field table
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task: "read-moments", 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 chrome-and-grime:<task>:<inputHash>:a<attempt> (the one task is read-moments), 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="chrome-and-grime:read-moments:$(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 = "chrome-and-grime:" + 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"), len(reply.get("moments", [])))
import { createHash } from "node:crypto";
const key = `chrome-and-grime:${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.moments || []).length);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "chrome-and-grime:" + 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 = "chrome-and-grime:" + 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 = "chrome-and-grime:#{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["moments"] || []).length].inspect
$key = "chrome-and-grime:" . $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"], " ", count($reply["moments"] ?? []), "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"chrome-and-grime:{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: chrome-and-grime:read-moments:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json
rev = json.load(open("input.json")) # the same run input as input.json: {"task": "read-moments", "moments": "1. Paid for noodles with my watch | what happened: ... | ...", "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": "chrome-and-grime:read-moments:" + 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": `chrome-and-grime:read-moments:${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", "chrome-and-grime:read-moments:"+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", "chrome-and-grime:read-moments:" + 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" => "chrome-and-grime:read-moments:#{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: chrome-and-grime:read-moments:" . 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", $"chrome-and-grime:read-moments:{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);