Formula Audit — API

Paste the formulas, get the spreadsheet audit.

API tokens Open the app

Run the spreadsheet audit from your own tools

Send the formula view of a block of cells — the text you get from Excel's Show Formulas (Ctrl+`), or a CSV/TSV export — and get back one JSON object: every defect located by its real A1 address with the problem, the evidence exactly as it appears in your paste and the corrected formula; the integrity checks run explicitly and reported pass, fail, unknown or not-applicable; a clean / fix-first / broken verdict; and a line-by-line reconciliation of every flag your own deterministic scan raised. Everything this app does goes through the SkillSafe App API — plain JSON over HTTPS — so you can wire it to the export step of a reporting pipeline, run it over a folder of workbooks in CI, or gate a model handover on the verdict. Every code step below is shown in cURL, Python, JavaScript, Go, Java, Ruby, PHP and C#; pick a language once and the whole page follows.

Basics

Base URL: https://api.skillsafe.ai/v1/app-api, app slug formula-audit. Every request sends Authorization: Bearer <token> and JSON bodies with Content-Type: application/json. Responses are wrapped in an envelope: {"data": …} on success, {"error": {"code", "message"}} on failure. The audit is produced by the gpt-terra model. Estimates are free; runs are metered against your credit balance. There is a single run task — one grid in, one audit out, no follow-up calls and no session state to carry.

StatusMeaning
401Missing or expired token — create a new session.
402Not enough credits — top up at skillsafe.ai/account/credits.
403The token isn't allowed to do this (e.g. a guest submitting a very large grid).
404Unknown job or record id.
5xxTransient platform error — retry with backoff.

Browsers enforce CORS for this API, so run these examples from a server, script or terminal — not from another website's frontend.

Step 0 — A tiny client

Every task below is a single HTTP call, so start with a short helper that adds the auth header, sends JSON and unwraps the data envelope. The later steps reuse it.

export API="https://api.skillsafe.ai/v1/app-api"
export TOKEN="YOUR_TOKEN"      # see step 1

# every call looks like:
#   curl -s "$API/..." -H "Authorization: Bearer $TOKEN" [-d '{json}']
# jq is used below to pull fields out of the {"data": ...} envelope
import json, requests

API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN"  # see step 1 — read it from your shell environment in real code

def api(method, path, body=None, **headers):
    res = requests.request(method, API + path, json=body,
                           headers={"Authorization": f"Bearer {TOKEN}", **headers})
    payload = res.json()
    if not res.ok:
        raise RuntimeError(payload.get("error", {}).get("message", res.reason))
    return payload["data"]
// Node 18+ (built-in fetch)
const API = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN"; // see step 1 — read it from your shell environment in real code

async function api(method, path, body, extraHeaders = {}) {
  const res = await fetch(API + path, {
    method,
    headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json", ...extraHeaders },
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  const json = await res.json();
  if (!res.ok) throw new Error(json.error?.message ?? res.statusText);
  return json.data;
}
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
)

const API = "https://api.skillsafe.ai/v1/app-api"

var token = os.Getenv("SKILLSAFE_TOKEN") // see step 1

func call(method, path string, body, out any) error {
	var buf bytes.Buffer
	if body != nil {
		json.NewEncoder(&buf).Encode(body)
	}
	req, _ := http.NewRequest(method, API+path, &buf)
	req.Header.Set("Authorization", "Bearer "+token)
	req.Header.Set("Content-Type", "application/json")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer res.Body.Close()
	var env struct {
		Data  json.RawMessage `json:"data"`
		Error *struct{ Message string `json:"message"` } `json:"error"`
	}
	json.NewDecoder(res.Body).Decode(&env)
	if res.StatusCode >= 400 {
		return fmt.Errorf("api %s %s: %s", method, path, env.Error.Message)
	}
	if out == nil {
		return nil
	}
	return json.Unmarshal(env.Data, out)
}
// Java 17+, no dependencies. Pair with your JSON library (Jackson, Gson…)
// to read fields out of the returned envelope.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class SkillSafe {
    static final String API = "https://api.skillsafe.ai/v1/app-api";
    static final String TOKEN = System.getenv("SKILLSAFE_TOKEN"); // see step 1
    static final HttpClient HTTP = HttpClient.newHttpClient();

    static String api(String method, String path, String jsonBody) throws Exception {
        var req = HttpRequest.newBuilder(URI.create(API + path))
            .header("Authorization", "Bearer " + TOKEN)
            .header("Content-Type", "application/json")
            .method(method, jsonBody == null
                ? HttpRequest.BodyPublishers.noBody()
                : HttpRequest.BodyPublishers.ofString(jsonBody))
            .build();
        var res = HTTP.send(req, HttpResponse.BodyHandlers.ofString());
        if (res.statusCode() >= 400) throw new RuntimeException(res.body());
        return res.body(); // envelope: {"data": …}
    }
}
require "net/http"
require "json"

API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = ENV.fetch("SKILLSAFE_TOKEN") # see step 1

def api(method, path, body = nil)
  uri = URI(API + path)
  req = Net::HTTP.const_get(method.capitalize).new(uri)
  req["Authorization"] = "Bearer #{TOKEN}"
  req["Content-Type"] = "application/json"
  req.body = body.to_json if body
  res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
  payload = JSON.parse(res.body)
  raise (payload.dig("error", "message") || res.message) unless res.is_a?(Net::HTTPSuccess)
  payload["data"]
end
<?php
const API = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = getenv("SKILLSAFE_TOKEN"); // see step 1

function api(string $method, string $path, ?array $body = null): mixed {
    global $TOKEN;
    $ch = curl_init(API . $path);
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST  => $method,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER     => [
            "Authorization: Bearer $TOKEN",
            "Content-Type: application/json",
        ],
        CURLOPT_POSTFIELDS     => $body === null ? null : json_encode($body),
    ]);
    $payload = json_decode(curl_exec($ch), true);
    $status  = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);
    if ($status >= 400) {
        throw new Exception($payload["error"]["message"] ?? "HTTP $status");
    }
    return $payload["data"];
}
// .NET 8+
using System.Net.Http.Json;
using System.Text.Json;

static class SkillSafe
{
    const string Api = "https://api.skillsafe.ai/v1/app-api";
    static readonly HttpClient Http = new();

