Drive Not For Sale from your own code
The one metered task, list, is available over HTTP. It takes the name of a place someone would never sell, its kind, a voice for the agent and a few true facts (one per line), and returns a status (fixer-upper, sold as is, or move-in ready), every fact sorted into one slot with a listing line and its honest truth, a headline, a short description, a rider for the sign, a closing line and a fact box.
The division of labour first: the page owns the rows, the keyword guess and the status rule; the model owns the judgement and the words; the page re-checks the reply. All three come from listkit.js, which you can run yourself (see Build the input yourself). It is a writing game: nothing is for sale, priced or valued.
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":"not-for-sale"}), 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, place, kind, facts, voice, presort) 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 a listing 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 slots and the status
Every run sends task: list; a missing or different one is still answered as list. Each fact gets one slot, checked in a fixed order:
- flaw - something about the place is wrong, worn, broken, stuck, missing, uncomfortable or hard to use (peeling, rusty, damp, creaky, cold, windy, noisy, cramped, steep, or works only one way).
- history - a year, an age, a past event, or someone who made, used or did something there before.
- view - what you can see, hear or smell from it, its light, or the sky, water or weather around it.
- neighbours - people, animals or other places nearby that are not part of the place itself.
- feature - everything else: what it is made of, what it holds or fits, what you do there.
The verdict (shown on the page as the status) then follows the flaws, first match wins: fixer_upper if three or more facts are flaw; otherwise as_is if one or two are; otherwise move_in. The page computes the same rule over the reply's own slots (ListKit.verdictFor) and flags a reply that disagrees.
The input
Every value is a string (objects are never sent). app.js builds it with ListKit.buildInput(form).input.
| Field | Required | What it holds |
|---|---|---|
task | yes | Always list. |
place | yes | What the person calls the place, or this place. Kept to its first 120 characters. |
kind | yes | Starts with home, room, outdoor, building, vehicle or thing, then a short description in brackets. |
facts | yes | Numbered lines, 1. ..., one true fact per line in the person's words. At most 16 facts are sent; a fact over 220 characters keeps its start and end around [...]. |
voice | yes | Starts with glossy, plain or classified, then a short description in brackets. |
presort | yes | The page's keyword guess, one line per fact: <n> | <slot>. Only a guess; the slot rules win. |
clip_note | no | What the page shortened, or absent. |
retry_note | no | Sent only on the page's one reformat retry. |
The reply
The reply is one JSON object in job.output.output. The page parses it with ListKit.parseReply and ListKit.normalize (a cut-off stream is closed by ListKit.closeJson and shown as "N of 7 sections recovered").
{"task": "list", "verdict": "fixer_upper | as_is | move_in", "verdict_reason": "...", "title": "...", "summary": "...",
"facts": [{"n": 1, "slot": "flaw | history | view | neighbours | feature", "listing": "...", "truth": "..."}],
"listing": {"headline": "...", "description": "...", "rider": "...", "closing": "..."},
"fact_box": [{"label": "...", "value": "...", "from": 1}]}
| Field | Rule |
|---|---|
facts | Exactly one entry per numbered fact, same numbers and order. listing: the fact as a listing line in the chosen voice, at most 16 words, a flaw covered with an agent phrase but never denied. truth: the same fact said plainly, at most 14 words. Neither adds anything the fact does not say. |
listing | A headline of at most 9 words; a description of 3 to 5 sentences and at most 90 words that uses the place's name once; a rider of 2 to 4 words for the sign; a closing line of at most 12 words on why it is not for sale. |
fact_box | 3 to 5 rows: a listing-sheet label, a value that only restates one fact (numbers copied exactly), and from, that fact's number. |
summary | Two sentences: the counts per slot (zeros left out), then what the headline leans on. |
The page then checks the reply against your facts (ListKit.check): every fact listed exactly once with both lines, the status against the reply's own slots, the keyword guess where it differs, the length rules, no fact-box number its fact does not give, no number anywhere that your facts do not give, no price, offer or contact detail, no "for sale", no property website or brand, and no internal field names in the prose.
Worked examples, one per status
These are the three free samples on the page: the exact input and a stand-in model's reply, unedited.
Window seat - move-in ready
Input:
{
"task": "list",
"place": "The window seat on the landing",
"kind": "room (one room or one corner of a home)",
"facts": "1. Built into the wall when the house was finished in 1931\n2. Looks straight down the street to the hills\n3. The cushion is a folded quilt in blue and white squares\n4. Fits one grown-up with a book and a blanket\n5. The kids across the street wave from their porch\n6. Rain on the glass is the best sound in the house",
"voice": "plain (a friendly local agent, plain words, warm and a little dry)",
"presort": "1 | history\n2 | view\n3 | feature\n4 | feature\n5 | neighbours\n6 | view"
}
Reply:
{
"task": "list",
"verdict": "move_in",
"verdict_reason": "You're getting a move-in ready listing because none of your facts point to something broken or worn.",
"title": "A Move-In Ready Window Seat With An Outlook",
"summary": "You have one fact about its past, two about the sights and sounds, two about what the place offers, and one about the kids across the street. The headline leans on when it was built and the outlook to the hills.",
"facts": [
{
"n": 1,
"slot": "history",
"listing": "Built into the wall when the house was finished, back in 1931.",
"truth": "It was built into the wall when the house was finished in 1931."
},
{
"n": 2,
"slot": "view",
"listing": "A straight line down the street to the hills.",
"truth": "It looks straight down the street to the hills."
},
{
"n": 3,
"slot": "feature",
"listing": "A folded quilt in blue and white squares serves as the cushion.",
"truth": "The cushion is a folded quilt, blue and white squares."
},
{
"n": 4,
"slot": "feature",
"listing": "Room for one grown-up, a book and a blanket.",
"truth": "It fits one grown-up with a book and a blanket."
},
{
"n": 5,
"slot": "neighbours",
"listing": "Across the street, the kids wave from their porch.",
"truth": "The kids across the street wave from their porch."
},
{
"n": 6,
"slot": "view",
"listing": "The best sound in the house: rain on the glass.",
"truth": "Rain on the glass is the best sound in the house."
}
],
"listing": {
"headline": "Built in 1931, with an outlook to the hills",
"description": "The window seat on the landing was built into the wall when the house was finished in 1931. It looks straight down the street to the hills, with rain on the glass as the best sound in the house. The cushion is a folded quilt in blue and white squares, with room for one grown-up, a book and a blanket. The kids across the street wave from their porch.",
"rider": "Built in 1931",
"closing": "The hills, the rain and the quilt make it irreplaceable."
},
"fact_box": [
{
"label": "Year built",
"value": "1931",
"from": 1
},
{
"label": "Outlook",
"value": "Down the street to the hills",
"from": 2
},
{
"label": "Cushion",
"value": "Blue and white quilt",
"from": 3
},
{
"label": "Seats",
"value": "One grown-up",
"from": 4
},
{
"label": "Across the street",
"value": "Kids wave from the porch",
"from": 5
}
]
}
Harbour bench - sold as is
Input:
{
"task": "list",
"place": "The bench at the end of the harbour wall",
"kind": "outdoor (a bench, a garden, a spot in a park or on a beach)",
"facts": "1. Green paint, peeling at both ends\n2. You can watch the fishing boats come in\n3. Someone carved two sets of initials into the left armrest in 1988\n4. Three gulls patrol it for chips\n5. Seats three, or two and a picnic\n6. The wind in winter could take your hat off",
"voice": "glossy (a breathless luxury agent, big adjectives, one exclamation mark at most)",
"presort": "1 | flaw\n2 | view\n3 | history\n4 | neighbours\n5 | feature\n6 | flaw"
}
Reply:
{
"task": "list",
"verdict": "as_is",
"verdict_reason": "You have two flaws, the peeling paint and the winter wind, which is why this one is sold as is.",
"title": "The Peeling Green Bench, Sold As Is",
"summary": "You have two flaws, one history, one view, one neighbour and one feature. The headline leans on the peeling green paint and the views of the fishing boats.",
"facts": [
{
"n": 1,
"slot": "flaw",
"listing": "Rare green paintwork, full of character at both ends.",
"truth": "Green paint is peeling at both ends."
},
{
"n": 2,
"slot": "view",
"listing": "Sought-after views of the fishing boats coming in.",
"truth": "You can watch the fishing boats come in."
},
{
"n": 3,
"slot": "history",
"listing": "Boasting two sets of initials, hand-carved into the left armrest in 1988.",
"truth": "Someone carved two sets of initials into the left armrest in 1988."
},
{
"n": 4,
"slot": "neighbours",
"listing": "Three gulls on constant patrol for chips.",
"truth": "Three gulls patrol it for chips."
},
{
"n": 5,
"slot": "feature",
"listing": "Generous seating for three, or two plus a picnic.",
"truth": "Seats three, or two and a picnic."
},
{
"n": 6,
"slot": "flaw",
"listing": "Naturally ventilated, with winter breezes strong enough to catch your hat.",
"truth": "The wind in winter could take your hat off."
}
],
"listing": {
"headline": "Charming Green Bench, Stunning Fishing-Boat Views!",
"description": "The bench at the end of the harbour wall offers sought-after views of the fishing boats coming in. Its green paintwork is full of character, peeling at both ends. Two sets of initials were hand-carved into the left armrest back in 1988. Three dedicated gulls patrol for chips, and there is generous seating for three, or two and a picnic. Winter brings bracing breezes brisk enough to catch your hat.",
"rider": "Carved In 1988",
"closing": "Initials carved into it in 1988 mean this bench is staying put."
},
"fact_box": [
{
"label": "Exterior",
"value": "Green paint, peeling at both ends",
"from": 1
},
{
"label": "Outlook",
"value": "Fishing boats coming in",
"from": 2
},
{
"label": "Year Carved",
"value": "Initials carved in 1988",
"from": 3
},
{
"label": "Seats",
"value": "Three, or two and a picnic",
"from": 5
},
{
"label": "Residents",
"value": "Three gulls, patrolling for chips",
"from": 4
}
]
}
First car - fixer-upper
Input:
{
"task": "list",
"place": "The 1997 hatchback",
"kind": "vehicle (a car, a van, a boat, a bike)",
"facts": "1. Bought with savings from a summer job in 2004\n2. The passenger door only opens from the inside\n3. The heater has two settings: off and volcano\n4. A cassette has been stuck in the tape deck since 2009, and it still plays\n5. Drove it to the coast and back every summer for six years\n6. The back seat folds flat and fits a mattress\n7. Rust has started around the rear wheel arches",
"voice": "classified (an old newspaper small ad, clipped phrases and a few abbreviations)",
"presort": "1 | history\n2 | flaw\n3 | feature\n4 | flaw\n5 | history\n6 | feature\n7 | flaw"
}
Reply:
{
"task": "list",
"verdict": "fixer_upper",
"verdict_reason": "You listed four flaws, so this one counts as a fixer-upper.",
"title": "The 1997 Hatchback: Rust, Heat, Stuck Tape",
"summary": "You gave four flaws, two pieces of history and one feature for this one. The headline leans on the stuck cassette and the rusty wheel arches.",
"facts": [
{
"n": 1,
"slot": "history",
"listing": "Bought w/ summer-job savings, 2004.",
"truth": "Bought with savings from a summer job, in 2004."
},
{
"n": 2,
"slot": "flaw",
"listing": "Pass. door, interior release only - security feature.",
"truth": "The passenger door only opens from inside."
},
{
"n": 3,
"slot": "flaw",
"listing": "Heating: off or volcano - all character, no half measures.",
"truth": "The heater only has two settings: off and volcano."
},
{
"n": 4,
"slot": "flaw",
"listing": "Cassette lodged in deck since 2009 - a permanent soundtrack, still plays.",
"truth": "A cassette has been stuck in the tape deck since 2009; it still plays."
},
{
"n": 5,
"slot": "history",
"listing": "Coastal round-trip, every summer, six years running.",
"truth": "Driven to the coast and back every summer for six years."
},
{
"n": 6,
"slot": "feature",
"listing": "Back seat folds flat - fits a mattress.",
"truth": "The back seat folds flat and fits a mattress."
},
{
"n": 7,
"slot": "flaw",
"listing": "Rust, rear wheel arches - a project for the right owner.",
"truth": "Rust has started around the rear wheel arches."
}
],
"listing": {
"headline": "STUCK CASSETTE SINCE 2009, RUSTY REAR ARCHES",
"description": "The 1997 hatchback, bought w/ summer-job savings in 2004, has clocked six summers of coastal round-trips. Pass. door opens from inside only, heating runs off or volcano, and rust has set in nr the rear wheel arches. A cassette's been lodged in the tape deck since 2009 and still plays, w/ the back seat folding flat to fit a mattress.",
"rider": "Stuck cassette, still plays",
"closing": "Six summers of coastal trips like this aren't for sale."
},
"fact_box": [
{
"label": "Doors",
"value": "Opens from inside only",
"from": 2
},
{
"label": "Heating",
"value": "Off or volcano setting",
"from": 3
},
{
"label": "Tape Deck",
"value": "Cassette stuck since 2009",
"from": 4
},
{
"label": "Rear Arches",
"value": "Rust has started",
"from": 7
},
{
"label": "Seats",
"value": "Fold flat, fit mattress",
"from": 6
}
]
}
Build the input yourself
Load the script in node (global.window = global; require("./listkit.js")), then:
const K = ListKit;
const form = K.form({ place: "The corner table at the old cafe", kind: "building", voice: "plain",
facts: "The table wobbles unless you fold a napkin under one leg\nYou can see the whole square from it\nThe owner's grandmother painted the mural behind it in 1962" });
K.rows(form); // one row per fact, with the page's slot guess
K.expected(K.rows(form)); // {verdict: "as_is", why: "1 flaw on file"} - the page's rule over its guesses
K.draft(K.rows(form), form); // the free draft sheet: stock agent phrases, your words as the truth
K.decode("Charming and cozy, needs TLC"); // listing-speak, decoded
const built = K.buildInput(form);
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 listing and every fact 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://not-for-sale.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://not-for-sale.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"]
// not-for-sale.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://not-for-sale.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://not-for-sale.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
}
// NotForSale.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 NotForSale {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://not-for-sale.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://not-for-sale.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://not-for-sale.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://not-for-sale.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":"not-for-sale"}' | 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": "not-for-sale"})["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: "not-for-sale" })).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": "not-for-sale"}, 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", "not-for-sale"), 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: "not-for-sale" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "not-for-sale"])["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 = "not-for-sale" }))!["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": "list", "place": "The bench at the end of the harbour wall", "kind": "outdoor (a bench, a garden, a spot in a park or on a beach)", "facts": "1. Green paint, peeling at both ends\n2. You can watch the fishing boats come in\n3. Someone carved two sets of initials into the left armrest in 1988\n4. Three gulls patrol it for chips\n5. Seats three, or two and a picnic\n6. The wind in winter could take your hat off", "voice": "glossy (a breathless luxury agent, big adjectives, one exclamation mark at most)", "presort": "1 | flaw\n2 | view\n3 | history\n4 | neighbours\n5 | feature\n6 | flaw"}
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": "list", "place": "The bench at the end of the harbour wall", "kind": "outdoor (a bench, a garden, a spot in a park or on a beach)", "facts": "1. Green paint, peeling at both ends\n2. You can watch the fishing boats come in\n3. Someone carved two sets of initials into the left armrest in 1988\n4. Three gulls patrol it for chips\n5. Seats three, or two and a picnic\n6. The wind in winter could take your hat off", "voice": "glossy (a breathless luxury agent, big adjectives, one exclamation mark at most)", "presort": "1 | flaw\n2 | view\n3 | history\n4 | neighbours\n5 | feature\n6 | flaw"}
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: "list", place, kind, facts, voice, presort, 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 lane and the input: a retry with the same key never bills twice. The page uses not-for-sale:<task>:<inputHash>:a<attempt> (the one task is list), 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="not-for-sale:list:$(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 = "not-for-sale:" + 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 = `not-for-sale:${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 := "not-for-sale:" + 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 = "not-for-sale:" + 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 = "not-for-sale:#{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 = "not-for-sale:" . $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 = $"not-for-sale:{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: not-for-sale:list:$(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": "list", "place": "The bench at the end of the harbour wall", "kind": "outdoor (a bench, a garden, a spot in a park or on a beach)", "facts": "1. Green paint, peeling at both ends\n2. You can watch the fishing boats come in\n3. Someone carved two sets of initials into the left armrest in 1988\n4. Three gulls patrol it for chips\n5. Seats three, or two and a picnic\n6. The wind in winter could take your hat off", "voice": "glossy (a breathless luxury agent, big adjectives, one exclamation mark at most)", "presort": "1 | flaw\n2 | view\n3 | history\n4 | neighbours\n5 | feature\n6 | flaw"}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "not-for-sale:list:" + 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": `not-for-sale:list:${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", "not-for-sale:list:"+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", "not-for-sale:list:" + 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" => "not-for-sale:list:#{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: not-for-sale:list:" . 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", $"not-for-sale:list:{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);