Drive Chunk Desk from your own code
The page does its free work in the browser: it splits a corpus sample into documents and blocks, estimates
tokens, chunks it with the strategy, size and overlap you choose (fixed, recursive,
heading, sentence), measures what that breaks, compares all four strategies, runs BM25
over the chunks for every query and raises flags CK-01..CK-15. Its two paid lanes are available over HTTP. You
send one JSON object with task set to "plan" or "retrieval", and the
model replies with one JSON object. The easiest way to build the facts string is the page itself:
Chunkit.buildInput(lane, Chunkit.analyze(sample, queries, settings), note) in
chunkit.js, which also runs under Node.
Base URL: https://api.skillsafe.ai/v1/app-api. Every call sends
Content-Type: application/json and, once you have a token,
Authorization: Bearer <token>. Both lanes run on gpt-terra with a 10%
publisher markup. A run needs a signed-in (user) token and is metered: the platform holds
the estimate's hold_credits while the run is in flight and then charges only
what it used (charged_credits). /estimate is free. A plain-text summary is at
/llms.txt.
The response envelope
Every response is {"data": ..., "error": null} on success or {"data": null, "error": {"code", "message"}} on failure.
Error codes
| HTTP | code | meaning |
|---|---|---|
| 400 | VALIDATION_ERROR | The body is not a JSON object, or a field is the wrong type. Every field of the run input is a string. |
| 401 | UNAUTHORIZED | No token, or an expired one: get a new one (step 2). |
| 402 | INSUFFICIENT_CREDITS | The balance is below the run's minimum. Top up at skillsafe.ai/account/billing/. |
| 403 | FORBIDDEN | A guest token tried a metered run: sign in for a personal token. |
| 404 | NOT_FOUND | Unknown job id. |
| 429 | RATE_LIMITED | Back off and retry with the same Idempotency-Key. |
The task field: two lanes
| task | what the model does |
|---|---|
"plan" | Picks the chunking (strategy, size, overlap, contextual prefix, rules), the cleaning to do before embedding and the embedding model (primary first), answers every plan flag in lane_flags, and builds a golden set from your queries. Skill: embedding-strategies. |
"retrieval" | Chooses keyword, dense or hybrid, the fusion (RRF or weighted), candidates per leg, the final k and reranking, routes every query to the leg it depends on, and gives an implementation with a snippet. Skill: hybrid-search-implementation. |
Input fields (all strings)
| field | when | content |
|---|---|---|
task | required | plan or retrieval |
sample | required | The corpus sample, each document introduced by a ==> name <== line. Past 30,000 characters the page cuts the middle on a paragraph boundary and leaves a [chunk-desk: ...] marker. |
facts | required | A JSON string: lane, summary (documents, tokens, structure, languages, boilerplate), settings (strategy, size, overlap), stats, compare (all four strategies), chunk_count, chunks (id, doc, tokens, heading_path, breaks, text preview), queries (n, query, identifiers, coverage, missing_terms, bm25_matched_chunks, bm25_top), flags (uid, id, sev, lanes, text, chunks, query), lane_flags, notes. It is Chunkit.factsOf(lane, A). |
queries | optional | The queries, one per line (the page searches the first 20). |
plan_handoff | retrieval only | A JSON string of a plan reply's verdict, chunking, primary model and preprocessing; send it with facts built at the plan's settings. |
note | optional | Your goal or constraint, up to 2,000 characters. |
retry_note | optional | Only on a reformat retry: what was wrong with the previous reply. |
The reply contracts
Both lanes share an envelope: task, verdict, headline, flag_answers[{flag, status: confirmed|dismissed, reason}], evaluation{metrics[], golden[{query, expected_chunks[], why}], steps[]}, risks[], questions[], summary.
task: "plan"
verdict (ready: keep the settings, adjust: same strategy with other numbers, prefix or cleaning, rework: another strategy), content_profile{kind, structure, languages[], notes}, chunking{strategy, size_tokens, overlap_tokens, contextual_prefix: none|heading_path|doc_title|summary, why, rules[]}, preprocessing[], models[{name, role: primary|alternative|budget, dimensions, max_input_tokens, multilingual, hosting: api|self-hosted, why, tradeoff}].
task: "retrieval"
verdict (hybrid, dense, keyword - the legs used), design{keyword_leg{use, engine, fields[], notes}, dense_leg{use, model, notes}, fusion{method: rrf|weighted|none, rrf_k, weights{dense, keyword}, why}, candidates_per_leg, final_k, rerank{use, model, top_n, why}, filters[]}, query_routes[{n, query, needs: keyword|dense|both, expected_chunks[], keyword_finds_it, why}], implementation{stack, steps[], snippet{language, code}}.
Worked examples
These are the page's own bundled examples (invented companies), with the input as app.js builds it and the saved model reply the page replays for free.
Plan: a NAS help center with an error-code table and CLI blocks
Input (sample and facts shortened here):
{
"task": "plan",
"sample": "==> errors.md <==\n# Bramblewyn NAS error and warning codes\n\nThis page lists the codes that BWOS shows on the front-panel display, in the web console under System > Event Log, and in the output of `bwctl`. Codes that begin with E are errors that stop the affected service until they are resolved. Codes that begin with W are warnings: the service keeps running, but the unit is telling you that something needs attention before it turns into an error. If a code is not listed here, write it down toget... (8767 characters in all)",
"queries": "what does E2210 mean\nhow do I clear E1043 after replacing the disk\nbwctl backup status --verbose\nmy nightly copies quit working after I updated the software\nthe box is way slower than it should be when moving files around\nwhich code means the NAS is getting too hot",
"facts": "(a JSON string; decoded: heading 160/0 over 23 chunks, 6 queries searched with BM25, 6 flags, lane_flags F1, F2, F6)",
"note": "Self-hosted is fine; we already run Postgres 16 for the product."
}
Reply:
{
"task": "plan",
"verdict": "adjust",
"headline": "Keep heading chunking, but raise size to 400 with 40 overlap, prefix the heading path, and split the code table by rows with its header repeated.",
"content_profile": {
"kind": "NAS product help center: error-code reference plus how-to and troubleshooting articles",
"structure": "3 Markdown docs (~2,600 tokens) with 15 headings, 16 paragraphs, 3 fenced bwctl command blocks and one 10-row error-code table; most sections are 80-230 tokens",
"languages": [
"English"
],
"notes": "The 6% 'other' share is code and identifiers (E2210, W0301, bwctl flags, BW-420), not a second language. Queries are split between exact codes/commands and loose paraphrases ('box', 'too hot', 'updated the software'), so identifiers must survive preprocessing verbatim."
},
"chunking": {
"strategy": "heading",
"size_tokens": 400,
"overlap_tokens": 40,
"contextual_prefix": "heading_path",
"why": "Heading is already the best fit here: 0% mid-sentence, no split code, and only 2 orphan headings where recursive leaves 8 (fixed cuts 47% of chunks mid-sentence). The problem is the 160-token cap. It splits whole sections like 'Scheduled backup jobs fail following a firmware upgrade' (C15/C16), the bwctl status section (C13/C14) and Link speed (C21/C22) that fit easily in 400 tokens, and it cuts the code table into four pieces where only the first has the header. At 400 every section except the ~550-token table stays whole. A 40-token overlap (10%) only applies where prose has to split, and the heading-path prefix gives the headerle
... (shortened here; the page shows it whole)
Retrieval: the same help center, on the plan's chunking
Input (with the plan handed over):
{
"task": "retrieval",
"sample": "==> errors.md <==\n# Bramblewyn NAS error and warning codes\n\nThis page lists the codes that BWOS shows on the front-panel display, in the web console under System > Event Log, and in the output of `bwctl`. Codes that begin with E are errors that stop the affected service until they are resolved. Codes that begin with W are warnings: the service keeps running, but the unit is telling you that something needs attention before it turns into an error. If a code is not listed here, write it down toget... (8767 characters in all)",
"queries": "what does E2210 mean\nhow do I clear E1043 after replacing the disk\nbwctl backup status --verbose\nmy nightly copies quit working after I updated the software\nthe box is way slower than it should be when moving files around\nwhich code means the NAS is getting too hot",
"facts": "(a JSON string; decoded: heading 400/40 over 9 chunks, 6 queries searched with BM25, 4 flags, lane_flags F2, F3, F4)",
"note": "Self-hosted is fine; we already run Postgres 16 for the product.",
"plan_handoff": "{\"verdict\":\"adjust\",\"chunking\":{\"strategy\":\"heading\",\"size_tokens\":400,\"overlap_tokens\":40,\"contextual_prefix\":\"heading_path\"},\"primary_model\":{\"name\":\"BAAI/bge-m3\",\"dimensions\":1024,\"max_input_tokens\":8192,\"multilingual\":true},\"preprocessing\":[\"Preserve identifiers verbatim: do not lowercase-fold or strip punctuation ..."
}
Reply:
{
"task": "retrieval",
"verdict": "hybrid",
"headline": "Hybrid in Postgres 16: tsvector keyword leg for codes and bwctl flags, pgvector bge-m3 dense leg for paraphrases, RRF k=60, bge-reranker-v2-m3 on the top 20.",
"design": {
"keyword_leg": {
"use": true,
"engine": "Postgres 16 full-text search (tsvector + GIN, ts_rank_cd)",
"fields": [
"text",
"heading_path"
],
"notes": "Queries 1-3 are carried by exact tokens (E2210, E1043, bwctl backup status --verbose) that the 'english' parser keeps as single lexemes (e2210, e1043, verbos). OR the query lexemes together instead of websearch_to_tsquery: a natural-language query ANDed would match nothing, and websearch treats a leading '-' in --verbose as negation. Weight heading_path as A so section titles like 'Clearing a code after you fix it' count. ts_rank_cd has no IDF, so treat it as a candidate generator and let fusion and the reranker order the results."
},
"dense_leg": {
"use": true,
"model": "BAAI/bge-m3 (1024 dims, dense output, the plan's primary model)",
"notes": "Store the heading_path-prefixed chunk text the plan embedded; embed queries with the same model and no prefix. Queries 4-6 are paraphrases ('nightly copies quit working after I updated the software', 'getting too hot', 'the box is way slower') whose key words (firmware upgrade, temperature, link speed) are not in the query, so only the dense leg can reach C6 and C3 for them. Use cosine distance (vector_cosine_ops) with an HNSW index."
},
"fusion": {
"method": "rrf",
"rrf_k": 60,
"weights": null,
"why": "
... (shortened here; the page shows it whole)
Retrieval: a fastener catalogue searched by part number
Input:
{
"task": "retrieval",
"sample": "==> hex-bolts.md <==\n# Kestrel Fixings catalogue: hex bolts and set screws\n\nAll prices are per 100 pieces, excluding VAT, in EUR. Part numbers follow the pattern type-thread-length-material, so HX-M8-40-A2 is a hex head fastener, M8 thread, 40 mm long, in A2 stainless. Fully threaded set screws are supplied to DIN 933 / ISO 4017; partially threaded bolts to DIN 931 / ISO 4014.\n\n### HX-M6-20-A2\nHex set screw, A2 stainless (304), M6 x 20 mm, fully threaded.\nStandard: DIN 933 / ISO 4017. Pack of 20... (4695 characters in all)",
"queries": "HX-M10-60-A4\nM8 x 40 A4 hex bolt DIN 933\nDIN 125 washer M10\nNT-M8-A2\ntightening torque for M12 8.8\nstainless nuts",
"facts": "(a JSON string; decoded: heading 150/0 over 14 chunks, 6 queries searched with BM25, 5 flags, lane_flags F1, F2, F3, F4, F5)",
"note": "Customers paste part numbers and specs from old invoices; exact matches matter more than nuance, and latency must stay low."
}
Reply:
{
"task": "retrieval",
"verdict": "keyword",
"headline": "Keyword-only BM25 with an exact part-number field: every query is carried by codes or catalogue words that already occur in the sample.",
"design": {
"keyword_leg": {
"use": true,
"engine": "OpenSearch BM25 (or Elasticsearch) with a keyword sub-field for part numbers",
"fields": [
"text",
"heading_path",
"part_numbers"
],
"notes": "Extract every ### heading (HX-..., NT-..., WS-...) into an exact-match part_numbers keyword field and boost it far above the text match so a pasted code ranks its own chunk first. Keep the standard tokenizer on text so HX-M10-60-A4 also splits into hx / m10 / 60 / a4 for partial pastes, and stem so 'nuts' matches 'nut'. Keep '8.8', 'DIN 933', 'M12' as tokens (the standard tokenizer does)."
},
"dense_leg": {
"use": false,
"model": null,
"notes": "Not used: all six queries are part numbers, sizes, standards or catalogue words present in the sample, and a dense model ranks near-miss codes (HX-M10-60-A2 vs -A4, NT-M8-A2 vs NT-M8-A4) as equally close. Skipping it also keeps latency low, as the note asks. Revisit only if real logs show paraphrased queries whose words are not in the catalogue."
},
"fusion": {
"method": "none",
"rrf_k": null,
"weights": null,
"why": "Single keyword leg, nothing to fuse; the exact part_numbers match and the BM25 text match are combined inside one bool query."
},
"candidates_per_leg": 20,
"final_k": 5,
"rerank": {
"use": false,
"model": null,
"top_n": null,
"why": "BM25 already puts th
... (shortened here; the page shows it whole)
The page re-checks the reply; the API does not
The page holds every reply to account (recon.js): a plan's verdict against the settings it changes, the primary model's input limit against the chunk size and its languages against the sample, every flag answered once, every chunk id real, and the plan re-run on the sample; a retrieval design's verdict against its legs, fusion and candidate numbers, one verbatim route per query, and every keyword_finds_it against the page's BM25. Over the API you get the model's reply as it is; run the same checks yourself with Recon.reconcile in recon.js, or paste the same sample into the page.
Step 1 — a tiny client helper
One function that sends JSON and unwraps the envelope.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here" # from https://chunk-desk.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://chunk-desk.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"]
// chunk-desk.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://chunk-desk.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://chunk-desk.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
}
// ChunkDesk.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 ChunkDesk {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://chunk-desk.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://chunk-desk.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://chunk-desk.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://chunk-desk.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":"chunk-desk"}' | 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": "chunk-desk"})["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: "chunk-desk" })).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": "chunk-desk"}, 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", "chunk-desk"), 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: "chunk-desk" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "chunk-desk"])["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 = "chunk-desk" }))!["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 (not wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model reads gpt-5.6-terra and model_alias gpt-terra.
# input.json: the run input object - {"task": "plan", "sample": "...", "queries": "...", "facts": "..."}
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": "plan", "sample": "...", "queries": "...", "facts": "...", ...}
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, sample, queries, facts, ...}
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 chunk-desk:<lane>:<inputHash>:a<attempt>. The job's output.output is the reply text; truncated: true means the balance cut it short.
KEY="chunk-desk:plan:$(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 lane's contract
echo "$J" | jq '.data | {charged_credits, truncated}'
import hashlib, time
key = "chunk-desk:" + 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 lane's JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("verdict"))
import { createHash } from "node:crypto";
const key = `chunk-desk:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.verdict);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "chunk-desk:" + 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 = "chunk-desk:" + 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 = "chunk-desk:#{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["verdict"]].inspect
$key = "chunk-desk:" . $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["verdict"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"chunk-desk:{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 (retrieval)
The same run as Server-Sent Events. retrieval.json is a retrieval input, with plan_handoff when it follows a plan.
# 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: chunk-desk:retrieval:$(shasum -a 256 retrieval.json | cut -c1-16):a1" --data @retrieval.json
rev = json.load(open("retrieval.json")) # {"task": "retrieval", "sample": "...", "queries": "...", "facts": "...", "plan_handoff": "..."}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "chunk-desk:retrieval:" + 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("retrieval.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": `chunk-desk:retrieval:${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("retrieval.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", "chunk-desk:retrieval:"+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("retrieval.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", "chunk-desk:retrieval:" + 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("retrieval.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "chunk-desk:retrieval:#{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("retrieval.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: chunk-desk:retrieval:" . 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("retrieval.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", $"chunk-desk:retrieval:{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);