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.
| Status | Meaning |
|---|---|
401 | Missing or expired token — create a new session. |
402 | Not enough credits — top up at skillsafe.ai/account/credits. |
403 | The token isn't allowed to do this (e.g. a guest submitting a very large grid). |
404 | Unknown job or record id. |
5xx | Transient platform error — retry with backoff. |
Browsers enforce CORS for this API, so run these examples from a server, script or terminal — not from another website's frontend.
Step 0 — A tiny client
Every task below is a single HTTP call, so start with a short helper that adds the auth
header, sends JSON and unwraps the data envelope. The later steps reuse it.
export API="https://api.skillsafe.ai/v1/app-api"
export TOKEN="YOUR_TOKEN" # see step 1
# every call looks like:
# curl -s "$API/..." -H "Authorization: Bearer $TOKEN" [-d '{json}']
# jq is used below to pull fields out of the {"data": ...} envelope
import json, requests
API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # see step 1 — read it from your shell environment in real code
def api(method, path, body=None, **headers):
res = requests.request(method, API + path, json=body,
headers={"Authorization": f"Bearer {TOKEN}", **headers})
payload = res.json()
if not res.ok:
raise RuntimeError(payload.get("error", {}).get("message", res.reason))
return payload["data"]
// Node 18+ (built-in fetch)
const API = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN"; // see step 1 — read it from your shell environment in real code
async function api(method, path, body, extraHeaders = {}) {
const res = await fetch(API + path, {
method,
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json", ...extraHeaders },
body: body === undefined ? undefined : JSON.stringify(body),
});
const json = await res.json();
if (!res.ok) throw new Error(json.error?.message ?? res.statusText);
return json.data;
}
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
const API = "https://api.skillsafe.ai/v1/app-api"
var token = os.Getenv("SKILLSAFE_TOKEN") // see step 1
func call(method, path string, body, out any) error {
var buf bytes.Buffer
if body != nil {
json.NewEncoder(&buf).Encode(body)
}
req, _ := http.NewRequest(method, API+path, &buf)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
var env struct {
Data json.RawMessage `json:"data"`
Error *struct{ Message string `json:"message"` } `json:"error"`
}
json.NewDecoder(res.Body).Decode(&env)
if res.StatusCode >= 400 {
return fmt.Errorf("api %s %s: %s", method, path, env.Error.Message)
}
if out == nil {
return nil
}
return json.Unmarshal(env.Data, out)
}
// Java 17+, no dependencies. Pair with your JSON library (Jackson, Gson…)
// to read fields out of the returned envelope.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class SkillSafe {
static final String API = "https://api.skillsafe.ai/v1/app-api";
static final String TOKEN = System.getenv("SKILLSAFE_TOKEN"); // see step 1
static final HttpClient HTTP = HttpClient.newHttpClient();
static String api(String method, String path, String jsonBody) throws Exception {
var req = HttpRequest.newBuilder(URI.create(API + path))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.method(method, jsonBody == null
? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
var res = HTTP.send(req, HttpResponse.BodyHandlers.ofString());
if (res.statusCode() >= 400) throw new RuntimeException(res.body());
return res.body(); // envelope: {"data": …}
}
}
require "net/http"
require "json"
API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = ENV.fetch("SKILLSAFE_TOKEN") # see step 1
def api(method, path, body = nil)
uri = URI(API + path)
req = Net::HTTP.const_get(method.capitalize).new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req.body = body.to_json if body
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
payload = JSON.parse(res.body)
raise (payload.dig("error", "message") || res.message) unless res.is_a?(Net::HTTPSuccess)
payload["data"]
end
<?php
const API = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = getenv("SKILLSAFE_TOKEN"); // see step 1
function api(string $method, string $path, ?array $body = null): mixed {
global $TOKEN;
$ch = curl_init(API . $path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer $TOKEN",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => $body === null ? null : json_encode($body),
]);
$payload = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) {
throw new Exception($payload["error"]["message"] ?? "HTTP $status");
}
return $payload["data"];
}
// .NET 8+
using System.Net.Http.Json;
using System.Text.Json;
static class SkillSafe
{
const string Api = "https://api.skillsafe.ai/v1/app-api";
static readonly HttpClient Http = new();
static SkillSafe() =>
Http.DefaultRequestHeaders.Authorization =
new("Bearer", Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN")); // see step 1
public static async Task<JsonElement> ApiAsync(HttpMethod method, string path, object? body = null)
{
var req = new HttpRequestMessage(method, Api + path);
if (body != null) req.Content = JsonContent.Create(body);
var res = await Http.SendAsync(req);
var json = await res.Content.ReadFromJsonAsync<JsonElement>();
if (!res.IsSuccessStatusCode)
throw new Exception(json.GetProperty("error").GetProperty("message").GetString());
return json.GetProperty("data");
}
}
Step 1 — Get a token
A guest token lets you check balances and estimate costs for free. For metered 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
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
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 field | Type | Notes |
|---|---|---|
grid | string, required | The 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. |
anchor | string | The 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_scope | string | whole (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_purpose | string | Free 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_datetime | string | Your 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_findings | array | What 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_stats | object | {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_note | string, optional | Only 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
/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.
| Field | Type | Meaning |
|---|---|---|
audit_title | string | A short title naming the sheet and what it does — e.g. Q3 commission run — formula audit. |
sheet_type | string | financial-model | calculation | tracker | unknown. What the model judged the sheet to be, from its structure and your sheet_purpose. |
verdict | string | clean | fix-first | broken. See the table below. |
headline | string | One sentence a reviewer could act on, naming the most consequential problem — or, for a clean sheet, what was verified. |
exec_summary | string | Two 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. |
assumptions | string[] | 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_questions | string[] | Questions for whoever owns the sheet, including anything clipped away that the audit needed and could not see. |
findings | array | The core deliverable — {cell, severity, category, problem, evidence, fix, confirms}, one per defect. Columns are listed below. [] for a clean sheet; never null. |
integrity_checks | array | {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_check | array | {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_steps | string[] | Ordered, concrete, short enough to do today — e.g. "Extend E7 to =SUM(E2:E6) and re-run the payroll upload". |
summary | string | Two sentences: the state of the sheet and the single thing to do first. |
The three verdict values:
| verdict | What it means |
|---|---|
clean | Nothing 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-first | The 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. |
broken | The 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:
| Column | Meaning |
|---|---|
cell | The real address of the offending cell, computed from anchor — E7, B14. The empty string for a finding about the sheet as a whole. |
severity | critical | high | medium | low. Anything unrecognised is normalised to medium by the client. |
category | One of range, consistency, hardcode, error, lookup, volatile, link, structure, logic, rounding, sign, units. Free text is passed through; an empty value becomes logic. |
problem | What 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. |
evidence | Text 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. |
fix | The replacement formula in full, or a concrete instruction when the problem is structural. Never "review this cell". |
confirms | The 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:
| status | What it means |
|---|---|
pass | Verified from the grid and it holds. |
fail | Verified and it does not hold — detail says by how much, in the sheet's own units. |
unknown | The 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-applicable | The 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:
| Case | What you get |
|---|---|
| Every id you sent | Each 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: true | The 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: false | The 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 sent | Send 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
/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.
| Event | Payload | Meaning |
|---|---|---|
job | {job_id, status} | Sent once, when the job is accepted — show "starting". |
delta | {text} | A chunk of the reply, in order. Append it; the accumulated length is your only progress signal (the total is not known in advance). 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.