Drive Most Obedient from your own code
The one metered task, letter, is available over HTTP. It takes who the letter is to and from, the relationship, a flourish and the message split into numbered sentences, and returns a verdict - proper, forward, scandalous or wants-more - with the letter (salutation, paragraphs, closing, signature, postscript), every modern word and its period stand-in, and every struck line. A natural use is a "send it as a letter" button in a chat or note-taking tool, or a stationery shop that prints the fair copy.
The division of labour first: the page owns the sweep, the date and the sheet; the model writes the words; the page re-checks the reply. The sentence splitter, the flags and the clipping come from letterkit.js, which runs in node as well as in the browser. The dateline is never asked of the model - LetterKit.dateline(ms) writes it ("Wednesday, the 21st of October, 2026"). LetterKit.check holds each reply to the message: salutation and closing present, sentence ids that exist, struck lines really left out, every stand-in found in the letter, no modern word left in it, and no capitalised name, number or pronoun that the message does not hold.
Get a token on the token page (step 2 below also shows how to mint a guest token in code).
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":"most-obedient"}), 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, message) 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: writing the letter 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 and the verdicts
Every run sends task: letter (the only task; a missing or unknown one is treated as letter). The verdict is decided by ordered, non-overlapping rules - the first that applies wins:
| Order | Verdict | When |
|---|---|---|
| 1 | wants-more | No sentence carries a point (news, a plan, a question, a request, thanks or an apology for something named, a feeling about something named). No letter; asks holds two or three questions. |
| 2 | scandalous | A sentence insults, mocks or gossips about a person. Each such sentence is listed in struck and left out of the letter entirely. |
| 3 | forward | A sentence swears, shouts in capitals or gives an order with no please. The letter keeps every point with period manners; every softened word is a swap. |
| 4 | proper | Otherwise. |
The declared input schema carries scalars only, so the array-valued fields (message, page_flags) travel JSON-encoded as strings (LetterKit.toWire does this). Every value is a string.
Input fields
| Field | Type | Notes |
|---|---|---|
task | string | letter. Required. |
to, from | string | As typed, up to 60 characters each; either may be empty. The salutation uses to; the signature is from exactly. |
relation | string | friend, family, sweetheart, acquaintance or business - sets the salutation and the closing. |
flourish | string | plain, genteel or effusive. |
message | string (JSON) | [{id, text}] - ids S1.., up to 30 sentences of up to 400 characters. Required. |
message_words | string | The message's total words - the letter runs at least this long, at most four times it and at most 220 words. |
page_flags | string (JSON) | [{ref, flag}] with flags modern:<word>, slang:<word>, emoji, shout, crude:<word>, unkind:<word>, command - the page's guesses, which the model judges for itself. |
clip_note, retry_note | string | What the page cut, when it cut something; and only on a reformat retry, what was wrong. |
The reply
The job's output.output is a string holding one JSON object: task, verdict, title, summary, salutation, paragraphs ([{text, uses}] - one to four, uses the sentence ids each is built from; [] for wants-more), closing, signature, postscript ({text, uses}, text without the "P.S." the page adds), swaps ([{ref, modern, period}] - period "" when a meaningless word was simply dropped), struck ([{ref, why}], scandalous only) and asks (wants-more only). Parse with LetterKit.parseReply, then normalize(reply), then check(reply, form), which returns the page's notes; letterText(reply, ms) gives the plain-text fair copy with the dateline.
Worked examples
Replies produced against the app's prompt for the five sample messages (made-up people; long fields shortened; the full replies ship in example-result.js). Together they cover every verdict.
a thank-you for a scarf and an invitation to tea, to a friend: proper
Input:
{
"task": "letter",
"to": "Harriet",
"from": "Eleanor",
"relation": "friend",
"flourish": "genteel",
"message": "[{\"id\":\"S1\",\"text\":\"Thank you so much for the scarf you knitted me, I wore it to work today and got three compliments!\"},{\"id\":\"S2\",\"text\":\"Are you free for tea next Saturday?\"},{\"id\":\"S3\",\"text\":\"I'll bring the lemon cake you liked last time.\"}]",
"message_words": "36",
"page_flags": "[]"
}
Reply:
{
"task": "letter",
"verdict": "proper",
"title": "Thanks for the scarf and tea",
"summary": "Your letter thanks Harriet for the scarf and the compliments it earned you, then asks Harriet to tea next Saturday and offers to bring the lemon cake Harriet liked before. As every line is warm and courteous with nothing unkind or forward, the verdict is proper.",
"salutation": "My dear Harriet,",
"paragraphs": [
{
"text": "I hope this finds you well, dear Harriet. I must thank you most sincerely for the scarf you knitted for me; I wore it to work today and received three compliments upon it.",
"uses": [
"S1"
]
},
{
"text": "I write further to ask whether you might be free to take tea with me next Saturday. Should you be at liberty, I shall bring the lemon cake which you liked on the last occasion.",
"uses": [
"S2",
"S3"
]
}
],
"closing": "Believe me, ever your affectionate friend,",
"signature": "Eleanor",
"postscript": {
"text": "",
"uses": []
},
"swaps": [],
"struck": [],
"asks": []
}
a leaking kitchen tap, to the landlord (business): proper
Input:
{
"task": "letter",
"to": "Mr. Okafor",
"from": "Rosa Quellbridge",
"relation": "business",
"flourish": "genteel",
"message": "[{\"id\":\"S1\",\"text\":\"The kitchen tap in flat 4 has been leaking since Monday.\"},{\"id\":\"S2\",\"text\":\"Could someone come and fix it this week?\"},{\"id\":\"S3\",\"text\":\"I'm home after 5pm on weekdays, or you can email me to arrange a time.\"},{\"id\":\"S4\",\"text\":\"Thanks for your help.\"}]",
"message_words": "38",
"page_flags": "[{\"ref\":\"S3\",\"flag\":\"modern:email\"}]"
}
Reply:
{
"task": "letter",
"verdict": "proper",
"title": "Request to fix the leaking tap",
"summary": "This turns your note to Mr. Okafor into a courteous business letter about the leaking tap, your wish to have it fixed this week, and the times you are free to arrange it. Every point you raised is included without any unkind or forward language, which is why the verdict is proper.",
"salutation": "Dear Mr. Okafor,",
"paragraphs": [
{
"text": "I hope this letter finds you well. I write to inform you that the kitchen tap in flat 4 has been leaking since Monday.",
"uses": [
"S1"
]
},
{
"text": "I should be glad if someone might come and fix it this week.",
"uses": [
"S2"
]
},
{
"text": "I am at home after five o'clock on weekdays, or you may write to me to arrange a time.",
"uses": [
"S3"
]
},
{
"text": "Thank you for your help in this matter. I am obliged to you.",
"uses": [
"S4"
]
}
],
"closing": "I have the honour to remain, your most obedient servant,",
"signature": "Rosa Quellbridge",
"postscript": {
"text": "",
"uses": []
},
"swaps": [
{
"ref": "S3",
"modern": "email",
"period": "write"
}
],
"struck": [],
"asks": []
}
"Bring my phone charger back ASAP", to family: forward
Input:
{
"task": "letter",
"to": "Tom",
"from": "Ben",
"relation": "family",
"flourish": "plain",
"message": "[{\"id\":\"S1\",\"text\":\"Bring my phone charger back ASAP.\"},{\"id\":\"S2\",\"text\":\"You took it from the kitchen on Sunday.\"},{\"id\":\"S3\",\"text\":\"The party was LIT lol.\"},{\"id\":\"S4\",\"text\":\"Text me when you're on your way.\"}]",
"message_words": "26",
"page_flags": "[{\"ref\":\"S1\",\"flag\":\"slang:asap\"},{\"ref\":\"S1\",\"flag\":\"modern:phone\"},{\"ref\":\"S1\",\"flag\":\"modern:charger\"},{\"ref\":\"S1\",\"flag\":\"command\"},{\"ref\":\"S3\",\"flag\":\"slang:lol\"},{\"ref\":\"S3\",\"flag\":\"slang:lit\"},{\"ref\":\"S3\",\"flag\":\"shout\"},{\"ref\":\"S4\",\"flag\":\"modern:text\"},{\"ref\":\"S4\",\"flag\":\"command\"}]"
}
Reply:
{
"task": "letter",
"verdict": "forward",
"title": "Requesting the return of an item",
"summary": "Your letter asks Tom to return your item without delay, recalls that Tom took it from the kitchen on Sunday, calls the party delightful, and asks Tom to send a line when on the way. Because your message gave Tom direct orders with no softening word and one word in capitals, the letter restates those points in a more polite period style.",
"salutation": "My dear Tom,",
"paragraphs": [
{
"text": "I hope this finds you well. I should be much obliged if you would return the little apparatus belonging to my pocket instrument without delay, for I recall that you took it from the kitchen on Sunday.",
"uses": [
"S1",
"S2"
]
},
{
"text": "I must tell you that the party was most delightful.",
"uses": [
"S3"
]
},
{
"text": "I should be glad if you would send me a line when you are on your way.",
"uses": [
"S4"
]
}
],
"closing": "Your ever affectionate,",
"signature": "Ben",
"postscript": {
"text": "",
"uses": []
},
"swaps": [
{
"ref": "S1",
"modern": "phone charger",
"period": "the little apparatus belonging to my pocket instrument"
},
{
"ref": "S1",
"modern": "ASAP",
"period": "without delay"
},
{
"ref": "S3",
"modern": "LIT",
"period": "delightful"
},
{
"ref": "S3",
"modern": "lol",
"period": ""
},
"... (5 in all)"
],
"struck": [],
"asks": []
}
the dance on Friday, with a line of gossip, to a friend: scandalous
Input:
{
"task": "letter",
"to": "Priya",
"from": "Jess",
"relation": "friend",
"flourish": "effusive",
"message": "[{\"id\":\"S1\",\"text\":\"OMG the dance on Friday was amazing!!\"},{\"id\":\"S2\",\"text\":\"I danced with the drummer from the band twice.\"},{\"id\":\"S3\",\"text\":\"Honestly Dana looked like a total mess and everyone was laughing at her.\"},{\"id\":\"S4\",\"text\":\"Can you come over on Sunday so I can tell you everything? 💃\"},{\"id\":\"S5\",\"text\":\"PS bring the photos from your phone!\"} ... (361 characters)",
"message_words": "48",
"page_flags": "[{\"ref\":\"S1\",\"flag\":\"slang:omg\"},{\"ref\":\"S3\",\"flag\":\"unkind:mess\"},{\"ref\":\"S3\",\"flag\":\"unkind:laughing at\"},{\"ref\":\"S4\",\"flag\":\"emoji\"},{\"ref\":\"S5\",\"flag\":\"modern:phone\"},{\"ref\":\"S5\",\"flag\":\"modern:photos\"}]"
}
Reply:
{
"task": "letter",
"verdict": "scandalous",
"title": "A letter about the Friday dance",
"summary": "This letter carries your news of Friday's dance, your dancing with the drummer, and your wish for Priya to call on Sunday, written in a warm and courteous style. One line mocking Dana was unkind, so it has been left out of the letter altogether, which is why this reply cannot stand as a proper one.",
"salutation": "My dear Priya,",
"paragraphs": [
{
"text": "I hope indeed that this letter finds you in very good health. The dance on Friday was quite delightful.",
"uses": [
"S1"
]
},
{
"text": "I had the pleasure of dancing with the drummer of the band, not once but twice.",
"uses": [
"S2"
]
},
{
"text": "Will you come and see me on Sunday, that I may tell you everything?",
"uses": [
"S4"
]
}
],
"closing": "Believe me, ever your affectionate friend,",
"signature": "Jess",
"postscript": {
"text": "I beg you will bring the pictures taken with your little pocket instrument.",
"uses": [
"S5"
]
},
"swaps": [
{
"ref": "S1",
"modern": "OMG",
"period": ""
},
{
"ref": "S4",
"modern": "💃",
"period": ""
},
{
"ref": "S5",
"modern": "photos",
"period": "pictures"
},
{
"ref": "S5",
"modern": "phone",
"period": "little pocket instrument"
}
],
"struck": [
{
"ref": "S3",
"why": "it mocks Dana's appearance and says others were laughing at Dana"
}
],
"asks": []
}
"hey lol" / "k thx": wants-more
Input:
{
"task": "letter",
"to": "Sam",
"from": "",
"relation": "friend",
"flourish": "genteel",
"message": "[{\"id\":\"S1\",\"text\":\"hey lol\"},{\"id\":\"S2\",\"text\":\"k thx 👍\"}]",
"message_words": "4",
"page_flags": "[{\"ref\":\"S1\",\"flag\":\"slang:lol\"},{\"ref\":\"S1\",\"flag\":\"slang:hey\"},{\"ref\":\"S2\",\"flag\":\"slang:thx\"},{\"ref\":\"S2\",\"flag\":\"slang:k\"},{\"ref\":\"S2\",\"flag\":\"emoji\"}]"
}
Reply:
{
"task": "letter",
"verdict": "wants-more",
"title": "A letter in want of news",
"summary": "Your note to Sam does not yet carry anything to answer - only a greeting and a word of thanks with nothing named. I cannot draw a letter from it until you say what the thanks are for, what news you have, or what you wish to ask. A few questions are offered below to help you say more.",
"salutation": "",
"paragraphs": [],
"closing": "",
"signature": "",
"postscript": {
"text": "",
"uses": []
},
"swaps": [],
"struck": [],
"asks": [
"What would you like to tell Sam?",
"What are you thanking Sam for?",
"Is there anything you'd like to ask Sam?"
]
}
Build the input yourself
Load the script in node (global.window = global; require("./letterkit.js")), then:
const K = LetterKit;
const form = K.form({ to: "Tom", from: "Ben", relation: "family", flourish: "plain",
message: "Bring my phone charger back ASAP.\nYou took it from the kitchen on Sunday." });
console.log(K.flags(form), K.lean(form), K.preview(form)); // the sweep, the page's lean, the word-for-word preview
const wire = K.toWire(K.buildInput(form).input); // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// const { notes } = K.check(reply, form); // the page's re-check of the reply
// K.letterText(reply, Date.now()) // the fair copy, with the page's dateline
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://most-obedient.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://most-obedient.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"]
// most-obedient.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://most-obedient.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://most-obedient.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
}
// MostObedient.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 MostObedient {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://most-obedient.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://most-obedient.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://most-obedient.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://most-obedient.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":"most-obedient"}' | 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": "most-obedient"})["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: "most-obedient" })).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": "most-obedient"}, 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", "most-obedient"), 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: "most-obedient" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "most-obedient"])["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 = "most-obedient" }))!["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": "letter", "to": "Harriet", "from": "Eleanor", "relation": "friend", "flourish": "genteel", "message": "[{\"id\":\"S1\",\"text\":\"Thank you so much for the scarf you knitted me, I wore it to work today and got three compliments!\"},{\"id\":\"S2\",\"text\":\"Are you free for tea next Saturday?\"}, ...]", "message_words": "36"}
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": "letter", "to": "Harriet", "from": "Eleanor", "relation": "friend", "flourish": "genteel", "message": "[{\"id\":\"S1\",\"text\":\"Thank you so much for the scarf you knitted me, I wore it to work today and got three compliments!\"},{\"id\":\"S2\",\"text\":\"Are you free for tea next Saturday?\"}, ...]", "message_words": "36"}
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: "letter", to, from, relation, flourish, message, ... (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 most-obedient:<task>:<inputHash>:a<attempt> (the one task is letter), 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="most-obedient:letter:$(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 = "most-obedient:" + 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"))
import { createHash } from "node:crypto";
const key = `most-obedient:${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);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "most-obedient:" + 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 = "most-obedient:" + 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 = "most-obedient:#{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 = "most-obedient:" . $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 = $"most-obedient:{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: most-obedient:letter:$(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": "letter", "to": "Harriet", "from": "Eleanor", "relation": "friend", "flourish": "genteel", "message": "[{\"id\":\"S1\",\"text\":\"Thank you so much for the scarf you knitted me, I wore it to work today and got three compliments!\"},{\"id\":\"S2\",\"text\":\"Are you free for tea next Saturday?\"}, ...]", "message_words": "36"}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "most-obedient:letter:" + 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": `most-obedient:letter:${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", "most-obedient:letter:"+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", "most-obedient:letter:" + 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" => "most-obedient:letter:#{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: most-obedient:letter:" . 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", $"most-obedient:letter:{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);