Recon Desk — API

Reconcile the ledger to the subledger from your close automation, your custody feed handler or a nightly cron job.

API tokens Open the app

Drive Recon Desk from your own code

The app is a thin client over a public REST API. Everything the page does — estimate a run, submit one, stream it, and keep the reconciliation on your account — is available to you directly. Base URL https://api.skillsafe.ai/v1/app-api. Every response is the same envelope: {"ok":true,"data":{...}} or {"ok":false,"error":{"code":"...","message":"..."}}.

What the model is and is not asked to do

This matters before you write a line of code, because it determines what you have to send. The measurement is not the model's job. The matching, the bucketing, the residual passes, the integrity checks and the decisive-break simulation all happen deterministically — in the browser in the app, and in your code if you are driving the API. What you send is that measurement, in facts, and what comes back is one cause and one note per break.

The binding is two-way. facts.admissible_causes[break_id] lists the only causes the model is permitted to choose for that break, and it is computed from the numbers: FX, for instance, is admissible only where the local amounts agree and the base amounts do not. After the run you check the reply back against the same object — step 6 — and a cause that was not on the list is an invention, not a judgement call.

POST /guest GET /me POST /estimate POST /run POST /run-stream GET /jobs/{job_id} POST /collections/recons/records POST /collections/recons/query POST /collections/recons/similar

Errors

Every failure is {"ok":false,"error":{"code":"...","message":"...","details":{...}}}. Branch on code, not on the message.

CodeHTTPWhat it means
UNAUTHORIZED401No token, an expired one, or a token minted for a different app. Mint a guest token or sign in on the token page.
INSUFFICIENT_CREDITS402The balance is below min_credits. /estimate is free and tells you this before you submit, so a 402 after submit means the preflight was skipped.
FORBIDDEN403The token is valid but not permitted here - most often a guest token on an app whose owner has not enabled sponsorship.
NOT_FOUND404Unknown job id, unknown record id, or a collection this release does not declare.
VALIDATION_ERROR400The body is not the shape the app expects. error.details.violations names the offending field. A run input over 1 MB of JSON lands here.
RATE_LIMITED429Too many requests. /collections/{name}/similar is the tightest at 30 per minute per IP - debounce it and prefer a where filter when an exact match will do.
JOB_FAILED502The model call failed upstream. Retry with the SAME Idempotency-Key: the platform replays rather than re-billing.

1. A tiny client

One helper, reused by every step below. Keep the token out of your source: read it from your own secret store, and never commit it. If you want to see or rotate the token this browser is using, the token page shows it, copies it and mints a fresh one — you never need the developer console.

# Every call is one POST to the same host. Keep your token in a shell
# variable; it is a bearer credential for this app only.
export SKILLSAFE_TOKEN="YOUR_TOKEN"
API="https://api.skillsafe.ai/v1/app-api"

call() {  # call <path> <json-body>
  curl -sS -X POST "$API$1" \
    -H "Authorization: Bearer $SKILLSAFE_TOKEN" \
    -H "Content-Type: application/json" \
    -d "$2"
}
import json, urllib.request

API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN"          # read it from your own secret store

def call(path, body=None, method="POST"):
    data = None if body is None else json.dumps(body).encode()
    req = urllib.request.Request(API + path, data=data, method=method)
    req.add_header("Authorization", "Bearer " + TOKEN)
    req.add_header("Content-Type", "application/json")
    with urllib.request.urlopen(req) as r:
        payload = json.load(r)
    if not payload.get("ok"):
        raise RuntimeError(payload.get("error"))
    return payload["data"]
const API = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN";   // read it from your own secret store

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

import (
    "bytes"
    "encoding/json"
    "errors"
    "net/http"
)

const api = "https://api.skillsafe.ai/v1/app-api"
const token = "YOUR_TOKEN" // read it from your own secret store

type envelope struct {
    OK    bool            `json:"ok"`
    Data  json.RawMessage `json:"data"`
    Error *struct {
        Code    string `json:"code"`
        Message string `json:"message"`
    } `json:"error"`
}

func call(path string, body any) (json.RawMessage, error) {
    var buf bytes.Buffer
    if body != nil {
        if err := json.NewEncoder(&buf).Encode(body); err != nil {
            return nil, err
        }
    }
    req, err := http.NewRequest("POST", api+path, &buf)
    if err != nil {
        return nil, err
    }
    req.Header.Set("Authorization", "Bearer "+token)
    req.Header.Set("Content-Type", "application/json")
    res, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer res.Body.Close()
    var env envelope
    if err := json.NewDecoder(res.Body).Decode(&env); err != nil {
        return nil, err
    }
    if !env.OK {
        return nil, errors.New(env.Error.Message)
    }
    return env.Data, nil
}
import java.net.URI;
import java.net.http.*;

