← Regress Desk / API
Tokens

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

taskwhat you getextra input
interpretA 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
scriptThe 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.

fieldrequiredmeaning
taskyesinterpret or script.
factsyesA JSON-encoded string with the browser's fit - see below. Build it with OlsKit.buildInput.
titlenoA label for the analysis, up to 160 characters.
contextnoYour notes: what the columns mean, units, how the data were collected, what you want to conclude. Up to 3,000 characters.
decisionscript onlyPlain text of an earlier interpret run (the page builds it with Recon.decisionText). Up to 6,000 characters.
questionnoAnswered in tldr as a bullet starting "Answer:". Up to 1,200 characters.
retry_notenoOnly 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

statuscodewhat to do
400validation_errorA field is missing or the wrong type. Every field is a string: facts must be a JSON-encoded string, not an object.
401unauthorizedThe token is missing, malformed or expired. Get a new one from the token page.
402payment_requiredThe balance is below min_credits. Call /estimate first and top up.
403forbiddenThe 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.
404not_foundUnknown job id, or the app slug does not exist.
409conflictThe same Idempotency-Key was replayed with a different body. Change the key or send the original input.
429rate_limitedToo many requests. Back off and retry; do not tight-loop.
5xxinternalA 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
}

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"}}

3. Check the session and the balance

call me
# {"ok":true,"data":{"subject_type":"user","username":"you","credits":51234}}

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.

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

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}

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"])'

Invariants worth asserting

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.