Drive Attested at Home from your own code
The one metered task, compile, is available over HTTP. It takes a household's word notes - one word per line, numbered [1], [2] and so on, with the page's quick reading of each line - and returns a verdict on the dictionary (a slim pamphlet, wants citations, or ready for the press), one entry per word (a respelling, a part of speech, one or two senses, the quoted use with its speaker and time, and any origin the notes give), up to three word-making habits of the household, up to three questions for the household, a title and a short preface.
The division of labour first: the page owns the line reading, the attestation rule and the verdict rule; the model owns the entries, the habits and the questions; the page re-checks the reply. All of it comes from wordkit.js, which you can run yourself (see Build the input yourself). Attested at Home never corrects a word.
Base URL and the envelope
Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response uses one envelope:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "...", "status": 402, "details": { ... } } }
The token is minted for this app (the guest call takes {"slug":"attested-at-home"}), 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, count, words, prescan) 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: compiling a dictionary is metered and needs a personal token. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
The task field, the statuses and the verdict
Every run sends task: compile; a missing or different one is still answered as compile. Each entry gets one status, checked in order: reconstructed if the line gives no meaning or is marked uncertain outside its quotes (a question mark, "not sure", "I think", "maybe", "something like", "we forget", "can't remember", "no idea", "sort of"); otherwise attested if a quoted use contains the headword; otherwise described. Each entry's pos is one of noun, verb, adjective, adverb, interjection, phrase or unknown.
The verdict is checked in order, first match wins: thin (shown as "a slim pamphlet") if there are fewer than 4 entries; otherwise uncited ("wants citations") if attested entries are fewer than half; otherwise ready ("ready for the press").
The input
Every value is a string (objects are never sent). app.js builds it with WordKit.buildInput(form).input.
| Field | Required | What it holds |
|---|---|---|
task | yes | Always compile. |
household | no | The household's or dictionary's name, up to 40 characters. May be empty. |
count | yes | How many numbered words follow. |
words | yes | The notes, one word per line, each starting [n]. Up to 40 lines; a line over 280 characters keeps its start and end with [...] where the middle was cut. |
prescan | yes | The page's reading, one line per word: <n> | <headword> | <status> | quote: yes/no. The model keeps the headword spelling and applies the status rule itself. |
clip_note | no | What the page shortened, if anything. |
retry_note | no | Sent only on the page's single reformat retry. |
The reply
The reply is one JSON object in job.output.output. The page parses it with WordKit.parseReply and WordKit.normalize (a cut-off stream is closed by WordKit.closeJson and whatever parsed is shown).
{"task": "compile", "verdict": "thin | uncited | ready", "verdict_reason": "...", "title": "...", "preface": "...",
"entries": [{"n": 1, "headword": "...", "say": "SNAK-oh", "pos": "noun | verb | adjective | adverb | interjection | phrase | unknown",
"status": "attested | described | reconstructed", "senses": ["..."],
"citation": {"quote": "...", "speaker": "...", "when": "..."}, "origin": "..."}],
"patterns": [{"name": "...", "how": "...", "examples": ["...", "..."]}],
"questions": ["..."]}
| Field | What it holds |
|---|---|
entries | One per numbered line, same n and headword. say a hyphenated respelling with the stressed syllable in capitals; senses one or two definitions of at most 20 words; citation copied from the notes for an attested word, otherwise null; origin only when the notes give one, otherwise empty. |
patterns | Zero to three word-making habits, each shown by two to four headwords. |
questions | One to three questions that each name a headword and ask for what its entry lacks. |
title, preface | "A Dictionary of" plus the household's name (or "A Household Dictionary"), and a two-sentence preface. |
The page then checks the reply against the notes (WordKit.check): the verdict against the reply's own counts and the page's reading, one entry per word with the same spelling, known statuses, every attested word carrying its quote, quotes, speakers and times word for word from the notes, origins sharing words with the notes, habits shown by two headwords, questions naming a headword, no he or she for anyone, and no internal field name in the prose.
Worked examples, one per verdict
These are the three free samples on the page: the exact input and a stand-in model's reply, unedited.
The Harbour Row flat - ready for the press (ready)
Input:
{
"task": "compile",
"household": "the Harbour Row flat",
"count": "7",
"words": "[1] blankie burrito - wrapping yourself in the sofa blanket so only your face shows. \"I'm in full blankie burrito, bring me tea\" (Mia, every Sunday)\n[2] snacko: any snack eaten standing at the open fridge. \"Just a quick snacko\" (Dad)\n[3] bedso = bedtime, said to the twins. \"Teeth, then bedso\" (Jonah)\n[4] the wobbles - the hour before dinner when everyone is hungry and cross. \"Careful, it's the wobbles\" (Mia)\n[5] glumph - the noise the old washing machine makes when it changes cycle. \"There goes the glumph\" (Ade, since we moved in)\n[6] crinkler - the TV remote, named after the crinkly plastic cover Gran put on it\n[7] flumpet - something to do with being tired? We forget who started it.",
"prescan": "1 | blankie burrito | attested | quote: yes\n2 | snacko | attested | quote: yes\n3 | bedso | attested | quote: yes\n4 | the wobbles | attested | quote: yes\n5 | glumph | attested | quote: yes\n6 | crinkler | described | quote: no\n7 | flumpet | reconstructed | quote: no"
}
Reply:
{
"task": "compile",
"verdict": "ready",
"verdict_reason": "Five of seven words have a quoted use.",
"title": "A Dictionary of the Harbour Row flat",
"preface": "This dictionary sets down the words one household has made for itself, each entry built only from what was written down about it. Read it the way you would read any family's private language, and add to it whenever a word earns its place.",
"entries": [
{
"n": 1,
"headword": "blankie burrito",
"say": "BLAN-kee buh-REE-toh",
"pos": "phrase",
"status": "attested",
"senses": [
"Wrapping yourself in the sofa blanket so only your face shows"
],
"citation": {
"quote": "I'm in full blankie burrito, bring me tea",
"speaker": "Mia",
"when": "every Sunday"
},
"origin": ""
},
{
"n": 2,
"headword": "snacko",
"say": "SNAK-oh",
"pos": "noun",
"status": "attested",
"senses": [
"Any snack eaten standing at the open fridge"
],
"citation": {
"quote": "Just a quick snacko",
"speaker": "Dad",
"when": ""
},
"origin": ""
},
{
"n": 3,
"headword": "bedso",
"say": "BED-soh",
"pos": "noun",
"status": "attested",
"senses": [
"Bedtime, said to the twins"
],
"citation": {
"quote": "Teeth, then bedso",
"speaker": "Jonah",
"when": ""
},
"origin": ""
},
{
"n": 4,
"headword": "the wobbles",
"say": "the WOB-uhlz",
"pos": "phrase",
"status": "attested",
"senses": [
"The hour before dinner when everyone is hungry and cross"
],
"citation": {
"quote": "Careful, it's the wobbles",
"speaker": "Mia",
"when": ""
},
"origin": ""
},
{
"n": 5,
"headword": "glumph",
"say": "GLUMF",
"pos": "noun",
"status": "attested",
"senses": [
"The noise the old washing machine makes when it changes cycle"
],
"citation": {
"quote": "There goes the glumph",
"speaker": "Ade",
"when": "since we moved in"
},
"origin": ""
},
{
"n": 6,
"headword": "crinkler",
"say": "CRINK-ler",
"pos": "noun",
"status": "described",
"senses": [
"The TV remote"
],
"citation": null,
"origin": "named after the crinkly plastic cover Gran put on it"
},
{
"n": 7,
"headword": "flumpet",
"say": "FLUM-pet",
"pos": "unknown",
"status": "reconstructed",
"senses": [
"Meaning uncertain, something to do with being tired"
],
"citation": null,
"origin": ""
}
],
"patterns": [
{
"name": "Shared -o ending",
"how": "Some coined words simply add an -o sound to the end, as if that alone makes a thing official.",
"examples": [
"snacko",
"bedso"
]
},
{
"name": "Ordinary words given new meanings",
"how": "An everyday word keeps its usual sound but is pointed at a new target, like a mood or a wrap.",
"examples": [
"blankie burrito",
"the wobbles"
]
}
],
"questions": [
"What does flumpet actually mean, beyond something to do with being tired?",
"Has anyone ever actually said crinkler out loud, and if so, what were the words?"
]
}
The Moss family - wants citations (uncited)
Input:
{
"task": "compile",
"household": "the Moss family",
"count": "6",
"words": "[1] spudge - to squash up on the sofa to make room for one more\n[2] toast weather: a grey, drizzly day that only toast can fix\n[3] the long way round - any story Grandad starts that takes twenty minutes to reach the point\n[4] nubbins - the last crusts of a loaf. \"Who ate all the nubbins?\" (Priya)\n[5] oojamaflip: whatever object you can't name right now\n[6] squizzle - maybe a sneeze and a giggle at once? not sure",
"prescan": "1 | spudge | described | quote: no\n2 | toast weather | described | quote: no\n3 | the long way round | described | quote: no\n4 | nubbins | attested | quote: yes\n5 | oojamaflip | described | quote: no\n6 | squizzle | reconstructed | quote: no"
}
Reply:
{
"task": "compile",
"verdict": "uncited",
"verdict_reason": "only one of six words has a quoted use",
"title": "A Dictionary of the Moss family",
"preface": "This dictionary sets down the words a household has made its own, so they are not lost to memory or to time. Use it to settle an old argument, explain a saying to a visitor, or simply enjoy how this family talks.",
"entries": [
{
"n": 1,
"headword": "spudge",
"say": "SPUJ",
"pos": "verb",
"status": "described",
"senses": [
"to squash up on the sofa to make room for one more"
],
"citation": null,
"origin": ""
},
{
"n": 2,
"headword": "toast weather",
"say": "TOHST WETH-er",
"pos": "phrase",
"status": "described",
"senses": [
"a grey, drizzly day that only toast can fix"
],
"citation": null,
"origin": ""
},
{
"n": 3,
"headword": "the long way round",
"say": "thuh LONG WAY ROWND",
"pos": "phrase",
"status": "described",
"senses": [
"any story Grandad starts that takes twenty minutes to reach the point"
],
"citation": null,
"origin": ""
},
{
"n": 4,
"headword": "nubbins",
"say": "NUB-inz",
"pos": "noun",
"status": "attested",
"senses": [
"the last crusts of a loaf"
],
"citation": {
"quote": "Who ate all the nubbins?",
"speaker": "Priya",
"when": ""
},
"origin": ""
},
{
"n": 5,
"headword": "oojamaflip",
"say": "OO-juh-muh-flip",
"pos": "noun",
"status": "described",
"senses": [
"whatever object you can't name right now"
],
"citation": null,
"origin": ""
},
{
"n": 6,
"headword": "squizzle",
"say": "SKWIZ-uhl",
"pos": "unknown",
"status": "reconstructed",
"senses": [
"Meaning uncertain, maybe a sneeze and a giggle at once"
],
"citation": null,
"origin": ""
}
],
"patterns": [
{
"name": "Everyday phrases, private meaning",
"how": "An ordinary phrase keeps its usual words but is given a meaning that only this household uses.",
"examples": [
"toast weather",
"the long way round"
]
}
],
"questions": [
"What does squizzle really mean - is it truly a sneeze and a giggle together, or something else?",
"Has anyone said spudge out loud so there's a quote to put in its entry?",
"Does anyone remember actual words for toast weather, so a quote can be added?"
]
}
A three-word start - a slim pamphlet (thin)
Input:
{
"task": "compile",
"household": "",
"count": "3",
"words": "[1] wiggle-waggle - the dog's whole-body hello when we get home. \"Look at that wiggle-waggle!\" (Tomás)\n[2] scronch - to scrunch your nose up at a bad smell\n[3] brumbly?",
"prescan": "1 | wiggle-waggle | attested | quote: yes\n2 | scronch | described | quote: no\n3 | brumbly | reconstructed | quote: no"
}
Reply:
{
"task": "compile",
"verdict": "thin",
"verdict_reason": "Three words so far - a dictionary wants at least four.",
"title": "A Household Dictionary",
"preface": "This little dictionary gathers the words a household has made its own, keeping each one's sense and story side by side. Read it as a work in progress, to be added to as more words and their stories come to light.",
"entries": [
{
"n": 1,
"headword": "wiggle-waggle",
"say": "WIG-uhl-WAG-uhl",
"pos": "noun",
"status": "attested",
"senses": [
"the dog's whole-body hello when we get home"
],
"citation": {
"quote": "Look at that wiggle-waggle!",
"speaker": "Tomás",
"when": ""
},
"origin": ""
},
{
"n": 2,
"headword": "scronch",
"say": "SKRONCH",
"pos": "verb",
"status": "described",
"senses": [
"to scrunch your nose up at a bad smell"
],
"citation": null,
"origin": ""
},
{
"n": 3,
"headword": "brumbly",
"say": "BRUM-blee",
"pos": "unknown",
"status": "reconstructed",
"senses": [
"Meaning uncertain"
],
"citation": null,
"origin": ""
}
],
"patterns": [],
"questions": [
"What does brumbly actually mean, and when would you use it?",
"Is there a moment you could quote where someone said scronch out loud?",
"Who in the house first said wiggle-waggle, and where did it come from?"
]
}
Build the input yourself
Load the script in node (global.window = global; require("./wordkit.js")), then:
const K = WordKit;
const form = K.form({ household: "Flat 2", notes: "snacko: a snack eaten at the open fridge. \"Just a quick snacko\" (Dad)\nthe wobbles - the cross hour before dinner\nflumpet - something to do with being tired?" });
K.entries(form).list; // [{n: 1, headword: "snacko", status: "attested", ...}, ...]
const built = K.buildInput(form);
built.page; // "thin" - fewer than four words
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) // the dictionary as Markdown
Step 1 — a tiny client helper
One function that sends JSON and unwraps the envelope. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here" # from https://attested-at-home.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://attested-at-home.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"]
// attested-at-home.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://attested-at-home.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://attested-at-home.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
}
// AttestedAtHome.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 AttestedAtHome {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://attested-at-home.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://attested-at-home.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://attested-at-home.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://attested-at-home.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":"attested-at-home"}' | 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": "attested-at-home"})["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: "attested-at-home" })).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": "attested-at-home"}, 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", "attested-at-home"), 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: "attested-at-home" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "attested-at-home"])["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 = "attested-at-home" }))!["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": "compile", "household": "the Moss family", "count": "6", "words": "[1] spudge - to squash up on the sofa ...\n...", "prescan": "1 | spudge | described | quote: no\n..."}
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": "compile", "household": "the Moss family", "count": "6", "words": "[1] spudge - to squash up on the sofa ...\n...", "prescan": "1 | spudge | described | quote: no\n..."}
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: "compile", household, count, words, prescan, clip_note} (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 task and the input: a retry with the same key never bills twice. The page uses attested-at-home:<task>:<inputHash>:a<attempt> (the one task is compile), 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="attested-at-home:compile:$(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 = "attested-at-home:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the reply as one JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("title"))
import { createHash } from "node:crypto";
const key = `attested-at-home:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.title);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "attested-at-home:" + 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 = "attested-at-home:" + 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 = "attested-at-home:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["title"]].inspect
$key = "attested-at-home:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", $reply["title"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"attested-at-home:{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: attested-at-home:compile:$(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": "compile", "household": "the Moss family", "count": "6", "words": "[1] spudge - to squash up on the sofa ...\n...", "prescan": "1 | spudge | described | quote: no\n..."}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "attested-at-home:compile:" + 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": `attested-at-home:compile:${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", "attested-at-home:compile:"+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", "attested-at-home:compile:" + 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" => "attested-at-home:compile:#{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: attested-at-home:compile:" . 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", $"attested-at-home:compile:{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);