public final class ReconDesk {
  static final String API = "https://api.skillsafe.ai/v1/app-api";
  static final String TOKEN = "YOUR_TOKEN"; // read it from your own secret store
  static final HttpClient HTTP = HttpClient.newHttpClient();

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

API = URI("https://api.skillsafe.ai/v1/app-api")
TOKEN = "YOUR_TOKEN" # read it from your own secret store

def call(path, body = nil)
  uri = URI(API.to_s + path)
  req = Net::HTTP::Post.new(uri)
  req["Authorization"] = "Bearer #{TOKEN}"
  req["Content-Type"] = "application/json"
  req.body = (body || {}).to_json
  res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
  payload = JSON.parse(res.body)
  raise payload.dig("error", "message").to_s unless payload["ok"]
  payload["data"]
end
<?php
const API = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN"; // read it from your own secret store

function call(string $path, array $body = []): array {
    $ch = curl_init(API . $path);
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => [
            "Authorization: Bearer " . TOKEN,
            "Content-Type: application/json",
        ],
        CURLOPT_POSTFIELDS => json_encode($body),
    ]);
    $payload = json_decode(curl_exec($ch), true);
    curl_close($ch);
    if (empty($payload["ok"])) {
        throw new RuntimeException($payload["error"]["message"] ?? "request failed");
    }
    return $payload["data"];
}
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;

static class ReconDesk {
    const string Api = "https://api.skillsafe.ai/v1/app-api";
    const string Token = "YOUR_TOKEN"; // read it from your own secret store
    static readonly HttpClient Http = new HttpClient();

    public static async Task<JsonElement> CallAsync(string path, object? body = null) {
        var req = new HttpRequestMessage(HttpMethod.Post, Api + path);
        req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token);
        req.Content = new StringContent(JsonSerializer.Serialize(body ?? new {}),
                                        Encoding.UTF8, "application/json");
        var res = await Http.SendAsync(req);
        var doc = JsonDocument.Parse(await res.Content.ReadAsStringAsync());
        if (!doc.RootElement.GetProperty("ok").GetBoolean())
            throw new Exception(doc.RootElement.GetProperty("error").GetProperty("message").GetString());
        return doc.RootElement.GetProperty("data");
    }
}

2. Get a token

A guest token is free and works immediately, but a metered run needs a signed-in subject with credits. The slug goes in the body; an X-App-Slug header returns 400.

# A guest token. The slug goes in the BODY - an X-App-Slug header 400s.
curl -sS -X POST "$API/guest" \
  -H "Content-Type: application/json" \
  -d '{"slug":"recon-desk"}'
# -> {"ok":true,"data":{"token":"...","guest_id":"..."}}

# A personal token (your own credits, your own history) comes from
# https://recon-desk.skillsafe.ai/tokens.html - sign in there, press
# "Copy shell export", and paste the line it gives you.
guest = call("/guest", {"slug": "recon-desk"})
TOKEN = guest["token"]
# A personal token instead: open https://recon-desk.skillsafe.ai/tokens.html,
# sign in, and use "Copy shell export".
const guest = await call("/guest", { slug: "recon-desk" });
// then use guest.token as the bearer for subsequent calls
// A personal token instead: https://recon-desk.skillsafe.ai/tokens.html
raw, err := call("/guest", map[string]string{"slug": "recon-desk"})
if err != nil {
    panic(err)
}
var guest struct {
    Token   string `json:"token"`
    GuestID string `json:"guest_id"`
}
_ = json.Unmarshal(raw, &guest)
String guest = call("/guest", "{\"slug\":\"recon-desk\"}");
// guest -> {"ok":true,"data":{"token":"...","guest_id":"..."}}
guest = call("/guest", { "slug" => "recon-desk" })
token = guest["token"]
$guest = call("/guest", ["slug" => "recon-desk"]);
$token = $guest["token"];
var guest = await ReconDesk.CallAsync("/guest", new { slug = "recon-desk" });
var token = guest.GetProperty("token").GetString();

3. Check the session

/me tells you who the token belongs to (subject_type is user or guest) and what the balance is. Compare it against min_credits from step 5 before you submit anything — a 402 after submit is a preflight you skipped.

curl -sS "$API/me" -H "Authorization: Bearer $SKILLSAFE_TOKEN"
me = call("/me", method="GET")
print(me["subject_type"], me["credits"])
const me = await call("/me", undefined, "GET");
console.log(me.subject_type, me.credits);
req, _ := http.NewRequest("GET", api+"/me", nil)
req.Header.Set("Authorization", "Bearer "+token)
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
HttpRequest me = HttpRequest.newBuilder(URI.create(API + "/me"))
    .header("Authorization", "Bearer " + TOKEN)
    .GET().build();
System.out.println(HTTP.send(me, HttpResponse.BodyHandlers.ofString()).body());
uri = URI(API.to_s + "/me")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
me = JSON.parse(res.body)["data"]
$ch = curl_init(API . "/me");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ["Authorization: Bearer " . TOKEN],
]);
$me = json_decode(curl_exec($ch), true)["data"];
curl_close($ch);
var req = new HttpRequestMessage(HttpMethod.Get, "https://api.skillsafe.ai/v1/app-api/me");
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token);
var res = await new HttpClient().SendAsync(req);

4. Build the input

The run input is three fields. facts is the measurement; gl_excerpt and sub_excerpt are the two extracts as text, cut on whole rows if they are long, with the cut marked so the model knows the figures were measured over rows it cannot see. The whole input caps at 1 MB of JSON.

