Explain code from your own scripts
Send code in any language — one file, a function, several files with file-name comment headers, or a grab-bag of snippets — say who the explanation is for, and get back one JSON object: a reading-level verdict, an overview, a walkthrough ordered by execution flow, an ASCII diagram drawn with your own names, the concepts a reader must know, the pitfalls hiding in the paste, three comprehension questions, and the ordered next steps. Everything this app does goes through the SkillSafe App API — plain JSON over HTTPS — so you can wire the explanation into an onboarding doc generator, a pull-request summary bot, or a script that walks a new hire through the modules they are about to own. Every code step below is shown in cURL, Python, JavaScript, Go, Java, Ruby, PHP and C#; pick a language once and the whole page follows.
Basics
Base URL: https://api.skillsafe.ai/v1/app-api, app slug
code-tutor. Every request sends
Authorization: Bearer <token> and JSON bodies with
Content-Type: application/json. Responses are wrapped in an envelope:
{"data": …} on success, {"error": {"code", "message"}} on failure.
The explanation itself is produced by the gpt-terra model. Estimates are
free; runs are metered against your credit balance. There is a single run task — one
paste in, one explanation out, no follow-up calls and no session state to carry.
| Status | Meaning |
|---|---|
401 | Missing or expired token — create a new session. |
402 | Not enough credits — top up at skillsafe.ai/account/credits. |
403 | The token isn't allowed to do this (e.g. a guest explaining a very large paste). |
404 | Unknown job or record id. |
5xx | Transient platform error — retry with backoff. |
Browsers enforce CORS for this API, so run these examples from a server, script or terminal — not from another website's frontend.
Step 0 — A tiny client
Every task below is a single HTTP call, so start with a short helper that adds the auth
header, sends JSON and unwraps the data envelope. The later steps reuse it.
export API="https://api.skillsafe.ai/v1/app-api"
export TOKEN="YOUR_TOKEN" # see step 1
# every call looks like:
# curl -s "$API/..." -H "Authorization: Bearer $TOKEN" [-d '{json}']
# jq is used below to pull fields out of the {"data": ...} envelope
import json, requests
API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # see step 1 — read it from your shell environment in real code
def api(method, path, body=None, **headers):
res = requests.request(method, API + path, json=body,
headers={"Authorization": f"Bearer {TOKEN}", **headers})
payload = res.json()
if not res.ok:
raise RuntimeError(payload.get("error", {}).get("message", res.reason))
return payload["data"]
// Node 18+ (built-in fetch)
const API = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN"; // see step 1 — read it from your shell environment in real code
async function api(method, path, body, extraHeaders = {}) {
const res = await fetch(API + path, {
method,
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json", ...extraHeaders },
body: body === undefined ? undefined : JSON.stringify(body),
});
const json = await res.json();
if (!res.ok) throw new Error(json.error?.message ?? res.statusText);
return json.data;
}
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
const API = "https://api.skillsafe.ai/v1/app-api"
var token = os.Getenv("SKILLSAFE_TOKEN") // see step 1
func call(method, path string, body, out any) error {
var buf bytes.Buffer
if body != nil {
json.NewEncoder(&buf).Encode(body)
}
req, _ := http.NewRequest(method, API+path, &buf)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
var env struct {
Data json.RawMessage `json:"data"`
Error *struct{ Message string `json:"message"` } `json:"error"`
}
json.NewDecoder(res.Body).Decode(&env)
if res.StatusCode >= 400 {
return fmt.Errorf("api %s %s: %s", method, path, env.Error.Message)
}
if out == nil {
return nil
}
return json.Unmarshal(env.Data, out)
}
// Java 17+, no dependencies. Pair with your JSON library (Jackson, Gson…)
// to read fields out of the returned envelope.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class SkillSafe {
static final String API = "https://api.skillsafe.ai/v1/app-api";
static final String TOKEN = System.getenv("SKILLSAFE_TOKEN"); // see step 1
static final HttpClient HTTP = HttpClient.newHttpClient();
static String api(String method, String path, String jsonBody) throws Exception {
var req = HttpRequest.newBuilder(URI.create(API + path))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.method(method, jsonBody == null
? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
var res = HTTP.send(req, HttpResponse.BodyHandlers.ofString());
if (res.statusCode() >= 400) throw new RuntimeException(res.body());
return res.body(); // envelope: {"data": …}
}
}
require "net/http"
require "json"
API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = ENV.fetch("SKILLSAFE_TOKEN") # see step 1
def api(method, path, body = nil)
uri = URI(API + path)
req = Net::HTTP.const_get(method.capitalize).new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req.body = body.to_json if body
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
payload = JSON.parse(res.body)
raise (payload.dig("error", "message") || res.message) unless res.is_a?(Net::HTTPSuccess)
payload["data"]
end
<?php
const API = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = getenv("SKILLSAFE_TOKEN"); // see step 1
function api(string $method, string $path, ?array $body = null): mixed {
global $TOKEN;
$ch = curl_init(API . $path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer $TOKEN",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => $body === null ? null : json_encode($body),
]);
$payload = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) {
throw new Exception($payload["error"]["message"] ?? "HTTP $status");
}
return $payload["data"];
}
// .NET 8+
using System.Net.Http.Json;
using System.Text.Json;
static class SkillSafe
{
const string Api = "https://api.skillsafe.ai/v1/app-api";
static readonly HttpClient Http = new();
static SkillSafe() =>
Http.DefaultRequestHeaders.Authorization =
new("Bearer", Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN")); // see step 1
public static async Task<JsonElement> ApiAsync(HttpMethod method, string path, object? body = null)
{
var req = new HttpRequestMessage(method, Api + path);
if (body != null) req.Content = JsonContent.Create(body);
var res = await Http.SendAsync(req);
var json = await res.Content.ReadFromJsonAsync<JsonElement>();
if (!res.IsSuccessStatusCode)
throw new Exception(json.GetProperty("error").GetProperty("message").GetString());
return json.GetProperty("data");
}
}
Step 1 — Get a token
A guest token lets you check balances and estimate costs for free. For metered explanation runs
billed to your own account, use your personal token: open the
token page, sign in with SkillSafe, and press
Copy shell export — it puts export SKILLSAFE_TOKEN="…" on your
clipboard, which every example below reads. Treat the token like a password: it can spend
your credits. For fully headless scripts, POST /guest mints a guest token with
no browser involved.
curl -s -X POST "$API/guest" \
-H "Content-Type: application/json" \
-d '{"slug":"code-tutor"}' | jq -r '.data.token'
token = api("POST", "/guest", {"slug": "code-tutor"})["token"]
const { token } = await api("POST", "/guest", { slug: "code-tutor" });
var guest struct{ Token string `json:"token"` }
err := call("POST", "/guest", map[string]string{"slug": "code-tutor"}, &guest)
String envelope = api("POST", "/guest", """
{"slug":"code-tutor"}""");
// token is at data.token in the returned JSON
token = api("POST", "/guest", { slug: "code-tutor" })["token"]
$token = api("POST", "/guest", ["slug" => "code-tutor"])["token"];
var guest = await SkillSafe.ApiAsync(HttpMethod.Post, "/guest",
new { slug = "code-tutor" });
var token = guest.GetProperty("token").GetString();
The app stores this browser's token under the localStorage key
skillsafe_app_token:code-tutor, on the app's own origin. The
token page reads and manages it for you — you never need
to open developer tools.
Step 2 — Check who you are and your balance
Returns subject_type ("user" or "guest"),
subject_id and your credits balance. Check this before explaining
a large paste.
curl -s "$API/me" -H "Authorization: Bearer $TOKEN" | jq '.data'
me = api("GET", "/me")
print(me["subject_type"], me["credits"])
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
var me struct {
SubjectType string `json:"subject_type"`
Credits int64 `json:"credits"`
}
err := call("GET", "/me", nil, &me)
String envelope = api("GET", "/me", null);
// data.subject_type, data.credits
me = api("GET", "/me")
puts "#{me["subject_type"]}: #{me["credits"]} credits"
$me = api("GET", "/me");
echo "{$me['subject_type']}: {$me['credits']} credits\n";
var me = await SkillSafe.ApiAsync(HttpMethod.Get, "/me");
Console.WriteLine($"{me.GetProperty("subject_type")}: {me.GetProperty("credits")} credits");
Step 3 — Estimate the cost
Send exactly the input you would send to /run; the response's
hold_credits is the worst-case cost. Nothing is charged and no job is created,
so estimating is free — useful when you are feeding in a whole module or a directory
of source files and want a ceiling before spending credits.
| Input field | Type | Notes |
|---|---|---|
code | string, required | The source to explain, up to 100000 characters, in any programming language: one file, a function, or several files concatenated with file-name comment headers such as // src/main.js or # utils.py. Very long pastes may be clipped middle-out, with a [... clipped ...] marker showing where. |
audience | string | new-dev | experienced-dev | non-programmer | unknown — who the explanation is for. Same facts, different depth: new-dev defines jargon on first use, spells out idioms and keeps every step small; experienced-dev skips syntax basics and spends the words on architecture, invariants, tricky control flow and why the code is written this way; non-programmer drops jargon entirely, explains by analogy and focuses on what the code accomplishes and where its behaviour could surprise. On unknown the explanation assumes new-dev and says so in the verdict. |
focus | string, optional | What the reader specifically wants explained or finds confusing, up to 20000 characters ("why the retry loop", "the regex on line 40"). When present, the walkthrough visits it explicitly and at least one quiz question tests it. |
prescan_facts | object, optional | What the app's free client-side prescan mechanically detected in the code: {"items": [], "hotspots": [], "signals": {}}. items and hotspots hold {id, label, lines} entries — the declarations found (i:fn:load_config, i:class:Report) and the complexity signals matched (h:deep-nesting, h:long-function, h:empty-catch, h:todo, h:long-line), each with the line numbers it was seen on. signals is a counter object: {"lines": 0, "functions": 0, "classes": 0, "imports": 0, "max_nesting": 0, "todos": 0, "language_guess": "unknown"}. Every id you send comes back in coverage_check. The web UI fills this from its own scan; API callers may omit the field or send {"items": [], "hotspots": [], "signals": {}}. |
retry_note | string, optional | Only set by the app's automatic reformat retry when a first reply was not valid JSON. Leave it out. |
cat > config.py <<'PY'
def load_config(path):
with open(path) as fh:
return fh.read().strip()
PY
jq -n --rawfile c config.py \
'{code: $c, audience: "new-dev", focus: "",
prescan_facts: {items: [], hotspots: [], signals: {}}}' > input.json
curl -s -X POST "$API/estimate" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d @input.json | jq '.data.hold_credits'
CODE = """def load_config(path):
with open(path) as fh:
return fh.read().strip()"""
payload = {
"code": CODE,
"audience": "new-dev",
"focus": "",
"prescan_facts": {"items": [], "hotspots": [], "signals": {}},
}
est = api("POST", "/estimate", payload)
print("worst case:", est.get("hold_credits", est.get("credits")), "credits")
const code = [
"def load_config(path):",
" with open(path) as fh:",
" return fh.read().strip()",
].join("\n");
const payload = {
code,
audience: "new-dev",
focus: "",
prescan_facts: { items: [], hotspots: [], signals: {} },
};
const est = await api("POST", "/estimate", payload);
console.log("worst case:", est.hold_credits ?? est.credits, "credits");
const code = `def load_config(path):
with open(path) as fh:
return fh.read().strip()`
payload := map[string]any{
"code": code,
"audience": "new-dev",
"focus": "",
"prescan_facts": map[string]any{
"items": []any{}, "hotspots": []any{}, "signals": map[string]any{},
},
}
var est struct{ HoldCredits int64 `json:"hold_credits"` }
err := call("POST", "/estimate", payload, &est)
String code = """
def load_config(path):
with open(path) as fh:
return fh.read().strip()""";
String jsonPayload = """
{"code": %s, "audience": "new-dev",
"focus": "",
"prescan_facts": {"items": [], "hotspots": [], "signals": {}}}
""".formatted(toJsonString(code));
String envelope = api("POST", "/estimate", jsonPayload);
// worst-case cost is at data.hold_credits
CODE_TEXT = <<~'PY'
def load_config(path):
with open(path) as fh:
return fh.read().strip()
PY
payload = { code: CODE_TEXT, audience: "new-dev",
focus: "",
prescan_facts: { items: [], hotspots: [], signals: {} } }
est = api("POST", "/estimate", payload)
puts "worst case: #{est["hold_credits"] || est["credits"]} credits"
$code = <<<'PY'
def load_config(path):
with open(path) as fh:
return fh.read().strip()
PY;
$payload = [
"code" => $code,
"audience" => "new-dev",
"focus" => "",
"prescan_facts" => ["items" => [], "hotspots" => [], "signals" => new stdClass()],
];
$est = api("POST", "/estimate", $payload);
echo "worst case: " . ($est["hold_credits"] ?? $est["credits"]) . " credits\n";
var code = """
def load_config(path):
with open(path) as fh:
return fh.read().strip()
""";
var payload = new {
code,
audience = "new-dev",
focus = "",
prescan_facts = new {
items = Array.Empty<object>(), hotspots = Array.Empty<object>(),
signals = new { },
},
};
var est = await SkillSafe.ApiAsync(HttpMethod.Post, "/estimate", payload);
Console.WriteLine($"worst case: {est.GetProperty("hold_credits")} credits");
prescan_facts is how you make the explanation answer for things you already
know about. Send {"items": [{"id": "i:fn:load_config", "label": "function
load_config", "lines": [1]}], "hotspots": [{"id": "h:empty-catch", "label": "empty catch or
except block", "lines": [7]}], "signals": {"lines": 3, "functions": 1, "classes": 0,
"imports": 0, "max_nesting": 2, "todos": 0, "language_guess": "python"}}
and every one of those ids comes back in coverage_check — addressed, or
set aside with the reason (a pattern hit can be a false positive: a "long function" that is
a flat data table is fine, and the explanation says so). Nothing you flag is silently
dropped.
Step 4 — Run the explanation and wait for the result
/run takes the same input as /estimate, places a credit hold and
returns a job_id. Poll /jobs/{job_id} every 1–2 seconds
until status is succeeded or failed (a run typically
takes 20–60 s). Always send an Idempotency-Key header so a network
retry can't start a second, double-charged run. The explanation is in output
— usually nested as output.output, and as a JSON string, so
parse defensively. The samples below print the name, reading level and verdict, then the
numbered walkthrough and the pitfalls, and write the ASCII diagram out to
flow.txt.
JOB_ID=$(curl -s -X POST "$API/run" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: explain-$(date +%s)" \
-d @input.json | jq -r '.data.job_id')
while :; do
JOB=$(curl -s "$API/jobs/$JOB_ID" -H "Authorization: Bearer $TOKEN")
STATUS=$(echo "$JOB" | jq -r '.data.status')
[ "$STATUS" = "succeeded" ] || [ "$STATUS" = "failed" ] && break
sleep 2
done
# unwrap the explanation once, then read it
echo "$JOB" | jq -r '.data.output.output' > explanation.json
jq -r '
"\(.explanation_name) [\(.reading_level)]: \(.verdict)",
"",
"WALKTHROUGH",
(.walkthrough[] | " \(.step). \(.title) - \(.code_ref)"),
"",
"PITFALLS",
(.pitfalls[] | " (\(.severity)) \(.title)"),
"",
"QUIZ",
(.quiz[] | " Q: \(.question)\n A: \(.answer)")' explanation.json
# and keep the ASCII flow diagram next to your notes
jq -r '.diagram' explanation.json > flow.txt
import time
job_id = api("POST", "/run", payload,
**{"Idempotency-Key": "explain-001"})["job_id"]
while True:
job = api("GET", f"/jobs/{job_id}")
if job["status"] in ("succeeded", "failed"):
break
time.sleep(1.5)
if job["status"] == "failed":
raise RuntimeError(job.get("error", "run failed"))
raw = job["output"]
if isinstance(raw, dict) and "output" in raw:
raw = raw["output"]
explanation = json.loads(raw) if isinstance(raw, str) else raw
print(f'{explanation["explanation_name"]} '
f'[{explanation["reading_level"]}]: {explanation["verdict"]}')
for step in explanation["walkthrough"]:
print(f' {step["step"]}. {step["title"]} ({step["code_ref"]})')
print(f' {step["detail"]}')
for concept in explanation["concepts"]:
print(f' * {concept["term"]}: {concept["explanation"]}')
for pitfall in explanation["pitfalls"]:
print(f' ({pitfall["severity"]}) {pitfall["title"]}')
for q in explanation["quiz"]:
print(f' Q: {q["question"]}\n A: {q["answer"]}')
for c in explanation["coverage_check"]:
print(f' {c["id"]}: {"ok" if c["addressed"] else "SET ASIDE"} - {c["note"]}')
with open("flow.txt", "w", encoding="utf-8") as fh:
fh.write(explanation["diagram"])
import { writeFileSync } from "node:fs";
const { job_id } = await api("POST", "/run", payload,
{ "Idempotency-Key": crypto.randomUUID() });
let job;
do {
await new Promise((r) => setTimeout(r, 1500));
job = await api("GET", `/jobs/${job_id}`);
} while (job.status !== "succeeded" && job.status !== "failed");
if (job.status === "failed") throw new Error(job.error ?? "run failed");
const raw = job.output?.output ?? job.output;
const explanation = typeof raw === "string" ? JSON.parse(raw) : raw;
console.log(`${explanation.explanation_name} [${explanation.reading_level}]: ${explanation.verdict}`);
for (const step of explanation.walkthrough) {
console.log(` ${step.step}. ${step.title} (${step.code_ref})`);
console.log(` ${step.detail}`);
}
for (const c of explanation.concepts) console.log(` * ${c.term}: ${c.explanation}`);
for (const p of explanation.pitfalls) console.log(` (${p.severity}) ${p.title}`);
for (const q of explanation.quiz) console.log(` Q: ${q.question}\n A: ${q.answer}`);
for (const c of explanation.coverage_check) {
console.log(` ${c.id}: ${c.addressed ? "ok" : "SET ASIDE"} - ${c.note}`);
}
writeFileSync("flow.txt", explanation.diagram);
var started struct{ JobID string `json:"job_id"` }
if err := call("POST", "/run", payload, &started); err != nil {
log.Fatal(err)
}
var job struct {
Status string `json:"status"`
Error string `json:"error"`
Output json.RawMessage `json:"output"`
}
for {
if err := call("GET", "/jobs/"+started.JobID, nil, &job); err != nil {
log.Fatal(err)
}
if job.Status == "succeeded" || job.Status == "failed" {
break
}
time.Sleep(1500 * time.Millisecond)
}
// job.Output is {"output": "<json string>"} — unwrap, unquote, then unmarshal:
type Explanation struct {
Name string `json:"explanation_name"`
ReadingLevel string `json:"reading_level"`
Verdict string `json:"verdict"`
Overview string `json:"overview"`
Walkthrough []struct {
Step int `json:"step"`
Title string `json:"title"`
Detail string `json:"detail"`
CodeRef string `json:"code_ref"`
} `json:"walkthrough"`
Diagram string `json:"diagram"`
Concepts []struct {
Term, Explanation string
} `json:"concepts"`
Pitfalls []struct {
Severity, Title, Detail string
CodeRef string `json:"code_ref"`
} `json:"pitfalls"`
Quiz []struct {
Question, Answer string
} `json:"quiz"`
NextSteps []string `json:"next_steps"`
Summary string `json:"summary"`
}
var wrapper struct{ Output string `json:"output"` }
json.Unmarshal(job.Output, &wrapper)
var explanation Explanation
json.Unmarshal([]byte(wrapper.Output), &explanation)
fmt.Printf("%s [%s]: %s\n", explanation.Name, explanation.ReadingLevel, explanation.Verdict)
for _, s := range explanation.Walkthrough {
fmt.Printf(" %d. %s (%s)\n", s.Step, s.Title, s.CodeRef)
}
for _, p := range explanation.Pitfalls {
fmt.Printf(" (%s) %s\n", p.Severity, p.Title)
}
for _, q := range explanation.Quiz {
fmt.Printf(" Q: %s\n A: %s\n", q.Question, q.Answer)
}
os.WriteFile("flow.txt", []byte(explanation.Diagram), 0o644)
String envelope = api("POST", "/run", jsonPayload);
String jobId = /* data.job_id via your JSON library */;
while (true) {
String job = api("GET", "/jobs/" + jobId, null);
String status = /* data.status */;
if (status.equals("succeeded") || status.equals("failed")) break;
Thread.sleep(1500);
}
// The explanation is at data.output.output as a JSON string — parse it again, then read
// explanation_name, reading_level, verdict, overview, walkthrough[] (step/title/detail/code_ref),
// diagram, concepts[] (term/explanation), pitfalls[] (severity/title/code_ref/detail),
// coverage_check[] (id/addressed/note), quiz[] (question/answer), next_steps[] and summary.
// Finally keep the ASCII diagram next to your notes:
// Files.writeString(Path.of("flow.txt"), diagram);
started = api("POST", "/run", payload)
job = nil
loop do
job = api("GET", "/jobs/#{started["job_id"]}")
break if %w[succeeded failed].include?(job["status"])
sleep 1.5
end
raise (job["error"] || "run failed") if job["status"] == "failed"
raw = job["output"].is_a?(Hash) ? job["output"].fetch("output", job["output"]) : job["output"]
explanation = raw.is_a?(String) ? JSON.parse(raw) : raw
puts "#{explanation["explanation_name"]} [#{explanation["reading_level"]}]: #{explanation["verdict"]}"
explanation["walkthrough"].each do |s|
puts " #{s["step"]}. #{s["title"]} (#{s["code_ref"]})"
puts " #{s["detail"]}"
end
explanation["concepts"].each { |c| puts " * #{c["term"]}: #{c["explanation"]}" }
explanation["pitfalls"].each { |p| puts " (#{p["severity"]}) #{p["title"]}" }
explanation["quiz"].each { |q| puts " Q: #{q["question"]}\n A: #{q["answer"]}" }
explanation["coverage_check"].each { |c| puts " #{c["id"]}: #{c["addressed"] ? "ok" : "SET ASIDE"}" }
File.write("flow.txt", explanation["diagram"])
$started = api("POST", "/run", $payload);
do {
sleep(2);
$job = api("GET", "/jobs/" . $started["job_id"]);
} while (!in_array($job["status"], ["succeeded", "failed"]));
if ($job["status"] === "failed") {
throw new Exception($job["error"] ?? "run failed");
}
$raw = is_array($job["output"]) ? ($job["output"]["output"] ?? $job["output"]) : $job["output"];
$explanation = is_string($raw) ? json_decode($raw, true) : $raw;
echo "{$explanation['explanation_name']} [{$explanation['reading_level']}]: {$explanation['verdict']}\n";
foreach ($explanation["walkthrough"] as $s) {
echo " {$s['step']}. {$s['title']} ({$s['code_ref']})\n";
echo " {$s['detail']}\n";
}
foreach ($explanation["concepts"] as $c) {
echo " * {$c['term']}: {$c['explanation']}\n";
}
foreach ($explanation["pitfalls"] as $p) {
echo " ({$p['severity']}) {$p['title']}\n";
}
foreach ($explanation["quiz"] as $q) {
echo " Q: {$q['question']}\n A: {$q['answer']}\n";
}
foreach ($explanation["coverage_check"] as $c) {
echo " {$c['id']}: " . ($c["addressed"] ? "ok" : "SET ASIDE") . "\n";
}
file_put_contents("flow.txt", $explanation["diagram"]);
var started = await SkillSafe.ApiAsync(HttpMethod.Post, "/run", payload);
var jobId = started.GetProperty("job_id").GetString();
JsonElement job;
while (true)
{
job = await SkillSafe.ApiAsync(HttpMethod.Get, $"/jobs/{jobId}");
var status = job.GetProperty("status").GetString();
if (status is "succeeded" or "failed") break;
await Task.Delay(1500);
}
var rawText = job.GetProperty("output").GetProperty("output").GetString();
using var doc = JsonDocument.Parse(rawText!);
var explanation = doc.RootElement;
Console.WriteLine($"{explanation.GetProperty("explanation_name")} " +
$"[{explanation.GetProperty("reading_level")}]: {explanation.GetProperty("verdict")}");
foreach (var s in explanation.GetProperty("walkthrough").EnumerateArray())
{
Console.WriteLine($" {s.GetProperty("step")}. {s.GetProperty("title")} " +
$"({s.GetProperty("code_ref")})");
}
foreach (var p in explanation.GetProperty("pitfalls").EnumerateArray())
{
Console.WriteLine($" ({p.GetProperty("severity")}) {p.GetProperty("title")}");
}
foreach (var q in explanation.GetProperty("quiz").EnumerateArray())
{
Console.WriteLine($" Q: {q.GetProperty("question")}");
Console.WriteLine($" A: {q.GetProperty("answer")}");
}
await File.WriteAllTextAsync("flow.txt",
explanation.GetProperty("diagram").GetString()!);
The model is asked for one JSON object and nothing else, but a stray code fence or preamble
is always possible. Strip a leading ```json fence, take the text between the
first { and the last }, and only then parse — that is what
the app does before it falls back to a retry_note reformat run.
The explanation object — output schema
One JSON object, always the same shape. Every array is present;
walkthrough is never empty, quiz carries exactly three questions
for real code, and pitfalls is empty only when genuinely nothing material is
visible — the app renders "None" for that rather than inventing a risk. If the paste
was too thin to explain responsibly (a two-line snippet), you still get this object: what is
there gets explained, the verdict says the paste is thin, and what you would
need to show lands in next_steps. If the paste is not code at all —
prose, a config dump, random text — you still get the object: the verdict says what it
is, reading_level is approachable, the walkthrough is a single
step saying what to paste instead, concepts, pitfalls and
quiz are empty arrays, and next_steps says "paste source code".
A paste spanning several files keeps its file-name comment headers, and the walkthrough
crosses them in execution order rather than file order.
| Field | Type | Meaning |
|---|---|---|
explanation_name | string | A short name taken from the code's own domain naming — its module, type or file names. Example: Rate limiter with sliding window. The app falls back to Untitled explanation if it is missing. |
reading_level | string | approachable (a careful read of the code suffices for this audience), moderate (the walkthrough is needed) or dense (the walkthrough, the concepts and prior exposure to the ideas involved are all needed). Scored for the audience you sent, not in the abstract. Anything else normalizes to moderate. |
verdict | string | One or two sentences: what this code does, plus the one thing to understand first. On audience: "unknown" it also names the audience that was assumed; if the prescan's language_guess was wrong, it says what the language actually is. |
overview | string | One or two paragraphs: the purpose, the shape of the solution, and how the pieces relate. |
walkthrough | array, non-empty | {step, title, detail, code_ref} — the flow, ordered by execution (entry point first), not by file order. Typically 4–10 steps; each step is one coherent unit of behaviour ("parse the header, bail on malformed input") rather than a line-by-line paraphrase. step is a 1-based integer, detail quotes the code it describes, and code_ref names the function or lines it covers (parseHeader, lines 12-31). If you sent a focus, one of these steps visits it explicitly. An empty walkthrough is treated as a malformed reply and triggers the app's reformat retry. |
diagram | string | A plain-ASCII flow diagram of the paste — call graph, pipeline, state machine or data flow, whichever fits — 4–20 lines, arrows like -->, using the paste's own names. No Unicode box-drawing required, so it survives a terminal, a code comment or a plain-text onboarding doc. |
concepts | array | {term, explanation} — 2–8 language idioms, algorithms, patterns or domain terms a reader at this audience level must know, each explained in 1–3 sentences as used in this paste, not as a dictionary definition. For non-programmer these arrive as analogies (a mutex is "a talking stick"). |
pitfalls | array | {severity, title, code_ref, detail}. severity is high (a reader who misses this will break something or misread what the code does) | medium (it works but will surprise or degrade) | low (a sharp edge worth knowing); anything else normalizes to medium. These are real risks visible in the paste — edge cases that misbehave, error paths that swallow failures, off-by-one hazards, surprising defaults, race windows, silent type coercions. Speculative risks about code that is not in the paste belong in next_steps instead, so an empty array is an honest answer. code_ref names the function and the line or line range the pitfall concerns, in the same form walkthrough uses (retryFetch, lines 44-58 or line 47), counted against the code you sent, marker lines included; it is omitted only when no single line is more responsible than another. |
coverage_check | array | {id, addressed, note} — one entry per prescan_facts item or hotspot you sent (i:fn:load_config, h:long-function, …), saying which walkthrough step or pitfall covers it, or why it was set aside (a pattern hit can be a false positive — a "long function" that is a flat data table is fine; the note says so). Nothing you flagged is silently dropped. |
quiz | array of 3 | {question, answer} — exactly three comprehension questions about this paste, each answerable purely from the code and the explanation, with a 1–3 sentence answer. At least one targets the trickiest part, or the focus if you sent one. Empty only for the not-code case. The app renders each behind a click so the reader answers before reading. |
next_steps | string[] | Ordered and concrete: read X next, trace input Y through the flow, ask the team about Z. |
summary | string | 3–5 sentences a reader could paste straight into onboarding notes. |
A small, realistic result for the config.py paste above, trimmed for length:
{
"explanation_name": "config.py - text file loader",
"reading_level": "approachable",
"verdict": "One function that reads a text file and hands back its contents with the
surrounding whitespace removed; the thing to understand first is the 'with'
block, which is what guarantees the file gets closed.",
"overview": "A single free function, 'load_config', takes a filesystem path, opens that
file for reading, reads all of it into memory as one string, and returns
that string with leading and trailing whitespace stripped. There is no
parsing here at all - the caller receives raw text and decides what it
means. The whole body is three lines, and its only moving part is the
context manager that owns the open file handle.",
"walkthrough": [
{ "step": 1, "title": "Take the path and open the file",
"detail": "'load_config(path)' accepts a single argument, the path to read, and
passes it straight to the built-in 'open(path)'. No mode is given, so
Python defaults to text mode for reading, decoding bytes with the
platform default encoding.",
"code_ref": "load_config, lines 1-2" },
{ "step": 2, "title": "Let the with block own the handle",
"detail": "'with open(path) as fh:' binds the open file to 'fh' for the length of
the block. When the block exits - normally or by exception - Python
closes the handle for you, so no file is left dangling.",
"code_ref": "load_config, line 2" },
{ "step": 3, "title": "Read everything, then trim and return",
"detail": "'fh.read()' pulls the entire file into one string, and '.strip()'
removes whitespace at both ends, which is what kills the trailing
newline nearly every text file carries. That trimmed string is the
return value.",
"code_ref": "load_config, line 3" }
],
"diagram": "caller\n |\n v\nload_config(path)\n |\n +--> open(path) --> fh
| |\n | v\n | fh.read()
| |\n v v\nreturn <---------- .strip()",
"concepts": [
{ "term": "Context manager (the with statement)",
"explanation": "'with open(path) as fh:' borrows the file for the duration of the
block and closes it on the way out, even if the body raises. It is
the standard Python way to make cleanup impossible to forget." },
{ "term": "Reading a whole file into memory",
"explanation": "'fh.read()' with no argument returns the entire contents as one
string. That is fine for a config file and wrong for a multi-gigabyte
log, where you would iterate over lines instead." }
],
"pitfalls": [
{ "severity": "medium",
"title": "A missing or unreadable path raises out of load_config",
"code_ref": "load_config, line 2",
"detail": "'open(path)' raises 'FileNotFoundError' or 'PermissionError' and nothing
here catches it, so the caller sees the exception. That may be exactly
what you want - but the caller has to know it." },
{ "severity": "low",
"title": "The encoding is whatever the platform defaults to",
"code_ref": "load_config, line 2",
"detail": "'open(path)' is called without 'encoding=', so the same file can decode
differently on two machines. Passing 'encoding=\"utf-8\"' removes the
surprise." }
],
"coverage_check": [
{ "id": "i:fn:load_config", "addressed": true,
"note": "The function under explanation; covered by all three walkthrough steps." }
],
"quiz": [
{ "question": "If the file is missing, what does load_config do?",
"answer": "It does not return - 'open(path)' raises FileNotFoundError and that
exception propagates to whoever called load_config." },
{ "question": "Why is the trailing newline of the file not in the returned string?",
"answer": "Because '.strip()' removes whitespace from both ends of the text, and a
trailing newline is whitespace." },
{ "question": "What closes the file, and when?",
"answer": "The 'with' block does, as soon as control leaves it - on the return, or
on an exception." }
],
"next_steps": [
"Find a caller of 'load_config' and see what it does with the raw string it gets back.",
"Decide whether the caller or this function should own the FileNotFoundError case.",
"Ask the team whether config files here are always UTF-8, and pin the encoding if so."
],
"summary": "'load_config' reads a whole text file and returns it stripped. …"
}
The explanation is a teaching aid, not an audit: it is grounded in the paste and never invents functions or behaviour the code does not show, but it only sees what you sent. If the code calls something defined elsewhere, the explanation says so and reasons only from the call site. Read it beside the code, and take the pitfalls as things to verify rather than as findings that have been proven.
Step 5 — Stream the explanation as it is written
/run-stream takes exactly the same body as /run but answers with
server-sent events, so you can show progress instead of a spinner — useful here
because the walkthrough and the diagram make for a long reply. This app's own progress panel
is this endpoint: it watches for the top-level JSON keys arriving in the delta stream and
lights up a step as each one appears. Events are separated by a blank line; each has an
event: line and a data: line carrying JSON.
| Event | Payload | Meaning |
|---|---|---|
job | {job_id, status} | Sent once, when the job is accepted — show "starting". |
delta | {text} | A chunk of the reply, in order. Append it; the accumulated length is your only progress signal (the total is not known in advance). |
done | {job_id, status, charged_credits, output} | The final, authoritative result — read the explanation from output.output rather than trusting concatenated deltas, and the settled price from charged_credits. |
error | {code, message} | Replaces done when the run fails. |
# -N disables buffering so events print as they arrive
curl -N -s -X POST "$API/run-stream" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: explain-$(date +%s)" \
-d @input.json
# event: job
# data: {"job_id":"job_...","status":"running"}
#
# event: delta
# data: {"text":"{\"explanation_name\":\"config.py"}
# ...
# event: done
# data: {"job_id":"job_...","status":"succeeded","charged_credits":418,"output":{"output":"{...}"}}
import json, requests
result = None
with requests.post(
API + "/run-stream",
headers={"Authorization": f"Bearer {TOKEN}",
"Idempotency-Key": "explain-001"},
json=payload,
stream=True,
) as r:
r.raise_for_status()
event = None
for line in r.iter_lines(decode_unicode=True):
if not line:
continue
if line.startswith("event:"):
event = line[len("event:"):].strip()
elif line.startswith("data:"):
data = json.loads(line[len("data:"):].strip())
if event == "delta":
print(".", end="", flush=True) # live progress
elif event == "done":
result = data
elif event == "error":
raise RuntimeError(data.get("message", "run failed"))
explanation = json.loads(result["output"]["output"]) # authoritative
print("charged:", result["charged_credits"], "-", explanation["explanation_name"])
for step in explanation["walkthrough"]:
print(f' {step["step"]}. {step["title"]}')
open("flow.txt", "w", encoding="utf-8").write(explanation["diagram"])
const res = await fetch(API + "/run-stream", {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify(payload),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buf = "", done = null;
for (;;) {
const chunk = await reader.read();
if (chunk.done) break;
buf += decoder.decode(chunk.value, { stream: true });
const frames = buf.split("\n\n");
buf = frames.pop();
for (const frame of frames) {
const name = /^event:\s*(.+)$/m.exec(frame)?.[1];
const body = /^data:\s*(.+)$/m.exec(frame)?.[1];
if (!name || !body) continue;
const data = JSON.parse(body);
if (name === "delta") process.stdout.write("."); // live progress
if (name === "done") done = data;
if (name === "error") throw new Error(data.message ?? "run failed");
}
}
const explanation = JSON.parse(done.output.output);
console.log(`\n${done.charged_credits} credits - ${explanation.explanation_name}`);
for (const step of explanation.walkthrough) console.log(` ${step.step}. ${step.title}`);
writeFileSync("flow.txt", explanation.diagram);
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", API+"/run-stream", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "explain-001")
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
var event string
var final map[string]any
sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 0, 64*1024), 4*1024*1024)
for sc.Scan() {
line := sc.Text()
switch {
case strings.HasPrefix(line, "event:"):
event = strings.TrimSpace(strings.TrimPrefix(line, "event:"))
case strings.HasPrefix(line, "data:"):
var data map[string]any
json.Unmarshal([]byte(strings.TrimPrefix(line, "data:")), &data)
switch event {
case "delta":
fmt.Print(".") // live progress
case "done":
final = data
case "error":
log.Fatal(data["message"])
}
}
}
// final["output"].(map[string]any)["output"].(string) is the explanation JSON —
// unmarshal it into the Explanation struct from step 4, then write explanation.Diagram to disk.
// Java 17+ — read the stream line by line instead of buffering the body.
var req = HttpRequest.newBuilder(URI.create(API + "/run-stream"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.header("Idempotency-Key", "explain-001")
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
.build();
var res = HTTP.send(req, HttpResponse.BodyHandlers.ofLines());
String event = null, done = null;
for (String line : (Iterable<String>) res.body()::iterator) {
if (line.startsWith("event:")) {
event = line.substring(6).trim();
} else if (line.startsWith("data:")) {
String data = line.substring(5).trim();
if ("delta".equals(event)) System.out.print("."); // live progress
else if ("done".equals(event)) done = data;
else if ("error".equals(event)) throw new RuntimeException(data);
}
}
// parse `done`, then parse data.output.output again — it is a JSON string holding
// explanation_name, reading_level, walkthrough[], diagram, concepts[], pitfalls[], quiz[] and the rest.
require "net/http"
require "json"
uri = URI(API + "/run-stream")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = "explain-001"
req.body = payload.to_json
event = nil
done = nil
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
http.request(req) do |res|
res.read_body do |chunk|
chunk.each_line do |line|
line = line.strip
if line.start_with?("event:")
event = line.delete_prefix("event:").strip
elsif line.start_with?("data:")
data = JSON.parse(line.delete_prefix("data:").strip)
case event
when "delta" then print "." # live progress
when "done" then done = data
when "error" then raise (data["message"] || "run failed")
end
end
end
end
end
end
explanation = JSON.parse(done["output"]["output"])
puts "\n#{done["charged_credits"]} credits - #{explanation["explanation_name"]}"
explanation["walkthrough"].each { |s| puts " #{s["step"]}. #{s["title"]}" }
File.write("flow.txt", explanation["diagram"])
$event = null;
$done = null;
$ch = curl_init(API . "/run-stream");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer $TOKEN",
"Content-Type: application/json",
"Idempotency-Key: explain-001",
],
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$event, &$done) {
foreach (explode("\n", $chunk) as $line) {
$line = trim($line);
if (str_starts_with($line, "event:")) {
$event = trim(substr($line, 6));
} elseif (str_starts_with($line, "data:")) {
$data = json_decode(trim(substr($line, 5)), true);
if ($event === "delta") { echo "."; } // live progress
elseif ($event === "done") { $done = $data; }
elseif ($event === "error") { throw new Exception($data["message"] ?? "run failed"); }
}
}
return strlen($chunk);
},
]);
curl_exec($ch);
curl_close($ch);
$explanation = json_decode($done["output"]["output"], true);
echo "\n{$done['charged_credits']} credits - {$explanation['explanation_name']}\n";
foreach ($explanation["walkthrough"] as $s) { echo " {$s['step']}. {$s['title']}\n"; }
file_put_contents("flow.txt", $explanation["diagram"]);
var req = new HttpRequestMessage(HttpMethod.Post, Api + "/run-stream") {
Content = JsonContent.Create(payload),
};
req.Headers.Add("Idempotency-Key", "explain-001");
using var res = await Http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());
string? evt = null, done = null;
while (await reader.ReadLineAsync() is { } line)
{
if (line.StartsWith("event:")) evt = line[6..].Trim();
else if (line.StartsWith("data:"))
{
var data = line[5..].Trim();
if (evt == "delta") Console.Write("."); // live progress
else if (evt == "done") done = data;
else if (evt == "error") throw new Exception(data);
}
}
using var final = JsonDocument.Parse(done!);
var text = final.RootElement.GetProperty("output").GetProperty("output").GetString();
using var explanationDoc = JsonDocument.Parse(text!);
var explanation = explanationDoc.RootElement;
Console.WriteLine(explanation.GetProperty("explanation_name"));
foreach (var s in explanation.GetProperty("walkthrough").EnumerateArray())
Console.WriteLine($" {s.GetProperty("step")}. {s.GetProperty("title")}");
await File.WriteAllTextAsync("flow.txt",
explanation.GetProperty("diagram").GetString()!);
In a browser, the native EventSource only speaks GET, and this endpoint is a
POST — read the fetch response body incrementally, as the JavaScript
sample above does. On an idempotent replay the server may answer with a plain JSON
envelope instead of an event stream; check the Content-Type before you start
parsing frames.