    static SkillSafe() =>
        Http.DefaultRequestHeaders.Authorization =
            new("Bearer", Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN")); // see step 1

    public static async Task<JsonElement> ApiAsync(HttpMethod method, string path, object? body = null)
    {
        var req = new HttpRequestMessage(method, Api + path);
        if (body != null) req.Content = JsonContent.Create(body);
        var res = await Http.SendAsync(req);
        var json = await res.Content.ReadFromJsonAsync<JsonElement>();
        if (!res.IsSuccessStatusCode)
            throw new Exception(json.GetProperty("error").GetProperty("message").GetString());
        return json.GetProperty("data");
    }
}

Step 1 — Get a token

POST /guest

A guest token lets you check balances and estimate costs for free. For metered audit runs billed to your own account, use your personal token: open the token page, sign in with SkillSafe, and press Copy shell export — it puts export SKILLSAFE_TOKEN="…" on your clipboard, which every example below reads. Treat the token like a password: it can spend your credits. For fully headless scripts, POST /guest mints a guest token with no browser involved.

curl -s -X POST "$API/guest" \
  -H "Content-Type: application/json" \
  -d '{"slug":"formula-audit"}' | jq -r '.data.token'
token = api("POST", "/guest", {"slug": "formula-audit"})["token"]
const { token } = await api("POST", "/guest", { slug: "formula-audit" });
var guest struct{ Token string `json:"token"` }
err := call("POST", "/guest", map[string]string{"slug": "formula-audit"}, &guest)
String envelope = api("POST", "/guest", """
    {"slug":"formula-audit"}""");
// token is at data.token in the returned JSON
token = api("POST", "/guest", { slug: "formula-audit" })["token"]
$token = api("POST", "/guest", ["slug" => "formula-audit"])["token"];
var guest = await SkillSafe.ApiAsync(HttpMethod.Post, "/guest",
    new { slug = "formula-audit" });
var token = guest.GetProperty("token").GetString();

The app stores this browser's token under the localStorage key skillsafe_app_token:formula-audit, on the app's own origin. The token page reads and manages it for you — you never need to open developer tools.

Step 2 — Check who you are and your balance

GET /me

Returns subject_type ("user" or "guest"), subject_id and your credits balance. Check this before sending a large grid.

curl -s "$API/me" -H "Authorization: Bearer $TOKEN" | jq '.data'
me = api("GET", "/me")
print(me["subject_type"], me["credits"])
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
var me struct {
	SubjectType string `json:"subject_type"`
	Credits     int64  `json:"credits"`
}
err := call("GET", "/me", nil, &me)
String envelope = api("GET", "/me", null);
// data.subject_type, data.credits
me = api("GET", "/me")
puts "#{me["subject_type"]}: #{me["credits"]} credits"
$me = api("GET", "/me");
echo "{$me['subject_type']}: {$me['credits']} credits\n";
var me = await SkillSafe.ApiAsync(HttpMethod.Get, "/me");
Console.WriteLine($"{me.GetProperty("subject_type")}: {me.GetProperty("credits")} credits");

Step 3 — Estimate the cost

POST /estimate

Send exactly the input you would send to /run; the response's hold_credits is the worst-case cost. Nothing is charged and no job is created, so estimating is free — useful when you are piping a wide model in and want a ceiling before spending credits.

Input fieldTypeNotes
gridstring, requiredThe pasted block of cells: rows separated by newlines, columns by tabs (or commas for a CSV paste). A cell beginning with = is a formula; anything else is a typed constant, a label or an error value. This is the formula view — what Excel shows under Show Formulas (Ctrl+`) — not the results view; send results and there is nothing to audit. Very large grids should be clipped on whole-row boundaries with an explicit [... N rows omitted ...] marker where the cut is, never mid-row.
anchorstringThe real-sheet address of the block's top-left cell, e.g. A1 or B7. Every address in the reply is computed from it, so the cells named in findings match your own sheet. Defaults to A1 when absent or unparseable.
audit_scopestringwhole (formulas and the logic they express) | formulas (mechanics only — no opinions on the business meaning) | model (financial-model integrity: balance, tie-out, roll-forward, sign conventions). Only model adds the statement-level integrity checks.
sheet_purposestringFree text describing what the sheet is meant to do; may be the empty string. When present it is the strongest evidence the model has about what the numbers should mean — it is what turns a formula check into an audit.
current_datetimestringYour current date, ISO 8601 with offset plus the weekday in parentheses: 2026-08-05T14:12:00-04:00 (Wednesday). Used whenever a date in the sheet matters — a period that has not closed yet, a rate effective from a future date.
prescan_findingsarrayWhat your own deterministic scan matched in the same grid: {id, cell, severity, label} objects. Ids are family:cell — range-short:E7, inconsistent:E5, hardcode:E2, err:, circular:, overlap:, external:, volatile:, lookup:, plug:, emptyref:. Every id you send comes back exactly once in coverage_check. The web UI fills this from its browser-side parser; API callers may send [].
prescan_statsobject{cells, formulas, constants, labels, error_cells, distinct_functions, rows_profiled, rows_sent, functions_used[]}. Counts from the same scan. rows_profiled vs rows_sent tells the audit how much of the sheet was clipped away, so it can ask about what it cannot see instead of assuming.
retry_notestring, optionalOnly set by the app's automatic reformat retry when a first reply was not valid JSON. Leave it out.
# The formula view of a small commission sheet. Columns are separated by TABS;
# rows by newlines. Keep the labels and the total row — they are the evidence.
cat > grid.tsv <<'GRID'
Rep	Sales	Quota	Over quota	Commission
A. Diaz	120000	100000	=B2-C2	=D2*0.04
R. Okafor	98000	100000	=B3-C3	=D3*0.04
T. Lin	145000	110000	=B4-C4	=D4*0.04
M. Haddad	132000	100000	=B5-C5	=D5*0.045
S. Novak	104000	100000	=B6-C6	=D6*0.04
Total			=SUM(D2:D6)	=SUM(E2:E5)
GRID

jq -n --rawfile grid grid.tsv \
  '{grid: $grid,
    anchor: "A1",
    audit_scope: "whole",
    sheet_purpose: "Monthly commission run. B is booked sales, C is quota, D the amount over quota, E commission at 4% of the excess. E7 feeds the payroll upload and came out light this month.",
    current_datetime: "2026-08-05T14:12:00-04:00 (Wednesday)",
    prescan_findings: [
      {id: "range-short:E7", cell: "E7", severity: "high",
       label: "=SUM(E2:E5) covers 4 of the 5 rows in E2:E6"},
      {id: "inconsistent:E5", cell: "E5", severity: "high",
       label: "E5 uses 0.045 where E2:E4 and E6 use 0.04"},
      {id: "hardcode:E2", cell: "E2", severity: "high",
       label: "0.04 typed inside the formula, repeated down E2:E6"}
    ],
    prescan_stats: {cells: 33, formulas: 12, constants: 10, labels: 11,
                    error_cells: 0, distinct_functions: 1,
                    rows_profiled: 7, rows_sent: 7, functions_used: ["SUM"]}}' > input.json

curl -s -X POST "$API/estimate" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d @input.json | jq '.data.hold_credits'
GRID = "\n".join("\t".join(row) for row in [
    ["Rep",       "Sales",  "Quota",  "Over quota",  "Commission"],
    ["A. Diaz",   "120000", "100000", "=B2-C2",      "=D2*0.04"],
    ["R. Okafor", "98000",  "100000", "=B3-C3",      "=D3*0.04"],
    ["T. Lin",    "145000", "110000", "=B4-C4",      "=D4*0.04"],
    ["M. Haddad", "132000", "100000", "=B5-C5",      "=D5*0.045"],
    ["S. Novak",  "104000", "100000", "=B6-C6",      "=D6*0.04"],
    ["Total",     "",       "",       "=SUM(D2:D6)", "=SUM(E2:E5)"],
])

payload = {
    "grid": GRID,
    "anchor": "A1",
    "audit_scope": "whole",
    "sheet_purpose": (
        "Monthly commission run. B is booked sales, C is quota, D the amount over "
        "quota, E commission at 4% of the excess. E7 feeds the payroll upload and "
        "came out light this month."
    ),
    "current_datetime": "2026-08-05T14:12:00-04:00 (Wednesday)",
    "prescan_findings": [
        {"id": "range-short:E7", "cell": "E7", "severity": "high",
         "label": "=SUM(E2:E5) covers 4 of the 5 rows in E2:E6"},
        {"id": "inconsistent:E5", "cell": "E5", "severity": "high",
         "label": "E5 uses 0.045 where E2:E4 and E6 use 0.04"},
        {"id": "hardcode:E2", "cell": "E2", "severity": "high",
         "label": "0.04 typed inside the formula, repeated down E2:E6"},
    ],
    "prescan_stats": {
        "cells": 33, "formulas": 12, "constants": 10, "labels": 11,
        "error_cells": 0, "distinct_functions": 1,
        "rows_profiled": 7, "rows_sent": 7, "functions_used": ["SUM"],
    },
}

est = api("POST", "/estimate", payload)
print("worst case:", est.get("hold_credits", est.get("credits")), "credits")
const grid = [
  ["Rep",       "Sales",  "Quota",  "Over quota",  "Commission"],
  ["A. Diaz",   "120000", "100000", "=B2-C2",      "=D2*0.04"],
  ["R. Okafor", "98000",  "100000", "=B3-C3",      "=D3*0.04"],
  ["T. Lin",    "145000", "110000", "=B4-C4",      "=D4*0.04"],
  ["M. Haddad", "132000", "100000", "=B5-C5",      "=D5*0.045"],
  ["S. Novak",  "104000", "100000", "=B6-C6",      "=D6*0.04"],
  ["Total",     "",       "",       "=SUM(D2:D6)", "=SUM(E2:E5)"],
].map((row) => row.join("\t")).join("\n");

const payload = {
  grid,
  anchor: "A1",
  audit_scope: "whole",
  sheet_purpose:
    "Monthly commission run. B is booked sales, C is quota, D the amount over quota, " +
    "E commission at 4% of the excess. E7 feeds the payroll upload and came out light this month.",
  current_datetime: "2026-08-05T14:12:00-04:00 (Wednesday)",
  prescan_findings: [
    { id: "range-short:E7", cell: "E7", severity: "high",
      label: "=SUM(E2:E5) covers 4 of the 5 rows in E2:E6" },
    { id: "inconsistent:E5", cell: "E5", severity: "high",
      label: "E5 uses 0.045 where E2:E4 and E6 use 0.04" },
    { id: "hardcode:E2", cell: "E2", severity: "high",
      label: "0.04 typed inside the formula, repeated down E2:E6" },
  ],
  prescan_stats: {
    cells: 33, formulas: 12, constants: 10, labels: 11,
    error_cells: 0, distinct_functions: 1,
    rows_profiled: 7, rows_sent: 7, functions_used: ["SUM"],
  },
};

const est = await api("POST", "/estimate", payload);
console.log("worst case:", est.hold_credits ?? est.credits, "credits");
const grid = "Rep\tSales\tQuota\tOver quota\tCommission\n" +
	"A. Diaz\t120000\t100000\t=B2-C2\t=D2*0.04\n" +
	"R. Okafor\t98000\t100000\t=B3-C3\t=D3*0.04\n" +
	"T. Lin\t145000\t110000\t=B4-C4\t=D4*0.04\n" +
	"M. Haddad\t132000\t100000\t=B5-C5\t=D5*0.045\n" +
	"S. Novak\t104000\t100000\t=B6-C6\t=D6*0.04\n" +
	"Total\t\t\t=SUM(D2:D6)\t=SUM(E2:E5)"

type Flag struct {
	ID       string `json:"id"`
	Cell     string `json:"cell"`
	Severity string `json:"severity"`
	Label    string `json:"label"`
}

payload := map[string]any{
	"grid":        grid,
	"anchor":      "A1",
	"audit_scope": "whole",
	"sheet_purpose": "Monthly commission run. B is booked sales, C is quota, D the amount " +
		"over quota, E commission at 4% of the excess. E7 feeds the payroll upload and " +
		"came out light this month.",
	"current_datetime": "2026-08-05T14:12:00-04:00 (Wednesday)",
	"prescan_findings": []Flag{
		{"range-short:E7", "E7", "high", "=SUM(E2:E5) covers 4 of the 5 rows in E2:E6"},
		{"inconsistent:E5", "E5", "high", "E5 uses 0.045 where E2:E4 and E6 use 0.04"},
		{"hardcode:E2", "E2", "high", "0.04 typed inside the formula, repeated down E2:E6"},
	},
	"prescan_stats": map[string]any{
		"cells": 33, "formulas": 12, "constants": 10, "labels": 11,
		"error_cells": 0, "distinct_functions": 1,
		"rows_profiled": 7, "rows_sent": 7,
		"functions_used": []string{"SUM"},
	},
}

var est struct{ HoldCredits int64 `json:"hold_credits"` }
err := call("POST", "/estimate", payload, &est)
// Tabs between columns; \n between rows. Written out explicitly so the
// separators are unambiguous.
String grid = String.join("\n",
    "Rep\tSales\tQuota\tOver quota\tCommission",
    "A. Diaz\t120000\t100000\t=B2-C2\t=D2*0.04",
    "R. Okafor\t98000\t100000\t=B3-C3\t=D3*0.04",
    "T. Lin\t145000\t110000\t=B4-C4\t=D4*0.04",
    "M. Haddad\t132000\t100000\t=B5-C5\t=D5*0.045",
    "S. Novak\t104000\t100000\t=B6-C6\t=D6*0.04",
    "Total\t\t\t=SUM(D2:D6)\t=SUM(E2:E5)");

String jsonPayload = """
    {"grid": %s,
     "anchor": "A1",
     "audit_scope": "whole",
     "sheet_purpose": "Monthly commission run. B is booked sales, C is quota, D the amount over quota, E commission at 4% of the excess. E7 feeds the payroll upload and came out light this month.",
     "current_datetime": "2026-08-05T14:12:00-04:00 (Wednesday)",
     "prescan_findings": [
       {"id": "range-short:E7", "cell": "E7", "severity": "high",
        "label": "=SUM(E2:E5) covers 4 of the 5 rows in E2:E6"},
       {"id": "inconsistent:E5", "cell": "E5", "severity": "high",
        "label": "E5 uses 0.045 where E2:E4 and E6 use 0.04"},
       {"id": "hardcode:E2", "cell": "E2", "severity": "high",
        "label": "0.04 typed inside the formula, repeated down E2:E6"}
     ],
     "prescan_stats": {"cells": 33, "formulas": 12, "constants": 10, "labels": 11,
                       "error_cells": 0, "distinct_functions": 1,
                       "rows_profiled": 7, "rows_sent": 7, "functions_used": ["SUM"]}}
    """.formatted(toJsonString(grid));

String envelope = api("POST", "/estimate", jsonPayload);
// worst-case cost is at data.hold_credits
GRID = [
  ["Rep",       "Sales",  "Quota",  "Over quota",  "Commission"],
  ["A. Diaz",   "120000", "100000", "=B2-C2",      "=D2*0.04"],
  ["R. Okafor", "98000",  "100000", "=B3-C3",      "=D3*0.04"],
  ["T. Lin",    "145000", "110000", "=B4-C4",      "=D4*0.04"],
  ["M. Haddad", "132000", "100000", "=B5-C5",      "=D5*0.045"],
  ["S. Novak",  "104000", "100000", "=B6-C6",      "=D6*0.04"],
  ["Total",     "",       "",       "=SUM(D2:D6)", "=SUM(E2:E5)"]
].map { |row| row.join("\t") }.join("\n")

payload = {
  grid: GRID,
  anchor: "A1",
  audit_scope: "whole",
  sheet_purpose: "Monthly commission run. B is booked sales, C is quota, D the amount over " \
                 "quota, E commission at 4% of the excess. E7 feeds the payroll upload and " \
                 "came out light this month.",
  current_datetime: "2026-08-05T14:12:00-04:00 (Wednesday)",
  prescan_findings: [
    { id: "range-short:E7", cell: "E7", severity: "high",
      label: "=SUM(E2:E5) covers 4 of the 5 rows in E2:E6" },
    { id: "inconsistent:E5", cell: "E5", severity: "high",
      label: "E5 uses 0.045 where E2:E4 and E6 use 0.04" },
    { id: "hardcode:E2", cell: "E2", severity: "high",
      label: "0.04 typed inside the formula, repeated down E2:E6" }
  ],
  prescan_stats: { cells: 33, formulas: 12, constants: 10, labels: 11,
                   error_cells: 0, distinct_functions: 1,
                   rows_profiled: 7, rows_sent: 7, functions_used: ["SUM"] }
}

est = api("POST", "/estimate", payload)
puts "worst case: #{est["hold_credits"] || est["credits"]} credits"
$grid = implode("\n", array_map(fn($row) => implode("\t", $row), [
    ["Rep",       "Sales",  "Quota",  "Over quota",  "Commission"],
    ["A. Diaz",   "120000", "100000", "=B2-C2",      "=D2*0.04"],
    ["R. Okafor", "98000",  "100000", "=B3-C3",      "=D3*0.04"],
    ["T. Lin",    "145000", "110000", "=B4-C4",      "=D4*0.04"],
    ["M. Haddad", "132000", "100000", "=B5-C5",      "=D5*0.045"],
    ["S. Novak",  "104000", "100000", "=B6-C6",      "=D6*0.04"],
    ["Total",     "",       "",       "=SUM(D2:D6)", "=SUM(E2:E5)"],
]));

$payload = [
    "grid"          => $grid,
    "anchor"        => "A1",
    "audit_scope"   => "whole",
    "sheet_purpose" => "Monthly commission run. B is booked sales, C is quota, D the amount "
                     . "over quota, E commission at 4% of the excess. E7 feeds the payroll "
                     . "upload and came out light this month.",
    "current_datetime" => "2026-08-05T14:12:00-04:00 (Wednesday)",
    "prescan_findings" => [
        ["id" => "range-short:E7", "cell" => "E7", "severity" => "high",
         "label" => "=SUM(E2:E5) covers 4 of the 5 rows in E2:E6"],
        ["id" => "inconsistent:E5", "cell" => "E5", "severity" => "high",
         "label" => "E5 uses 0.045 where E2:E4 and E6 use 0.04"],
        ["id" => "hardcode:E2", "cell" => "E2", "severity" => "high",
         "label" => "0.04 typed inside the formula, repeated down E2:E6"],
    ],
    "prescan_stats" => [
        "cells" => 33, "formulas" => 12, "constants" => 10, "labels" => 11,
        "error_cells" => 0, "distinct_functions" => 1,
        "rows_profiled" => 7, "rows_sent" => 7, "functions_used" => ["SUM"],
    ],
];

$est = api("POST", "/estimate", $payload);
echo "worst case: " . ($est["hold_credits"] ?? $est["credits"]) . " credits\n";
var rows = new[] {
    new[] { "Rep",       "Sales",  "Quota",  "Over quota",  "Commission" },
    new[] { "A. Diaz",   "120000", "100000", "=B2-C2",      "=D2*0.04" },
    new[] { "R. Okafor", "98000",  "100000", "=B3-C3",      "=D3*0.04" },
    new[] { "T. Lin",    "145000", "110000", "=B4-C4",      "=D4*0.04" },
    new[] { "M. Haddad", "132000", "100000", "=B5-C5",      "=D5*0.045" },
    new[] { "S. Novak",  "104000", "100000", "=B6-C6",      "=D6*0.04" },
    new[] { "Total",     "",       "",       "=SUM(D2:D6)", "=SUM(E2:E5)" },
};
var grid = string.Join("\n", rows.Select(r => string.Join("\t", r)));

var payload = new {
    grid,
    anchor = "A1",
    audit_scope = "whole",
    sheet_purpose = "Monthly commission run. B is booked sales, C is quota, D the amount over " +
                    "quota, E commission at 4% of the excess. E7 feeds the payroll upload and " +
                    "came out light this month.",
    current_datetime = "2026-08-05T14:12:00-04:00 (Wednesday)",
    prescan_findings = new object[] {
        new { id = "range-short:E7", cell = "E7", severity = "high",
              label = "=SUM(E2:E5) covers 4 of the 5 rows in E2:E6" },
        new { id = "inconsistent:E5", cell = "E5", severity = "high",
              label = "E5 uses 0.045 where E2:E4 and E6 use 0.04" },
        new { id = "hardcode:E2", cell = "E2", severity = "high",
              label = "0.04 typed inside the formula, repeated down E2:E6" },
    },
    prescan_stats = new {
        cells = 33, formulas = 12, constants = 10, labels = 11,
        error_cells = 0, distinct_functions = 1,
        rows_profiled = 7, rows_sent = 7, functions_used = new[] { "SUM" },
    },
};

var est = await SkillSafe.ApiAsync(HttpMethod.Post, "/estimate", payload);
Console.WriteLine($"worst case: {est.GetProperty("hold_credits")} credits");

prescan_findings is how you make the audit answer for things you already know about. Send {"id": "range-short:E7", "cell": "E7", "severity": "high", "label": "=SUM(E2:E5) covers 4 of the 5 rows in E2:E6"} and that id comes back in coverage_check — confirmed as a defect with a matching entry in findings, or set aside with the reason. Nothing you flag is silently dropped, which makes it the field to assert on in an automated check. Send [] and the audit still runs; it simply has one less source of evidence.

Step 4 — Run the audit and wait for the result

POST /run
GET /jobs/{job_id}

/run takes the same input as /estimate, places a credit hold and returns a job_id. Poll /jobs/{job_id} every 1–2 seconds until status is succeeded or failed (a run typically takes 30–90 s, since the reply carries a finding per defect, the integrity checks and the coverage reconciliation). Always send an Idempotency-Key header so a network retry can't start a second, double-charged run. The reply is in output — usually nested as output.output, and as a JSON string, so parse defensively. The samples below print the verdict, the findings with their evidence and fixes, the integrity checks and the coverage reconciliation, then save the whole object to audit.json.

JOB_ID=$(curl -s -X POST "$API/run" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: fa-$(date +%s)" \
  -d @input.json | jq -r '.data.job_id')

while :; do
  JOB=$(curl -s "$API/jobs/$JOB_ID" -H "Authorization: Bearer $TOKEN")
  STATUS=$(echo "$JOB" | jq -r '.data.status')
  [ "$STATUS" = "succeeded" ] || [ "$STATUS" = "failed" ] && break
  sleep 2
done

# unwrap the audit once, then read it
echo "$JOB" | jq -r '.data.output.output' > audit.json

jq -r '
  "\(.audit_title) [\(.verdict)] — \(.sheet_type)",
  "\(.headline)",
  "",
  "FINDINGS",
  (.findings[] |
     "  \(.cell // "sheet") [\(.severity)/\(.category)] \(.problem)",
     "      evidence: \(.evidence)",
     "      fix:      \(.fix)"),
  "",
  "INTEGRITY",
  (.integrity_checks[] | "  [\(.status)] \(.check) - \(.detail)"),
  "",
  "COVERAGE",
  (.coverage_check[] | "  \(.id): \(if .addressed then "confirmed" else "SET ASIDE" end) - \(.note)"),
  "",
  "NEXT",
  (.next_steps[] | "  - \(.)")' \
  audit.json

# only let the sheet through when the audit says clean
jq -e '.verdict == "clean"' audit.json > /dev/null \
  || { echo "fix the sheet before anyone uses these numbers"; exit 1; }
import time

job_id = api("POST", "/run", payload,
             **{"Idempotency-Key": "fa-001"})["job_id"]

while True:
    job = api("GET", f"/jobs/{job_id}")
    if job["status"] in ("succeeded", "failed"):
        break
    time.sleep(1.5)

if job["status"] == "failed":
    raise RuntimeError(job.get("error", "run failed"))

raw = job["output"]
if isinstance(raw, dict) and "output" in raw:
    raw = raw["output"]
audit = json.loads(raw) if isinstance(raw, str) else raw

print(f'{audit["audit_title"]} [{audit["verdict"]}] — {audit["sheet_type"]}')
print(audit["headline"])
for f in audit["findings"]:
    print(f'  {f["cell"] or "sheet":<8} [{f["severity"]}/{f["category"]}] {f["problem"]}')
    print(f'      evidence: {f["evidence"]}')
    print(f'      fix:      {f["fix"]}')
for c in audit["integrity_checks"]:
    print(f'  [{c["status"]}] {c["check"]} - {c["detail"]}')
for c in audit["coverage_check"]:
    print(f'  {c["id"]}: {"confirmed" if c["addressed"] else "SET ASIDE"} - {c["note"]}')
for a in audit["assumptions"]:
    print("  assumed:", a)
for q in audit["open_questions"]:
    print("  ask:", q)
for s in audit["next_steps"]:
    print("  next:", s)

with open("audit.json", "w", encoding="utf-8") as fh:
    json.dump(audit, fh, indent=2)

if audit["verdict"] != "clean":
    raise SystemExit(f'verdict is {audit["verdict"]} — fix the sheet before using the numbers')
import { writeFileSync } from "node:fs";

const { job_id } = await api("POST", "/run", payload,
  { "Idempotency-Key": crypto.randomUUID() });

let job;
do {
  await new Promise((r) => setTimeout(r, 1500));
  job = await api("GET", `/jobs/${job_id}`);
} while (job.status !== "succeeded" && job.status !== "failed");

if (job.status === "failed") throw new Error(job.error ?? "run failed");

const raw = job.output?.output ?? job.output;
const audit = typeof raw === "string" ? JSON.parse(raw) : raw;

console.log(`${audit.audit_title} [${audit.verdict}] — ${audit.sheet_type}`);
console.log(audit.headline);
for (const f of audit.findings) {
  console.log(`  ${f.cell || "sheet"} [${f.severity}/${f.category}] ${f.problem}`);
  console.log(`      evidence: ${f.evidence}`);
  console.log(`      fix:      ${f.fix}`);
}
for (const c of audit.integrity_checks) {
  console.log(`  [${c.status}] ${c.check} - ${c.detail}`);
}
for (const c of audit.coverage_check) {
  console.log(`  ${c.id}: ${c.addressed ? "confirmed" : "SET ASIDE"} - ${c.note}`);
}
for (const s of audit.next_steps) console.log(`  next: ${s}`);

writeFileSync("audit.json", JSON.stringify(audit, null, 2));

if (audit.verdict !== "clean") process.exitCode = 1;
var started struct{ JobID string `json:"job_id"` }
if err := call("POST", "/run", payload, &started); err != nil {
	log.Fatal(err)
}

var job struct {
	Status string          `json:"status"`
	Error  string          `json:"error"`
	Output json.RawMessage `json:"output"`
}
for {
	if err := call("GET", "/jobs/"+started.JobID, nil, &job); err != nil {
		log.Fatal(err)
	}
	if job.Status == "succeeded" || job.Status == "failed" {
		break
	}
	time.Sleep(1500 * time.Millisecond)
}

// job.Output is {"output": "<json string>"} — unwrap, then unmarshal:
type Audit struct {
	AuditTitle    string   `json:"audit_title"`
	SheetType     string   `json:"sheet_type"`
	Verdict       string   `json:"verdict"`
	Headline      string   `json:"headline"`
	ExecSummary   string   `json:"exec_summary"`
	Assumptions   []string `json:"assumptions"`
	OpenQuestions []string `json:"open_questions"`
	Findings      []struct {
		Cell     string `json:"cell"`
		Severity string `json:"severity"`
		Category string `json:"category"`
		Problem  string `json:"problem"`
		Evidence string `json:"evidence"`
		Fix      string `json:"fix"`
		Confirms string `json:"confirms"`
	} `json:"findings"`
	IntegrityChecks []struct {
		Check  string `json:"check"`
		Status string `json:"status"`
		Detail string `json:"detail"`
	} `json:"integrity_checks"`
	CoverageCheck []struct {
		ID        string `json:"id"`
		Addressed bool   `json:"addressed"`
		Note      string `json:"note"`
	} `json:"coverage_check"`
	NextSteps []string `json:"next_steps"`
	Summary   string   `json:"summary"`
}
var wrapper struct{ Output string `json:"output"` }
json.Unmarshal(job.Output, &wrapper)
var audit Audit
json.Unmarshal([]byte(wrapper.Output), &audit)

fmt.Printf("%s [%s] — %s\n%s\n", audit.AuditTitle, audit.Verdict, audit.SheetType, audit.Headline)
for _, f := range audit.Findings {
	fmt.Printf("  %s [%s/%s] %s\n      evidence: %s\n      fix:      %s\n",
		f.Cell, f.Severity, f.Category, f.Problem, f.Evidence, f.Fix)
}
for _, c := range audit.IntegrityChecks {
	fmt.Printf("  [%s] %s - %s\n", c.Status, c.Check, c.Detail)
}
for _, c := range audit.CoverageCheck {
	fmt.Printf("  %s: %v - %s\n", c.ID, c.Addressed, c.Note)
}
os.WriteFile("audit.json", []byte(wrapper.Output), 0o644)
String envelope = api("POST", "/run", jsonPayload);
String jobId = /* data.job_id via your JSON library */;

while (true) {
    String job = api("GET", "/jobs/" + jobId, null);
    String status = /* data.status */;
    if (status.equals("succeeded") || status.equals("failed")) break;
    Thread.sleep(1500);
}
// The reply is at data.output.output as a JSON string — parse it again, then read
// audit_title, sheet_type (financial-model|calculation|tracker|unknown),
// verdict (clean|fix-first|broken), headline, exec_summary,
// assumptions[], open_questions[],
// findings[] (cell/severity/category/problem/evidence/fix/confirms) — [] for a clean
//   sheet, never null,
// integrity_checks[] (check/status/detail) — always at least one entry,
// coverage_check[] (id/addressed/note) — exactly one per prescan_findings id,
// next_steps[] and summary.
// Finally keep it on disk:
//   Files.writeString(Path.of("audit.json"), auditJson);
started = api("POST", "/run", payload)

job = nil
loop do
  job = api("GET", "/jobs/#{started["job_id"]}")
  break if %w[succeeded failed].include?(job["status"])
  sleep 1.5
end
raise (job["error"] || "run failed") if job["status"] == "failed"

raw = job["output"].is_a?(Hash) ? job["output"].fetch("output", job["output"]) : job["output"]
audit = raw.is_a?(String) ? JSON.parse(raw) : raw

puts "#{audit["audit_title"]} [#{audit["verdict"]}] — #{audit["sheet_type"]}"
puts audit["headline"]
audit["findings"].each do |f|
  puts "  #{f["cell"]} [#{f["severity"]}/#{f["category"]}] #{f["problem"]}"
  puts "      evidence: #{f["evidence"]}"
  puts "      fix:      #{f["fix"]}"
end
audit["integrity_checks"].each { |c| puts "  [#{c["status"]}] #{c["check"]} - #{c["detail"]}" }
audit["coverage_check"].each { |c| puts "  #{c["id"]}: #{c["addressed"] ? "confirmed" : "SET ASIDE"}" }
audit["next_steps"].each { |s| puts "  next: #{s}" }

File.write("audit.json", JSON.pretty_generate(audit))
exit 1 unless audit["verdict"] == "clean"
$started = api("POST", "/run", $payload);

do {
    sleep(2);
    $job = api("GET", "/jobs/" . $started["job_id"]);
} while (!in_array($job["status"], ["succeeded", "failed"]));

if ($job["status"] === "failed") {
    throw new Exception($job["error"] ?? "run failed");
}

$raw = is_array($job["output"]) ? ($job["output"]["output"] ?? $job["output"]) : $job["output"];
$audit = is_string($raw) ? json_decode($raw, true) : $raw;

echo "{$audit['audit_title']} [{$audit['verdict']}] — {$audit['sheet_type']}\n";
echo $audit["headline"] . "\n";
foreach ($audit["findings"] as $f) {
    echo "  {$f['cell']} [{$f['severity']}/{$f['category']}] {$f['problem']}\n";
    echo "      evidence: {$f['evidence']}\n";
    echo "      fix:      {$f['fix']}\n";
}
foreach ($audit["integrity_checks"] as $c) {
    echo "  [{$c['status']}] {$c['check']} - {$c['detail']}\n";
}
foreach ($audit["coverage_check"] as $c) {
    echo "  {$c['id']}: " . ($c["addressed"] ? "confirmed" : "SET ASIDE") . "\n";
}

file_put_contents("audit.json", json_encode($audit, JSON_PRETTY_PRINT));
var started = await SkillSafe.ApiAsync(HttpMethod.Post, "/run", payload);
var jobId = started.GetProperty("job_id").GetString();

JsonElement job;
while (true)
{
    job = await SkillSafe.ApiAsync(HttpMethod.Get, $"/jobs/{jobId}");
    var status = job.GetProperty("status").GetString();
    if (status is "succeeded" or "failed") break;
    await Task.Delay(1500);
}

var rawText = job.GetProperty("output").GetProperty("output").GetString();
using var doc = JsonDocument.Parse(rawText!);
var audit = doc.RootElement;

Console.WriteLine($"{audit.GetProperty("audit_title")} " +
                  $"[{audit.GetProperty("verdict")}] — {audit.GetProperty("sheet_type")}");
Console.WriteLine(audit.GetProperty("headline"));
foreach (var f in audit.GetProperty("findings").EnumerateArray())
{
    Console.WriteLine($"  {f.GetProperty("cell")} [{f.GetProperty("severity")}/" +
                      $"{f.GetProperty("category")}] {f.GetProperty("problem")}");
    Console.WriteLine($"      evidence: {f.GetProperty("evidence")}");
    Console.WriteLine($"      fix:      {f.GetProperty("fix")}");
}
foreach (var c in audit.GetProperty("integrity_checks").EnumerateArray())
{
    Console.WriteLine($"  [{c.GetProperty("status")}] {c.GetProperty("check")} - " +
                      $"{c.GetProperty("detail")}");
}
foreach (var c in audit.GetProperty("coverage_check").EnumerateArray())
{
    Console.WriteLine($"  {c.GetProperty("id")}: {c.GetProperty("addressed")}");
}

await File.WriteAllTextAsync("audit.json", rawText!);

The model is asked for one JSON object and nothing else, but a stray code fence or preamble is always possible. Strip a leading ```json fence, take the text between the first { and the last }, and only then parse — that is what the app does before it falls back to a retry_note reformat run.

The reply object — output schema

One JSON object, always the same shape. Every claim in it is grounded in what you sent: the cells, the formulas and the numbers come from grid and sheet_purpose alone, never from invention. evidence is always text that actually appears in the submitted grid — a formula as pasted, a typed constant, an error value — so you can grep for it; and fix is always actionable: the replacement formula written out in full where a formula is wrong, or a concrete instruction ("move the 0.04 into a labelled input cell at B1 and reference $B$1") where the problem is structural. Addresses are computed from anchor, so they are the addresses in your sheet, not offsets into the paste. findings is always an array — [] for a clean sheet, never null — integrity_checks always carries at least one entry, and the verdict must follow from the findings: no clean alongside a critical finding, no broken for a sheet whose only problem is a hardcoded constant.

FieldTypeMeaning
audit_titlestringA short title naming the sheet and what it does — e.g. Q3 commission run — formula audit.
sheet_typestringfinancial-model | calculation | tracker | unknown. What the model judged the sheet to be, from its structure and your sheet_purpose.
verdictstringclean | fix-first | broken. See the table below.
headlinestringOne sentence a reviewer could act on, naming the most consequential problem — or, for a clean sheet, what was verified.
exec_summarystringTwo or three short paragraphs, separated by blank lines: what the sheet computes, what state it is in, what a reader should do about it. Plain sentences, no bullet markup.
assumptionsstring[]Anything that had to be assumed because the paste did not settle it. Read these first: a wrong assumption invalidates the finding built on it.
open_questionsstring[]Questions for whoever owns the sheet, including anything clipped away that the audit needed and could not see.
findingsarrayThe core deliverable — {cell, severity, category, problem, evidence, fix, confirms}, one per defect. Columns are listed below. [] for a clean sheet; never null.
integrity_checksarray{check, status, detail} — the checks that were run, each with a status, always at least one entry. A check is never omitted to mean "not run": if it could not be verified it comes back unknown with the reason.
coverage_checkarray{id, addressed, note} — one entry per prescan_findings id you sent, each appearing exactly once, no more and no fewer. See the semantics below.
next_stepsstring[]Ordered, concrete, short enough to do today — e.g. "Extend E7 to =SUM(E2:E6) and re-run the payroll upload".
summarystringTwo sentences: the state of the sheet and the single thing to do first.

The three verdict values:

verdictWhat it means
cleanNothing needs changing before the sheet is used. findings may be empty — but integrity_checks still shows the work, because an audit that finds nothing must say what it checked. This is the case to gate an automated handover on.
fix-firstThe sheet is structurally sound but has defects that change the numbers or will bite later. This is the normal answer for a real working sheet: fix what is in findings, in severity order, then re-run.
brokenThe sheet cannot be trusted as it stands — an error value in a load-bearing path, a circular chain, a balance sheet that does not balance, a total that omits real rows. Do not publish numbers from it.

Each entry in findings:

ColumnMeaning
cellThe real address of the offending cell, computed from anchor — E7, B14. The empty string for a finding about the sheet as a whole.
severitycritical | high | medium | low. Anything unrecognised is normalised to medium by the client.
categoryOne of range, consistency, hardcode, error, lookup, volatile, link, structure, logic, rounding, sign, units. Free text is passed through; an empty value becomes logic.
problemWhat is wrong and what it does to the numbers — not just "wrong range" but what the wrong range costs. A finding with an empty problem is dropped by the client.
evidenceText that appears verbatim in the grid you sent — the formula as pasted. This is the column to audit: if it is not in your paste, the finding is not about your sheet.
fixThe replacement formula in full, or a concrete instruction when the problem is structural. Never "review this cell".
confirmsThe prescan_findings id this finding corresponds to, or the empty string when the audit found it on its own. Join on it to line findings up against your own scan.

Each entry in integrity_checks:

statusWhat it means
passVerified from the grid and it holds.
failVerified and it does not hold — detail says by how much, in the sheet's own units.
unknownThe check applies but the pasted block does not contain enough to verify it; detail names what is missing. Common when the grid was clipped.
not-applicableThe check does not apply to this kind of sheet — balance-sheet checks on a commission tracker.

Four checks always run: totals tie to their components over the full extent of the block; no cell is an error value and no formula would produce one for plausible inputs; formulas are consistent across each copied block; inputs are separable from calculations. Under audit_scope: "model" — or on any sheet that is plainly a financial model — four more join them: the balance sheet balances in every period present, cash rolls forward, net income ties into retained earnings and the cash flow statement, and sign conventions are consistent across statements.

coverage_check semantics:

CaseWhat you get
Every id you sentEach prescan_findings id appears in coverage_check exactly once — no more and no fewer. Nothing you flagged is silently dropped, which makes this the field to assert on in an automated check.
addressed: trueThe audit agrees it is a defect, and there is a matching entry in findings carrying the same id in its confirms field. note is usually empty.
addressed: falseThe flag was deliberately set aside; note gives the reason — "row 14 is a deliberate override, labelled as such in column A", "the 0.35 in column C is the standard margin and is applied consistently". Setting a flag aside with a reason is a good answer; ignoring it is not.
Nothing sentSend prescan_findings: [] and coverage_check comes back empty. The rest of the reply is unaffected — the audit still reads the whole grid and judges it.

A small, realistic result for the commission sheet above (long strings wrapped for readability):

{
  "audit_title": "Monthly commission run — formula audit",
  "sheet_type": "calculation",
  "verdict": "fix-first",
  "headline": "The commission total in E7 sums only E2:E5 and omits S. Novak's row, understating
               the payroll upload by the value of E6.",
  "exec_summary": "The sheet pays commission at a rate applied to the amount each rep booked
                   over quota: column D is sales minus quota, column E is D times the rate, and
                   row 7 totals both columns.

                   Two defects change the numbers. The commission total in E7 covers E2:E5 while
                   the block runs to E6, so one rep's commission never reaches the total — which
                   is exactly the shortfall described in the sheet purpose. Separately, E5
                   applies 0.045 where every other row applies 0.04; nothing in the grid labels
                   that row as an exception.

                   Both fixes are one-line edits. The rate itself is typed inside five formulas
                   rather than held in an input cell, which is how the E5 drift happened and
                   how it will happen again.",
  "assumptions": [
    "The 4% in the sheet purpose is the intended rate for every rep, so the 0.045 in E5 is
     drift rather than a documented exception."
  ],
  "open_questions": [
    "Is M. Haddad on a negotiated 4.5% rate? If so, label the row and move the rate into its
     own cell so the next reader does not read it as an error."
  ],
  "findings": [
    { "cell": "E7",
      "severity": "critical",
      "category": "range",
      "problem": "The commission total stops at row 5 while the commission block runs to row 6,
                  so S. Novak's commission is missing from the figure that feeds payroll. The
                  total in D7 covers D2:D6, so the two totals are computed over different
                  extents and cannot be reconciled against each other.",
      "evidence": "=SUM(E2:E5)",
      "fix": "=SUM(E2:E6)",
      "confirms": "range-short:E7" },
    { "cell": "E5",
      "severity": "high",
      "category": "consistency",
      "problem": "E5 multiplies by 0.045 where E2, E3, E4 and E6 multiply by 0.04. Either one
                  rep is being overpaid by 12.5% of their commission, or a negotiated rate is
                  recorded nowhere but inside a formula.",
      "evidence": "=D5*0.045",
      "fix": "=D5*0.04 — or, if 4.5% is intentional, put it in a labelled rate cell on row 5
              and reference that cell.",
      "confirms": "inconsistent:E5" },
    { "cell": "",
      "severity": "medium",
      "category": "hardcode",
      "problem": "The commission rate is typed inside every formula in column E rather than
                  held in one input cell. Changing the rate means editing five formulas, and a
                  single missed edit is invisible — which is what E5 already is.",
      "evidence": "=D2*0.04",
      "fix": "Put the rate in a labelled input cell — e.g. B9 holding 0.04 with the label
              'Commission rate' in A9 — and write column E as =D2*$B$9.",
      "confirms": "hardcode:E2" }
  ],
  "integrity_checks": [
    { "check": "Totals tie to their components",
      "status": "fail",
      "detail": "D7 sums D2:D6 correctly, but E7 sums E2:E5 and omits E6. The commission total
                 is understated by the whole of S. Novak's commission." },
    { "check": "No cell is an error value, and no formula would produce one",
      "status": "pass",
      "detail": "No error values in the grid; column D subtracts and column E multiplies, so no
                 division by a cell that could be blank or zero." },
    { "check": "Consistent formulas across each copied block",
      "status": "fail",
      "detail": "D2:D6 share one shape. E2:E6 do not: E5 uses 0.045 against 0.04 elsewhere." },
    { "check": "Inputs are separable from calculations",
      "status": "fail",
      "detail": "Sales and quota are typed cells, but the commission rate exists only inside
                 the five formulas in column E." },
    { "check": "Balance sheet balances",
      "status": "not-applicable",
      "detail": "This is a commission calculation, not a financial model." }
  ],
  "coverage_check": [
    { "id": "range-short:E7", "addressed": true, "note": "" },
    { "id": "inconsistent:E5", "addressed": true, "note": "" },
    { "id": "hardcode:E2", "addressed": true,
      "note": "Raised once for the column rather than per cell; the fix moves the rate into an
               input cell." }
  ],
  "next_steps": [
    "Change E7 to =SUM(E2:E6) and re-run the payroll upload — this is the shortfall you saw.",
    "Confirm M. Haddad's rate, then either correct E5 to =D5*0.04 or label the exception.",
    "Move the rate into a labelled input cell and repoint column E at it before next month."
  ],
  "summary": "The sheet's logic is right but its total is not: E7 leaves a rep out and E5 pays a
              different rate from everyone else. Fix E7 first — it is the one that already went
              out with the payroll."
}

This is an AI-generated review of pasted text, not accounting, tax or investment advice: it sees only the cells you sent, never the workbook, its named ranges, its other sheets or the rows that were clipped away. Check assumptions, confirm every evidence string really is in your grid, and let a human apply the fixes.

Step 5 — Stream the audit as it is written

POST /run-stream

/run-stream takes exactly the same body as /run but answers with server-sent events, so you can show progress instead of a spinner — useful here because the findings, the integrity checks and the coverage reconciliation make for a long reply. This app's own progress panel is this endpoint. Events are separated by a blank line; each has an event: line and a data: line carrying JSON.

EventPayloadMeaning
job{job_id, status}Sent once, when the job is accepted — show "starting".
delta{text}A chunk of the reply, in order. Append it; the accumulated length is your only progress signal (the total is not known in advance). The app advances its step list by watching for the "audit_title", "sheet_type", "verdict", "headline", "findings", "integrity_checks", "coverage_check", "next_steps" and "summary" keys as they arrive.
done{job_id, status, charged_credits, output}The final, authoritative result — read the audit from output.output rather than trusting concatenated deltas, and the settled price from charged_credits.
error{code, message}Replaces done when the run fails.
# -N disables buffering so events print as they arrive
curl -N -s -X POST "$API/run-stream" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: fa-$(date +%s)" \
  -d @input.json

# event: job
# data: {"job_id":"job_...","status":"running"}
#
# event: delta
# data: {"text":"{\"audit_title\":\"Monthly commission run"}
# ...
# event: done
# data: {"job_id":"job_...","status":"succeeded","charged_credits":384,"output":{"output":"{...}"}}
import json, requests

result = None
with requests.post(
    API + "/run-stream",
    headers={"Authorization": f"Bearer {TOKEN}",
             "Idempotency-Key": "fa-001"},
    json=payload,
    stream=True,
) as r:
    r.raise_for_status()
    event = None
    for line in r.iter_lines(decode_unicode=True):
        if not line:
            continue
        if line.startswith("event:"):
            event = line[len("event:"):].strip()
        elif line.startswith("data:"):
            data = json.loads(line[len("data:"):].strip())
            if event == "delta":
                print(".", end="", flush=True)          # live progress
            elif event == "done":
                result = data
            elif event == "error":
                raise RuntimeError(data.get("message", "run failed"))

audit = json.loads(result["output"]["output"])             # authoritative
print("charged:", result["charged_credits"], "-", audit["audit_title"])
print("verdict:", audit["verdict"])
for f in audit["findings"]:
    print(f'  {f["cell"]} [{f["severity"]}] {f["evidence"]} -> {f["fix"]}')
for c in audit["integrity_checks"]:
    print(f'  [{c["status"]}] {c["check"]}')
with open("audit.json", "w", encoding="utf-8") as fh:
    json.dump(audit, fh, indent=2)
const res = await fetch(API + "/run-stream", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${TOKEN}`,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify(payload),
});

const reader = res.body.getReader();
const decoder = new TextDecoder();
let buf = "", done = null;

for (;;) {
  const chunk = await reader.read();
  if (chunk.done) break;
  buf += decoder.decode(chunk.value, { stream: true });
  const frames = buf.split("\n\n");
  buf = frames.pop();
  for (const frame of frames) {
    const name = /^event:\s*(.+)$/m.exec(frame)?.[1];
    const body = /^data:\s*(.+)$/m.exec(frame)?.[1];
    if (!name || !body) continue;
    const data = JSON.parse(body);
    if (name === "delta") process.stdout.write(".");   // live progress
    if (name === "done") done = data;
    if (name === "error") throw new Error(data.message ?? "run failed");
  }
}

const audit = JSON.parse(done.output.output);
console.log(`\n${done.charged_credits} credits - ${audit.audit_title} [${audit.verdict}]`);
for (const f of audit.findings) {
  console.log(`  ${f.cell} [${f.severity}] ${f.evidence} -> ${f.fix}`);
}
for (const c of audit.integrity_checks) {
  console.log(`  [${c.status}] ${c.check}`);
}
writeFileSync("audit.json", JSON.stringify(audit, null, 2));
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", API+"/run-stream", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "fa-001")

res, err := http.DefaultClient.Do(req)
if err != nil {
	log.Fatal(err)
}
defer res.Body.Close()

var event string
var final map[string]any
sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 0, 64*1024), 4*1024*1024)
for sc.Scan() {
	line := sc.Text()
	switch {
	case strings.HasPrefix(line, "event:"):
		event = strings.TrimSpace(strings.TrimPrefix(line, "event:"))
	case strings.HasPrefix(line, "data:"):
		var data map[string]any
		json.Unmarshal([]byte(strings.TrimPrefix(line, "data:")), &data)
		switch event {
		case "delta":
			fmt.Print(".") // live progress
		case "done":
			final = data
		case "error":
			log.Fatal(data["message"])
		}
	}
}
// final["output"].(map[string]any)["output"].(string) is the audit JSON —
// unmarshal it into the Audit struct from step 4, then write it to audit.json.
// Java 17+ — read the stream line by line instead of buffering the body.
var req = HttpRequest.newBuilder(URI.create(API + "/run-stream"))
    .header("Authorization", "Bearer " + TOKEN)
    .header("Content-Type", "application/json")
    .header("Idempotency-Key", "fa-001")
    .POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
    .build();

var res = HTTP.send(req, HttpResponse.BodyHandlers.ofLines());
String event = null, done = null;
for (String line : (Iterable<String>) res.body()::iterator) {
    if (line.startsWith("event:")) {
        event = line.substring(6).trim();
    } else if (line.startsWith("data:")) {
        String data = line.substring(5).trim();
        if ("delta".equals(event)) System.out.print(".");   // live progress
        else if ("done".equals(event)) done = data;
        else if ("error".equals(event)) throw new RuntimeException(data);
    }
}
// parse `done`, then parse data.output.output again — it is a JSON string holding
// audit_title, sheet_type, verdict, headline, exec_summary, assumptions[],
// open_questions[], findings[] (cell/severity/category/problem/evidence/fix/confirms),
// integrity_checks[] (check/status/detail), coverage_check[] (id/addressed/note),
// next_steps[] and summary.
require "net/http"
require "json"

uri = URI(API + "/run-stream")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = "fa-001"
req.body = payload.to_json

event = nil
done = nil
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
  http.request(req) do |res|
    res.read_body do |chunk|
      chunk.each_line do |line|
        line = line.strip
        if line.start_with?("event:")
          event = line.delete_prefix("event:").strip
        elsif line.start_with?("data:")
          data = JSON.parse(line.delete_prefix("data:").strip)
          case event
          when "delta" then print "."           # live progress
          when "done"  then done = data
          when "error" then raise (data["message"] || "run failed")
          end
        end
      end
    end
  end
end

audit = JSON.parse(done["output"]["output"])
puts "\n#{done["charged_credits"]} credits - #{audit["audit_title"]} [#{audit["verdict"]}]"
audit["findings"].each { |f| puts "  #{f["cell"]} [#{f["severity"]}] #{f["evidence"]} -> #{f["fix"]}" }
audit["integrity_checks"].each { |c| puts "  [#{c["status"]}] #{c["check"]}" }
File.write("audit.json", JSON.pretty_generate(audit))
$event = null;
$done  = null;

$ch = curl_init(API . "/run-stream");
curl_setopt_array($ch, [
    CURLOPT_POST       => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer $TOKEN",
        "Content-Type: application/json",
        "Idempotency-Key: fa-001",
    ],
    CURLOPT_POSTFIELDS => json_encode($payload),
    CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$event, &$done) {
        foreach (explode("\n", $chunk) as $line) {
            $line = trim($line);
            if (str_starts_with($line, "event:")) {
                $event = trim(substr($line, 6));
            } elseif (str_starts_with($line, "data:")) {
                $data = json_decode(trim(substr($line, 5)), true);
                if ($event === "delta") { echo "."; }        // live progress
                elseif ($event === "done") { $done = $data; }
                elseif ($event === "error") { throw new Exception($data["message"] ?? "run failed"); }
            }
        }
        return strlen($chunk);
    },
]);
curl_exec($ch);
curl_close($ch);

$audit = json_decode($done["output"]["output"], true);
echo "\n{$done['charged_credits']} credits - {$audit['audit_title']} [{$audit['verdict']}]\n";
foreach ($audit["findings"] as $f) {
    echo "  {$f['cell']} [{$f['severity']}] {$f['evidence']} -> {$f['fix']}\n";
}
foreach ($audit["integrity_checks"] as $c) {
    echo "  [{$c['status']}] {$c['check']}\n";
}
file_put_contents("audit.json", json_encode($audit, JSON_PRETTY_PRINT));
var req = new HttpRequestMessage(HttpMethod.Post, Api + "/run-stream") {
    Content = JsonContent.Create(payload),
};
req.Headers.Add("Idempotency-Key", "fa-001");

using var res = await Http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());

string? evt = null, done = null;
while (await reader.ReadLineAsync() is { } line)
{
    if (line.StartsWith("event:")) evt = line[6..].Trim();
    else if (line.StartsWith("data:"))
    {
        var data = line[5..].Trim();
        if (evt == "delta") Console.Write(".");            // live progress
        else if (evt == "done") done = data;
        else if (evt == "error") throw new Exception(data);
    }
}

using var final = JsonDocument.Parse(done!);
var text = final.RootElement.GetProperty("output").GetProperty("output").GetString();
using var auditDoc = JsonDocument.Parse(text!);
var audit = auditDoc.RootElement;
Console.WriteLine($"{audit.GetProperty("audit_title")} [{audit.GetProperty("verdict")}]");
foreach (var f in audit.GetProperty("findings").EnumerateArray())
{
    Console.WriteLine($"  {f.GetProperty("cell")} [{f.GetProperty("severity")}] " +
                      $"{f.GetProperty("evidence")} -> {f.GetProperty("fix")}");
}
await File.WriteAllTextAsync("audit.json", text!);

In a browser, the native EventSource only speaks GET, and this endpoint is a POST — read the fetch response body incrementally, as the JavaScript sample above does. On an idempotent replay the server may answer with a plain JSON envelope instead of an event stream; check the Content-Type before you start parsing frames.