{
  "facts": { ... the measurement, see the schema below ... },
  "gl_excerpt": "security_id,account,posting_date,base_amount\nUS0378331005,EQ-TRD-01,2026-05-06,540000.00\n...",
  "sub_excerpt": "security_id,account,posting_date,base_amount\nUS0378331005,EQ-TRD-01,2026-05-06,540000.00\n..."
}

The facts object in full

Field for field, this is what the app sends. Anything you omit, the model simply cannot use — and break_ids and admissible_causes are what make the reply checkable, so they are not optional in practice.

{
  "ok": true,
  "version": "1.0.0",
  "scope": "Equities / EQ-TRD / May 2026",
  "tolerance": 0.01,
  "materiality": 1000,

  "key": {
    "parts": ["security"],            // the columns the two sides were aligned on
    "label": "security",
    "coverage_pct": 100,              // share of all rows the key aligned
    "scoreboard": [                   // the alternatives, and why they lost
      {"parts": ["security"], "coverage": 100, "unique_share": 100,
       "duplicate_rows_gl": 0, "duplicate_rows_sub": 0}
    ]
  },

  "sides": {
    "gl":  {"label": "gl",  "rows": 10, "columns": 10, "base_total": 1736248.40,
            "delimiter": ",", "headerless": false, "roles": ["security -> security_id", "..."],
            "unparsed_rows": 0, "total_rows_seen": 0, "date_order": "month-first"},
    "sub": {"label": "sub", "rows": 10, "...": "..."}
  },

  "counts": {"matched": 6, "amount_break": 2, "quantity_break": 1,
             "timing_break": 1, "mapping_break": 0, "gl_only": 0, "sub_only": 0},
  "matched_pct": {"gl": 60, "sub": 60},

  "totals": {
    "gl_base": 1736248.40, "sub_base": 1643018.40,
    "net_residual": 93230,
    "break_delta_sum": 93230,
    "residual_explained_by_breaks": true
  },

  "break_ids": ["B8", "B9", "B4", "B3"],   // sorted by |base_delta| descending
  "matched_ids": ["B1", "B2", "B5", "B6", "B7", "B10"],

  "breaks": [
    {
      "id": "B4",
      "key": "GB0002634946",
      "bucket": "amount_break",     // matched | amount_break | quantity_break
                                    // timing_break | mapping_break | gl_only | sub_only
      "security": "GB0002634946",
      "description": "BAE Systems purchase",
      "gl_rows": 1, "sub_rows": 1,
      "gl_account": "EQ-TRD-01", "sub_account": "EQ-TRD-01",
      "gl_qty": 3000, "sub_qty": 3000, "qty_delta": 0,
      "gl_local": 246000, "sub_local": 246000, "local_delta": 0,
      "gl_base": 312420, "sub_base": 311190, "base_delta": 1230,
      "gl_posting": "2026-05-11", "sub_posting": "2026-05-11",
      "pct_of_sub": 0.4,
      "pct_refused": null,           // "base is zero" | "line changed sign" when pct_of_sub is null
      "material": true,
      "admissible_causes": ["fx"],   // the ONLY causes the reply may choose from
      "cause_evidence": {            // the engine's own words; keys starting "_" are exclusions
        "fx": "local amounts agree to $0.01 but base amounts differ by $1,230.00"
      },
      "engine_explanation": null     // split_posting | sign_flip | unit_scale | near_date
    }
  ],

  "admissible_causes": {"B8": ["data_quality"], "B4": ["fx"], "...": []},

  "engine_findings": {
    "split_postings": [], "sign_flips": [], "unit_scales": [],
    "near_date_pairs": [], "mapping_candidates": 0,
    "offsetting_pairs": [["B5", "B6"]]
  },

  "decisive": {
    "sufficient_break_ids": [],   // resolving any ONE of these alone clears the residual
    "sufficient_pairs": [],       // only searched when no single break does
    "net_residual": 93230,
    "within_materiality": false
  },

  "integrity": [ {"check": "total_does_not_foot", "side": "gl",
                  "stated": 838000, "measured": 832038, "delta": 5962} ],
  "data_notes": []
}

5. Estimate first - it is free

/estimate creates no job and charges nothing. It returns hold_credits (what will be reserved, priced at the full output cap — not what you pay), min_credits, the resolved model, the model_alias and the publisher markup_bps. It is also the cheapest proof that your input shape is valid.

call /estimate "$(cat input.json)"
# -> {"ok":true,"data":{
#      "hold_credits": 1600, "min_credits": 170,
#      "model": "gpt-5.6-terra", "model_alias": "gpt-terra",
#      "markup_bps": 1000 }}
#
# /estimate is FREE: it creates no job and charges nothing. hold_credits is
# what is RESERVED (it prices the full output cap), not what you pay.
est = call("/estimate", run_input)
print(est["hold_credits"], est["min_credits"], est["model"], est["markup_bps"])
if me["credits"] < est["min_credits"]:
    raise SystemExit("top up first - a run would 402")
