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.
Errors
Every failure is {"ok":false,"error":{"code":"...","message":"...","details":{...}}}. Branch on code, not on the message.
| Code | HTTP | What it means |
|---|---|---|
UNAUTHORIZED | 401 | No token, an expired one, or a token minted for a different app. Mint a guest token or sign in on the token page. |
INSUFFICIENT_CREDITS | 402 | The 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. |
FORBIDDEN | 403 | The token is valid but not permitted here - most often a guest token on an app whose owner has not enabled sponsorship. |
NOT_FOUND | 404 | Unknown job id, unknown record id, or a collection this release does not declare. |
VALIDATION_ERROR | 400 | The 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_LIMITED | 429 | Too 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_FAILED | 502 | The 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.htmlraw, 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 failedHttpRequest 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 faileduri = 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 failedvar 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 failed7. 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"], [])])
EOFUNCLASSIFIED = "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
| Cause | When the engine admits it |
|---|---|
timing | The 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. |
fx | Both 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. |
mapping | The 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_missing | The 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_accrual | The same rounded delta magnitude appears on three or more breaks and is below materiality. |
data_quality | A sign flip, an exact unit-scale factor, a split posting, or a quantity difference at an unchanged amount. |
unclassified | None 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"
}
}