Drive FromJson Desk from your own code
The web page's two paid lanes are available over HTTP. You send one JSON object with a
task (review or write) and a facts string. The
model replies with one JSON object: a verdict and one finding per problem for a review, or the
model file, its unit test and its fetch function for a write.
The model does not read your payload or your Dart code by itself. The page does the mechanical
part in your browser, for free: dartkit.js decodes the payload the way
jsonDecode does (keeping which numbers are int and which are
double), merges every sample into one schema and generates a skeleton model, and
dartcheck.js runs a pasted model's fromJson against every sample and
records where it throws. facts.js then packs the result as facts. When
you call the API directly there is no page to do this, so you build
facts yourself, in the same shape (see The facts
string).
Base URL and the envelope
Every endpoint below is under https://api.skillsafe.ai/v1/app-api. Every call sends
Content-Type: application/json and, once you have a token,
Authorization: Bearer YOUR_TOKEN. The SDK leaves the header out when it has no token.
HTTP 2xx { "data": { ... } }
non-2xx { "error": { "code": "...", "message": "...", "details": { ... } } }
This is how the vendored SDK (sdk.js) reads a response. It checks the HTTP status
(res.ok), not a field in the body. On a 2xx it returns data. Otherwise
it throws an error carrying the HTTP status, plus code and
details from the body's error object, with error.message
as the message. If the body has no error.message it uses the HTTP status text, and
if the body is not JSON it treats it as empty. So branch on the HTTP status first.
The body is the input object itself. The SDK sends
JSON.stringify(input) as the body of /estimate, /run and
/run-stream, with no wrapper. Before every estimate and run the page passes the
input through Dartkit.mustBeObject, which throws run input must be a JSON
object for anything that is not a non-array object.
Errors
Only the cases the SDK or the page actually handle are listed. The code has no special case
for any other status (429 included): it is thrown like any other error.
| What you see | What the code does with it | What to do |
|---|---|---|
HTTP 401 from a run | The page treats it as an expired session: it forgets the user it had and says "Your session expired - sign in again." | Get a fresh token (step 2). |
HTTP 402 from a run | The page says "Not enough credits for this run." and links to billing. Before a run it also compares the balance from /me with the estimate: it disables the button below min_credits and warns below hold_credits that the run may be cut short. | Top up the signed-in account, then run again. |
| Any other non-2xx | The SDK throws with the status, error.code and error.message. The page shows "The run failed: " and the message, or, for /estimate, "Could not price this run: " and the message. | Read error.code and error.message. Fix the body, or retry with the same Idempotency-Key. |
An SSE error event on /run-stream | When the stream ends, the SDK throws with the event's message (or "job failed"), code and job_id. This error has no HTTP status. | Report it; the job id names the job. |
| A stream that breaks off | If some reply text had arrived, the page closes the cut-off JSON (Recon.closeJson), shows the sections that parsed and names how many arrived. | Keep what parsed, or run again. |
A job with status: "failed" | The SDK's poller stops once the status is succeeded or failed. | Read the job's error and decide whether to retry. |
The task field: two lanes
task picks the lane. One system prompt serves both lanes and task
decides which one runs. The page only ever sends one of these two values:
task | What it does | What facts is built from |
|---|---|---|
review | Checks a pasted Dart model against the payload. It explains what each problem the browser's check found does at runtime and gives the corrected Dart lines, with a verdict that is never looser than the browser's verdict_floor. | The payload, the inferred schema and the check of your model: Facts.reviewFacts(P, M, check, code). |
write | Writes the model file, plus its unit test and its fetch function when you ask for them, in null-safe Dart 3 on dart:convert (plus package:http for the fetch function and a test package for the test), with no code generation. | The payload, the inferred schema and the browser's generated skeleton: Facts.writeFacts(P, M, skeleton, {platform, fetch, test, requests, existing}). |
If task is missing or not recognised, SKILL.md tells the model to answer
as review when facts has a check object and as
write otherwise, and to set lane in the reply to the lane it chose. The
page's review facts carry no key named check, so do not rely on this fallback.
Always send task.
Input fields
These are the fields declared to the platform in input-schema.json, in order:
| Field | Type | Required | Meaning |
|---|---|---|---|
task | string | yes | "review" or "write". |
facts | string | yes | A JSON string, not a nested object. The page builds it with JSON.stringify(facts), and the model is told to parse it first. Its keys depend on the lane (see below). |
question | string | no | Your question for the model. Facts.buildInput trims it, cuts it to 600 characters and leaves it out when it is empty. The model answers it in summary. |
retry_note | string | no | Only for a reformat retry. The page adds it to the body of the second attempt when the first reply could not be parsed. It is not part of the hashed input. Do not send it on a first attempt. |
When the page cannot parse a reply it retries once, as one extra run, with this
retry_note:
Your previous reply was not the single valid JSON object the instructions require (<parse error>). Reply again with ONLY the JSON object for task '<lane>' - no prose, no code fences; Dart code inside JSON strings with newlines escaped as \n; every array present (empty arrays where there is nothing to say).
If the second reply does not parse either, the page shows the raw text.
The facts string
Facts.buildInput(task, facts, question) wraps the lane's facts object as
{ task, facts: JSON.stringify(facts), question? }. SKILL.md tells the
model that everything in facts is authoritative: the schema's types, the presence
and null counts, the check's flags and which samples decode. It never recounts or re-infers them.
As an API caller you must build the same shape.
review facts, top-level keys (in the order reviewFacts writes them):
| Key | Contents |
|---|---|
title | The root class name. |
platform | "vm" (Android, iOS, desktop) or "web". On the VM as int throws on 12.0 and as double throws on 12; on the web a whole double passes as int. |
root_class | The class the checker ran the samples through, or "" when no class reads the payload. |
samples, decoded | How many samples there are, and how many decode without an exception. |
verdict_floor | The loosest verdict the browser allows: rewrite when no class reads the payload or the root class reads fewer than half of the payload's top-level keys, else fix_first when any error or warn flag exists, else decodes. |
schema | One row per JSON key: { class, key, field, type, seen, nulls, kinds }. seen is "present/objects"; kinds counts the JSON kinds seen, e.g. "int 1, double 1". At most 160 rows. |
flags | The check's findings, each { id, code, severity, cls, key, title, detail } and sometimes line. Ids are C1, C2, ... sorted errors first. severity is error (throws or loses data at runtime), warn or info. |
classes | Each pasted class: { name, line, fields: [{ name, type, line }], reads: [{ key, via, check, null_ok, line }], written: [keys] }. |
payload_excerpt | Whole samples, first to last, up to about 6,000 characters. A sample is never cut in half unless the first one alone is over the budget. |
model_code | The pasted Dart with line numbers as NN| . Past 24,000 characters the middle is cut on whole lines, with a marker naming the lines left out. |
notes | Plain-text notes about what was cut or not sent. |
write facts, top-level keys (in the order writeFacts writes them):
| Key | Contents |
|---|---|
title, root_class | The root class name the browser chose. |
platform | "vm" or "web". |
style | "pattern" (a Dart 3 switch over a map pattern) or "cast" (json['key'] as T named arguments). |
root | "object", "list" or "ndjson" (one object per line). |
samples | How many samples there are. |
schema | As in review, plus optional: true when the key is missing in some samples. |
classes | The class names the browser chose, root first. |
skeleton | The browser's generated model file, which already decodes every sample. Past 30,000 characters it is cut after the last whole class that fits, with a comment naming the classes left out. |
payload_excerpt | Whole samples, as in review. |
options | { fetch, test, background }: write a fetch function, write a unit test, and parse with compute(). background is only true with fetch, and only when the payload has at least 200 samples or 100,000 characters. |
notes | Plain-text notes, including the schema's own notes. |
requests | Only when given: your own instructions, trimmed. Past 6,000 characters they are cut at a line end and notes says so. |
existing_model | Only when given: your current Dart code, line-numbered, whose class and field names the model keeps where they fit. The page sends whatever is in its model box. |
The reply contract
The reply is one JSON object in the job's output text,
output.output, with no prose and no code fences around it. Dart code sits inside
JSON strings with newlines escaped. SKILL.md requires every array to be present
(empty, never omitted or null) and caps note and why at 70
words, headline at 30 and summary at 100.
review
{
"lane": "review",
"verdict": "decodes" | "fix_first" | "rewrite",
"headline": "one sentence the developer reads first",
"findings": [ { "flag": "C2", "note": "what goes wrong at runtime and why", "fix": "the corrected Dart line or lines" } ],
"summary": "what to change first, and what the model will do once it is changed"
}
verdict can match facts.verdict_floor or be stricter, never looser:
decodes means every sample decodes and round-trips as it is, fix_first
means line-level fixes, rewrite means the model does not describe this payload.
findings answers every error and warn flag, one item per
flag in the order given; a problem no flag covers gets flag: "". fix is
"" only when no code change is the answer. The page's parser
(Recon.normalizeReview) shows an invalid or missing verdict as
fix_first, blanks a flag that is not of the form C plus a
number, and drops findings with neither note nor fix.
write
{
"lane": "write",
"headline": "one sentence on what the files do",
"files": [ { "path": "lib/models/order.dart", "role": "model", "code": "..." } ],
"decisions": [ { "key": "placed_at", "choice": "DateTime", "why": "why the field is typed or named this way" } ],
"questions": [ "what the developer must decide that the payload cannot tell you" ],
"summary": "how to use the files"
}
files holds exactly one model file, then one fetch file
when options.fetch is true and one test file when
options.test is true, under lib/ and test/. The model file
starts with import 'dart:convert';. decisions has two to eight items;
its key is a JSON key exactly as the schema spells it, or "" for a
choice about the file as a whole. The page's parser (Recon.normalizeWrite) guesses
a missing role from the path and drops files with no code.
The page checks every reply against its own reading of your input before showing it
(recon.js): for a review, that the verdict is not looser than the floor, that every
error and warn flag is answered and that each fix reads keys the payload has with
types the samples satisfy; for a write, it runs the written model file through the same checker
against every sample. A direct API caller gets only the raw reply, so run your own checks.
Worked examples: one body per lane
These two bodies are short on purpose: a two-sample payload of products, one of which has a
whole-number price and an extra sale key. Real payloads give much
longer facts. Both were produced by running the page's own dartkit.js,
dartcheck.js and facts.js on this payload, so the field names and
values are what the page itself would send; in the page all of this is built in the browser.
Save the one you want as body.json, exactly as shown, on one line. Steps 4 to 6
send that file unchanged.
The payload, pasted as a JSON list:
[{"id": 7, "price": 12.5, "name": "Mug"}, {"id": 8, "price": 4, "name": "Tea", "sale": true}]
review: a Product model that reads price as double
The pasted model reads json['price'] as double, which throws on the Dart VM for the
second sample's 4 (flag C1, an error), and never reads
sale (flag C2, info). One of two samples decodes, so the floor is
fix_first. The body carries an optional question.
The model it was checked against (the page line-numbers it into model_code):
class Product {
final int id;
final double price;
final String name;
const Product({required this.id, required this.price, required this.name});
factory Product.fromJson(Map<String, dynamic> json) => Product(
id: json['id'] as int,
price: json['price'] as double,
name: json['name'] as String,
);
Map<String, dynamic> toJson() => {'id': id, 'price': price, 'name': name};
}
body.json for the review:
{"task":"review","facts":"{\"title\":\"Product\",\"platform\":\"vm\",\"root_class\":\"Product\",\"samples\":2,\"decoded\":1,\"verdict_floor\":\"fix_first\",\"schema\":[{\"class\":\"Product\",\"key\":\"id\",\"field\":\"id\",\"type\":\"int\",\"seen\":\"2/2\",\"nulls\":0,\"kinds\":\"int 2\"},{\"class\":\"Product\",\"key\":\"price\",\"field\":\"price\",\"type\":\"double\",\"seen\":\"2/2\",\"nulls\":0,\"kinds\":\"int 1, double 1\"},{\"class\":\"Product\",\"key\":\"name\",\"field\":\"name\",\"type\":\"String\",\"seen\":\"2/2\",\"nulls\":0,\"kinds\":\"string 2\"},{\"class\":\"Product\",\"key\":\"sale\",\"field\":\"sale\",\"type\":\"bool?\",\"seen\":\"1/2\",\"nulls\":0,\"kinds\":\"bool 1\"}],\"flags\":[{\"id\":\"C1\",\"code\":\"type\",\"severity\":\"error\",\"cls\":\"Product\",\"key\":\"price\",\"title\":\"wrong type for the decoded value\",\"detail\":\"4 decodes as an int, which is not a double on the Dart VM - fails in 1 of 2 samples.\",\"line\":8},{\"id\":\"C2\",\"code\":\"key_unread\",\"severity\":\"info\",\"cls\":\"Product\",\"key\":\"sale\",\"title\":\"payload key the model never reads\",\"detail\":\"'sale' is in 1 of 2 samples and fromJson never reads it, so it is dropped on decode and lost on the way back out.\"}],\"classes\":[{\"name\":\"Product\",\"line\":1,\"fields\":[{\"name\":\"id\",\"type\":\"int\",\"line\":2},{\"name\":\"price\",\"type\":\"double\",\"line\":3},{\"name\":\"name\",\"type\":\"String\",\"line\":4}],\"reads\":[{\"key\":\"id\",\"via\":\"json['id']\",\"check\":\"int\",\"null_ok\":false,\"line\":7},{\"key\":\"price\",\"via\":\"json['price']\",\"check\":\"double\",\"null_ok\":false,\"line\":8},{\"key\":\"name\",\"via\":\"json['name']\",\"check\":\"String\",\"null_ok\":false,\"line\":9}],\"written\":[\"id\",\"price\",\"name\"]}],\"payload_excerpt\":\"{\\n \\\"id\\\": 7,\\n \\\"price\\\": 12.5,\\n \\\"name\\\": \\\"Mug\\\"\\n}\\n{\\n \\\"id\\\": 8,\\n \\\"price\\\": 4,\\n \\\"name\\\": \\\"Tea\\\",\\n \\\"sale\\\": true\\n}\",\"model_code\":\"1| class Product {\\n2| final int id;\\n3| final double price;\\n4| final String name;\\n5| const Product({required this.id, required this.price, required this.name});\\n6| factory Product.fromJson(Map<String, dynamic> json) => Product(\\n7| id: json['id'] as int,\\n8| price: json['price'] as double,\\n9| name: json['name'] as String,\\n10| );\\n11| Map<String, dynamic> toJson() => {'id': id, 'price': price, 'name': name};\\n12| }\",\"notes\":[]}","question":"Why does the second product fail?"}
write: the same payload, pattern style, with a test and a fetch function
No model is pasted, so there is no existing_model. options asks for the fetch function and the unit test; two samples are far below the background threshold. skeleton is the browser's own generated model: price is read as num and converted with toDouble(), and sale is read outside the pattern as bool?.
body.json for the write:
{"task":"write","facts":"{\"title\":\"Product\",\"platform\":\"vm\",\"style\":\"pattern\",\"root\":\"list\",\"root_class\":\"Product\",\"samples\":2,\"schema\":[{\"class\":\"Product\",\"key\":\"id\",\"field\":\"id\",\"type\":\"int\",\"seen\":\"2/2\",\"nulls\":0,\"kinds\":\"int 2\",\"optional\":false},{\"class\":\"Product\",\"key\":\"price\",\"field\":\"price\",\"type\":\"double\",\"seen\":\"2/2\",\"nulls\":0,\"kinds\":\"int 1, double 1\",\"optional\":false},{\"class\":\"Product\",\"key\":\"name\",\"field\":\"name\",\"type\":\"String\",\"seen\":\"2/2\",\"nulls\":0,\"kinds\":\"string 2\",\"optional\":false},{\"class\":\"Product\",\"key\":\"sale\",\"field\":\"sale\",\"type\":\"bool?\",\"seen\":\"1/2\",\"nulls\":0,\"kinds\":\"bool 1\",\"optional\":true}],\"classes\":[\"Product\"],\"skeleton\":\"import 'dart:convert';\\n\\n/// The response body is a JSON list of Product objects.\\nList<Product> parseProducts(String body) => [\\n for (final e$0 in jsonDecode(body) as List<dynamic>) Product.fromJson(e$0 as Map<String, dynamic>),\\n ];\\n\\nclass Product {\\n final int id;\\n /// the JSON has both whole numbers and decimals, so it is read as num and converted.\\n final double price;\\n final String name;\\n /// missing in 1 of 2 samples.\\n final bool? sale;\\n\\n const Product({\\n required this.id,\\n required this.price,\\n required this.name,\\n this.sale,\\n });\\n\\n factory Product.fromJson(Map<String, dynamic> json) {\\n return switch (json) {\\n {\\n 'id': int id,\\n 'price': num price,\\n 'name': String name,\\n } =>\\n Product(\\n id: id,\\n price: price.toDouble(),\\n name: name,\\n sale: json['sale'] as bool?,\\n ),\\n _ => throw const FormatException('Failed to load Product.'),\\n };\\n }\\n\\n Map<String, dynamic> toJson() {\\n return {\\n 'id': id,\\n 'price': price,\\n 'name': name,\\n if (sale != null) 'sale': sale,\\n };\\n }\\n}\\n\",\"payload_excerpt\":\"{\\n \\\"id\\\": 7,\\n \\\"price\\\": 12.5,\\n \\\"name\\\": \\\"Mug\\\"\\n}\\n{\\n \\\"id\\\": 8,\\n \\\"price\\\": 4,\\n \\\"name\\\": \\\"Tea\\\",\\n \\\"sale\\\": true\\n}\",\"options\":{\"fetch\":true,\"test\":true,\"background\":false},\"notes\":[]}"}
Idempotency
/run and /run-stream accept an Idempotency-Key request
header. The SDK sends it only when the caller passes a key. The page passes one on every run
attempt, with the lane in it:
fromjson-desk:<lane>:<hash>:a<attempt>
For the two worked examples above, serialised the way JSON.stringify does it (no whitespace):
fromjson-desk:review:41a9e83f-9ea:a1 (first attempt)
fromjson-desk:review:41a9e83f-9ea:a2 (the one reformat retry)
fromjson-desk:write:11a56c72-90a:a1
<hash> is computed over JSON.stringify(input), where the input is
the one buildInput produced, before any retry_note is added. It
is a djb2-style 32-bit hash (h = 5381; for each UTF-16 code unit,
h = h*33 + c, modulo 232), written as lowercase hex, then -,
then the string length in hex. <attempt> is 1 for a run and
2 for the reformat retry. The code comments give the reason: a reformat retry stays
distinguishable from a deliberate re-run, and a network blip never double-bills. The SDK also
notes that an idempotent replay of /run-stream may come back as a plain JSON result
instead of an event stream (step 6 handles both).
The samples below compute the same formula over the exact bytes they send, so a
body.json saved with a trailing newline gets a different key than the page would use.
Your key only has to stay stable for one logical attempt, so any stable hash of your body works
for your own calls.
1. A tiny client
This helper sends a JSON body that is already serialised. It sets
Content-Type: application/json, adds Authorization: Bearer ... only when
a token is set, returns data on a 2xx, and raises an error with the status,
error.code and error.message otherwise, as sdk.js does. It
takes the body as a string so that a run's Idempotency-Key is computed over the exact bytes it
sends. The later steps assume the helper is in scope.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="${SKILLSAFE_TOKEN:-YOUR_TOKEN}" # from https://fromjson-desk.skillsafe.ai/tokens.html
# api METHOD PATH [BODY_FILE] [extra curl args...]
# Prints the response body; exits non-zero (--fail-with-body) on a non-2xx status.
api() {
local method="$1" path="$2" body="${3:-}"
shift 2; [ $# -gt 0 ] && shift
local args=(-sS --fail-with-body -X "$method" "$BASE$path" -H "Content-Type: application/json")
if [ -n "$TOKEN" ]; then args+=(-H "Authorization: Bearer $TOKEN"); fi
if [ -n "$body" ]; then args+=(--data-binary "@$body"); fi
curl "${args[@]}" "$@"
}
# idem_key LANE BODY_FILE ATTEMPT -> fromjson-desk:<lane>:<hash>:a<attempt>
# Hashes bytes, so it equals the page's UTF-16 hash only for an ASCII body.
idem_key() {
local h=5381 n=0 b
for b in $(od -An -v -tu1 "$2"); do h=$(( (h * 33 + b) & 0xFFFFFFFF )); n=$((n + 1)); done
printf 'fromjson-desk:%s:%x-%x:a%s\n' "$1" "$h" "$n" "$3"
}
import json, os, time, urllib.error, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = os.environ.get("SKILLSAFE_TOKEN", "YOUR_TOKEN") # from https://fromjson-desk.skillsafe.ai/tokens.html
class ApiError(Exception):
def __init__(self, status, code, message, details=None):
super().__init__(f"{status} {code}: {message}")
self.status, self.code, self.details = status, code, details
def open_request(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else body.encode("utf-8"))
req.add_header("Content-Type", "application/json")
if TOKEN:
req.add_header("Authorization", "Bearer " + TOKEN)
for k, v in (headers or {}).items():
req.add_header(k, v)
try:
return urllib.request.urlopen(req, timeout=300)
except urllib.error.HTTPError as e:
try:
err = json.load(e).get("error") or {}
except ValueError:
err = {}
raise ApiError(e.code, err.get("code"), err.get("message") or e.reason, err.get("details")) from None
def call(method, path, body=None, headers=None):
"""body is an already-serialised JSON string (or None). Returns `data`."""
with open_request(method, path, body, headers) as r:
return json.load(r).get("data")
def idem_key(lane, body, attempt):
units = body.encode("utf-16-le")
h = 5381
for i in range(0, len(units), 2):
h = (h * 33 + int.from_bytes(units[i:i + 2], "little")) & 0xFFFFFFFF
return f"fromjson-desk:{lane}:{h:x}-{len(units) // 2:x}:a{attempt}"
// Node 18+ as an ES module (.mjs, for top-level await).
const BASE = "https://api.skillsafe.ai/v1/app-api";
let token = "YOUR_TOKEN"; // paste from https://fromjson-desk.skillsafe.ai/tokens.html
function apiError(res, json) {
const err = new Error((json.error && json.error.message) || res.statusText);
err.status = res.status;
err.code = json.error && json.error.code;
err.details = json.error && json.error.details;
return err;
}
function send(method, path, body, headers = {}) {
const h = { "Content-Type": "application/json", ...headers };
if (token) h.Authorization = "Bearer " + token;
return fetch(BASE + path, { method, headers: h, body });
}
// body is an already-serialised JSON string (or undefined). Returns `data`.
async function call(method, path, body, headers) {
const res = await send(method, path, body, headers);
const json = await res.json().catch(() => ({}));
if (!res.ok) throw apiError(res, json);
return json.data;
}
// The page's own formula (app.js inputHash), over the exact string you send.
function idemKey(lane, body, attempt) {
let h = 5381;
for (let i = 0; i < body.length; i++) h = ((h << 5) + h + body.charCodeAt(i)) | 0;
return `fromjson-desk:${lane}:${(h >>> 0).toString(16)}-${body.length.toString(16)}:a${attempt}`;
}
package main
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
"unicode/utf16"
)
const base = "https://api.skillsafe.ai/v1/app-api"
// From https://fromjson-desk.skillsafe.ai/tokens.html
var token = os.Getenv("SKILLSAFE_TOKEN")
type APIError struct {
Status int
Code, Message string
}
func (e *APIError) Error() string { return fmt.Sprintf("%d %s: %s", e.Status, e.Code, e.Message) }
func send(method, path string, body []byte, headers map[string]string) (*http.Response, error) {
req, err := http.NewRequest(method, base+path, bytes.NewReader(body))
if err != nil {
return nil, err
}
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)
}
return http.DefaultClient.Do(req)
}
// decode unwraps `data`, or returns an *APIError on a non-2xx status.
func decode(res *http.Response) (map[string]any, error) {
defer res.Body.Close()
raw, _ := io.ReadAll(res.Body)
var env struct {
Data map[string]any `json:"data"`
Error *struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
_ = json.Unmarshal(raw, &env)
if res.StatusCode < 200 || res.StatusCode > 299 {
e := &APIError{Status: res.StatusCode, Message: res.Status}
if env.Error != nil {
e.Code = env.Error.Code
if env.Error.Message != "" {
e.Message = env.Error.Message
}
}
return nil, e
}
return env.Data, nil
}
func call(method, path string, body []byte, headers map[string]string) (map[string]any, error) {
res, err := send(method, path, body, headers)
if err != nil {
return nil, err
}
return decode(res)
}
func idemKey(lane string, body []byte, attempt int) string {
units := utf16.Encode([]rune(string(body)))
h := uint32(5381)
for _, c := range units {
h = h*33 + uint32(c)
}
return fmt.Sprintf("fromjson-desk:%s:%x-%x:a%d", lane, h, len(units), attempt)
}
// Keep the compiler quiet until steps 5 and 6 use these.
var _, _, _ = bufio.NewReader, strings.HasPrefix, time.Sleep
// Java 11+, with Jackson (com.fasterxml.jackson.core:jackson-databind) for JSON.
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.*;
import java.net.URI;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.Map;
public class FromJsonDesk {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
// From https://fromjson-desk.skillsafe.ai/tokens.html
static String token = System.getenv().getOrDefault("SKILLSAFE_TOKEN", "YOUR_TOKEN");
static final HttpClient HTTP = HttpClient.newHttpClient();
static final ObjectMapper JSON = new ObjectMapper();
static class ApiException extends RuntimeException {
final int status; final String code;
ApiException(int status, String code, String message) {
super(status + " " + code + ": " + message);
this.status = status; this.code = code;
}
}
static HttpRequest request(String method, String path, String body, Map<String, String> headers) {
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Content-Type", "application/json")
.method(method, body == null ? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(body));
if (token != null && !token.isEmpty()) b.header("Authorization", "Bearer " + token);
headers.forEach(b::header);
return b.build();
}
static JsonNode parse(String raw) {
try { JsonNode n = JSON.readTree(raw); return n == null ? JSON.createObjectNode() : n; }
catch (IOException e) { return JSON.createObjectNode(); }
}
static ApiException error(int status, JsonNode json) {
JsonNode err = json.path("error");
return new ApiException(status, err.path("code").asText(null), err.path("message").asText("HTTP " + status));
}
/** Returns the `data` node. */
static JsonNode call(String method, String path, String body, Map<String, String> headers) throws Exception {
HttpResponse<String> res = HTTP.send(request(method, path, body, headers), HttpResponse.BodyHandlers.ofString());
JsonNode json = parse(res.body());
if (res.statusCode() / 100 != 2) throw error(res.statusCode(), json);
return json.path("data");
}
static String idemKey(String lane, String body, int attempt) {
int h = 5381;
for (int i = 0; i < body.length(); i++) h = (h << 5) + h + body.charAt(i);
return "fromjson-desk:" + lane + ":" + Integer.toHexString(h) + "-"
+ Integer.toHexString(body.length()) + ":a" + attempt;
}
// The methods in steps 2 to 6 go inside this class.
}
require "json"
require "net/http"
require "uri"
BASE = "https://api.skillsafe.ai/v1/app-api"
$token = ENV.fetch("SKILLSAFE_TOKEN", "YOUR_TOKEN") # from https://fromjson-desk.skillsafe.ai/tokens.html
class ApiError < StandardError
attr_reader :status, :code
def initialize(status, code, message)
super("#{status} #{code}: #{message}")
@status, @code = status, code
end
end
def build_request(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri)
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer #{$token}" unless $token.to_s.empty?
headers.each { |k, v| req[k] = v }
req.body = body if body
[uri, req]
end
def raise_for(res)
return if res.is_a?(Net::HTTPSuccess)
json = (JSON.parse(res.body.to_s) rescue {})
err = (json.is_a?(Hash) && json["error"]) || {}
raise ApiError.new(res.code.to_i, err["code"], err["message"] || res.message)
end
# body is an already-serialised JSON string (or nil). Returns `data`.
def call(method, path, body = nil, headers = {})
uri, req = build_request(method, path, body, headers)
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true, read_timeout: 300) { |http| http.request(req) }
raise_for(res)
JSON.parse(res.body)["data"]
end
def idem_key(lane, body, attempt)
units = body.encode("UTF-16LE").unpack("v*")
h = units.reduce(5381) { |acc, c| (acc * 33 + c) & 0xFFFFFFFF }
format("fromjson-desk:%s:%x-%x:a%d", lane, h, units.length, attempt)
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = getenv("SKILLSAFE_TOKEN") ?: "YOUR_TOKEN"; // from https://fromjson-desk.skillsafe.ai/tokens.html
class ApiError extends Exception {
public $status; public $apiCode;
function __construct($status, $code, $message) {
parent::__construct("$status $code: $message");
$this->status = $status; $this->apiCode = $code;
}
}
function header_lines(array $extra = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($extra as $k => $v) $h[] = "$k: $v";
return $h;
}
function api_error($status, $raw) {
$json = json_decode((string)$raw, true);
$err = is_array($json) ? ($json["error"] ?? []) : [];
return new ApiError($status, $err["code"] ?? null, $err["message"] ?? "HTTP $status");
}
// $body is an already-serialised JSON string (or null). Returns `data`.
function call($method, $path, $body = null, array $headers = []) {
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => header_lines($headers)]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($status < 200 || $status > 299) throw api_error($status, $raw);
return json_decode($raw, true)["data"] ?? null;
}
function idem_key($lane, $body, $attempt) {
$units = array_values(unpack("v*", mb_convert_encoding($body, "UTF-16LE", "UTF-8")) ?: []);
$h = 5381;
foreach ($units as $c) $h = ($h * 33 + $c) & 0xFFFFFFFF;
return sprintf("fromjson-desk:%s:%x-%x:a%d", $lane, $h, count($units), $attempt);
}
// .NET 6+ top-level program.
using System.Net;
using System.Text;
using System.Text.Json;
const string Base = "https://api.skillsafe.ai/v1/app-api";
// From https://fromjson-desk.skillsafe.ai/tokens.html
string token = Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN") ?? "YOUR_TOKEN";
var http = new HttpClient { Timeout = TimeSpan.FromMinutes(5) };
HttpRequestMessage Request(HttpMethod method, string path, string? body, Dictionary<string, string>? headers)
{
var req = new HttpRequestMessage(method, Base + path);
// HttpClient carries Content-Type on the content, so a GET goes without it.
if (body != null) req.Content = new StringContent(body, Encoding.UTF8, "application/json");
if (!string.IsNullOrEmpty(token)) req.Headers.TryAddWithoutValidation("Authorization", "Bearer " + token);
foreach (var kv in headers ?? new()) req.Headers.TryAddWithoutValidation(kv.Key, kv.Value);
return req;
}
HttpRequestException ApiError(HttpStatusCode status, string raw)
{
string? code = null, msg = status.ToString();
try
{
var root = JsonDocument.Parse(raw).RootElement;
if (root.ValueKind == JsonValueKind.Object && root.TryGetProperty("error", out var err)
&& err.ValueKind == JsonValueKind.Object)
{
if (err.TryGetProperty("code", out var c)) code = c.GetString();
if (err.TryGetProperty("message", out var m)) msg = m.GetString();
}
}
catch (JsonException) { }
return new HttpRequestException($"{(int)status} {code}: {msg}", null, status);
}
// Returns the `data` element.
async Task<JsonElement> Call(HttpMethod method, string path, string? body = null, Dictionary<string, string>? headers = null)
{
using var res = await http.SendAsync(Request(method, path, body, headers));
var raw = await res.Content.ReadAsStringAsync();
if (!res.IsSuccessStatusCode) throw ApiError(res.StatusCode, raw);
return JsonDocument.Parse(raw).RootElement.GetProperty("data").Clone();
}
string IdemKey(string lane, string body, int attempt)
{
uint h = 5381;
foreach (char c in body) h = unchecked(h * 33 + c);
return $"fromjson-desk:{lane}:{h:x}-{body.Length:x}:a{attempt}";
}
2. Get a token
There are two kinds of token, and the tokens page shows the one this
browser holds for fromjson-desk. A guest token is minted
automatically and is enough for free calls such as /me and /estimate.
An AI review or write is metered, so it needs a signed-in (personal) token: the
page sends a guest to sign in before it runs anything, unless /estimate reported
sponsor_enabled: true.
- Signed-in token (to run). Open /tokens.html, press Sign in with SkillSafe, then press Copy token. Forget on this device there deletes the token from that browser only; it is not a server-side revocation, so a copy you pasted elsewhere keeps working until it expires. Treat a token you have shared as compromised.
- Guest token (to look). The SDK gets one with
POST /guestand the body{"slug": "fromjson-desk"}, sent with no token. It readsdata.tokenand keepsdata.guest_id, so that a later sign-in can migrate the guest wallet.
# Guest token (needs jq). No Authorization header on this call.
TOKEN=$(curl -sS --fail-with-body -X POST "$BASE/guest" \
-H "Content-Type: application/json" --data '{"slug":"fromjson-desk"}' | jq -r '.data.token')
# Or a personal token copied from https://fromjson-desk.skillsafe.ai/tokens.html
TOKEN="YOUR_TOKEN"
def guest_token():
global TOKEN
TOKEN = "" # the guest call carries no token
TOKEN = call("POST", "/guest", json.dumps({"slug": "fromjson-desk"}))["token"]
return TOKEN
# Or: TOKEN = "YOUR_TOKEN" (personal, from /tokens.html)
async function guestToken() {
token = ""; // the guest call carries no token
const data = await call("POST", "/guest", JSON.stringify({ slug: "fromjson-desk" }));
token = data.token;
return data; // { token, guest_id, ... }
}
// Or: token = "YOUR_TOKEN"; (personal, from /tokens.html)
func guestToken() error {
token = "" // the guest call carries no token
data, err := call("POST", "/guest", []byte(`{"slug":"fromjson-desk"}`), nil)
if err != nil {
return err
}
token, _ = data["token"].(string)
return nil
}
// Or: SKILLSAFE_TOKEN=YOUR_TOKEN (personal, from /tokens.html)
static void guestToken() throws Exception {
token = ""; // the guest call carries no token
token = call("POST", "/guest", "{\"slug\":\"fromjson-desk\"}", Map.of()).path("token").asText();
}
// Or: SKILLSAFE_TOKEN=YOUR_TOKEN (personal, from /tokens.html)
def guest_token
$token = "" # the guest call carries no token
$token = call("POST", "/guest", JSON.generate(slug: "fromjson-desk"))["token"]
end
# Or: $token = "YOUR_TOKEN" (personal, from /tokens.html)
function guest_token() {
global $TOKEN;
$TOKEN = ""; // the guest call carries no token
$TOKEN = call("POST", "/guest", json_encode(["slug" => "fromjson-desk"]))["token"];
return $TOKEN;
}
// Or: $TOKEN = "YOUR_TOKEN"; (personal, from /tokens.html)
async Task GuestToken()
{
token = ""; // the guest call carries no token
var data = await Call(HttpMethod.Post, "/guest", "{\"slug\":\"fromjson-desk\"}");
token = data.GetProperty("token").GetString()!;
}
// Or: SKILLSAFE_TOKEN=YOUR_TOKEN (personal, from /tokens.html)
3. Check the session and the balance
GET /me returns the caller: subject_type, subject_id,
credits and sometimes profile. The page counts the caller as signed in
only when subject_type is "user", shows credits as the
balance, and compares it with the estimate before a run. Call it before a run to make sure you
hold a signed-in token.
api GET /me
# {"data":{"subject_type":"user","subject_id":"...","credits":...}}
me = call("GET", "/me")
signed_in = me.get("subject_type") == "user"
print(me.get("subject_type"), me.get("credits"), "signed in" if signed_in else "guest")
const me = await call("GET", "/me");
const signedIn = me.subject_type === "user";
console.log(me.subject_type, me.credits, signedIn ? "signed in" : "guest");
me, err := call("GET", "/me", nil, nil)
if err != nil {
panic(err)
}
signedIn := me["subject_type"] == "user"
fmt.Println(me["subject_type"], me["credits"], signedIn)
JsonNode me = call("GET", "/me", null, Map.of());
boolean signedIn = "user".equals(me.path("subject_type").asText());
System.out.println(me.path("subject_type").asText() + " " + me.path("credits") + " " + signedIn);
me = call("GET", "/me")
signed_in = me["subject_type"] == "user"
puts "#{me["subject_type"]} #{me["credits"]} #{signed_in ? "signed in" : "guest"}"
$me = call("GET", "/me");
$signedIn = ($me["subject_type"] ?? null) === "user";
echo ($me["subject_type"] ?? "?") . " " . ($me["credits"] ?? "?") . " " . ($signedIn ? "signed in" : "guest") . "\n";
var me = await Call(HttpMethod.Get, "/me");
var signedIn = me.GetProperty("subject_type").GetString() == "user";
Console.WriteLine($"{me.GetProperty("subject_type")} {me.GetProperty("credits")} {(signedIn ? "signed in" : "guest")}");
4. Price it
POST /estimate takes the same body you would send to a run. The SDK describes it as
the "worst-case credit cost of a run with this input (no charge, no job)". The page
reads these fields from the response:
| Field | How the page uses it |
|---|---|
hold_credits | Shown as the credits the run "reserves" for the lane. If the balance is under it, the page warns that the run may be cut short. |
min_credits | Shown as the minimum, when present. The page disables its run button while the balance is below it. |
sponsor_enabled | The page lets a guest start a run only when this is true; otherwise it asks the user to sign in. |
warnings | When present and non-empty, shown next to the price as "input warnings: " followed by the warnings joined with ; . |
hold_credits is a reservation, not the price. The charge for a run is reported
after it as charged_credits (steps 5 and 6), and the page shows that number with
the result. The page converts credits to dollars at 10,000 credits per dollar, and prices each
lane on its own input.
The response may carry a warnings array, which the page shows next to the price. A warning is not a rejection: the page still shows the hold and still lets the run go ahead, so check warnings yourself before you run. What we observed on this app on 2026-09-28, with a guest token: a body with both declared fields ({"task": "write", "facts": "{}"}) returned a hold with an empty warnings array and input_checked: true; an empty body {} still returned a hold, with the warnings "missing required field 'task'" and "missing required field 'facts'"; a body wrapped as {"input": {...}} also returned a hold, with those two warnings plus one naming the unknown field input and listing the declared fields; and a bare JSON string was rejected with HTTP 400 validation_error ("Body must be a JSON object of the app's input fields"). The estimate does not look inside the facts string, so a well-formed body with wrong facts prices cleanly: check warnings yourself, and build facts as the page does.
# body.json is one of the worked examples ("task": "review" or "write").
api POST /estimate body.json | jq '.data | {hold_credits, min_credits, sponsor_enabled, warnings}'
with open("body.json", encoding="utf-8") as f:
body = f.read()
est = call("POST", "/estimate", body)
print("hold", est.get("hold_credits"), "min", est.get("min_credits"), "sponsor", est.get("sponsor_enabled"))
for w in est.get("warnings") or []:
print("warning:", w)
import { readFile } from "node:fs/promises";
const body = await readFile("body.json", "utf8");
const est = await call("POST", "/estimate", body);
console.log("hold", est.hold_credits, "min", est.min_credits, "sponsor", est.sponsor_enabled);
for (const w of est.warnings || []) console.log("warning:", w);
body, err := os.ReadFile("body.json")
if err != nil {
panic(err)
}
est, err := call("POST", "/estimate", body, nil)
if err != nil {
panic(err)
}
fmt.Println("hold", est["hold_credits"], "min", est["min_credits"], "sponsor", est["sponsor_enabled"])
if ws, ok := est["warnings"].([]any); ok {
for _, w := range ws {
fmt.Println("warning:", w)
}
}
String body = Files.readString(Path.of("body.json"), StandardCharsets.UTF_8);
JsonNode est = call("POST", "/estimate", body, Map.of());
System.out.println("hold " + est.path("hold_credits") + " min " + est.path("min_credits")
+ " sponsor " + est.path("sponsor_enabled"));
for (JsonNode w : est.path("warnings")) System.out.println("warning: " + w.asText());
body = File.read("body.json", encoding: "UTF-8")
est = call("POST", "/estimate", body)
puts "hold #{est["hold_credits"]} min #{est["min_credits"]} sponsor #{est["sponsor_enabled"]}"
(est["warnings"] || []).each { |w| puts "warning: #{w}" }
$body = file_get_contents("body.json");
$est = call("POST", "/estimate", $body);
printf("hold %s min %s sponsor %s\n", $est["hold_credits"] ?? "?", $est["min_credits"] ?? "?",
var_export($est["sponsor_enabled"] ?? null, true));
foreach ($est["warnings"] ?? [] as $w) echo "warning: " . (is_string($w) ? $w : json_encode($w)) . "\n";
var body = await File.ReadAllTextAsync("body.json");
var est = await Call(HttpMethod.Post, "/estimate", body);
string Field(string name) => est.TryGetProperty(name, out var v) ? v.ToString() : "?";
Console.WriteLine($"hold {Field("hold_credits")} min {Field("min_credits")} sponsor {Field("sponsor_enabled")}");
if (est.TryGetProperty("warnings", out var ws) && ws.ValueKind == JsonValueKind.Array)
foreach (var w in ws.EnumerateArray()) Console.WriteLine("warning: " + w);
5. Run it, then poll the job
/run and /run-stream are metered. They spend the
signed-in user's credits, so send a signed-in token (step 2). The page itself
always uses /run-stream (step 6); /run plus polling is the SDK's other
way to run the same body.
POST /run with the body and an Idempotency-Key header returns
data.job_id. Then poll GET /jobs/{id} until status is
succeeded or failed. The SDK's poller asks every second and gives up
after 180 seconds by default. The SDK reads these fields from a finished job:
| Field | Meaning |
|---|---|
status | succeeded or failed once the job is finished. |
output.output | The reply text: the lane's JSON object (see the reply contract). |
charged_credits | The credits charged for this job. |
truncated | true when the reply hit the run's output cap. The page reports it as "cut short by the available balance", shows the sections that arrived, and suggests topping up and running again. |
error | Why a failed job failed. |
If the reply does not parse, do what the page does. Send the same body once more
with a retry_note field (the text is in Input fields) and the key
ending in :a2. Compute the hash over the body without the note. This is a
second run and it is charged.
LANE=$(jq -r '.task' body.json) # "review" or "write"
KEY=$(idem_key "$LANE" body.json 1) # fromjson-desk:<lane>:<hash>:a1
JOB=$(api POST /run body.json -H "Idempotency-Key: $KEY" | jq -r '.data.job_id')
for i in $(seq 1 180); do
api GET "/jobs/$JOB" > job.json
STATUS=$(jq -r '.data.status' job.json)
if [ "$STATUS" = succeeded ] || [ "$STATUS" = failed ]; then break; fi
sleep 1
done
jq '.data | {status, charged_credits, truncated, error}' job.json
jq -r '.data.output.output' job.json | jq . # the lane's JSON object
with open("body.json", encoding="utf-8") as f:
body = f.read()
lane = json.loads(body)["task"] # "review" or "write"
key = idem_key(lane, body, 1)
job_id = call("POST", "/run", body, {"Idempotency-Key": key})["job_id"]
deadline = time.time() + 180
while True:
job = call("GET", f"/jobs/{job_id}")
if job.get("status") in ("succeeded", "failed"):
break
if time.time() > deadline:
raise TimeoutError("job timed out")
time.sleep(1)
if job["status"] == "failed":
raise RuntimeError(f"job failed: {job.get('error')}")
reply = json.loads(job["output"]["output"]) # the lane's JSON object
print(reply["lane"], reply["headline"])
print("charged", job.get("charged_credits"), "truncated", job.get("truncated") is True)
import { readFile } from "node:fs/promises";
const body = await readFile("body.json", "utf8");
const lane = JSON.parse(body).task; // "review" or "write"
const key = idemKey(lane, body, 1);
const { job_id } = await call("POST", "/run", body, { "Idempotency-Key": key });
const start = Date.now();
let job;
for (;;) {
job = await call("GET", "/jobs/" + encodeURIComponent(job_id));
if (job.status === "succeeded" || job.status === "failed") break;
if (Date.now() - start > 180000) throw new Error("job timed out");
await new Promise((r) => setTimeout(r, 1000));
}
if (job.status === "failed") throw new Error("job failed: " + JSON.stringify(job.error));
const reply = JSON.parse(job.output.output); // the lane's JSON object
console.log(reply.lane, reply.headline);
console.log("charged", job.charged_credits, "truncated", job.truncated === true);
body, err := os.ReadFile("body.json")
if err != nil {
panic(err)
}
var in struct {
Task string `json:"task"` // "review" or "write"
}
if err := json.Unmarshal(body, &in); err != nil {
panic(err)
}
key := idemKey(in.Task, body, 1)
started, err := call("POST", "/run", body, map[string]string{"Idempotency-Key": key})
if err != nil {
panic(err)
}
jobID, _ := started["job_id"].(string)
var job map[string]any
deadline := time.Now().Add(180 * time.Second)
for {
job, err = call("GET", "/jobs/"+jobID, nil, nil)
if err != nil {
panic(err)
}
if s := job["status"]; s == "succeeded" || s == "failed" {
break
}
if time.Now().After(deadline) {
panic("job timed out")
}
time.Sleep(time.Second)
}
if job["status"] == "failed" {
panic(fmt.Sprint("job failed: ", job["error"]))
}
out, _ := job["output"].(map[string]any)
text, _ := out["output"].(string)
var reply map[string]any
if err := json.Unmarshal([]byte(text), &reply); err != nil {
panic(err) // not the JSON object: retry once with retry_note (see above)
}
fmt.Println(reply["lane"], reply["headline"])
fmt.Println("charged", job["charged_credits"], "truncated", job["truncated"] == true)
static JsonNode runAndWait() throws Exception {
String body = Files.readString(Path.of("body.json"), StandardCharsets.UTF_8);
String lane = JSON.readTree(body).path("task").asText(); // "review" or "write"
String key = idemKey(lane, body, 1);
String jobId = call("POST", "/run", body, Map.of("Idempotency-Key", key)).path("job_id").asText();
long deadline = System.currentTimeMillis() + 180_000;
JsonNode job;
for (;;) {
job = call("GET", "/jobs/" + jobId, null, Map.of());
String s = job.path("status").asText();
if (s.equals("succeeded") || s.equals("failed")) break;
if (System.currentTimeMillis() > deadline) throw new RuntimeException("job timed out");
Thread.sleep(1000);
}
if (job.path("status").asText().equals("failed")) throw new RuntimeException("job failed: " + job.path("error"));
JsonNode reply = JSON.readTree(job.path("output").path("output").asText()); // the lane's JSON object
System.out.println(reply.path("lane").asText() + ": " + reply.path("headline").asText());
System.out.println("charged " + job.path("charged_credits") + " truncated " + job.path("truncated").asBoolean(false));
return reply;
}
body = File.read("body.json", encoding: "UTF-8")
lane = JSON.parse(body)["task"] # "review" or "write"
key = idem_key(lane, body, 1)
job_id = call("POST", "/run", body, "Idempotency-Key" => key)["job_id"]
deadline = Time.now + 180
job = nil
loop do
job = call("GET", "/jobs/#{URI.encode_www_form_component(job_id)}")
break if %w[succeeded failed].include?(job["status"])
raise "job timed out" if Time.now > deadline
sleep 1
end
raise "job failed: #{job["error"].inspect}" if job["status"] == "failed"
reply = JSON.parse(job.dig("output", "output")) # the lane's JSON object
puts "#{reply["lane"]}: #{reply["headline"]}"
puts "charged #{job["charged_credits"]} truncated #{job["truncated"] == true}"
$body = file_get_contents("body.json");
$lane = json_decode($body, true)["task"]; // "review" or "write"
$key = idem_key($lane, $body, 1);
$jobId = call("POST", "/run", $body, ["Idempotency-Key" => $key])["job_id"];
$deadline = time() + 180;
do {
$job = call("GET", "/jobs/" . rawurlencode($jobId));
if (in_array($job["status"] ?? "", ["succeeded", "failed"], true)) break;
if (time() > $deadline) throw new RuntimeException("job timed out");
sleep(1);
} while (true);
if ($job["status"] === "failed") throw new RuntimeException("job failed: " . json_encode($job["error"] ?? null));
$reply = json_decode($job["output"]["output"], true); // the lane's JSON object
echo $reply["lane"] . ": " . $reply["headline"] . "\n";
echo "charged " . ($job["charged_credits"] ?? "?") . " truncated " . var_export(($job["truncated"] ?? false) === true, true) . "\n";
var body = await File.ReadAllTextAsync("body.json");
var lane = JsonDocument.Parse(body).RootElement.GetProperty("task").GetString()!; // "review" or "write"
var key = IdemKey(lane, body, 1);
var started = await Call(HttpMethod.Post, "/run", body, new() { ["Idempotency-Key"] = key });
var jobId = started.GetProperty("job_id").GetString()!;
var deadline = DateTime.UtcNow.AddSeconds(180);
JsonElement job;
while (true)
{
job = await Call(HttpMethod.Get, "/jobs/" + Uri.EscapeDataString(jobId));
var s = job.GetProperty("status").GetString();
if (s == "succeeded" || s == "failed") break;
if (DateTime.UtcNow > deadline) throw new TimeoutException("job timed out");
await Task.Delay(1000);
}
if (job.GetProperty("status").GetString() == "failed") throw new Exception("job failed: " + job.GetProperty("error"));
var reply = JsonDocument.Parse(job.GetProperty("output").GetProperty("output").GetString()!).RootElement;
Console.WriteLine($"{reply.GetProperty("lane")}: {reply.GetProperty("headline")}");
var truncated = job.TryGetProperty("truncated", out var t) && t.ValueKind == JsonValueKind.True;
Console.WriteLine($"charged {job.GetProperty("charged_credits")} truncated {truncated}");
6. Or stream it
POST /run-stream takes the same body and Idempotency-Key header. It is
metered like /run and needs a signed-in token. When the response's
Content-Type is text/event-stream, the SDK reads it as server-sent
events: events are separated by a blank line, each has an event: line and one or
more data: lines, and the SDK joins the data: lines into one JSON value.
This is how the SDK handles each event:
| Event | Data | What the SDK does |
|---|---|---|
job | The job that was started. | Passes it to onJob. The page uses it to move its progress display on. |
delta | { "text": "..." } | Passes text to onDelta. |
done | { job_id, status, charged_credits, output, ... } | Keeps it as the result. The reply text is output.output. |
error | { message, code, job_id } | Once the stream ends, throws with message (or "job failed"), code and job_id. |
The SDK also keeps a pending event as the result, the same way as done,
and skips events whose data is empty or is not valid JSON.
Read the reply from done. The page takes
done.output.output and falls back to the joined delta texts only when that
is missing; its code notes that browsers receive ticks, not deltas, from this endpoint.
done.truncated means the reply was cut short, as on a job, and the page refreshes
the balance from /me after a run that reports charged_credits.
A replay may not be a stream. According to the SDK, on an idempotent replay
/run-stream can answer with a plain JSON envelope instead of an event stream. So
check the Content-Type first. If it is not text/event-stream, read the
response as { data } or { error }, exactly as in step 1.
LANE=$(jq -r '.task' body.json) # "review" or "write"
KEY=$(idem_key "$LANE" body.json 1)
# -N turns off buffering so events print as they arrive.
api POST /run-stream body.json -N -H "Idempotency-Key: $KEY" | tee stream.txt
# Pull the reply out of the done event (a replay prints plain JSON instead: use .data).
awk '/^event:/ { ev = $2 } /^data:/ { if (ev == "done") { sub(/^data: ?/, ""); print } }' stream.txt \
| jq -r '.output.output' | jq .
with open("body.json", encoding="utf-8") as f:
body = f.read()
key = idem_key(json.loads(body)["task"], body, 1)
text, done, failure = "", None, None
def handle(event, data):
global text, done, failure
if not data:
return
try:
payload = json.loads(data)
except ValueError:
return
if event == "delta":
text += payload.get("text") or ""
elif event == "job":
print("job", payload)
elif event in ("done", "pending"):
done = payload
elif event == "error":
failure = payload
with open_request("POST", "/run-stream", body, {"Idempotency-Key": key}) as r:
if "text/event-stream" not in (r.headers.get("Content-Type") or ""):
done = json.load(r).get("data") # idempotent replay: plain JSON
else:
event, data = "message", ""
for raw in r:
line = raw.decode("utf-8").rstrip("\r\n")
if line.startswith("event:"):
event = line[6:].strip()
elif line.startswith("data:"):
data += line[5:].strip()
elif line == "":
handle(event, data)
event, data = "message", ""
if failure:
raise RuntimeError(f"{failure.get('code')}: {failure.get('message') or 'job failed'} (job {failure.get('job_id')})")
full = ((done or {}).get("output") or {}).get("output") or text
reply = json.loads(full) # the lane's JSON object
print(reply["lane"], reply["headline"])
print("charged", (done or {}).get("charged_credits"), "truncated", (done or {}).get("truncated") is True)
import { readFile } from "node:fs/promises";
const body = await readFile("body.json", "utf8");
const key = idemKey(JSON.parse(body).task, body, 1);
const res = await send("POST", "/run-stream", body, { "Idempotency-Key": key });
let done = null, failure = null, text = "";
if ((res.headers.get("content-type") || "").indexOf("text/event-stream") === -1) {
const json = await res.json().catch(() => ({}));
if (!res.ok) throw apiError(res, json);
done = json.data; // idempotent replay: plain JSON
} else {
// The same parser as sdk.js _readSse.
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
for (;;) {
const chunk = await reader.read();
if (chunk.done) break;
buffer += decoder.decode(chunk.value, { stream: true });
let idx;
while ((idx = buffer.indexOf("\n\n")) >= 0) {
const raw = buffer.slice(0, idx);
buffer = buffer.slice(idx + 2);
let event = "message", dataStr = "";
for (const line of raw.split("\n")) {
if (line.startsWith("event:")) event = line.slice(6).trim();
else if (line.startsWith("data:")) dataStr += line.slice(5).trim();
}
if (!dataStr) continue;
let data;
try { data = JSON.parse(dataStr); } catch { continue; }
if (event === "delta") text += data.text || "";
else if (event === "job") console.log("job", data);
else if (event === "done" || event === "pending") done = data;
else if (event === "error") failure = data;
}
}
}
if (failure) throw Object.assign(new Error(failure.message || "job failed"), { code: failure.code, job_id: failure.job_id });
const reply = JSON.parse((done && done.output && done.output.output) || text); // the lane's JSON object
console.log(reply.lane, reply.headline);
console.log("charged", done && done.charged_credits, "truncated", !!(done && done.truncated));
body, err := os.ReadFile("body.json")
if err != nil {
panic(err)
}
var in struct {
Task string `json:"task"`
}
_ = json.Unmarshal(body, &in)
res, err := send("POST", "/run-stream", body, map[string]string{"Idempotency-Key": idemKey(in.Task, body, 1)})
if err != nil {
panic(err)
}
var done, failure map[string]any
var text strings.Builder
if !strings.Contains(res.Header.Get("Content-Type"), "text/event-stream") {
if done, err = decode(res); err != nil { // idempotent replay: plain JSON
panic(err)
}
} else {
defer res.Body.Close()
sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 0, 64*1024), 16*1024*1024)
event, data := "message", ""
dispatch := func() {
var payload map[string]any
if data == "" || json.Unmarshal([]byte(data), &payload) != nil {
return
}
switch event {
case "delta":
s, _ := payload["text"].(string)
text.WriteString(s)
case "job":
fmt.Println("job", payload)
case "done", "pending":
done = payload
case "error":
failure = payload
}
}
for sc.Scan() {
line := strings.TrimRight(sc.Text(), "\r")
switch {
case strings.HasPrefix(line, "event:"):
event = strings.TrimSpace(line[6:])
case strings.HasPrefix(line, "data:"):
data += strings.TrimSpace(line[5:])
case line == "":
dispatch()
event, data = "message", ""
}
}
}
if failure != nil {
panic(fmt.Sprint(failure["code"], ": ", failure["message"], " (job ", failure["job_id"], ")"))
}
full := text.String()
if out, ok := done["output"].(map[string]any); ok {
if s, ok := out["output"].(string); ok && s != "" {
full = s
}
}
var reply map[string]any
if err := json.Unmarshal([]byte(full), &reply); err != nil {
panic(err)
}
fmt.Println(reply["lane"], reply["headline"])
fmt.Println("charged", done["charged_credits"], "truncated", done["truncated"] == true)
static JsonNode stream() throws Exception {
String body = Files.readString(Path.of("body.json"), StandardCharsets.UTF_8);
String key = idemKey(JSON.readTree(body).path("task").asText(), body, 1);
HttpResponse<InputStream> res = HTTP.send(request("POST", "/run-stream", body, Map.of("Idempotency-Key", key)),
HttpResponse.BodyHandlers.ofInputStream());
JsonNode done = null, failure = null;
StringBuilder text = new StringBuilder();
String ctype = res.headers().firstValue("content-type").orElse("");
if (!ctype.contains("text/event-stream")) {
JsonNode json = parse(new String(res.body().readAllBytes(), StandardCharsets.UTF_8));
if (res.statusCode() / 100 != 2) throw error(res.statusCode(), json);
done = json.path("data"); // idempotent replay: plain JSON
} else {
try (BufferedReader in = new BufferedReader(new InputStreamReader(res.body(), StandardCharsets.UTF_8))) {
String event = "message", line;
StringBuilder data = new StringBuilder();
while ((line = in.readLine()) != null) {
if (line.startsWith("event:")) { event = line.substring(6).trim(); continue; }
if (line.startsWith("data:")) { data.append(line.substring(5).trim()); continue; }
if (!line.isEmpty()) continue;
if (data.length() > 0) {
JsonNode payload = null;
try { payload = JSON.readTree(data.toString()); } catch (IOException e) { /* skip */ }
if (payload != null) {
switch (event) {
case "delta": text.append(payload.path("text").asText("")); break;
case "job": System.out.println("job " + payload); break;
case "done": case "pending": done = payload; break;
case "error": failure = payload; break;
default: break;
}
}
}
event = "message"; data.setLength(0);
}
}
}
if (failure != null) throw new RuntimeException(failure.path("code").asText() + ": "
+ failure.path("message").asText("job failed") + " (job " + failure.path("job_id").asText() + ")");
String full = done != null ? done.path("output").path("output").asText("") : "";
JsonNode reply = JSON.readTree(full.isEmpty() ? text.toString() : full); // the lane's JSON object
System.out.println(reply.path("lane").asText() + ": " + reply.path("headline").asText());
System.out.println("charged " + (done == null ? "?" : done.path("charged_credits"))
+ " truncated " + (done != null && done.path("truncated").asBoolean(false)));
return reply;
}
body = File.read("body.json", encoding: "UTF-8")
key = idem_key(JSON.parse(body)["task"], body, 1)
uri, req = build_request("POST", "/run-stream", body, "Idempotency-Key" => key)
text, done, failure = +"", nil, nil
handle = lambda do |event, data|
next if data.empty?
payload = (JSON.parse(data) rescue nil)
next if payload.nil?
case event
when "delta" then text << payload["text"].to_s
when "job" then puts "job #{payload}"
when "done", "pending" then done = payload
when "error" then failure = payload
end
end
Net::HTTP.start(uri.host, uri.port, use_ssl: true, read_timeout: 300) do |http|
http.request(req) do |res|
unless res["Content-Type"].to_s.include?("text/event-stream")
raw = res.read_body
raise_for(res)
done = JSON.parse(raw)["data"] # idempotent replay: plain JSON
next
end
buffer = +""
res.read_body do |chunk|
buffer << chunk
while (idx = buffer.index("\n\n"))
raw = buffer.slice!(0, idx + 2)
event, data = "message", +""
raw.split("\n").each do |line|
if line.start_with?("event:") then event = line[6..].strip
elsif line.start_with?("data:") then data << line[5..].strip
end
end
handle.call(event, data)
end
end
end
end
raise "#{failure["code"]}: #{failure["message"] || "job failed"} (job #{failure["job_id"]})" if failure
full = done&.dig("output", "output") || text
reply = JSON.parse(full) # the lane's JSON object
puts "#{reply["lane"]}: #{reply["headline"]}"
puts "charged #{done&.fetch("charged_credits", nil)} truncated #{done&.fetch("truncated", nil) == true}"
$body = file_get_contents("body.json");
$key = idem_key(json_decode($body, true)["task"], $body, 1);
$text = ""; $done = null; $failure = null; $buffer = ""; $isStream = null; $plain = "";
$handle = function ($event, $data) use (&$text, &$done, &$failure) {
if ($data === "") return;
$payload = json_decode($data, true);
if (!is_array($payload)) return;
if ($event === "delta") $text .= $payload["text"] ?? "";
elseif ($event === "job") echo "job " . $data . "\n";
elseif ($event === "done" || $event === "pending") $done = $payload;
elseif ($event === "error") $failure = $payload;
};
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => header_lines(["Idempotency-Key" => $key]),
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$buffer, &$isStream, &$plain, $handle) {
if ($isStream === null) {
$isStream = strpos((string)curl_getinfo($ch, CURLINFO_CONTENT_TYPE), "text/event-stream") !== false;
}
if (!$isStream) { $plain .= $chunk; return strlen($chunk); }
$buffer .= $chunk;
while (($idx = strpos($buffer, "\n\n")) !== false) {
$raw = substr($buffer, 0, $idx);
$buffer = substr($buffer, $idx + 2);
$event = "message"; $data = "";
foreach (explode("\n", $raw) as $line) {
if (strncmp($line, "event:", 6) === 0) $event = trim(substr($line, 6));
elseif (strncmp($line, "data:", 5) === 0) $data .= trim(substr($line, 5));
}
$handle($event, $data);
}
return strlen($chunk);
},
]);
if (curl_exec($ch) === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if (!$isStream) {
if ($status < 200 || $status > 299) throw api_error($status, $plain);
$done = json_decode($plain, true)["data"] ?? null; // idempotent replay: plain JSON
}
if ($failure) throw new RuntimeException(($failure["code"] ?? "") . ": " . ($failure["message"] ?? "job failed"));
$reply = json_decode($done["output"]["output"] ?? $text, true); // the lane's JSON object
echo $reply["lane"] . ": " . $reply["headline"] . "\n";
echo "charged " . ($done["charged_credits"] ?? "?") . " truncated " . var_export(($done["truncated"] ?? false) === true, true) . "\n";
var body = await File.ReadAllTextAsync("body.json");
var key = IdemKey(JsonDocument.Parse(body).RootElement.GetProperty("task").GetString()!, body, 1);
using var req = Request(HttpMethod.Post, "/run-stream", body, new() { ["Idempotency-Key"] = key });
using var res = await http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
JsonElement? done = null, failure = null;
var text = new StringBuilder();
var ctype = res.Content.Headers.ContentType?.MediaType ?? "";
if (ctype != "text/event-stream")
{
var raw = await res.Content.ReadAsStringAsync();
if (!res.IsSuccessStatusCode) throw ApiError(res.StatusCode, raw);
done = JsonDocument.Parse(raw).RootElement.GetProperty("data").Clone(); // idempotent replay
}
else
{
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());
string ev = "message", data = "";
string? line;
while ((line = await reader.ReadLineAsync()) != null)
{
if (line.StartsWith("event:")) { ev = line[6..].Trim(); continue; }
if (line.StartsWith("data:")) { data += line[5..].Trim(); continue; }
if (line.Length != 0) continue;
if (data.Length > 0)
{
JsonElement? payload = null;
try { payload = JsonDocument.Parse(data).RootElement.Clone(); } catch (JsonException) { }
if (payload is JsonElement p)
{
if (ev == "delta" && p.TryGetProperty("text", out var tx)) text.Append(tx.GetString());
else if (ev == "job") Console.WriteLine("job " + p);
else if (ev == "done" || ev == "pending") done = p;
else if (ev == "error") failure = p;
}
}
ev = "message"; data = "";
}
}
if (failure is JsonElement f) throw new Exception($"{f.GetProperty("code")}: {f.GetProperty("message")}");
string full = text.ToString();
if (done is JsonElement d && d.TryGetProperty("output", out var o) && o.ValueKind == JsonValueKind.Object
&& o.TryGetProperty("output", out var s) && s.ValueKind == JsonValueKind.String) full = s.GetString()!;
var reply = JsonDocument.Parse(full).RootElement; // the lane's JSON object
Console.WriteLine($"{reply.GetProperty("lane")}: {reply.GetProperty("headline")}");
What the page does that you now do
- Reads the payload, infers the schema and generates the skeleton (
dartkit.js), checks a pasted model against every sample (dartcheck.js), and packs both asfacts(facts.js). - Prices each lane on its own input with
/estimate, disables the run while the balance is belowmin_credits, and warns while it is belowhold_credits. - Sends one run per attempt with a stable Idempotency-Key, and retries once with
retry_notewhen the reply does not parse. - Keeps whatever parsed when a stream ends early, and reports a truncated reply.
- Holds the reply to its own reading of the input (
recon.js) and names every disagreement, including running a written model file through the same checker.
Or skip all of this and use FromJson Desk in the browser. The schema, the generated model and the check against your samples are free there, even as a guest.