const est = await call("/estimate", runInput);
if (me.credits < est.min_credits) throw new Error("top up first - a run would 402");
raw, err := call("/estimate", runInput)
if err != nil {
    panic(err)
}
var est struct {
    Hold  int    `json:"hold_credits"`
    Min   int    `json:"min_credits"`
    Model string `json:"model"`
}
_ = json.Unmarshal(raw, &est)
String est = call("/estimate", runInputJson);
// {"ok":true,"data":{"hold_credits":1600,"min_credits":170,
//                    "model":"gpt-5.6-terra","model_alias":"gpt-terra"}}
est = call("/estimate", run_input)
abort("top up first") if me["credits"] < est["min_credits"]
$est = call("/estimate", $runInput);
if ($me["credits"] < $est["min_credits"]) {
    throw new RuntimeException("top up first - a run would 402");
}
var est = await ReconDesk.CallAsync("/estimate", runInput);
var min = est.GetProperty("min_credits").GetInt32();

6. Run it

/run starts a job and returns immediately; poll /jobs/{job_id} until it is succeeded or failed. Always send an Idempotency-Key: derive it from a hash of the input plus one nonce per deliberate run, so a network retry replays the job you already paid for while a genuine re-run still gets a fresh answer.

# Idempotency-Key is a content hash of the input plus one nonce per
# deliberate press of Run. A network retry with the SAME key replays the
# job you already paid for instead of starting a second one.
curl -sS -X POST "$API/run" \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: recon-4f2a91-1" \
  -d "$(cat input.json)"
# -> {"ok":true,"data":{"job_id":"job_...","status":"running"}}

curl -sS "$API/jobs/job_..." -H "Authorization: Bearer $SKILLSAFE_TOKEN"
# poll until status is "succeeded" or "failed"
import time, urllib.request

def run(run_input, key):
    data = json.dumps(run_input).encode()
    req = urllib.request.Request(API + "/run", data=data, method="POST")
    req.add_header("Authorization", "Bearer " + TOKEN)
    req.add_header("Content-Type", "application/json")
    req.add_header("Idempotency-Key", key)
    with urllib.request.urlopen(req) as r:
        job = json.load(r)["data"]
    while job.get("status") not in ("succeeded", "failed"):
        time.sleep(1)
        job = call("/jobs/" + job["job_id"], method="GET")
    if job["status"] == "failed":
        raise RuntimeError(job.get("error"))
    return json.loads(job["output"] if isinstance(job["output"], str) else json.dumps(job["output"]))

