Drive Regress Desk from your own code
A reading, not a verdict on your research. The model reads the OLS fit your browser (or your script) computed; it never refits or recomputes a number, and it is never sent your table. A coefficient is an association among the rows you gave, not proof of cause.
Everything the web page does is available over HTTP. Fit your table with the page's own
olskit.js (statsmodels conventions - patsy coding, classical and HC3 errors, the
summary() statistics, VIF, Breusch-Pagan, influence - checked against statsmodels 0.14.6), send the
facts, and get back a verdict (sound, fixable, respecify) and
either a reading of every term and assumption or a statsmodels script that reproduces the fit,
checks it against the browser's coefficients and applies the fixes. The natural loop: fit, read,
script, fix, refit.
Two lanes: the task field
| task | what you get | extra input |
|---|---|---|
interpret | A reading of every non-intercept term with the browser's evidence grade (clear, fragile, none), what each of six assumption checks means for this fit, your claims judged against the fit, and what the fit cannot show. | none |
script | The fixes (HC3, rows to inspect, a collinear term, a log transform, logit or Poisson for 0/1 or count outcomes) and one complete statsmodels script: same FORMULA, an EXPECTED dict of the browser's coefficients checked with numpy.isclose, VIF, Breusch-Pagan and Cook's distance, then the fixes. | decision: the text of an earlier interpret run (optional) |
Both lanes return the same envelope: lane, verdict, headline, tldr, the lane body, next_steps and prescan_responses. Worked examples: interpret, script.
Input fields
Every field is a string.
| field | required | meaning |
|---|---|---|
task | yes | interpret or script. |
facts | yes | A JSON-encoded string with the browser's fit - see below. Build it with OlsKit.buildInput. |
title | no | A label for the analysis, up to 160 characters. |
context | no | Your notes: what the columns mean, units, how the data were collected, what you want to conclude. Up to 3,000 characters. |
decision | script only | Plain text of an earlier interpret run (the page builds it with Recon.decisionText). Up to 6,000 characters. |
question | no | Answered in tldr as a bullet starting "Answer:". Up to 1,200 characters. |
retry_note | no | Only on a retry after a malformed reply. |
The facts string
settings (the canonical patsy formula, outcome and transform, intercept,
rows used and dropped); fit (nobs, df, R-squared, adjusted R-squared, F and its p,
log-likelihood, AIC, BIC, residual standard error); coefficients - one per design
column, named as statsmodels names them, with coef, std_err, t, p, the 95% interval, HC3 std_err,
z, p and interval, evidence and VIF; outcome and predictors
summaries; diagnostics (Omnibus, Jarque-Bera, skew, kurtosis, Durbin-Watson, condition
number, Breusch-Pagan, max VIF); assumption_checks (six, each holds,
violated or unclear); influence (thresholds, counts and up to 8
rows, ids R1.., with line, pandas row_index, leverage, studentized residual and Cook's
distance); flags (F1.. with severity, category, message and refs);
browser_verdict; and clipped. The table itself is never sent.
Building the body
The simplest way to get a body that matches the page byte for byte is to run the page's own module
in Node. olskit.js has no dependencies and exports itself with module.exports.
// make-body.js - build the exact body the page sends, with the page's own code.
// Save https://regress-desk.skillsafe.ai/olskit.js next to this file, then:
// node make-body.js data.csv "price_k ~ sqft + beds + C(neighborhood)" interpret "Home sales" "notes" > body.json
const fs = require("fs");
const K = require("./olskit.js");
const [csv, formula = "", lane = "interpret", title = "", context = "", decision = ""] = process.argv.slice(2);
const set = { lane, title, context, decision, formula, table: fs.readFileSync(csv, "utf8"), question: "" };
const A = K.analyze(set);
if (A.empty) throw new Error(A.errors.join("; ") || "need a header row, a numeric outcome and more rows than coefficients");
const body = K.mustBeObject(K.buildInput(A, set));
console.error("formula:", A.formula.text, "| browser verdict:", A.hint, "| flags:", A.flags.map(f => f.id + " " + f.category).join(", "));
console.error("idempotency key: regress-desk:" + lane + ":" + K.hashInput(body) + ":a1");
process.stdout.write(JSON.stringify(body));
Base URL and the envelope
Every endpoint lives under https://api.skillsafe.ai/v1/app-api and every response uses
the same envelope, so one helper covers the whole API:
{"ok": true, "data": {"job_id": "job_...", "status": "queued"}}
{"ok": false, "error": {"code": "payment_required", "message": "..."}}
The token is minted for this app (the guest endpoint takes {"slug":"regress-desk"} in
its body), so no slug header is needed afterwards. Send it as Authorization: Bearer ….
The input object IS the request body. There is no {"input": …}
wrapper. A wrapped body is answered with an unknown field 'input' warning, and the
model never sees your text.
Error codes
| status | code | what to do |
|---|---|---|
| 400 | validation_error | A field is missing or the wrong type. Every field is a string: facts must be a JSON-encoded string, not an object. |
| 401 | unauthorized | The token is missing, malformed or expired. Get a new one from the token page. |
| 402 | payment_required | The balance is below min_credits. Call /estimate first and top up. |
| 403 | forbidden | The token is valid but not for this app, or a guest token tried a metered run. A guest cannot run; sign in for a personal token. |
| 404 | not_found | Unknown job id, or the app slug does not exist. |
| 409 | conflict | The same Idempotency-Key was replayed with a different body. Change the key or send the original input. |
| 429 | rate_limited | Too many requests. Back off and retry; do not tight-loop. |
| 5xx | internal | A server-side failure. Retry with the SAME Idempotency-Key so you are not billed twice. |
1. A tiny client
One helper that sends the token, unwraps data and raises on ok: false.
The token comes from the token page (Copy token or
Copy shell export); step 2 covers the kinds of token and minting one from code.
# Every call is the same three things: the base URL, your bearer token,
# and a JSON body. Keep the token in a shell variable.
BASE="https://api.skillsafe.ai/v1/app-api"
SLUG="regress-desk"
TOKEN="$SKILLSAFE_TOKEN" # from https://regress-desk.skillsafe.ai/tokens.html
call() { # call <path> [json-body]
if [ -n "$2" ]; then
curl -sS -X POST "$BASE/$1" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "$2"
else
curl -sS "$BASE/$1" -H "Authorization: Bearer $TOKEN"
fi
}
import json, os, urllib.error, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
SLUG = "regress-desk"
TOKEN = os.environ.get("SKILLSAFE_TOKEN", "YOUR_TOKEN") # from https://regress-desk.skillsafe.ai/tokens.html
def call(path, body=None):
"""Returns the unwrapped `data`, or raises with the API error code."""
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(f"{BASE}/{path}", data=data, method="POST" if body is not None else "GET")
req.add_header("Authorization", f"Bearer {TOKEN}")
if body is not None:
req.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(req) as r:
payload = json.load(r)
except urllib.error.HTTPError as e:
payload = json.load(e)
if not payload.get("ok"):
err = payload.get("error", {})
raise RuntimeError(f"{err.get('code')}: {err.get('message')}")
return payload["data"]
import { readFileSync } from "node:fs";
const BASE = "https://api.skillsafe.ai/v1/app-api";
const SLUG = "regress-desk";
// Paste the token from https://regress-desk.skillsafe.ai/tokens.html into a file named "token",
// or replace the fallback with it.
let TOKEN = "YOUR_TOKEN";
try { TOKEN = readFileSync("token", "utf8").trim(); } catch {}
async function call(path, body) {
const res = await fetch(`${BASE}/${path}`, {
method: body ? "POST" : "GET",
headers: {
Authorization: `Bearer ${TOKEN}`,
...(body ? { "Content-Type": "application/json" } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
const payload = await res.json();
if (!payload.ok) throw new Error(`${payload.error.code}: ${payload.error.message}`);
return payload.data;
}
package main
import (
"bufio"
"bytes"
"crypto/sha256"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
const (
base = "https://api.skillsafe.ai/v1/app-api"
slug = "regress-desk"
)
var token = os.Getenv("SKILLSAFE_TOKEN") // from https://regress-desk.skillsafe.ai/tokens.html
type envelope struct {
OK bool `json:"ok"`
Data json.RawMessage `json:"data"`
Error struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
func call(path string, body any) (json.RawMessage, error) {
method := http.MethodGet
var rdr io.Reader
if body != nil {
method = http.MethodPost
b, _ := json.Marshal(body)
rdr = bytes.NewReader(b)
}
req, _ := http.NewRequest(method, base+"/"+path, rdr)
req.Header.Set("Authorization", "Bearer "+token)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var env envelope
if err := json.NewDecoder(res.Body).Decode(&env); err != nil {
return nil, err
}
if !env.OK {
return nil, fmt.Errorf("%s: %s", env.Error.Code, env.Error.Message)
}
return env.Data, nil
}
import java.net.URI;
import java.net.http.*;
public class RegressDesk {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final String SLUG = "regress-desk";
static final String TOKEN = System.getenv().getOrDefault("SKILLSAFE_TOKEN", "YOUR_TOKEN");
static final HttpClient HTTP = HttpClient.newHttpClient();
static String call(String path, String jsonBody) throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(BASE + "/" + path))
.header("Authorization", "Bearer " + TOKEN);
if (jsonBody != null) {
b.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody));
} else {
b.GET();
}
HttpResponse<String> res = HTTP.send(b.build(), HttpResponse.BodyHandlers.ofString());
// The envelope is always {"ok":true,"data":...} or {"ok":false,"error":...}.
return res.body();
}
}
require "json"
require "net/http"
require "uri"
BASE = "https://api.skillsafe.ai/v1/app-api"
SLUG = "regress-desk"
TOKEN = ENV.fetch("SKILLSAFE_TOKEN", "YOUR_TOKEN") # from https://regress-desk.skillsafe.ai/tokens.html
def call(path, body = nil)
uri = URI("#{BASE}/#{path}")
req = body ? Net::HTTP::Post.new(uri) : Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
if body
req["Content-Type"] = "application/json"
req.body = JSON.generate(body)
end
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
payload = JSON.parse(res.body)
raise "#{payload['error']['code']}: #{payload['error']['message']}" unless payload["ok"]
payload["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
const SLUG = "regress-desk";
define("TOKEN", getenv("SKILLSAFE_TOKEN") ?: "YOUR_TOKEN"); // from /tokens.html
function call(string $path, ?array $body = null) {
$ch = curl_init(BASE . "/" . $path);
$headers = ["Authorization: Bearer " . TOKEN];
if ($body !== null) {
$headers[] = "Content-Type: application/json";
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$payload = json_decode(curl_exec($ch), true);
curl_close($ch);
if (empty($payload["ok"])) {
throw new RuntimeException($payload["error"]["code"] . ": " . $payload["error"]["message"]);
}
return $payload["data"];
}
using System.Net.Http.Json;
using System.Text.Json;
static class RegressDesk
{
const string Base = "https://api.skillsafe.ai/v1/app-api";
const string Slug = "regress-desk";
static readonly string Token =
Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN") ?? "YOUR_TOKEN";
static readonly HttpClient Http = new();
public static async Task<JsonElement> Call(string path, object? body = null)
{
var req = new HttpRequestMessage(body is null ? HttpMethod.Get : HttpMethod.Post, $"{Base}/{path}");
req.Headers.Add("Authorization", $"Bearer {Token}");
if (body is not null) req.Content = JsonContent.Create(body);
var res = await Http.SendAsync(req);
var payload = await res.Content.ReadFromJsonAsync<JsonElement>();
if (!payload.GetProperty("ok").GetBoolean())
{
var e = payload.GetProperty("error");
throw new Exception($"{e.GetProperty("code")}: {e.GetProperty("message")}");
}
return payload.GetProperty("data");
}
}
2. Get a token
The easiest route is the token page: it shows the token this browser
already holds, with Copy token and Copy shell export buttons, and
a sign-in button for a personal token. A guest token, minted with
POST /guest and {"slug":"regress-desk"}, can call /me and
/estimate; the run is metered, so /run and /run-stream need
a personal token.
# The token page is the shortest path. It shows the token this browser holds and
# hands you a ready-made shell export:
#
# https://regress-desk.skillsafe.ai/tokens.html
# export SKILLSAFE_TOKEN="..."
#
# To mint a guest token from the command line instead. A guest token is enough
# for /me and /estimate; a run needs a personal token from signing in.
curl -sS -X POST "https://api.skillsafe.ai/v1/app-api/guest" \
-H "Content-Type: application/json" -d '{"slug":"regress-desk"}'
# {"ok":true,"data":{"token":"…","subject_type":"guest"}}
# Open https://regress-desk.skillsafe.ai/tokens.html and press "Copy token",
# or mint a guest token here. A guest token can call /me and /estimate but
# cannot start a metered run.
import json, urllib.request
req = urllib.request.Request(
"https://api.skillsafe.ai/v1/app-api/guest", data=b'{"slug": "regress-desk"}', method="POST")
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req) as r:
TOKEN = json.load(r)["data"]["token"]
// Open https://regress-desk.skillsafe.ai/tokens.html and press "Copy token",
// or mint a guest token here. A guest token can call /me and /estimate but
// cannot start a metered run.
const res = await fetch("https://api.skillsafe.ai/v1/app-api/guest", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ slug: "regress-desk" }),
});
const TOKEN = (await res.json()).data.token;
// Open https://regress-desk.skillsafe.ai/tokens.html and press "Copy token",
// or mint a guest token here. A guest token can call /me and /estimate but
// cannot start a metered run.
guestReq, _ := http.NewRequest(http.MethodPost,
"https://api.skillsafe.ai/v1/app-api/guest", bytes.NewReader([]byte(`{"slug":"regress-desk"}`)))
guestReq.Header.Set("Content-Type", "application/json")
guestRes, err := http.DefaultClient.Do(guestReq)
if err != nil {
panic(err)
}
defer guestRes.Body.Close()
var guest struct {
Data struct {
Token string `json:"token"`
} `json:"data"`
}
_ = json.NewDecoder(guestRes.Body).Decode(&guest)
fmt.Println(guest.Data.Token)
// Open https://regress-desk.skillsafe.ai/tokens.html and press "Copy token",
// or mint a guest token here. A guest token can call /me and /estimate but
// cannot start a metered run.
var http = HttpClient.newHttpClient();
var guestReq = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/guest"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{\"slug\":\"regress-desk\"}"))
.build();
HttpResponse<String> guest = http.send(guestReq, HttpResponse.BodyHandlers.ofString());
System.out.println(guest.body()); // {"ok":true,"data":{"token":"…","subject_type":"guest"}}
# Open https://regress-desk.skillsafe.ai/tokens.html and press "Copy token",
# or mint a guest token here. A guest token can call /me and /estimate but
# cannot start a metered run.
require "json"
require "net/http"
require "uri"
uri = URI("https://api.skillsafe.ai/v1/app-api/guest")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req.body = JSON.generate({ slug: "regress-desk" })
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
TOKEN = JSON.parse(res.body)["data"]["token"]
<?php
// Open https://regress-desk.skillsafe.ai/tokens.html and press "Copy token",
// or mint a guest token here. A guest token can call /me and /estimate but
// cannot start a metered run.
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/guest");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(["slug" => "regress-desk"]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json"]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$guest = json_decode(curl_exec($ch), true);
curl_close($ch);
echo $guest["data"]["token"];
// Open https://regress-desk.skillsafe.ai/tokens.html and press "Copy token",
// or mint a guest token here. A guest token can call /me and /estimate but
// cannot start a metered run.
using var http = new HttpClient();
var guestReq = new HttpRequestMessage(HttpMethod.Post, "https://api.skillsafe.ai/v1/app-api/guest");
guestReq.Content = new StringContent("{\"slug\":\"regress-desk\"}", Encoding.UTF8, "application/json");
var guestRes = await http.SendAsync(guestReq);
var guest = await guestRes.Content.ReadFromJsonAsync<JsonElement>();
Console.WriteLine(guest.GetProperty("data").GetProperty("token").GetString());
3. Check the session and the balance
call me
# {"ok":true,"data":{"subject_type":"user","username":"you","credits":51234}}
me = call("me")
print(me["subject_type"], me.get("credits"))
const me = await call("me");
console.log(me.subject_type, me.credits);
raw, err := call("me", nil)
if err != nil {
panic(err)
}
var me struct {
SubjectType string `json:"subject_type"`
Credits int `json:"credits"`
}
_ = json.Unmarshal(raw, &me)
fmt.Println(me.SubjectType, me.Credits)
System.out.println(call("me", null));
// {"ok":true,"data":{"subject_type":"user","username":"you","credits":51234}}
me = call("me")
puts "#{me['subject_type']} #{me['credits']}"
<?php
$me = call("me");
echo $me["subject_type"], " ", $me["credits"], PHP_EOL;
var me = await RegressDesk.Call("me");
Console.WriteLine(me.GetProperty("subject_type").GetString());
4. Price the run (free)
/estimate returns the model binding and the credits a run would reserve. It creates no
job and charges nothing. Expect model_alias gpt-terra and
markup_bps 1000 (a 10% markup). hold_credits is a
reservation, not the price: it is held against your balance while the run executes
and released afterwards. min_credits is the least balance that can start a run. What
you actually pay is charged_credits, reported on the finished job and in the
done event, and it is usually far lower than the hold. The body is the input object
itself, with no {"input": …} wrapper. /estimate does not validate the
body, so check the shape yourself: an object whose every value is a string, task equal
to interpret or script,
facts non-empty, and facts a JSON string that parses to an object (this is
what the page's own guard, OlsKit.mustBeObject, refuses to spend without).
# body.json is the input object itself - no {"input": ...} wrapper. Build it with
# make-body.js above, or by hand. estimate does not validate it, so check the shape first:
python3 -c 'import json;b=json.load(open("body.json"));assert isinstance(b,dict) and b.get("task") in ("interpret","script") and all(isinstance(v,str) for v in b.values()) and all(b.get(k,"").strip() for k in ("facts",)) and isinstance(json.loads(b["facts"]),dict)'
INPUT=$(cat body.json)
call estimate "$INPUT"
# {"ok":true,"data":{"model":"...","model_alias":"gpt-terra",
# "markup_bps":1000,"hold_credits":...,"min_credits":...,"sponsor_enabled":false,
# "warnings":[]}}
#
# estimate creates no job and charges nothing. hold_credits is RESERVED, not the
# price; charged_credits after the run is the actual cost, usually far lower.
INPUT = json.load(open("body.json")) # built by make-body.js above, or by hand
assert isinstance(INPUT, dict) and INPUT.get("task") in ("interpret", "script")
assert all(isinstance(v, str) for v in INPUT.values())
assert all(INPUT.get(k, "").strip() for k in ("facts",))
assert isinstance(json.loads(INPUT["facts"]), dict) # facts is a JSON STRING
est = call("estimate", INPUT)
print(est["model_alias"], est["markup_bps"], est["hold_credits"], est.get("warnings"))
me = call("me")
if me.get("credits", 0) < est["min_credits"]:
raise SystemExit("top up first: balance is below min_credits")
const INPUT = JSON.parse(readFileSync("body.json", "utf8")); // built by make-body.js above
if (!INPUT || typeof INPUT !== "object" || !["interpret", "script"].includes(INPUT.task)) throw new Error("task must be interpret or script");
for (const [k, v] of Object.entries(INPUT)) if (typeof v !== "string") throw new Error(k + " must be a string");
for (const k of ["facts"]) if (!(INPUT[k] || "").trim()) throw new Error(k + " is required");
JSON.parse(INPUT.facts); // throws unless facts is a JSON string
const est = await call("estimate", INPUT);
console.log(est.model_alias, est.markup_bps, est.hold_credits, est.warnings);
const me = await call("me");
if ((me.credits ?? 0) < est.min_credits) throw new Error("top up first");
raw, _ := os.ReadFile("body.json") // built by make-body.js above
var input map[string]string // every field is a string, facts included
if err := json.Unmarshal(raw, &input); err != nil {
panic("body.json must be an object of strings: " + err.Error())
}
if input["task"] != "interpret" && input["task"] != "script" {
panic("task must be interpret or script")
}
for _, k := range []string{"facts"} {
if strings.TrimSpace(input[k]) == "" {
panic(k + " is required")
}
}
var facts map[string]any
if err := json.Unmarshal([]byte(input["facts"]), &facts); err != nil {
panic("facts must be a JSON string holding an object")
}
est, err := call("estimate", input)
if err != nil {
panic(err)
}
fmt.Println(string(est)) // model_alias gpt-terra, markup_bps 1000, hold_credits, min_credits
String input = Files.readString(Path.of("body.json")); // built by make-body.js above
if (!input.matches("(?s)\\s*\\{.*\"task\"\\s*:\\s*\"(interpret|script)\".*\\}\\s*"))
throw new IllegalStateException("body.json must be an object with task interpret or script");
String lane = input.replaceAll("(?s).*\"task\"\\s*:\\s*\"(interpret|script)\".*", "$1");
for (String k : new String[] {"facts"})
if (!input.contains("\"" + k + "\"")) throw new IllegalStateException(k + " is required");
String est = call("estimate", input);
System.out.println(est); // model_alias gpt-terra, markup_bps 1000, hold_credits, min_credits
INPUT = JSON.parse(File.read("body.json")) # built by make-body.js above
raise "task must be interpret or script" unless %w[interpret script].include?(INPUT["task"])
INPUT.each { |k, v| raise "#{k} must be a string" unless v.is_a?(String) }
%w[facts].each { |k| raise "#{k} is required" if INPUT[k].to_s.strip.empty? }
raise "facts must hold an object" unless JSON.parse(INPUT["facts"]).is_a?(Hash)
est = call("estimate", INPUT)
puts est["model_alias"], est["markup_bps"], est["hold_credits"]
<?php
$input = json_decode(file_get_contents("body.json"), true); // built by make-body.js above
if (!is_array($input) || !in_array($input["task"] ?? "", ["interpret", "script"], true)) { throw new Exception("task must be interpret or script"); }
foreach ($input as $k => $v) { if (!is_string($v)) { throw new Exception("$k must be a string"); } }
foreach (["facts"] as $k) { if (trim($input[$k] ?? "") === "") { throw new Exception("$k is required"); } }
if (!is_array(json_decode($input["facts"], true))) { throw new Exception("facts must be a JSON string"); }
$est = call("estimate", $input);
echo $est["model_alias"], " ", $est["markup_bps"], " ", $est["hold_credits"], PHP_EOL;
var input = File.ReadAllText("body.json"); // built by make-body.js above
using var doc = JsonDocument.Parse(input);
var root = doc.RootElement;
var lane = root.GetProperty("task").GetString();
if (lane != "interpret" && lane != "script") throw new Exception("task must be interpret or script");
foreach (var p in root.EnumerateObject())
if (p.Value.ValueKind != JsonValueKind.String) throw new Exception($"{p.Name} must be a string");
JsonDocument.Parse(root.GetProperty("facts").GetString()!); // facts is a JSON string
var est = await RegressDesk.Call("estimate", root);
Console.WriteLine(est); // model_alias gpt-terra, markup_bps 1000, hold_credits, min_credits
5. Run it, then poll
POST /run returns a job_id; poll GET /jobs/{id} until it is
terminal. The reply is a string at data.output.output: JSON.parse
it (step 7). Send an Idempotency-Key built from the lane, a hash of the input and the
attempt number, regress-desk:<lane>:<hash>:a<attempt> (for example
regress-desk:interpret:yuw9r81nmm2n6:a1), so a retried request returns the same job instead
of billing a second run. Use one key per distinct input: a changed table, roles, weights or notes (so changed
facts) or a changed reading are a new hash, the same fit in the other lane are a new key, and replaying
an old key with a different body is a 409. The page uses
OlsKit.hashInput(body) for the hash (it covers task, title,
context, facts, decision and question;
make-body.js prints the key); any stable digest of the body works from other
languages. Leave retry_note out of the hash and bump the attempt instead.
# Always send an Idempotency-Key derived from the input. A retried request with
# the same key returns the SAME job instead of billing a second run.
LANE=$(printf '%s' "$INPUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["task"])') # interpret or script
KEY="regress-desk:$LANE:$(printf '%s' "$INPUT" | shasum -a 256 | cut -c1-16):a1"
JOB=$(curl -sS -X POST "$BASE/run" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-d "$INPUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["job_id"])')
while :; do
OUT=$(call "jobs/$JOB")
STATUS=$(printf '%s' "$OUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["status"])')
[ "$STATUS" = "succeeded" ] && break
[ "$STATUS" = "failed" ] && echo "$OUT" && exit 1
sleep 2
done
# {"ok":true,"data":{"job_id":"job_...","status":"succeeded",
# "output":{"output":"{\"lane\":\"interpret\",\"verdict\":\"fixable\",\"headline\":\"...\", ...}"},
# "charged_credits":...,"truncated":false}}
printf '%s' "$OUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["output"]["output"])' > reply.json
import hashlib, time
digest = hashlib.sha256(json.dumps(INPUT, sort_keys=True).encode()).hexdigest()[:16]
key = f"regress-desk:{INPUT['task']}:{digest}:a1"
req = urllib.request.Request(f"{BASE}/run", data=json.dumps(INPUT).encode(), method="POST")
req.add_header("Authorization", f"Bearer {TOKEN}")
req.add_header("Content-Type", "application/json")
req.add_header("Idempotency-Key", key)
with urllib.request.urlopen(req) as r:
job_id = json.load(r)["data"]["job_id"]
while True:
job = call(f"jobs/{job_id}")
if job["status"] in ("succeeded", "failed"):
break
time.sleep(2)
if job["status"] == "failed":
raise RuntimeError(job.get("error"))
text = job["output"]["output"] # the reply, as a string
print("charged", job.get("charged_credits"), "truncated", job.get("truncated"))
import { createHash } from "node:crypto";
const digest = createHash("sha256").update(JSON.stringify(INPUT)).digest("hex").slice(0, 16);
const key = `regress-desk:${INPUT.task}:${digest}:a1`;
const started = await fetch(`${BASE}/run`, {
method: "POST",
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json", "Idempotency-Key": key },
body: JSON.stringify(INPUT),
}).then((r) => r.json());
if (!started.ok) throw new Error(`${started.error.code}: ${started.error.message}`);
let job = started.data;
while (job.status !== "succeeded" && job.status !== "failed") {
await new Promise((r) => setTimeout(r, 2000));
job = await call(`jobs/${job.job_id}`);
}
if (job.status === "failed") throw new Error(JSON.stringify(job.error));
const text = job.output.output; // the reply, as a string
console.log(job.charged_credits, job.truncated);
body, _ := json.Marshal(input)
sum := sha256.Sum256(body)
key := fmt.Sprintf("regress-desk:%s:%x:a1", input["task"], sum[:8])
req, _ := http.NewRequest(http.MethodPost, base+"/run", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", key)
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
var started struct {
Data struct {
JobID string `json:"job_id"`
} `json:"data"`
}
_ = json.NewDecoder(res.Body).Decode(&started)
res.Body.Close()
var jobOutput string
for {
raw, err := call("jobs/"+started.Data.JobID, nil)
if err != nil {
panic(err)
}
var job struct {
Status string `json:"status"`
Output struct {
Output string `json:"output"`
} `json:"output"`
Charged int `json:"charged_credits"`
Truncated bool `json:"truncated"`
}
_ = json.Unmarshal(raw, &job)
if job.Status == "succeeded" {
jobOutput = job.Output.Output
fmt.Println(job.Charged, job.Truncated)
break
}
if job.Status == "failed" {
panic(string(raw))
}
time.Sleep(2 * time.Second)
}
String key = "regress-desk:" + lane + ":" + sha256Hex(input).substring(0, 16) + ":a1";
HttpRequest run = HttpRequest.newBuilder(URI.create(BASE + "/run"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.header("Idempotency-Key", key)
.POST(HttpRequest.BodyPublishers.ofString(input)).build();
String started = HTTP.send(run, HttpResponse.BodyHandlers.ofString()).body();
String jobId = started.replaceAll(".*\"job_id\":\"([^\"]+)\".*", "$1");
while (true) {
String job = call("jobs/" + jobId, null);
if (job.contains("\"status\":\"succeeded\"")) { System.out.println(job); break; }
if (job.contains("\"status\":\"failed\"")) throw new RuntimeException(job);
Thread.sleep(2000);
}
// Parse data.output.output (a string holding the reply JSON) with your JSON library.
// sha256Hex: HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(input.getBytes(UTF_8)))
require "digest"
key = "regress-desk:#{INPUT['task']}:#{Digest::SHA256.hexdigest(JSON.generate(INPUT))[0, 16]}:a1"
uri = URI("#{BASE}/run")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = key
req.body = JSON.generate(INPUT)
job = JSON.parse(Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }.body)["data"]
until %w[succeeded failed].include?(job["status"])
sleep 2
job = call("jobs/#{job['job_id']}")
end
raise job.inspect if job["status"] == "failed"
text = job["output"]["output"] # the reply, as a string
puts job["charged_credits"], job["truncated"]
<?php
$key = "regress-desk:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$ch = curl_init(BASE . "/run");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($input),
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . TOKEN, "Content-Type: application/json", "Idempotency-Key: " . $key],
CURLOPT_RETURNTRANSFER => true,
]);
$job = json_decode(curl_exec($ch), true)["data"];
curl_close($ch);
while (!in_array($job["status"], ["succeeded", "failed"], true)) {
sleep(2);
$job = call("jobs/" . $job["job_id"]);
}
if ($job["status"] === "failed") { throw new RuntimeException(json_encode($job)); }
$text = $job["output"]["output"]; // the reply, as a string
echo $job["charged_credits"], PHP_EOL;
using System.Security.Cryptography;
var json = input; // the body.json text from step 4
var key = $"regress-desk:{lane}:" + Convert.ToHexString(SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(json)))[..16].ToLower() + ":a1";
var req = new HttpRequestMessage(HttpMethod.Post, "https://api.skillsafe.ai/v1/app-api/run");
req.Headers.Add("Authorization", $"Bearer {Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN") ?? "YOUR_TOKEN"}");
req.Headers.Add("Idempotency-Key", key);
req.Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
var started = await (await new HttpClient().SendAsync(req)).Content.ReadFromJsonAsync<JsonElement>();
var jobId = started.GetProperty("data").GetProperty("job_id").GetString();
JsonElement job;
while (true)
{
job = await RegressDesk.Call($"jobs/{jobId}");
var status = job.GetProperty("status").GetString();
if (status == "succeeded") break;
if (status == "failed") throw new Exception(job.ToString());
await Task.Delay(2000);
}
var output = job.GetProperty("output").GetProperty("output").GetString()!; // the reply, as a string
6. Or stream it
POST /run-stream takes the same body and headers and answers with server-sent events:
job (the job id), delta (chunks of the reply) and done (the
status, charged_credits, truncated and, when present, the full
output). A browser page may receive only tick heartbeats and then
done, never a delta, so take the reply from done.output.output
when it is there, fall back to the concatenated deltas, and fall back again to
GET /jobs/{id}.
# Server-sent events. `delta` events carry chunks of the reply; `done` carries the
# status, charged_credits and the truncated flag. Ignore `tick` heartbeats.
curl -N -X POST "$BASE/run-stream" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-H "Accept: text/event-stream" \
-d "$INPUT"
# event: job {"job_id":"job_..."}
# event: delta {"text":"{\"lane\":\"interpret\",\"verdict\":\"fixable\",\"headline\":\"The"}
# event: done {"status":"succeeded","charged_credits":...,"truncated":false}
req = urllib.request.Request(f"{BASE}/run-stream", data=json.dumps(INPUT).encode(), method="POST")
for h, v in (("Authorization", f"Bearer {TOKEN}"), ("Content-Type", "application/json"),
("Idempotency-Key", key), ("Accept", "text/event-stream")):
req.add_header(h, v)
raw, done, event = "", {}, None
with urllib.request.urlopen(req) as stream:
for line in stream:
line = line.decode().rstrip("\n")
if line.startswith("event: "):
event = line[7:]
elif line.startswith("data: ") and event == "delta":
raw += json.loads(line[6:]).get("text", "")
elif line.startswith("data: ") and event == "done":
done = json.loads(line[6:])
text = (done.get("output") or {}).get("output") or raw
print(done.get("status"), done.get("charged_credits"), done.get("truncated"))
const res = await fetch(`${BASE}/run-stream`, {
method: "POST",
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json", "Idempotency-Key": key, Accept: "text/event-stream" },
body: JSON.stringify(INPUT),
});
const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = "", raw = "", event = null, done = null;
for (;;) {
const { value, done: end } = await reader.read();
if (end) break;
buf += dec.decode(value, { stream: true });
let i;
while ((i = buf.indexOf("\n")) >= 0) {
const line = buf.slice(0, i); buf = buf.slice(i + 1);
if (line.startsWith("event: ")) event = line.slice(7);
else if (line.startsWith("data: ") && event === "delta") raw += JSON.parse(line.slice(6)).text || "";
else if (line.startsWith("data: ") && event === "done") done = JSON.parse(line.slice(6));
}
}
const streamed = done?.output?.output || raw; // browsers may get only ticks + done
console.log(done, streamed.length);
req, _ = http.NewRequest(http.MethodPost, base+"/run-stream", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", key)
req.Header.Set("Accept", "text/event-stream")
res, err = http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var raw strings.Builder
event := ""
sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 1<<20), 1<<20)
for sc.Scan() {
line := sc.Text()
switch {
case strings.HasPrefix(line, "event: "):
event = line[7:]
case strings.HasPrefix(line, "data: ") && event == "delta":
var d struct{ Text string `json:"text"` }
_ = json.Unmarshal([]byte(line[6:]), &d)
raw.WriteString(d.Text)
case strings.HasPrefix(line, "data: ") && event == "done":
fmt.Println("done:", line[6:])
}
}
HttpRequest stream = HttpRequest.newBuilder(URI.create(BASE + "/run-stream"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.header("Idempotency-Key", key)
.header("Accept", "text/event-stream")
.POST(HttpRequest.BodyPublishers.ofString(input)).build();
HTTP.send(stream, HttpResponse.BodyHandlers.ofLines()).body().forEach(line -> {
// "event: delta" lines are followed by "data: {\"text\":...}"; "event: done" by the status.
if (line.startsWith("data: ")) System.out.println(line.substring(6));
});
uri = URI("#{BASE}/run-stream")
req = Net::HTTP::Post.new(uri)
{ "Authorization" => "Bearer #{TOKEN}", "Content-Type" => "application/json",
"Idempotency-Key" => key, "Accept" => "text/event-stream" }.each { |k, v| req[k] = v }
req.body = JSON.generate(INPUT)
raw, event = +"", nil
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |h|
h.request(req) do |res|
res.read_body do |chunk|
chunk.each_line do |line|
line = line.chomp
if line.start_with?("event: ") then event = line[7..]
elsif line.start_with?("data: ") && event == "delta" then raw << JSON.parse(line[6..])["text"].to_s
elsif line.start_with?("data: ") && event == "done" then puts line[6..]
end
end
end
end
end
<?php
$raw = ""; $event = null;
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($input),
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . TOKEN, "Content-Type: application/json", "Idempotency-Key: " . $key, "Accept: text/event-stream"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$raw, &$event) {
foreach (explode("\n", $chunk) as $line) {
if (str_starts_with($line, "event: ")) $event = substr($line, 7);
elseif (str_starts_with($line, "data: ") && $event === "delta") $raw .= json_decode(substr($line, 6), true)["text"] ?? "";
elseif (str_starts_with($line, "data: ") && $event === "done") echo substr($line, 6), PHP_EOL;
}
return strlen($chunk);
},
]);
curl_exec($ch);
curl_close($ch);
var sreq = new HttpRequestMessage(HttpMethod.Post, "https://api.skillsafe.ai/v1/app-api/run-stream");
sreq.Headers.Add("Authorization", $"Bearer {Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN") ?? "YOUR_TOKEN"}");
sreq.Headers.Add("Idempotency-Key", key);
sreq.Headers.Add("Accept", "text/event-stream");
sreq.Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
using var sres = await new HttpClient().SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var sr = new StreamReader(await sres.Content.ReadAsStreamAsync());
var raw = new System.Text.StringBuilder(); string? ev = null, line;
while ((line = await sr.ReadLineAsync()) != null)
{
if (line.StartsWith("event: ")) ev = line[7..];
else if (line.StartsWith("data: ") && ev == "delta") raw.Append(JsonSerializer.Deserialize<JsonElement>(line[6..]).GetProperty("text").GetString());
else if (line.StartsWith("data: ") && ev == "done") Console.WriteLine(line[6..]);
}
7. Parse the reply
The reply is a JSON object serialised as a string. Parse it, then check the lane.
# The reply is a JSON string inside data.output.output. Pull it out and parse it:
printf '%s' "$JOB" | python3 -c 'import sys,json;r=json.loads(json.load(sys.stdin)["output"]["output"]);print(r["verdict"],r["headline"])'
reply = json.loads(job["output"]["output"])
assert reply["lane"] == INPUT["task"], "the model answered as another lane"
print(reply["verdict"], reply["headline"])
for c in reply.get("coefficients", []): # interpret lane
print(c["term"], c["evidence"], c["reading"])
open("fix.py", "w").write(reply.get("script", "")) # script lane
const reply = JSON.parse(job.output.output);
if (reply.lane !== INPUT.task) throw new Error("the model answered as another lane");
console.log(reply.verdict, reply.headline);
for (const c of reply.coefficients || []) console.log(c.term, c.evidence, c.reading); // interpret lane
if (reply.script) require("fs").writeFileSync("fix.py", reply.script); // script lane
var reply struct {
Lane, Verdict, Headline, Script string
Coefficients []struct{ Term, Evidence, Reading string }
}
if err := json.Unmarshal([]byte(job.Output.Output), &reply); err != nil { panic(err) }
fmt.Println(reply.Verdict, reply.Headline)
// With any JSON library (Jackson shown): the reply is a string that holds a JSON object.
JsonNode reply = new ObjectMapper().readTree(outputString);
System.out.println(reply.get("verdict").asText() + " " + reply.get("headline").asText());
reply = JSON.parse(job["output"]["output"])
raise "the model answered as another lane" unless reply["lane"] == INPUT["task"]
puts [reply["verdict"], reply["headline"]].join(" ")
$reply = json_decode($job["output"]["output"], true);
if ($reply["lane"] !== $input["task"]) { throw new Exception("the model answered as another lane"); }
echo $reply["verdict"], " ", $reply["headline"], "\n";
var reply = JsonSerializer.Deserialize<JsonElement>(outputString);
Console.WriteLine($"{reply.GetProperty("verdict")} {reply.GetProperty("headline")}");
Invariants worth asserting
- The verdict is never looser than
facts.browser_verdict(respecify < fixable < sound) unless the medium or high flags that set it were dismissed. - Every flag id appears once in
prescan_responses. - In the interpret lane there is one reading per non-intercept coefficient, with the same
evidenceas facts, and one entry per assumption check with the samestatus. - Every number in the prose exists in
factsor your notes (after rounding to 3 significant figures). - In the script lane
FORMULAisfacts.settings.formulaverbatim,EXPECTEDcarries every coefficient with its facts value, imports come only from pandas, numpy and statsmodels,cov_type="HC3"appears when heteroskedasticity was confirmed, and any dropped row index is anRrow'srow_index. - The page's
recon.jschecks all of this; you can run it in Node the same way asolskit.js.
The output contract
{
"lane": "interpret" | "script",
"verdict": "sound" | "fixable" | "respecify",
"headline": "...",
"tldr": ["..."],
// interpret:
"coefficients": [{"term", "evidence": "clear|fragile|none", "reading"}],
"assumptions": [{"check", "status": "holds|violated|unclear", "meaning"}],
"claims": [{"claim", "support": "supported|partly|not_supported", "why"}],
"cautions": ["..."],
// script:
"fixes": [{"fix", "why", "refs": "F1"}],
"script": "import pandas as pd ...",
"assumptions": ["..."],
"checks": ["..."],
// both:
"next_steps": ["..."],
"prescan_responses": [{"ref": "F1", "verdict": "confirmed|dismissed", "note": "..."}]
}
Worked example: interpret
The page's "home prices" example: 64 illustrative home sales, price_k ~ sqft + beds + age_yr + C(neighborhood). The browser fits it (one row dropped for a missing price), finds heteroskedasticity and grades every term clear. The body (facts abbreviated):
{
"task": "interpret",
"title": "Home sales, three neighborhoods, 2025",
"context": "Sale prices in thousands of dollars for single-family homes sold in 2025 in three neighborhoods. sqft is interior floor area, age_yr is years since the house was built. I want to tell the client that bedrooms add value on top of size.",
"question": "Is each extra bedroom worth about 26k once size is accounted for?",
"facts": "JSON string of: {\n \"settings\": {\n \"formula\": \"price_k ~ C(neighborhood) + sqft + beds + age_yr\",\n \"formula_source\": \"user\",\n \"outcome\": \"price_k\",\n \"outcome_transform\": null,\n \"intercept\": true,\n \"rows_in_table\": 64,\n \"rows_used\": 63,\n \"rows_dropped_missing\": 1,\n \"confidence\": 0.95,\n \"robust\": \"HC3 (z statistics, as statsmodels reports cov_type='HC3')\"\n },\n \"fit\": {\n \"nobs\": 63,\n \"df_model\": 5,\n \"df_resid\": 57,\n \"r_squared\": 0.6672,\n \"adj_r_squared\": 0.638,\n \"f_statistic\": 22.85,\n \"f_pvalue\": 1.649e-12,\n \"log_likelihood\": -314.226,\n \"aic\": 640.452,\n \"bic\": 653.311,\n \"residual_std_error\": 37.2934\n },\n \"coefficients\": [\n {\n \"term\": \"Intercept\",\n \"coef\": 38.1154,\n \"std_err\": 24.5804,\n \"t\": 1.551,\n \"p\": 0.1265,\n \"ci_low\": -11.106,\n \"ci_high\": 87.3368,\n \"hc3_std_err\": 20.5264,\n \"hc3_z\": 1.857,\n \"hc3_p\": 0.06333,\n \"hc3_ci_low\": -2.11552,\n \"hc3_ci_high\": 78.3463,\n \"evidence\": \"none\",\n \"vif\": null\n },\n {\n \"term\": \"C(neighborhood)[T.Old Town]\",\n \"coef\": 40.0961,\n \"std_err\": 11.3266,\n \"t\": 3.54,\n \"p\": 0.0008048,\n \"ci_low\": 17.4149,\n \"ci_high\": 62.7773,\n \"hc3_std_err\": 12.3518,\n \"hc3_z\": 3.246,\n \"hc3_p\": 0.00117,\n \"hc3_ci_low\": 15.8871,\n \"hc3_ci_high\": 64.3051,\n \"evidence\": \"clear\",\n \"vif\": 1.186\n },\n \"... 4 more\"\n ],\n \"outcome\": \"{...}\",\n \"predictors\": \"[...]\",\n \"diagnostics\": \"{...}\",\n \"assumption_checks\": [\n {\n \"check\": \"constant_variance\",\n \"status\": \"violated\",\n \"basis\": \"Breusch-Pagan p 0.0152\"\n },\n {\n \"check\": \"normal_residuals\",\n \"status\": \"violated\",\n \"basis\": \"Jarque-Bera p 0.0379, 63 rows\"\n },\n {\n \"check\": \"independence\",\n \"status\": \"holds\",\n \"basis\": \"Durbin-Watson 1.61765 (meaningful only if the rows are in time or sequence order)\"\n },\n {\n \"check\": \"collinearity\",\n \"status\": \"holds\",\n \"basis\": \"largest VIF 1.79386, condition number 8691.93\"\n },\n {\n \"check\": \"influence\",\n \"status\": \"holds\",\n \"basis\": \"3 row(s) with Cook's distance above 4/n, 0 above 1\"\n },\n {\n \"check\": \"outcome_type\",\n \"status\": \"holds\",\n \"basis\": \"63 distinct outcome values\"\n }\n ],\n \"influence\": \"{...}\",\n \"flags\": \"[...]\",\n \"browser_verdict\": \"fixable\",\n \"clipped\": \"[...]\"\n}"
}
The saved reply the page replays for free (the model's JSON, pretty-printed):
{
"lane": "interpret",
"verdict": "fixable",
"headline": "The fit finds clear, positive associations for Old Town and Riverside location, floor area and bedroom count, and a clear negative association for age, but heteroskedastic and non-normal residuals mean the HC3 standard errors reported alongside the classical ones are the safer reference.",
"tldr": [
"Answer: the beds coefficient is 25.8902 (95% CI 10.7816 to 40.9988) in price_k, close to your 26 figure, holding neighborhood, sqft and age_yr fixed; it is an association, not a causal worth, since beds was not randomly assigned.",
"All five non-intercept terms (Old Town, Riverside, sqft, beds, age_yr) show clear evidence under both the classical and HC3 standard errors.",
"Breusch-Pagan (p 0.0152) and Jarque-Bera (p 0.0379) flag non-constant variance and non-normal residuals, so the HC3 standard errors should be treated as primary.",
"Three rows (R1, R2, R3) sit above the Cook's distance threshold of 0.0634921 and one row (R4) has a studentized residual of 3.12; these are worth a data check before the fit is trusted."
],
"coefficients": [
{
"term": "C(neighborhood)[T.Old Town]",
"evidence": "clear",
"reading": "Holding sqft, beds and age_yr fixed, a home in Old Town goes with a price_k 40.0961 higher than a comparable Eastside home (95% CI 17.4149 to 62.7773)."
},
{
"term": "C(neighborhood)[T.Riverside]",
"evidence": "clear",
"reading": "Holding sqft, beds and age_yr fixed, a home in Riverside goes with a price_k 33.1208 higher than a comparable Eastside home (95% CI 9.32647 to 56.9152)."
},
{
"term": "sqft",
"evidence": "clear",
"reading": "Holding neighborhood, beds and age_yr fixed, each extra square foot of interior floor area goes with 0.0723654 more in price_k (95% CI 0.0367393 to 0.107992)."
},
{
"term": "beds",
"evidence": "clear",
"reading": "Holding neighborhood, sqft and age_yr fixed, each additional bedroom goes with 25.8902 more in price_k (95% CI 10.7816 to 40.9988)."
},
{
"term": "age_yr",
"evidence": "clear",
"reading": "Holding neighborhood, sqft and beds fixed, each additional year of age goes with 0.482497 less in price_k (95% CI -0.902588 to -0.0624058)."
}
],
"assumptions": [
{
"check": "constant_variance",
"status": "violated",
"meaning": "Breusch-Pagan LM 14.0644 (p 0.0152) shows the residual spread changes with the predictors, so the classical standard errors understate uncertainty; the HC3 standard errors and intervals reported alongside are the safer reference for all five terms."
},
{
"check": "normal_residuals",
"status": "violated",
"meaning": "Jarque-Bera 6.54488 (p 0.0379, skew 0.672321, kurtosis 3.82778) over 63 rows shows the residuals depart from normality, so the classical t-based p-values and intervals rest on a shakier approximation."
},
{
"check": "independence",
"status": "holds",
"meaning": "Durbin-Watson 1.61765 does not flag serial correlation, but this only matters if the 63 rows are in time or sequence order; if they are not, this check does not apply to the fit."
},
{
"check": "collinearity",
"status": "holds",
"meaning": "The largest VIF is 1.79386 (sqft) and the condition number is 8691.93; VIFs this low mean the standard errors are not inflated by collinearity even though predictors sit on different scales."
},
{
"check": "influence",
"status": "holds",
"meaning": "3 rows (R1, R2, R3) sit above a Cook's distance of 4/n = 0.0634921 and 0 are above 1, so overall influence is judged to hold, though those rows are worth a closer look."
},
{
"check": "outcome_type",
"status": "holds",
"meaning": "63 distinct values for price_k over 63 rows support a continuous-outcome OLS rather than a binary or count model."
}
],
"claims": [
{
"claim": "Bedrooms add value on top of size, so the client can be told more bedrooms raises the price independent of square footage.",
"support": "partly",
"why": "The beds coefficient is 25.8902 (95% CI 10.7816 to 40.9988), evidence clear under both classical and HC3 standard errors, holding neighborhood, sqft and age_yr fixed, so more bedrooms does go with a higher price_k in these 63 homes. But beds was not randomly assigned, so this is an association, not the causal add value the client framing implies."
},
{
"claim": "Each extra bedroom is worth about 26k once size is accounted for.",
"support": "partly",
"why": "The beds coefficient, 25.8902 (95% CI 10.7816 to 40.9988) in price_k, is close to 26 and clear under both standard errors, holding sqft, neighborhood and age_yr fixed. The magnitude matches, but worth implies a causal value that this observational fit cannot establish."
}
],
"cautions": [
"These are associations among the 63 homes used, not causal effects; neighborhood, size, bedroom count and age were not randomly assigned.",
"The fit only covers sqft from 852 to 2612, beds from 1 to 5, and age_yr from 3 to 80; it says nothing about homes outside those ranges.",
"Other features that affect price, such as lot size, condition, finishes or school zone, are not in the model and could be omitted variables behind the neighborhood, size or bedroom associations.",
"price_k is the recorded 2025 sale price in thousands of dollars for these homes; the fit says nothing about list price, appraisal value or price in other years."
],
"next_steps": [
"Treat the HC3 standard errors as primary given the Breusch-Pagan flag (F1): all five terms stay clear evidence under HC3, but the classical errors alone are not reliable here.",
"Check rows R1 (line 33, H032), R2 (line 65, H064) and R3 (line 17, H016) for data-entry errors given their Cook's distance above 0.0634921 (F4).",
"Check row R4 (line 64, H063) given its studentized residual of 3.12 (F5).",
"Confirm whether the one dropped row (F6) is missing at random before treating the fit as representative of all 64 sales.",
"When telling the client about bedrooms, present 25.8902 (95% CI 10.7816 to 40.9988) as an association holding sqft, neighborhood and age_yr fixed, not as a guaranteed per-bedroom price."
],
"prescan_responses": [
{
"ref": "F1",
"verdict": "confirmed",
"note": "Breusch-Pagan LM 14.0644 (p 0.0152) shows non-constant residual variance; use the HC3 standard errors and intervals reported alongside the classical ones as the primary reference."
},
{
"ref": "F2",
"verdict": "confirmed",
"note": "Jarque-Bera 6.54488 (p 0.0379, skew 0.672321, kurtosis 3.82778) shows non-normal residuals over 63 rows; read the classical t-based p-values and intervals with that in mind."
},
{
"ref": "F3",
"verdict": "dismissed",
"note": "Condition number 8691.93 is high, but the largest VIF is only 1.79386 and assumption_checks marks collinearity as holds, so this reflects predictors on different scales, not collinearity distorting the coefficients."
},
{
"ref": "F4",
"verdict": "confirmed",
"note": "3 rows (R1 line 33 H032, R2 line 65 H064, R3 line 17 H016) exceed the Cook's distance threshold of 0.0634921; worth checking for data-entry errors."
},
{
"ref": "F5",
"verdict": "confirmed",
"note": "R4 (line 64, H063) has a studentized residual of 3.12, beyond the 3 threshold; worth checking this row."
},
{
"ref": "F6",
"verdict": "confirmed",
"note": "1 row was dropped for a missing value used in the formula; the context does not say whether that row differs systematically from the rest, so this cannot be dismissed."
}
]
}
Worked example: script
The same fit with the reading above handed over as decision. The body (facts and decision abbreviated):
{
"task": "script",
"title": "Home sales, three neighborhoods, 2025",
"context": "Sale prices in thousands of dollars for single-family homes sold in 2025 in three neighborhoods. sqft is interior floor area, age_yr is years since the house was built. I want to tell the client that bedrooms add value on top of size.",
"question": "",
"decision": "Verdict: fixable.\nThe fit finds clear, positive associations for Old Town and Riverside location, floor area and bedroom count, and a clear negative association for age, but heteroskedastic and non-normal residuals mean the HC3 standard errors reported alongside the classical ones are the safer refe ...",
"facts": "JSON string of: {\n \"settings\": {\n \"formula\": \"price_k ~ C(neighborhood) + sqft + beds + age_yr\",\n \"formula_source\": \"user\",\n \"outcome\": \"price_k\",\n \"outcome_transform\": null,\n \"intercept\": true,\n \"rows_in_table\": 64,\n \"rows_used\": 63,\n \"rows_dropped_missing\": 1,\n \"confidence\": 0.95,\n \"robust\": \"HC3 (z statistics, as statsmodels reports cov_type='HC3')\"\n },\n \"fit\": {\n \"nobs\": 63,\n \"df_model\": 5,\n \"df_resid\": 57,\n \"r_squared\": 0.6672,\n \"adj_r_squared\": 0.638,\n \"f_statistic\": 22.85,\n \"f_pvalue\": 1.649e-12,\n \"log_likelihood\": -314.226,\n \"aic\": 640.452,\n \"bic\": 653.311,\n \"residual_std_error\": 37.2934\n },\n \"coefficients\": [\n {\n \"term\": \"Intercept\",\n \"coef\": 38.1154,\n \"std_err\": 24.5804,\n \"t\": 1.551,\n \"p\": 0.1265,\n \"ci_low\": -11.106,\n \"ci_high\": 87.3368,\n \"hc3_std_err\": 20.5264,\n \"hc3_z\": 1.857,\n \"hc3_p\": 0.06333,\n \"hc3_ci_low\": -2.11552,\n \"hc3_ci_high\": 78.3463,\n \"evidence\": \"none\",\n \"vif\": null\n },\n {\n \"term\": \"C(neighborhood)[T.Old Town]\",\n \"coef\": 40.0961,\n \"std_err\": 11.3266,\n \"t\": 3.54,\n \"p\": 0.0008048,\n \"ci_low\": 17.4149,\n \"ci_high\": 62.7773,\n \"hc3_std_err\": 12.3518,\n \"hc3_z\": 3.246,\n \"hc3_p\": 0.00117,\n \"hc3_ci_low\": 15.8871,\n \"hc3_ci_high\": 64.3051,\n \"evidence\": \"clear\",\n \"vif\": 1.186\n },\n \"... 4 more\"\n ],\n \"outcome\": \"{...}\",\n \"predictors\": \"[...]\",\n \"diagnostics\": \"{...}\",\n \"assumption_checks\": [\n {\n \"check\": \"constant_variance\",\n \"status\": \"violated\",\n \"basis\": \"Breusch-Pagan p 0.0152\"\n },\n {\n \"check\": \"normal_residuals\",\n \"status\": \"violated\",\n \"basis\": \"Jarque-Bera p 0.0379, 63 rows\"\n },\n {\n \"check\": \"independence\",\n \"status\": \"holds\",\n \"basis\": \"Durbin-Watson 1.61765 (meaningful only if the rows are in time or sequence order)\"\n },\n {\n \"check\": \"collinearity\",\n \"status\": \"holds\",\n \"basis\": \"largest VIF 1.79386, condition number 8691.93\"\n },\n {\n \"check\": \"influence\",\n \"status\": \"holds\",\n \"basis\": \"3 row(s) with Cook's distance above 4/n, 0 above 1\"\n },\n {\n \"check\": \"outcome_type\",\n \"status\": \"holds\",\n \"basis\": \"63 distinct outcome values\"\n }\n ],\n \"influence\": \"{...}\",\n \"flags\": \"[...]\",\n \"browser_verdict\": \"fixable\",\n \"clipped\": \"[...]\"\n}"
}
The saved reply. Its script was run with statsmodels 0.14.6 on the example table: the reproduction check matched every coefficient and the HC3 refit ran.
{
"lane": "script",
"verdict": "fixable",
"headline": "The script reproduces the browser's OLS fit of price_k on neighborhood, sqft, beds and age_yr for the 63 rows used and applies HC3 robust standard errors plus sensitivity refits of the flagged rows, keeping the fixable verdict.",
"tldr": [
"The script reproduces the OLS fit of price_k ~ C(neighborhood) + sqft + beds + age_yr on the 63 rows used and checks each coefficient against EXPECTED with numpy.isclose.",
"It refits with cov_type HC3 because Breusch-Pagan LM 14.0644 (p 0.0152) found heteroskedasticity (F1).",
"It refits without R1 (line 33, H032), R2 (line 65, H064) and R3 (line 17, H016), the rows with Cooks distance above 0.0634921 (F4), to see whether the estimates move.",
"It also refits without R4 (line 64, H063), which has a studentized residual of 3.12 (F5), and keeps the base fit for comparison."
],
"fixes": [
{
"fix": "Refit with heteroskedasticity-robust standard errors (cov_type HC3) for all coefficients",
"why": "Breusch-Pagan LM 14.0644 (p 0.0152) shows non-constant residual variance, so classical standard errors understate uncertainty.",
"refs": "F1"
},
{
"fix": "Refit dropping rows R1 (line 33, H032), R2 (line 65, H064) and R3 (line 17, H016), the points with Cooks distance above 0.0634921, and compare coefficients to the full-sample fit",
"why": "These rows exceed the influence threshold and could be data-entry errors distorting the estimates.",
"refs": "F4"
},
{
"fix": "Refit dropping row R4 (line 64, H063), which has a studentized residual of 3.12, and compare coefficients to the full-sample fit",
"why": "A studentized residual beyond the threshold of 3 flags a poorly fit observation worth checking for a data-entry error.",
"refs": "F5"
}
],
"script": "# Reproduces the OLS fit the browser computed for price_k ~ C(neighborhood) + sqft + beds + age_yr\n# and applies the fixes chosen for this fixable verdict.\n\nimport pandas as pd\nimport numpy as np\nimport statsmodels.formula.api as smf\nfrom statsmodels.stats.outliers_influence import variance_inflation_factor, OLSInfluence\nfrom statsmodels.stats.diagnostic import het_breuschpagan\n\nDATA_PATH = 'data.csv' # AUTHOR_INPUT_NEEDED: confirm this is the pasted table saved under this name\nFORMULA = 'price_k ~ C(neighborhood) + sqft + beds + age_yr'\n\ndf = pd.read_csv(DATA_PATH, skipinitialspace=True)\n\nbase_fit = smf.ols(FORMULA, data=df).fit()\nprint(base_fit.summary())\n\nEXPECTED = {\n 'Intercept': 38.1154,\n 'C(neighborhood)[T.Old Town]': 40.0961,\n 'C(neighborhood)[T.Riverside]': 33.1208,\n 'sqft': 0.0723654,\n 'beds': 25.8902,\n 'age_yr': -0.482497,\n}\n\nprint('Reproduction check (rtol=1e-4):')\nall_ok = True\nfor term, expected_coef in EXPECTED.items():\n got = base_fit.params.get(term)\n ok = got is not None and np.isclose(got, expected_coef, rtol=1e-4)\n if not ok:\n all_ok = False\n print('MISMATCH:', term, 'expected', expected_coef, 'got', got)\nprint('All terms matched EXPECTED.' if all_ok else 'See MISMATCH lines above.')\n\nprint('Variance inflation factors:')\ndesign = base_fit.model.exog\ndesign_names = base_fit.model.exog_names\nfor i, name in enumerate(design_names):\n if name == 'Intercept':\n continue\n print(name, variance_inflation_factor(design, i))\n\nprint('Breusch-Pagan test (formula has an intercept):')\nbp_lm, bp_p, bp_f, bp_f_p = het_breuschpagan(base_fit.resid, base_fit.model.exog)\nprint('LM:', bp_lm, 'p:', bp_p, 'F:', bp_f, 'F p:', bp_f_p)\n\nprint('Cooks distance (top rows):')\ninfluence = OLSInfluence(base_fit)\ncooks_d = influence.cooks_distance[0]\ntop_cooks = pd.Series(cooks_d, index=df.index).sort_values(ascending=False).head(8)\nprint(top_cooks)\n\n# Fix 1 (F1): heteroskedasticity-robust standard errors\nhc3_fit = smf.ols(FORMULA, data=df).fit(cov_type='HC3')\nprint('HC3 refit summary:')\nprint(hc3_fit.summary())\n\n# Fix 2 (F4): sensitivity refit dropping the three high-Cooks-distance rows\n# R1 line 33 (H032, row_index 31), R2 line 65 (H064, row_index 63), R3 line 17 (H016, row_index 15)\ndrop_influential = [31, 63, 15]\ndf_no_influential = df.drop(index=drop_influential)\nrefit_no_influential = smf.ols(FORMULA, data=df_no_influential).fit()\nprint('Refit without R1, R2, R3 (compare to base_fit above):')\nprint(refit_no_influential.summary())\n\n# Fix 3 (F5): sensitivity refit dropping the studentized-residual outlier\n# R4 line 64 (H063, row_index 62)\ndrop_outlier = [62]\ndf_no_outlier = df.drop(index=drop_outlier)\nrefit_no_outlier = smf.ols(FORMULA, data=df_no_outlier).fit()\nprint('Refit without R4 (compare to base_fit above):')\nprint(refit_no_outlier.summary())\n",
"assumptions": [
"The pasted table is saved as data.csv in the scripts working directory with a header row containing neighborhood, sqft, beds, age_yr and price_k.",
"neighborhood is read as text so C(neighborhood) treats it as categorical, and sqft, beds, age_yr and price_k are numeric.",
"The CSV is the same table the browser fitted, including the one row with a missing value in a formula column, so pandas and statsmodels drop it the same way."
],
"checks": [
"That the reproduction check prints no MISMATCH line against EXPECTED.",
"Whether the HC3 summary keeps all five terms significant, matching the classical fits clear evidence.",
"Whether dropping R1, R2, R3 or R4 shifts the beds or sqft coefficients enough to change any terms evidence.",
"Whether the Breusch-Pagan LM and the sqft VIF the script prints match 14.0644 and 1.794 reported here."
],
"next_steps": [
"Run the script against the pasted table saved as data.csv, confirm the reproduction check prints no mismatch, and compare the classical and HC3 summaries to confirm all five terms stay clear under HC3.",
"Inspect rows R1 (line 33, H032), R2 (line 65, H064) and R3 (line 17, H016) for data-entry errors and see whether excluding them shifts the beds or sqft coefficients.",
"Inspect row R4 (line 64, H063) for a data-entry error given its studentized residual of 3.12.",
"Confirm whether the one dropped row (F6) is missing at random before treating the fit as representative of all 64 sales.",
"When telling the client that bedrooms add value on top of size, present the beds coefficient of 25.8902 (95% CI 10.7816 to 40.9988) as an association holding neighborhood, sqft and age_yr fixed, not as a guaranteed per-bedroom price."
],
"prescan_responses": [
{
"ref": "F1",
"verdict": "confirmed",
"note": "Breusch-Pagan LM 14.0644 (p 0.0152) shows non-constant residual variance, so the script fits with cov_type HC3 as the primary reference."
},
{
"ref": "F2",
"verdict": "confirmed",
"note": "Jarque-Bera 6.54488 (p 0.0379, skew 0.672321, kurtosis 3.82778) shows the residuals are not normal, so with 63 rows the classical t-based inference rests on a shakier approximation."
},
{
"ref": "F3",
"verdict": "dismissed",
"note": "Condition number 8691.93 is high but the max VIF is only 1.794 and collinearity holds, so this reflects predictors on different scales rather than collinearity needing a fix."
},
{
"ref": "F4",
"verdict": "confirmed",
"note": "Three rows (R1, R2, R3) exceed the Cooks distance threshold of 0.0634921, so the script refits without them to check whether the estimates move."
},
{
"ref": "F5",
"verdict": "confirmed",
"note": "Row R4 has a studentized residual of 3.12, beyond the threshold of 3, so the script refits without it as a sensitivity check."
},
{
"ref": "F6",
"verdict": "confirmed",
"note": "One row was dropped for a missing value the formula uses, and whether it is missing at random cannot be settled from the facts, so the fit is representative of all 64 sales only if that holds."
}
]
}
Truncation and partial results
If your balance sits between min_credits and hold_credits, the run still
executes with a smaller output cap and the job carries "truncated": true. The JSON may
then stop mid-object: close it (the page's Recon.closeJson does this) and show the
sections that arrived, saying how many of the lane's sections were recovered, rather than treating
a clipped reply as complete.