report = run(run_input, "recon-4f2a91-1")
async function run(runInput, key) {
  const res = await fetch(`${API}/run`, {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${TOKEN}`,
      "Content-Type": "application/json",
      "Idempotency-Key": key,
    },
    body: JSON.stringify(runInput),
  });
  let job = (await res.json()).data;
  while (job.status !== "succeeded" && job.status !== "failed") {
    await new Promise((r) => setTimeout(r, 1000));
    job = await call(`/jobs/${job.job_id}`, undefined, "GET");
  }
  if (job.status === "failed") throw new Error(job.error?.message);
  return typeof job.output === "string" ? JSON.parse(job.output) : job.output;
}

const report = await run(runInput, "recon-4f2a91-1");
buf := new(bytes.Buffer)
_ = json.NewEncoder(buf).Encode(runInput)
req, _ := http.NewRequest("POST", api+"/run", buf)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "recon-4f2a91-1")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
// then poll GET /jobs/{job_id} until status is succeeded or failed
HttpRequest run = HttpRequest.newBuilder(URI.create(API + "/run"))
    .header("Authorization", "Bearer " + TOKEN)
    .header("Content-Type", "application/json")
    .header("Idempotency-Key", "recon-4f2a91-1")
    .POST(HttpRequest.BodyPublishers.ofString(runInputJson))
    .build();
String started = HTTP.send(run, HttpResponse.BodyHandlers.ofString()).body();
// then poll GET /jobs/{job_id} until status is succeeded or failed
uri = URI(API.to_s + "/run")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = "recon-4f2a91-1"
req.body = run_input.to_json
job = JSON.parse(Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }.body)["data"]

until %w[succeeded failed].include?(job["status"])
  sleep 1
  job = call("/jobs/#{job['job_id']}")
end
$ch = curl_init(API . "/run");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer " . TOKEN,
        "Content-Type: application/json",
        "Idempotency-Key: recon-4f2a91-1",
    ],
    CURLOPT_POSTFIELDS => json_encode($runInput),
]);
$job = json_decode(curl_exec($ch), true)["data"];
curl_close($ch);
// then poll GET /jobs/{job_id} until status is succeeded or failed
var run = new HttpRequestMessage(HttpMethod.Post, Api + "/run");
run.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token);
run.Headers.Add("Idempotency-Key", "recon-4f2a91-1");
run.Content = new StringContent(JsonSerializer.Serialize(runInput),
                                Encoding.UTF8, "application/json");
var started = await new HttpClient().SendAsync(run);
// then poll GET /jobs/{job_id} until status is succeeded or failed

7. Or stream it

/run-stream is the same job over server-sent events. The frame name arrives on the event: line and the payload on data:; frames are separated by a blank line. Concatenate every delta payload's text to rebuild the reply, and take charged_credits and truncated off the done frame.

curl -N -sS -X POST "$API/run-stream" \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: recon-4f2a91-1" \
  -d "$(cat input.json)"

# The frame NAME arrives on the "event:" line, the payload on "data:".
# event: job     data: {"job_id":"job_..."}
# event: delta   data: {"text":"{\"title\":\"GL to sub"}
# event: done    data: {"job_id":"job_...","status":"succeeded",
#                       "charged_credits":312,"output":"{...}"}
# event: error   data: {"code":"INSUFFICIENT_CREDITS","message":"..."}
import urllib.request

req = urllib.request.Request(API + "/run-stream", data=json.dumps(run_input).encode(), method="POST")
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Content-Type", "application/json")
req.add_header("Idempotency-Key", "recon-4f2a91-1")

buf, event = "", "message"
with urllib.request.urlopen(req) as r:
    for line in r:
        line = line.decode().rstrip("\n")
        if line.startswith("event:"):
            event = line[6:].strip()
        elif line.startswith("data:"):
            payload = json.loads(line[5:].strip())
            if event == "delta":
                buf += payload.get("text", "")
            elif event == "done":
                buf = buf or payload.get("output", "")
report = json.loads(buf)
const res = await fetch(`${API}/run-stream`, {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${TOKEN}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "recon-4f2a91-1",
  },
  body: JSON.stringify(runInput),
});

const reader = res.body.getReader();
const dec = new TextDecoder();
let buffer = "", text = "";
for (;;) {
  const { value, done } = await reader.read();
  if (done) break;
  buffer += dec.decode(value, { stream: true });
  let i;
  while ((i = buffer.indexOf("\n\n")) >= 0) {
    const frame = buffer.slice(0, i);
    buffer = buffer.slice(i + 2);
    let name = "message", data = "";
    for (const line of frame.split("\n")) {
      if (line.startsWith("event:")) name = line.slice(6).trim();
      else if (line.startsWith("data:")) data += line.slice(5).trim();
    }
    if (!data) continue;
    const payload = JSON.parse(data);
    if (name === "delta") text += payload.text || "";
    if (name === "done") text = text || payload.output || "";
  }
}
const report = JSON.parse(text);
// Read the response body line by line. The frame name is on "event:",
// the payload on "data:", and frames are separated by a blank line.
scanner := bufio.NewScanner(res.Body)
event := "message"
var text strings.Builder
for scanner.Scan() {
    line := scanner.Text()
    switch {
    case strings.HasPrefix(line, "event:"):
        event = strings.TrimSpace(line[6:])
    case strings.HasPrefix(line, "data:"):
        var payload struct {
            Text   string `json:"text"`
            Output string `json:"output"`
        }
        _ = json.Unmarshal([]byte(strings.TrimSpace(line[5:])), &payload)
        if event == "delta" {
            text.WriteString(payload.Text)
        }
    }
}
HttpResponse<Stream<String>> res = HTTP.send(
    HttpRequest.newBuilder(URI.create(API + "/run-stream"))
        .header("Authorization", "Bearer " + TOKEN)
        .header("Content-Type", "application/json")
        .header("Idempotency-Key", "recon-4f2a91-1")
        .POST(HttpRequest.BodyPublishers.ofString(runInputJson)).build(),
    HttpResponse.BodyHandlers.ofLines());

StringBuilder text = new StringBuilder();
final String[] event = { "message" };
res.body().forEach(line -> {
  if (line.startsWith("event:")) event[0] = line.substring(6).trim();
  else if (line.startsWith("data:") && event[0].equals("delta")) {
    text.append(line.substring(5).trim());  // parse the JSON and take .text
  }
});
uri = URI(API.to_s + "/run-stream")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = "recon-4f2a91-1"
req.body = run_input.to_json

text = +""
event = "message"
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
  http.request(req) do |res|
    res.read_body do |chunk|
      chunk.each_line do |line|
        if line.start_with?("event:") then event = line[6..].strip
        elsif line.start_with?("data:") && event == "delta"
          text << (JSON.parse(line[5..].strip)["text"] || "")
        end
      end
    end
  end
end
report = JSON.parse(text)
$text = "";
$event = "message";
$ch = curl_init(API . "/run-stream");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer " . TOKEN,
        "Content-Type: application/json",
        "Idempotency-Key: recon-4f2a91-1",
    ],
    CURLOPT_POSTFIELDS => json_encode($runInput),
    CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$text, &$event) {
        foreach (explode("\n", $chunk) as $line) {
            if (str_starts_with($line, "event:")) {
                $event = trim(substr($line, 6));
            } elseif (str_starts_with($line, "data:") && $event === "delta") {
                $payload = json_decode(trim(substr($line, 5)), true);
                $text .= $payload["text"] ?? "";
            }
        }
        return strlen($chunk);
    },
]);
curl_exec($ch);
curl_close($ch);
$report = json_decode($text, true);
var req = new HttpRequestMessage(HttpMethod.Post, Api + "/run-stream");
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token);
req.Headers.Add("Idempotency-Key", "recon-4f2a91-1");
req.Content = new StringContent(JsonSerializer.Serialize(runInput),
                                Encoding.UTF8, "application/json");

using var res = await Http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());
var text = new StringBuilder();
var evt = "message";
while (await reader.ReadLineAsync() is string line) {
    if (line.StartsWith("event:")) evt = line[6..].Trim();
    else if (line.StartsWith("data:") && evt == "delta") {
        var payload = JsonDocument.Parse(line[5..].Trim()).RootElement;
        if (payload.TryGetProperty("text", out var t)) text.Append(t.GetString());
    }
}

8. Check the reply before you use it

This is the step that makes the whole thing trustworthy, and it is pure set arithmetic against the facts you already have. Report the four failure modes separately — a missing break, a duplicated one, a matched row smuggled onto the report, and an id that is not a row at all — because they are four different mistakes and lumping them together hides which one happened.

# The reply is only usable once it has been checked against facts.
# The three tests that matter, all of them set-comparisons:
#
#   1. the set of break_id in the reply == facts.break_ids, each exactly once
#   2. every cause is in facts.admissible_causes[break_id]
#   3. where that list is exactly ["unclassified"], the cause is
#      "unclassified" and the note is the verbatim required string
python3 - "$PWD/input.json" "$PWD/report.json" <<'EOF'
import json, sys
facts = json.load(open(sys.argv[1]))["facts"]
report = json.load(open(sys.argv[2]))
got = [b["break_id"] for b in report["breaks"]]
print("missing:  ", [i for i in facts["break_ids"] if i not in got])
print("extra:    ", [i for i in got if i not in facts["break_ids"]])
print("duplicate:", [i for i in set(got) if got.count(i) > 1])
print("bad cause:", [b["break_id"] for b in report["breaks"]
                     if b["cause"] not in facts["admissible_causes"].get(b["break_id"], [])])
EOF
UNCLASSIFIED = "cause not determinable from the extracts provided"

def check(facts, report):
    got = [b["break_id"] for b in report["breaks"]]
    problems = {
        "missing":     [i for i in facts["break_ids"] if i not in got],
        "duplicated":  sorted({i for i in got if got.count(i) > 1}),
        "matched_row": [i for i in got if i in facts.get("matched_ids", [])],
        "not_a_row":   [i for i in got
                        if i not in facts["break_ids"]
                        and i not in facts.get("matched_ids", [])],
        "inadmissible": [],
        "bad_unclassified": [],
    }
    for b in report["breaks"]:
        allowed = facts["admissible_causes"].get(b["break_id"], [])
        if b.get("cause") not in allowed:
            problems["inadmissible"].append(f"{b['break_id']} -> {b.get('cause')}")
        if allowed == ["unclassified"] and (
            b.get("cause") != "unclassified" or b.get("note", "").lower() != UNCLASSIFIED
        ):
            problems["bad_unclassified"].append(b["break_id"])
    return {k: v for k, v in problems.items() if v}

problems = check(run_input["facts"], report)
if problems:
    raise SystemExit(f"the reply does not honour the contract: {problems}")
const UNCLASSIFIED = "cause not determinable from the extracts provided";

function check(facts, report) {
  const got = report.breaks.map((b) => b.break_id);
  const count = (id) => got.filter((x) => x === id).length;
  const matched = facts.matched_ids || [];
  const problems = {
    missing: facts.break_ids.filter((id) => !got.includes(id)),
    duplicated: [...new Set(got.filter((id) => count(id) > 1))],
    matchedRow: got.filter((id) => matched.includes(id)),
    notARow: got.filter((id) => !facts.break_ids.includes(id) && !matched.includes(id)),
    inadmissible: [],
    badUnclassified: [],
  };
  for (const b of report.breaks) {
    const allowed = facts.admissible_causes[b.break_id] || [];
    if (!allowed.includes(b.cause)) problems.inadmissible.push(`${b.break_id} -> ${b.cause}`);
    if (allowed.length === 1 && allowed[0] === "unclassified" &&
        (b.cause !== "unclassified" || (b.note || "").toLowerCase() !== UNCLASSIFIED)) {
      problems.badUnclassified.push(b.break_id);
    }
  }
  return Object.fromEntries(Object.entries(problems).filter(([, v]) => v.length));
}

const problems = check(runInput.facts, report);
if (Object.keys(problems).length) throw new Error(JSON.stringify(problems));
allowed := map[string][]string{}
_ = json.Unmarshal(factsAdmissibleCauses, &allowed)

seen := map[string]int{}
for _, b := range report.Breaks {
    seen[b.BreakID]++
    ok := false
    for _, c := range allowed[b.BreakID] {
        if c == b.Cause {
            ok = true
            break
        }
    }
    if !ok {
        log.Printf("inadmissible cause on %s: %s", b.BreakID, b.Cause)
    }
}
for _, id := range facts.BreakIDs {
    if seen[id] != 1 {
        log.Printf("break %s appears %d times, expected exactly 1", id, seen[id])
    }
}
// 1. every id in facts.break_ids appears exactly once in report.breaks
// 2. every cause is in facts.admissible_causes[id]
// 3. an ["unclassified"] list requires the verbatim note
Map<String, Long> seen = report.breaks.stream()
    .collect(Collectors.groupingBy(b -> b.breakId, Collectors.counting()));
for (String id : facts.breakIds) {
  long c = seen.getOrDefault(id, 0L);
  if (c != 1) System.out.println("break " + id + " appears " + c + " times, expected 1");
}
for (var b : report.breaks) {
  if (!facts.admissibleCauses.getOrDefault(b.breakId, List.of()).contains(b.cause)) {
    System.out.println("inadmissible cause on " + b.breakId + ": " + b.cause);
  }
}
UNCLASSIFIED = "cause not determinable from the extracts provided"

got = report["breaks"].map { |b| b["break_id"] }
missing    = facts["break_ids"] - got
duplicated = got.tally.select { |_, c| c > 1 }.keys
inadmissible = report["breaks"].reject do |b|
  Array(facts["admissible_causes"][b["break_id"]]).include?(b["cause"])
end.map { |b| "#{b['break_id']} -> #{b['cause']}" }

raise "contract broken" unless (missing + duplicated + inadmissible).empty?
const UNCLASSIFIED = "cause not determinable from the extracts provided";

$got = array_column($report["breaks"], "break_id");
$missing = array_values(array_diff($facts["break_ids"], $got));
$counts = array_count_values($got);
$duplicated = array_keys(array_filter($counts, fn($c) => $c > 1));
$inadmissible = [];
foreach ($report["breaks"] as $b) {
    $allowed = $facts["admissible_causes"][$b["break_id"]] ?? [];
    if (!in_array($b["cause"], $allowed, true)) {
        $inadmissible[] = $b["break_id"] . " -> " . $b["cause"];
    }
}
if ($missing || $duplicated || $inadmissible) {
    throw new RuntimeException("the reply does not honour the contract");
}
const string Unclassified = "cause not determinable from the extracts provided";

var got = report.Breaks.Select(b => b.BreakId).ToList();
var missing = facts.BreakIds.Where(id => !got.Contains(id)).ToList();
var duplicated = got.GroupBy(x => x).Where(g => g.Count() > 1).Select(g => g.Key).ToList();
var inadmissible = report.Breaks
    .Where(b => !facts.AdmissibleCauses.GetValueOrDefault(b.BreakId, new List<string>()).Contains(b.Cause))
    .Select(b => $"{b.BreakId} -> {b.Cause}")
    .ToList();

if (missing.Any() || duplicated.Any() || inadmissible.Any())
    throw new Exception("the reply does not honour the contract");

The output contract

Exactly this shape, and nothing outside it. The parser in app.js reads these fields and no others; a missing cause stays missing rather than being defaulted, because defaulting it would put a verdict on the report that the model never gave.

{
  "title": "GL to subledger reconciliation - Equities, May 2026",
  "summary": "Three to five sentences. Leads with the integrity finding when there is one; states the residual and the matched percentage; names the break that would clear the residual alone, even when it is one of the smaller ones.",
  "breaks": [
    { "break_id": "B4",
      "cause": "fx",
      "note": "The custody system priced the GBP leg at the 27 May rate while the ledger used month-end." },
    { "break_id": "B9",
      "cause": "unclassified",
      "note": "cause not determinable from the extracts provided" }
  ],
  "resolution_steps": ["Re-pull the rate table for 11 May and re-price the GBP leg."],
  "questions": ["Which rate source is authoritative for the equities book?"],
  "unverified": ["Whether the late posting cleared in the following period."]
}

The six causes, and the seventh answer

CauseWhen the engine admits it
timingThe posting dates differ with the amounts equal, or a counterpart sits a few days away with the same amount, or the row is within the cut-off window of the latest posting date in an extract that spans more than one date.
fxBoth sides carry a local and a base amount, the local amounts agree within tolerance, and the base amounts do not. Struck off when the locals also differ, when the two sides are equal and opposite, or when the ratio is an exact factor of a hundred or a thousand.
mappingThe amounts agree but the accounts differ, or the same security and amount sits under a different account on the other side, or an equal and opposite break sits elsewhere in the book.
duplicate_missingThe key carries a different number of lines on the two sides, or it exists on one side only and no pass found a counterpart, or the implied unit price matches while the quantity does not.
fee_accrualThe same rounded delta magnitude appears on three or more breaks and is below materiality.
data_qualityA sign flip, an exact unit-scale factor, a split posting, or a quantity difference at an unchanged amount.
unclassifiedNone of the six is supported. The list is then exactly ["unclassified"] and the note must be the verbatim string cause not determinable from the extracts provided. This is a correct answer, not a failure.

9. Keep the reconciliation

The recons collection is declared by this release with acl_read: owner and acl_write: user, so a record belongs to the account that wrote it. Record creation is POST /collections/{name}/records. where takes operator objects, and order_by is silently ignored in favour of a sort object — the default is created_at desc.

# Record creation is POST /collections/{name}/records - note "records".
call /collections/recons/records '{"doc":{
  "title":"GL to subledger reconciliation - Equities, May 2026",
  "scope":"Equities / EQ-TRD / May 2026",
  "summary":"Net residual of $93,230 with 60% matched on both sides.",
  "causes":"fx, data_quality, duplicate_missing, timing",
  "state":"open-residual",
  "breaks":4, "residual":93230, "ran_at":"2026-05-31T18:00:00Z"
}}'

# where takes OPERATOR OBJECTS; order_by is ignored in favour of sort.
call /collections/recons/query '{
  "where": {"state": {"eq": "one-break-clears"}},
  "sort": {"field": "ran_at", "dir": "desc"},
  "limit": 12
}'

# Semantic search over the four declared embed fields.
call /collections/recons/similar '{"text":"the month the custody cash would not tie","limit":8}'
call("/collections/recons/records", {"doc": {
    "title": "GL to subledger reconciliation - Equities, May 2026",
    "scope": "Equities / EQ-TRD / May 2026",
    "summary": "Net residual of $93,230 with 60% matched on both sides.",
    "causes": "fx, data_quality, duplicate_missing, timing",
    "state": "open-residual",
    "breaks": 4, "residual": 93230,
    "ran_at": "2026-05-31T18:00:00Z",
}})

page = call("/collections/recons/query", {
    "where": {"state": {"eq": "one-break-clears"}},
    "sort": {"field": "ran_at", "dir": "desc"},
    "limit": 12,
})
for rec in page["records"]:
    print(rec["doc"]["title"], rec["doc"]["residual"])
await call("/collections/recons/records", { doc: {
  title: "GL to subledger reconciliation - Equities, May 2026",
  scope: "Equities / EQ-TRD / May 2026",
  summary: "Net residual of $93,230 with 60% matched on both sides.",
  causes: "fx, data_quality, duplicate_missing, timing",
  state: "open-residual",
  breaks: 4, residual: 93230,
  ran_at: "2026-05-31T18:00:00Z",
}});

const page = await call("/collections/recons/query", {
  where: { state: { eq: "one-break-clears" } },
  sort: { field: "ran_at", dir: "desc" },
  limit: 12,
});

// similar() returns the record ARRAY; query() returns { records }.
const hits = await call("/collections/recons/similar",
  { text: "the month the custody cash would not tie", limit: 8 });
const rows = Array.isArray(hits) ? hits : hits.records;
_, err = call("/collections/recons/records", map[string]any{
    "doc": map[string]any{
        "title":    "GL to subledger reconciliation - Equities, May 2026",
        "scope":    "Equities / EQ-TRD / May 2026",
        "state":    "open-residual",
        "breaks":   4,
        "residual":  93230,
        "ran_at":   "2026-05-31T18:00:00Z",
    },
})

_, err = call("/collections/recons/query", map[string]any{
    "where": map[string]any{"state": map[string]string{"eq": "one-break-clears"}},
    "sort":  map[string]string{"field": "ran_at", "dir": "desc"},
    "limit": 12,
})
call("/collections/recons/records",
     "{\"doc\":{\"title\":\"GL to subledger reconciliation\"," +
     "\"scope\":\"Equities / EQ-TRD / May 2026\"," +
     "\"state\":\"open-residual\",\"breaks\":4,\"residual\":93230," +
     "\"ran_at\":\"2026-05-31T18:00:00Z\"}}");

call("/collections/recons/query",
     "{\"where\":{\"state\":{\"eq\":\"one-break-clears\"}}," +
     "\"sort\":{\"field\":\"ran_at\",\"dir\":\"desc\"},\"limit\":12}");
call("/collections/recons/records", { "doc" => {
  "title"    => "GL to subledger reconciliation - Equities, May 2026",
  "scope"    => "Equities / EQ-TRD / May 2026",
  "state"    => "open-residual",
  "breaks"   => 4,
  "residual" => 93_230,
  "ran_at"   => "2026-05-31T18:00:00Z",
}})

page = call("/collections/recons/query", {
  "where" => { "state" => { "eq" => "one-break-clears" } },
  "sort"  => { "field" => "ran_at", "dir" => "desc" },
  "limit" => 12,
})
call("/collections/recons/records", ["doc" => [
    "title"    => "GL to subledger reconciliation - Equities, May 2026",
    "scope"    => "Equities / EQ-TRD / May 2026",
    "state"    => "open-residual",
    "breaks"   => 4,
    "residual" => 93230,
    "ran_at"   => "2026-05-31T18:00:00Z",
]]);

$page = call("/collections/recons/query", [
    "where" => ["state" => ["eq" => "one-break-clears"]],
    "sort"  => ["field" => "ran_at", "dir" => "desc"],
    "limit" => 12,
]);
await ReconDesk.CallAsync("/collections/recons/records", new {
    doc = new {
        title = "GL to subledger reconciliation - Equities, May 2026",
        scope = "Equities / EQ-TRD / May 2026",
        state = "open-residual",
        breaks = 4,
        residual = 93230,
        ran_at = "2026-05-31T18:00:00Z",
    }
});

var page = await ReconDesk.CallAsync("/collections/recons/query", new {
    where = new { state = new { eq = "one-break-clears" } },
    sort = new { field = "ran_at", dir = "desc" },
    limit = 12,
});

The declared document

Four fields are embedded for semantic search — title, scope, summary and causes. The platform never backfills vectors, so records written before a field was embedded stay unsearchable by it. Documents cap at 64 KB each.

{
  "name": "recons",
  "acl_read": "owner",
  "acl_write": "user",
  "embed": ["title", "scope", "summary", "causes"],
  "fields": {
    "title": "string", "scope": "string", "summary": "string", "causes": "string",
    "state": "string",          // clean | within-materiality | one-break-clears | open-residual | integrity
    "gl_rows": "number", "sub_rows": "number", "breaks": "number",
    "residual": "number", "matched_pct": "number", "integrity": "number",
    "ran_at": "string",         // ISO 8601; sort on this, not on created_at
    "doc_md": "string",         // the rendered break report
    "context": "object",        // the two extracts, so a restore can re-measure
    "model": "object", "meta": "object"
  }
}