Drive Odds Desk from your own code
Everything the web page does is available over HTTP: post one pasted basket of prediction-market
legs, name one of four lanes in task, and get the same structured worksheet back as
one JSON object. The natural uses are the ones a browser tab is bad at — re-running the sizing
worksheet over a whole book every morning, holding a nightly risk review against yesterday's
numbers, or wiring the read lane into the step where a basket is first written down, so the
resolution rules get read before anyone thinks about stake.
Say the important thing first: Odds Desk is a non-advisory worksheet. It documents the consequences of the numbers the caller supplies and never recommends a position. It does not predict outcomes, does not fetch market data, and does not substitute a price for a belief. If you send a leg with no stated fair probability, the reply says the leg is not sizeable — it will not invent a number to fill the hole. Build your product on that boundary rather than against it.
Base URL and the envelope
Every endpoint lives under https://api.skillsafe.ai/v1/app-api and every response uses the same
envelope, so one helper covers the whole API:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "...", "status": 402, "details": { ... } } }
Every call sends exactly two headers: Authorization: Bearer <token> and
Content-Type: application/json. Nothing else is required and nothing else is read.
There is no app-slug header. The slug appears in exactly one place in the whole API — the JSON
body of POST /guest, as {"slug": "odds-desk"} — because the token you
get back is already scoped to this app, and every later call is identified by the token alone.
The endpoints in play: POST /guest, GET /me, POST /estimate,
POST /run, GET /jobs/{job_id} and POST /run-stream. The
request body of /estimate, /run and /run-stream is
the input object itself, posted bare. There is no wrapper key around it: the object
whose first field is task is the whole body.
The task field
This comes before everything else because it decides what you get back. Odds Desk is four
worksheets over one basket, not one worksheet with options. task selects the lane, the
lane fixes which leg keys are populated and which named checks come back, and the lanes are never
blended. The envelope around the answer is identical in all four cases, so one parser handles all
of them; what changes is the per-leg keys and the checks list.
task | the lane | what it returns |
|---|---|---|
read |
The inspection lane | What each leg actually resolves on, before anyone thinks about size. Per-leg
resolves_on, ambiguity, implied_read and
movers[] — dated, checkable items that would move the market, each naming
where it would be checked. Six named checks. This is a reading of the paste,
not research: there is no browser behind it, so its value is in exposing what the question as
written does and does not settle. |
size |
The decision lane | The judgement around arithmetic you already have. Per-leg stake_note,
suggested_band, cap_status and concentration_note.
Seven checks. Bands are phrased as consequences of your own inputs, never as a
single correct number, and a negative-edge leg's band is exactly zero. Kelly is treated as a
ceiling, not a target. |
plan |
The production lane | The execution worksheet a person works from by hand. Per-leg entry_condition (with
a price limit in the same notation you pasted), invalidation, exit and
monitor, plus an actions[] calendar that covers every resolution date
in the basket. Seven checks. Nothing in it executes without the user acting. |
risk |
The verification lane | Run last, and usually run on the plan. Per-leg risk_type,
data_quality and settlement_risk across compliance, data quality,
privacy, settlement and execution. Eight checks, the last of which is a
go / hold / revise gate with its condition attached. Eligibility is flagged, never assured. |
task is required. A missing or unrecognised value does not error: the closest lane is
chosen, that lane's contract is produced in full, and the first sentence of
exec_summary says which lane it picked. That is a worse outcome than sending the field,
because your code cannot branch on a sentence. Send it.
One worked minimal input per lane. The bare object is the request body of /estimate,
/run and /run-stream — note there is no input key
anywhere:
read
{
"task": "read",
"basket": "market | venue | side | price | resolves | source\nFed cuts at the December 2026 FOMC | Kalshi | YES | 62c | 2026-12-10 | FOMC statement\nCPI YoY above 3.0% for November 2026 | Kalshi | YES | 0.48 | 2026-12-10 | BLS CPI release\nIncumbent wins the 2026 GA runoff | Polymarket | YES | +150 | | ",
"currency": "USD",
"context": "First pass. I have not decided stakes yet, I want to know what these actually settle on."
}
size
{
"task": "size",
"basket": "market | venue | side | price | stake | fair | resolves | source | theme | liquidity\nFed cuts at the December 2026 FOMC | Kalshi | YES | 62c | 3000 | 71% | 2026-12-10 | FOMC statement | rates | 12000\nFed cuts at the December 2026 FOMC | Polymarket | NO | 41c | 1200 | 33% | 2026-12-10 | FOMC statement | rates | 40000\nCPI YoY above 3.0% for November 2026 | Kalshi | YES | 0.48 | 2500 | 44% | 2026-12-10 | BLS CPI release | rates | 6000\nIncumbent wins the 2026 GA runoff | Polymarket | YES | +150 | 1800 | | | | politics | 900",
"bankroll": "25k",
"currency": "USD",
"kelly_fraction": "half",
"per_leg_cap_pct": "5",
"theme_cap_pct": "15",
"context": "Personal account, no leverage. I am comfortable holding to settlement."
}
plan
{
"task": "plan",
"basket": "market: Fed cuts at the December 2026 FOMC\nvenue: Kalshi\nside: YES\nprice: 62c\nstake: 3000\nfair: 71%\nresolves: 2026-12-10\nsource: FOMC statement\ntheme: rates\nliquidity: 12000\n\nmarket: CPI YoY above 3.0% for November 2026\nvenue: Kalshi\nside: YES\nprice: 0.48\nstake: 2500\nfair: 44%\nresolves: 2026-12-10\nsource: BLS CPI release\ntheme: rates",
"bankroll": "25000",
"currency": "USD",
"kelly_fraction": "half",
"per_leg_cap_pct": "5",
"theme_cap_pct": "15",
"context": "Funded on Kalshi only. Polymarket access is not set up yet.",
"handoff": {
"from_lane": "size",
"verdict": "The rates theme carries 26% of bankroll against a 15% theme cap, so the group has to shrink before anything is entered.",
"notes": [
"L3 is a negative-edge leg at the stated fair value; the size lane put its band at $0.",
"L1 and L3 settle on the same day off the same policy path."
]
}
}
risk
{
"task": "risk",
"basket": "market | venue | side | price | stake | fair | resolves | source | theme\nFed cuts at the December 2026 FOMC | Kalshi | YES | 62c | 3000 | 71% | 2026-12-10 | FOMC statement | rates\nIncumbent wins the 2026 GA runoff | Polymarket | YES | +150 | 1800 | | | politics",
"bankroll": "25000",
"currency": "USD",
"kelly_fraction": "half",
"per_leg_cap_pct": "5",
"theme_cap_pct": "15",
"context": "An internal agent places the orders from a queue I approve each morning. Per-order cap is $2,000. No daily cap is configured yet.",
"handoff": {
"from_lane": "plan",
"verdict": "Two legs are enterable as written; the GA runoff leg has no resolution date and cannot be planned.",
"notes": [
"The plan lane put the GA runoff leg at cannot-be-entered pending a stated resolution rule.",
"Entry on L1 is worked in tranches because the stake is a quarter of quoted depth."
]
}
}
The handoff object in the last two is how the lanes chain: run plan, then
feed its verdict and a few of its lines into a risk run as
{from_lane, verdict, notes[]}. It is treated as the user's own prior work — built
on, and contradicted openly in findings if it disagrees with what the basket says,
rather than silently overridden.
Error codes
| code | status | what to do |
|---|---|---|
unauthorized | 401 | The token is missing, malformed or expired. The Authorization header is the only thing that identifies the caller, so a 401 means that header, not a missing slug. Get a fresh token from the token page. |
payment_required | 402 | The balance is below min_credits for this input. Call /estimate first — it is free — and top up. Remember the hold differs per lane, so a balance that clears read may not clear risk. |
forbidden | 403 | The token is valid but not for this app, or a guest token tried a metered run. Mint a token for this app, or sign in for a personal one. |
not_found | 404 | Unknown job_id, or an unknown slug in the /guest body. |
conflict | 409 | The same Idempotency-Key was replayed with a different body. Because the body is the input object itself, changing one character of basket — or switching task from plan to risk — is a different body. Derive the key from the input and bump an attempt suffix when the input really changed. |
validation_error | 422 | A required field is missing or is the wrong type. basket is the one that is usually missing: it is the whole evidence base, and an empty string is not a basket. A body that is not valid JSON at all comes back as a 400. |
rate_limited | 429 | Too many requests. Back off and retry with a delay; do not tight-loop a poll. |
internal | 5xx | A server-side failure, reported as server_error on a plain 500. Retry with the same Idempotency-Key so you are not billed twice for one run. |
1. Get a token
The easiest route is the token page: it shows the token this browser already holds, with Copy token and Copy shell export buttons, and a sign-in button for a personal token. Nothing there needs a developer tool — it reads the same storage the app itself uses and prints the token for you.
From code, POST /guest mints an anonymous token. This is the one and only place
the app slug appears, and it goes in the JSON body as {"slug": "odds-desk"}.
The token that comes back is already scoped to this app, so no later call needs to say which app it
is talking to.
A guest token can call /me and /estimate. Running a lane
is metered, so it needs a personal token from signing in — a guest run comes
back as 403 forbidden.
# The token page is the shortest path. It shows the token this browser already
# holds and hands you a ready-made shell export:
#
# https://odds-desk.skillsafe.ai/tokens.html
# export SKILLSAFE_TOKEN="..."
#
# To mint a guest token from the command line instead. This is the ONLY call in
# the whole API that mentions the app slug, and it goes in the JSON body:
curl -sS -X POST "https://api.skillsafe.ai/v1/app-api/guest" \
-H "Content-Type: application/json" \
-d '{"slug": "odds-desk"}'
# {"ok":true,"data":{"token":"sk_guest_...","guest_id":"g_...","subject_type":"guest"}}
#
# A guest token can call /me and /estimate. Running a lane is metered, so it
# needs a personal token from signing in on the token page.
# Open https://odds-desk.skillsafe.ai/tokens.html and press "Copy token", or
# mint a guest token here. A guest can call /me and /estimate but cannot run a
# lane. The slug goes in the BODY of /guest -- there is no slug header anywhere.
import json
import urllib.request
req = urllib.request.Request(
"https://api.skillsafe.ai/v1/app-api/guest",
data=json.dumps({"slug": "odds-desk"}).encode(),
method="POST",
)
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req) as r:
guest = json.load(r)["data"]
TOKEN = guest["token"]
print(guest["subject_type"], TOKEN[:12] + "...")
// Open https://odds-desk.skillsafe.ai/tokens.html and press "Copy token", or
// mint a guest token here. A guest can call /me and /estimate but cannot run a
// lane. The slug goes in the BODY of /guest -- there is no slug header anywhere.
const res = await fetch("https://api.skillsafe.ai/v1/app-api/guest", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ slug: "odds-desk" }),
});
const guest = (await res.json()).data;
const TOKEN = guest.token;
console.log(guest.subject_type, TOKEN.slice(0, 12) + "...");
// Open https://odds-desk.skillsafe.ai/tokens.html and press "Copy token", or
// mint a guest token here. A guest can call /me and /estimate but cannot run a
// lane. The slug goes in the BODY of /guest -- there is no slug header anywhere.
guestBody := []byte(`{"slug": "odds-desk"}`)
guestReq, _ := http.NewRequest(http.MethodPost,
"https://api.skillsafe.ai/v1/app-api/guest", bytes.NewReader(guestBody))
guestReq.Header.Set("Content-Type", "application/json")
guestRes, err := http.DefaultClient.Do(guestReq)
if err != nil {
panic(err)
}
defer guestRes.Body.Close()
var guest struct {
Data struct {
Token string `json:"token"`
SubjectType string `json:"subject_type"`
} `json:"data"`
}
_ = json.NewDecoder(guestRes.Body).Decode(&guest)
fmt.Println(guest.Data.SubjectType, guest.Data.Token)
// Open https://odds-desk.skillsafe.ai/tokens.html and press "Copy token", or
// mint a guest token here. A guest can call /me and /estimate but cannot run a
// lane. The slug goes in the BODY of /guest -- there is no slug header anywhere.
var http = HttpClient.newHttpClient();
var guestReq = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/guest"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{\"slug\": \"odds-desk\"}"))
.build();
HttpResponse<String> guest = http.send(guestReq, HttpResponse.BodyHandlers.ofString());
System.out.println(guest.body());
// {"ok":true,"data":{"token":"sk_guest_...","subject_type":"guest"}}
# Open https://odds-desk.skillsafe.ai/tokens.html and press "Copy token", or
# mint a guest token here. A guest can call /me and /estimate but cannot run a
# lane. The slug goes in the BODY of /guest -- there is no slug header anywhere.
require "json"
require "net/http"
require "uri"
uri = URI("https://api.skillsafe.ai/v1/app-api/guest")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req.body = JSON.generate({ "slug" => "odds-desk" })
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
guest = JSON.parse(res.body)["data"]
TOKEN = guest["token"]
puts "#{guest['subject_type']} #{TOKEN[0, 12]}..."
<?php
// Open https://odds-desk.skillsafe.ai/tokens.html and press "Copy token", or
// mint a guest token here. A guest can call /me and /estimate but cannot run a
// lane. The slug goes in the BODY of /guest -- there is no slug header anywhere.
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/guest");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(["slug" => "odds-desk"]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json"]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$guest = json_decode(curl_exec($ch), true)["data"];
curl_close($ch);
echo $guest["subject_type"], " ", substr($guest["token"], 0, 12), "...", PHP_EOL;
// Open https://odds-desk.skillsafe.ai/tokens.html and press "Copy token", or
// mint a guest token here. A guest can call /me and /estimate but cannot run a
// lane. The slug goes in the BODY of /guest -- there is no slug header anywhere.
using var http = new HttpClient();
var guestReq = new HttpRequestMessage(HttpMethod.Post,
"https://api.skillsafe.ai/v1/app-api/guest");
guestReq.Content = new StringContent("{\"slug\": \"odds-desk\"}",
Encoding.UTF8, "application/json");
var guestRes = await http.SendAsync(guestReq);
var guest = (await guestRes.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("data");
Console.WriteLine(guest.GetProperty("subject_type").GetString());
Console.WriteLine(guest.GetProperty("token").GetString());
2. A tiny client
One helper covers the whole API, because there is only one envelope and only two headers. It adds
Authorization, adds Content-Type when there is a body, optionally adds an
Idempotency-Key, unwraps data and raises on error. Write it
once and the rest of this page is four calls.
# Every call is the same three things: the base URL, your bearer token, and a
# JSON body. Two headers, no more. Keep the token in a shell variable.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="YOUR_TOKEN" # from https://odds-desk.skillsafe.ai/tokens.html
# call <path> [json-body] [idempotency-key]
call() {
if [ -n "$2" ]; then
if [ -n "$3" ]; then
curl -sS -X POST "$BASE/$1" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $3" \
-d "$2"
else
curl -sS -X POST "$BASE/$1" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "$2"
fi
else
curl -sS "$BASE/$1" -H "Authorization: Bearer $TOKEN"
fi
}
# Unwrap the envelope and fail loudly on the error branch.
unwrap() {
python3 -c '
import sys, json
p = json.load(sys.stdin)
if not p.get("ok"):
e = p.get("error", {})
sys.exit("%s: %s" % (e.get("code"), e.get("message")))
json.dump(p["data"], sys.stdout)
'
}
import json
import urllib.error
import urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # from https://odds-desk.skillsafe.ai/tokens.html
class OddsDeskError(RuntimeError):
def __init__(self, code, message, status):
super().__init__("%s: %s" % (code, message))
self.code = code
self.status = status
# POST `body` (the bare input object) or GET, and return the unwrapped `data`.
def call(path, body=None, idempotency_key=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(
BASE + "/" + path, data=data, method="POST" if data else "GET")
req.add_header("Authorization", "Bearer " + TOKEN)
if data:
req.add_header("Content-Type", "application/json")
if idempotency_key:
req.add_header("Idempotency-Key", idempotency_key)
try:
with urllib.request.urlopen(req) as r:
payload = json.load(r)
except urllib.error.HTTPError as exc:
payload = json.load(exc)
if not payload.get("ok"):
err = payload.get("error", {})
raise OddsDeskError(err.get("code"), err.get("message"), err.get("status"))
return payload["data"]
const BASE = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN"; // from https://odds-desk.skillsafe.ai/tokens.html
/** POST `body` (the bare input object) or GET, and return unwrapped `data`. */
async function call(path, body, idempotencyKey) {
const headers = { Authorization: `Bearer ${TOKEN}` };
if (body !== undefined) headers["Content-Type"] = "application/json";
if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;
const res = await fetch(`${BASE}/${path}`, {
method: body === undefined ? "GET" : "POST",
headers,
body: body === undefined ? undefined : JSON.stringify(body),
});
const payload = await res.json().catch(() => ({}));
if (!payload.ok) {
const err = new Error(`${payload.error?.code}: ${payload.error?.message}`);
err.code = payload.error?.code;
err.status = res.status;
throw err;
}
return payload.data;
}
// One helper for the whole API: two headers, an optional idempotency key, and
// the envelope unwrapped to raw JSON the caller decodes into its own struct.
const base = "https://api.skillsafe.ai/v1/app-api"
const token = "YOUR_TOKEN" // from https://odds-desk.skillsafe.ai/tokens.html
type apiError struct {
Code string `json:"code"`
Message string `json:"message"`
Status int `json:"status"`
}
func (e *apiError) Error() string { return e.Code + ": " + e.Message }
func call(path string, body []byte, idempotencyKey string) (json.RawMessage, error) {
method := http.MethodGet
var reader io.Reader
if body != nil {
method = http.MethodPost
reader = bytes.NewReader(body)
}
req, err := http.NewRequest(method, base+"/"+path, reader)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+token)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
if idempotencyKey != "" {
req.Header.Set("Idempotency-Key", idempotencyKey)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var payload struct {
OK bool `json:"ok"`
Data json.RawMessage `json:"data"`
Error *apiError `json:"error"`
}
if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
return nil, err
}
if !payload.OK {
if payload.Error != nil {
return nil, payload.Error
}
return nil, fmt.Errorf("http %d", res.StatusCode)
}
return payload.Data, nil
}
// One helper for the whole API. The bodies are plain JSON strings here so the
// sample stays dependency-free; swap in Jackson or Gson in real code.
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final String TOKEN = "YOUR_TOKEN"; // from https://odds-desk.skillsafe.ai/tokens.html
static final HttpClient HTTP = HttpClient.newHttpClient();
static String call(String path, String body, String idempotencyKey)
throws IOException, InterruptedException {
var builder = HttpRequest.newBuilder(URI.create(BASE + "/" + path))
.header("Authorization", "Bearer " + TOKEN);
if (body == null) {
builder = builder.GET();
} else {
builder = builder
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body));
}
if (idempotencyKey != null) {
builder = builder.header("Idempotency-Key", idempotencyKey);
}
HttpResponse<String> res = HTTP.send(builder.build(),
HttpResponse.BodyHandlers.ofString());
if (res.statusCode() >= 400) {
// {"ok":false,"error":{"code":"validation_error", ...}}
throw new IOException("odds-desk " + res.statusCode() + ": " + res.body());
}
return res.body();
}
require "json"
require "net/http"
require "uri"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # from https://odds-desk.skillsafe.ai/tokens.html
class OddsDeskError < StandardError
attr_reader :code, :status
def initialize(code, message, status)
super("#{code}: #{message}")
@code = code
@status = status
end
end
# POST `body` (the bare input object) or GET, and return the unwrapped `data`.
def call(path, body = nil, idempotency_key = nil)
uri = URI("#{BASE}/#{path}")
req = body.nil? ? Net::HTTP::Get.new(uri) : Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
unless body.nil?
req["Content-Type"] = "application/json"
req.body = JSON.generate(body)
end
req["Idempotency-Key"] = idempotency_key if idempotency_key
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
payload = JSON.parse(res.body)
unless payload["ok"]
e = payload["error"] || {}
raise OddsDeskError.new(e["code"], e["message"], e["status"])
end
payload["data"]
end
<?php
// One helper for the whole API: two headers, an optional idempotency key, and
// the envelope unwrapped.
const BASE = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN"; // from https://odds-desk.skillsafe.ai/tokens.html
function call(string $path, ?array $body = null, ?string $idempotencyKey = null): array
{
$headers = ["Authorization: Bearer " . TOKEN];
$ch = curl_init(BASE . "/" . $path);
if ($body !== null) {
$headers[] = "Content-Type: application/json";
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
if ($idempotencyKey !== null) {
$headers[] = "Idempotency-Key: " . $idempotencyKey;
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$payload = json_decode(curl_exec($ch), true);
curl_close($ch);
if (empty($payload["ok"])) {
$e = $payload["error"] ?? [];
throw new RuntimeException(($e["code"] ?? "error") . ": " . ($e["message"] ?? ""));
}
return $payload["data"];
}
// One helper for the whole API: two headers, an optional idempotency key, and
// the envelope unwrapped to a JsonElement.
static class OddsDesk
{
const string Base = "https://api.skillsafe.ai/v1/app-api";
const string Token = "YOUR_TOKEN"; // from https://odds-desk.skillsafe.ai/tokens.html
static readonly HttpClient Http = new HttpClient();
public static async Task<JsonElement> Call(
string path, object body = null, string idempotencyKey = null)
{
var method = body is null ? HttpMethod.Get : HttpMethod.Post;
var req = new HttpRequestMessage(method, $"{Base}/{path}");
req.Headers.Add("Authorization", "Bearer " + Token);
if (body is not null)
{
req.Content = new StringContent(
JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
}
if (idempotencyKey is not null)
{
req.Headers.Add("Idempotency-Key", idempotencyKey);
}
var res = await Http.SendAsync(req);
var payload = await res.Content.ReadFromJsonAsync<JsonElement>();
if (!payload.GetProperty("ok").GetBoolean())
{
var err = payload.GetProperty("error");
throw new Exception(err.GetProperty("code").GetString()
+ ": " + err.GetProperty("message").GetString());
}
return payload.GetProperty("data");
}
}
3. Check the session and the balance
GET /me tells you whether the token is a guest or a person, and what the balance is.
subject_type is guest or user — a guest can price a run
but cannot start one — and credits is the wallet balance in credits. Compare it
against min_credits from the next step before you run, so a shortfall surfaces as your
own clear message rather than a 402 halfway through a batch.
call me | unwrap
# {"subject_type":"user","username":"you","credits":51234}
#
# subject_type is "guest" or "user". A guest can price a run but not start one.
me = call("me")
print(me["subject_type"], me.get("credits"))
if me["subject_type"] != "user":
raise SystemExit("sign in for a personal token: a guest cannot run a lane")
const me = await call("me");
console.log(me.subject_type, me.credits);
if (me.subject_type !== "user") {
throw new Error("sign in for a personal token: a guest cannot run a lane");
}
raw, err := call("me", nil, "")
if err != nil {
panic(err)
}
var me struct {
SubjectType string `json:"subject_type"`
Username string `json:"username"`
Credits int `json:"credits"`
}
_ = json.Unmarshal(raw, &me)
fmt.Println(me.SubjectType, me.Credits)
if me.SubjectType != "user" {
panic("sign in for a personal token: a guest cannot run a lane")
}
System.out.println(call("me", null, null));
// {"ok":true,"data":{"subject_type":"user","username":"you","credits":51234}}
//
// A "guest" subject_type can price a run with /estimate but cannot start one.
me = call("me")
puts "#{me['subject_type']} #{me['credits']}"
raise "sign in for a personal token" unless me["subject_type"] == "user"
<?php
$me = call("me");
echo $me["subject_type"], " ", $me["credits"], PHP_EOL;
if ($me["subject_type"] !== "user") {
throw new RuntimeException("sign in for a personal token: a guest cannot run a lane");
}
var me = await OddsDesk.Call("me");
Console.WriteLine(me.GetProperty("subject_type").GetString());
Console.WriteLine(me.GetProperty("credits").GetInt32());
if (me.GetProperty("subject_type").GetString() != "user")
{
throw new Exception("sign in for a personal token: a guest cannot run a lane");
}
4. Price the run — free
POST /estimate takes the same body /run takes, creates no job and charges
nothing. The input object is exactly what the app's own form submits:
| field | type | meaning |
|---|---|---|
task | string, required | read, size, plan or risk. The lane. See the section above — it decides the whole shape of the answer. |
basket | string, required | The pasted basket text, as a person would type it. Either a table with a header row — pipe, tab, comma or semicolon delimited, and markdown tables with their --- separator row are fine — or a run of key: value blocks separated by blank lines. Recognised columns: market, venue, side, price, stake, fair, resolves, source, theme, liquidity, notes. This is the entire evidence base: a leg that is not in here does not exist for the run. |
bankroll | string | The account size the percentages are taken against, as typed: "25000" or "25k" both parse. Leave it out and the sizing lane has no denominator, which comes back as a finding rather than an error. |
currency | string | "USD". Every money figure in the reply carries this unit. |
kelly_fraction | string | full, half, third or quarter. The fraction of full Kelly the bands are expressed at. Your number, never a default of the app's choosing. |
per_leg_cap_pct | string | The per-leg cap as a percentage of bankroll, e.g. "5". Drives cap_status on every leg. |
theme_cap_pct | string | The cap on any one theme, e.g. "15". Legs that share a theme resolve off the same world, so this is the number that catches the correlated-bet-as-diversification mistake. |
context | string, optional | Free text about the account, jurisdiction or mandate: which venues are funded, whether an agent places the orders, what the account is not allowed to hold. Read, never argued with, and recorded in assumptions. |
handoff | object, optional | {from_lane, verdict, notes[]}. Set it when one lane runs on another lane's output — the usual chain is size then plan then risk. Treated as the caller's own prior work. |
engine_facts | object | The arithmetic, sent as ground truth. See below — this is the field that makes the answer accountable. |
retry_note | string, optional | Send only on a retry, when a previous reply failed to parse or came back truncated. The instruction is obeyed exactly. |
How a quote is written
Prices are read in whatever notation the paste uses, per leg, and the reply answers in the same notation. All five of these are accepted for the same leg:
| notation | example | reads as |
|---|---|---|
| cents | 62c | 62% implied |
| decimal probability | 0.62 | 62% implied |
| percent | 62% | 62% implied |
| American odds | +150 / -200 | 40% / 66.7% implied |
| fractional | 3/2 | 40% implied |
The convention that decides how every number reads: a leg's price is the cost of
the side named in side, and fair is your own probability for that same
side. A NO leg quoted at 45c with a fair value of 60% is a stated 15-point edge on NO,
not on YES. If a leg's numbers only make sense under the other reading, that is exactly what
findings is for — it will say so rather than quietly reinterpret you.
engine_facts, honestly
In the browser, engine_facts is computed for free before the run by a local parser and
pricer: implied probabilities, edges in points, expected values, full and fractional Kelly, cap
status, theme totals, the resolution calendar, and a flags[] array of deterministic
findings each carrying a unique key. An API caller may compute and send their own, or
omit the field entirely — the run still works, and the model reads basket either
way.
What makes it worth sending is the reconciliation contract. Two rules hold when the field is present:
no engine number is ever contradicted (a disagreement shows up in
findings with the arithmetic, not as a quietly different figure), and every
flags[].key you send comes back exactly once in coverage_check, in
the order you sent them. That turns a fact your own tooling already established into something the
reply is held to. An entry with addressed: false is a legitimate answer — the flag
deliberately set aside with the reason in note — and is a different thing from
silence. A key that never appears at all is a failed run, not a passing one.
The shape, in full:
{
"format": "pipe table",
"currency": "USD",
"bankroll": 25000,
"kelly_fraction": "half",
"per_leg_cap_pct": 5,
"theme_cap_pct": 15,
"totals": {
"leg_count": 4,
"priced_legs": 4,
"legs_with_fair_value": 3,
"total_stake": 8500,
"total_profit_if_all_win": 6042.15,
"total_expected_value": 331.2,
"expected_value_pct_of_stake": 3.9,
"kelly_stake_total": 2604.17,
"at_risk_pct_of_bankroll": 34,
"worst_single_theme_loss": 6700,
"worst_single_theme": "rates"
},
"legs": [
{
"ref": "L1",
"market": "Fed cuts at the December 2026 FOMC",
"venue": "Kalshi",
"side": "YES",
"theme": "rates",
"quote": "62c",
"implied_pct": 62,
"fair_pct": 71,
"edge_pts": 9,
"stake": 3000,
"stake_pct_of_bankroll": 12,
"profit_if_win": 1838.71,
"expected_value": 435,
"kelly_full_pct": 23.68,
"kelly_stake_at_fraction": 2960.53,
"cap_status": "over",
"resolves": "2026-12-10",
"days_to_resolution": 113,
"resolution_source": "FOMC statement",
"liquidity": 12000,
"notes": ""
}
],
"legs_omitted": 0,
"themes": [
{ "theme": "rates", "legs": 3, "stake": 6700, "refs": ["L1", "L2", "L3"] },
{ "theme": "politics", "legs": 1, "stake": 1800, "refs": ["L4"] }
],
"calendar": [
{ "month": "2026-12", "legs": 3, "stake": 6700 },
{ "month": "undated", "legs": 1, "stake": 1800 }
],
"flags": [
{ "key": "CAP-BREACH@L1", "id": "CAP-BREACH", "severity": "high", "legs": ["L1"],
"label": "Stake breaches the per-leg cap - $3,000 is 12% of bankroll against a 5% cap" },
{ "key": "NEG-EDGE@L3", "id": "NEG-EDGE", "severity": "critical", "legs": ["L3"],
"label": "Negative edge as stated - fair 44% against a 48% quote is -4.0 points" },
{ "key": "FAIR-MISSING@L4", "id": "FAIR-MISSING", "severity": "high", "legs": ["L4"],
"label": "No stated fair probability - no edge, no expected value, no Kelly size" },
{ "key": "THEME-CONC@L1+L2+L3", "id": "THEME-CONC", "severity": "high",
"legs": ["L1", "L2", "L3"],
"label": "Theme exposure breaches the theme cap - rates carries $6,700, 26.8% against 15%" }
],
"flags_omitted": 0
}
flags[].key is the id plus the legs it fired on (NEG-EDGE@L3,
THEME-CONC@L1+L2+L3), with a #2 suffix if the same id fires twice on the
same legs. The key, not the id, is what coverage_check is keyed on, because the same id
fires once per leg and a bare id could not be reconciled exactly once. These are the ids worth
raising by hand if you compute your own facts:
| id | severity | fires when |
|---|---|---|
PRICE-BAD | critical | The quote could not be read in any of the five notations. |
NEG-EDGE | critical | The stated fair value is below the quote, so the leg is negative-edge on the caller's own numbers. |
KELLY-NEG | critical | The Kelly fraction computes to zero or below. |
BANKROLL-OVER | critical | Total stake exceeds the stated bankroll. |
RESOLVE-PAST | critical | The resolution date has already passed. |
STAKE-MISSING | high | No planned stake on a leg. |
FAIR-MISSING | high | No stated fair probability, so the leg has no edge, no expected value and no Kelly size. |
OVER-KELLY | high | The planned stake exceeds the chosen Kelly fraction. |
CAP-BREACH | high | A leg breaches per_leg_cap_pct. |
THEME-CONC | high | A theme breaches theme_cap_pct. |
LIQ-SHORT | high | The stake is larger than the quoted depth in liquidity. |
RESOLVE-MISSING | high | No resolution date on a leg. |
SOURCE-MISSING | high | No named resolution source. |
ROW-UNREAD | high | A pasted row was not read as a leg at all. |
FAIR-EQ-PRICE | medium | The stated fair value equals the market, so there is nothing to size. |
DUP-LEG | medium | The same market and side appears twice; the exposure is double-counted. |
BOTH-SIDES | medium | Both sides of one market are held, netting the directional view out. |
OVERROUND | medium | Both sides cost more than 100 together — that gap is the venue's take. |
ARB-LOOK | medium | Both sides cost less than 100 together. Re-check both quotes are live, same market, same resolution before believing it. |
SOURCE-VAGUE | medium | The resolution source is not a nameable authority. |
SIDE-MISSING | medium | No side stated, so the leg is read as YES. |
BANKROLL-MISSING | medium | No bankroll given, so nothing can be expressed as a share of it. |
THIN-EDGE | low | The edge sits inside plausible fees and slippage. |
LONG-DATED | low | Capital is tied up beyond a year. |
SINGLE-LEG | low | A basket of one is concentration by construction. |
VENUE-MISSING | low | No venue named on a leg. |
What /estimate returns is the model binding — model,
model_alias, markup_bps — and the reservation:
hold_credits is what gets held, min_credits is the balance you must clear
to start, and sponsor_enabled says whether the app is covering the run.
hold_credits is a reservation, not a price. It prices the full output
cap, so the charged_credits you see after settlement is usually far lower, often a small
fraction of the hold. Budget against hold_credits, report against
charged_credits.
The hold differs per lane, because the lanes do not produce the same amount of
output: risk carries eight checks and three keys per leg, read carries six
checks and a movers[] array. So estimate the lane you are about to run. A quote taken
for read and then spent on risk is how a batch job discovers
402 payment_required at leg fourteen.
# The body of /estimate is the INPUT OBJECT ITSELF. There is no wrapper key:
# no "input", no "fields" -- the object starting with "task" is the whole body.
INPUT='{"task": "size", "basket": "market | venue | side | price | stake | fair | resolves | source | theme | liquidity\nFed cuts at the December 2026 FOMC | Kalshi | YES | 62c | 3000 | 71% | 2026-12-10 | FOMC statement | rates | 12000\nFed cuts at the December 2026 FOMC | Polymarket | NO | 41c | 1200 | 33% | 2026-12-10 | FOMC statement | rates | 40000\nCPI YoY above 3.0% for November 2026 | Kalshi | YES | 0.48 | 2500 | 44% | 2026-12-10 | BLS CPI release | rates | 6000\nIncumbent wins the 2026 GA runoff | Polymarket | YES | +150 | 1800 | | | | politics | 900", "bankroll": "25000", "currency": "USD", "kelly_fraction": "half", "per_leg_cap_pct": "5", "theme_cap_pct": "15", "context": "Personal account, no leverage. Comfortable holding to settlement.", "engine_facts": {"format": "pipe table", "currency": "USD", "bankroll": 25000, "kelly_fraction": "half", "per_leg_cap_pct": 5, "theme_cap_pct": 15, "totals": {"leg_count": 4, "priced_legs": 4, "legs_with_fair_value": 3, "total_stake": 8500, "total_expected_value": 331.2, "kelly_stake_total": 2604.17, "at_risk_pct_of_bankroll": 34, "worst_single_theme_loss": 6700, "worst_single_theme": "rates"}, "legs": [{"ref": "L1", "market": "Fed cuts at the December 2026 FOMC", "venue": "Kalshi", "side": "YES", "theme": "rates", "quote": "62c", "implied_pct": 62, "fair_pct": 71, "edge_pts": 9, "stake": 3000, "stake_pct_of_bankroll": 12, "profit_if_win": 1838.71, "expected_value": 435, "kelly_full_pct": 23.68, "kelly_stake_at_fraction": 2960.53, "cap_status": "over", "resolves": "2026-12-10", "days_to_resolution": 113, "resolution_source": "FOMC statement", "liquidity": 12000, "notes": ""}], "legs_omitted": 3, "themes": [{"theme": "rates", "legs": 3, "stake": 6700, "refs": ["L1", "L2", "L3"]}, {"theme": "politics", "legs": 1, "stake": 1800, "refs": ["L4"]}], "calendar": [{"month": "2026-12", "legs": 3, "stake": 6700}, {"month": "undated", "legs": 1, "stake": 1800}], "flags": [{"key": "CAP-BREACH@L1", "id": "CAP-BREACH", "severity": "high", "legs": ["L1"], "label": "Stake breaches the per-leg cap - $3,000 is 12% of bankroll against a 5% cap"}, {"key": "NEG-EDGE@L3", "id": "NEG-EDGE", "severity": "critical", "legs": ["L3"], "label": "Negative edge as stated - fair 44% against a 48% quote is -4.0 points"}, {"key": "FAIR-MISSING@L4", "id": "FAIR-MISSING", "severity": "high", "legs": ["L4"], "label": "No stated fair probability - the leg has no edge, no EV and no Kelly size"}, {"key": "THEME-CONC@L1+L2+L3", "id": "THEME-CONC", "severity": "high", "legs": ["L1", "L2", "L3"], "label": "Theme exposure breaches the theme cap - rates carries $6,700, 26.8% against a 15% cap"}], "flags_omitted": 0}}'
call estimate "$INPUT" | unwrap
# {"model":"gpt-5.6-terra","model_alias":"gpt-terra","markup_bps":1000,
# "hold_credits":2480,"min_credits":340,"sponsor_enabled":false}
#
# estimate is FREE: no job, no charge. hold_credits is a RESERVATION against the
# full output cap, not the price -- charged_credits after settlement is normally
# a fraction of it. And the hold differs per lane, so estimate the lane you are
# about to run: pricing "read" tells you nothing useful about "risk".
INPUT = {
"task": "size",
"basket": "market | venue | side | price | stake | fair | resolves | source | theme | liquidity\nFed cuts at the December 2026 FOMC | Kalshi | YES | 62c | 3000 | 71% | 2026-12-10 | FOMC statement | rates | 12000\nFed cuts at the December 2026 FOMC | Polymarket | NO | 41c | 1200 | 33% | 2026-12-10 | FOMC statement | rates | 40000\nCPI YoY above 3.0% for November 2026 | Kalshi | YES | 0.48 | 2500 | 44% | 2026-12-10 | BLS CPI release | rates | 6000\nIncumbent wins the 2026 GA runoff | Polymarket | YES | +150 | 1800 | | | | politics | 900",
"bankroll": "25000",
"currency": "USD",
"kelly_fraction": "half",
"per_leg_cap_pct": "5",
"theme_cap_pct": "15",
"context": "Personal account, no leverage. Comfortable holding to settlement.",
"engine_facts": {
"format": "pipe table",
"currency": "USD",
"bankroll": 25000,
"kelly_fraction": "half",
"per_leg_cap_pct": 5,
"theme_cap_pct": 15,
"totals": {
"leg_count": 4, "priced_legs": 4, "legs_with_fair_value": 3,
"total_stake": 8500, "total_expected_value": 331.2,
"kelly_stake_total": 2604.17, "at_risk_pct_of_bankroll": 34,
"worst_single_theme_loss": 6700, "worst_single_theme": "rates",
},
"legs": [
{
"ref": "L1", "market": "Fed cuts at the December 2026 FOMC",
"venue": "Kalshi", "side": "YES", "theme": "rates",
"quote": "62c", "implied_pct": 62, "fair_pct": 71, "edge_pts": 9,
"stake": 3000, "stake_pct_of_bankroll": 12, "profit_if_win": 1838.71,
"expected_value": 435, "kelly_full_pct": 23.68,
"kelly_stake_at_fraction": 2960.53, "cap_status": "over",
"resolves": "2026-12-10", "days_to_resolution": 113,
"resolution_source": "FOMC statement", "liquidity": 12000, "notes": "",
},
# ... one entry per leg, same refs and same order as your own table
],
"legs_omitted": 0,
"themes": [
{"theme": "rates", "legs": 3, "stake": 6700, "refs": ["L1", "L2", "L3"]},
{"theme": "politics", "legs": 1, "stake": 1800, "refs": ["L4"]},
],
"calendar": [
{"month": "2026-12", "legs": 3, "stake": 6700},
{"month": "undated", "legs": 1, "stake": 1800},
],
"flags": [
{"key": "CAP-BREACH@L1", "id": "CAP-BREACH", "severity": "high",
"legs": ["L1"],
"label": "Stake breaches the per-leg cap - $3,000 is 12% of bankroll against a 5% cap"},
{"key": "NEG-EDGE@L3", "id": "NEG-EDGE", "severity": "critical",
"legs": ["L3"],
"label": "Negative edge as stated - fair 44% against a 48% quote is -4.0 points"},
{"key": "FAIR-MISSING@L4", "id": "FAIR-MISSING", "severity": "high",
"legs": ["L4"],
"label": "No stated fair probability - no edge, no EV and no Kelly size"},
{"key": "THEME-CONC@L1+L2+L3", "id": "THEME-CONC", "severity": "high",
"legs": ["L1", "L2", "L3"],
"label": "Theme exposure breaches the theme cap - rates carries $6,700, 26.8%"},
],
"flags_omitted": 0,
},
}
# The body IS the input object -- no wrapper key. estimate is free.
quote = call("estimate", INPUT)
print(quote["hold_credits"], quote["min_credits"], quote["model_alias"])
# Compare against the balance yourself so a shortfall is your message, not a 402.
if me["credits"] < quote["min_credits"]:
raise SystemExit("balance %d is under min_credits %d"
% (me["credits"], quote["min_credits"]))
const INPUT = {
task: "size",
basket: "market | venue | side | price | stake | fair | resolves | source | theme | liquidity\nFed cuts at the December 2026 FOMC | Kalshi | YES | 62c | 3000 | 71% | 2026-12-10 | FOMC statement | rates | 12000\nFed cuts at the December 2026 FOMC | Polymarket | NO | 41c | 1200 | 33% | 2026-12-10 | FOMC statement | rates | 40000\nCPI YoY above 3.0% for November 2026 | Kalshi | YES | 0.48 | 2500 | 44% | 2026-12-10 | BLS CPI release | rates | 6000\nIncumbent wins the 2026 GA runoff | Polymarket | YES | +150 | 1800 | | | | politics | 900",
bankroll: "25000",
currency: "USD",
kelly_fraction: "half",
per_leg_cap_pct: "5",
theme_cap_pct: "15",
context: "Personal account, no leverage. Comfortable holding to settlement.",
engine_facts: {
format: "pipe table",
currency: "USD",
bankroll: 25000,
kelly_fraction: "half",
per_leg_cap_pct: 5,
theme_cap_pct: 15,
totals: {
leg_count: 4, priced_legs: 4, legs_with_fair_value: 3,
total_stake: 8500, total_expected_value: 331.2,
kelly_stake_total: 2604.17, at_risk_pct_of_bankroll: 34,
worst_single_theme_loss: 6700, worst_single_theme: "rates",
},
legs: [
{
ref: "L1", market: "Fed cuts at the December 2026 FOMC",
venue: "Kalshi", side: "YES", theme: "rates",
quote: "62c", implied_pct: 62, fair_pct: 71, edge_pts: 9,
stake: 3000, stake_pct_of_bankroll: 12, profit_if_win: 1838.71,
expected_value: 435, kelly_full_pct: 23.68,
kelly_stake_at_fraction: 2960.53, cap_status: "over",
resolves: "2026-12-10", days_to_resolution: 113,
resolution_source: "FOMC statement", liquidity: 12000, notes: "",
},
// ... one entry per leg, same refs and same order as your own table
],
legs_omitted: 0,
themes: [
{ theme: "rates", legs: 3, stake: 6700, refs: ["L1", "L2", "L3"] },
{ theme: "politics", legs: 1, stake: 1800, refs: ["L4"] },
],
calendar: [
{ month: "2026-12", legs: 3, stake: 6700 },
{ month: "undated", legs: 1, stake: 1800 },
],
flags: [
{ key: "CAP-BREACH@L1", id: "CAP-BREACH", severity: "high", legs: ["L1"],
label: "Stake breaches the per-leg cap - $3,000 is 12% against a 5% cap" },
{ key: "NEG-EDGE@L3", id: "NEG-EDGE", severity: "critical", legs: ["L3"],
label: "Negative edge as stated - fair 44% against a 48% quote" },
{ key: "FAIR-MISSING@L4", id: "FAIR-MISSING", severity: "high", legs: ["L4"],
label: "No stated fair probability - no edge, no EV, no Kelly size" },
{ key: "THEME-CONC@L1+L2+L3", id: "THEME-CONC", severity: "high",
legs: ["L1", "L2", "L3"],
label: "Theme exposure breaches the theme cap - rates carries $6,700" },
],
flags_omitted: 0,
},
};
// The body IS the input object -- no wrapper key. estimate is free.
const quote = await call("estimate", INPUT);
console.log(quote.hold_credits, quote.min_credits, quote.model_alias);
if (me.credits < quote.min_credits) {
throw new Error(`balance ${me.credits} is under min_credits ${quote.min_credits}`);
}
// The request body is the input object itself -- no wrapper key. Modelled as
// typed structs so the field names cannot drift.
type Leg struct {
Ref string `json:"ref"`
Market string `json:"market"`
Venue string `json:"venue"`
Side string `json:"side"`
Theme string `json:"theme"`
Quote string `json:"quote"`
ImpliedPct float64 `json:"implied_pct"`
FairPct float64 `json:"fair_pct"`
EdgePts float64 `json:"edge_pts"`
Stake float64 `json:"stake"`
StakePctOfBankroll float64 `json:"stake_pct_of_bankroll"`
ProfitIfWin float64 `json:"profit_if_win"`
ExpectedValue float64 `json:"expected_value"`
KellyFullPct float64 `json:"kelly_full_pct"`
KellyStakeAtFraction float64 `json:"kelly_stake_at_fraction"`
CapStatus string `json:"cap_status"`
Resolves string `json:"resolves"`
DaysToResolution int `json:"days_to_resolution"`
ResolutionSource string `json:"resolution_source"`
Liquidity float64 `json:"liquidity"`
Notes string `json:"notes"`
}
type Flag struct {
Key string `json:"key"`
ID string `json:"id"`
Severity string `json:"severity"`
Legs []string `json:"legs"`
Label string `json:"label"`
}
type EngineFacts struct {
Format string `json:"format"`
Currency string `json:"currency"`
Bankroll float64 `json:"bankroll"`
KellyFraction string `json:"kelly_fraction"`
PerLegCapPct float64 `json:"per_leg_cap_pct"`
ThemeCapPct float64 `json:"theme_cap_pct"`
Totals map[string]any `json:"totals"`
Legs []Leg `json:"legs"`
LegsOmitted int `json:"legs_omitted"`
Themes []map[string]any `json:"themes"`
Calendar []map[string]any `json:"calendar"`
Flags []Flag `json:"flags"`
FlagsOmitted int `json:"flags_omitted"`
}
type Input struct {
Task string `json:"task"`
Basket string `json:"basket"`
Bankroll string `json:"bankroll"`
Currency string `json:"currency"`
KellyFraction string `json:"kelly_fraction"`
PerLegCapPct string `json:"per_leg_cap_pct"`
ThemeCapPct string `json:"theme_cap_pct"`
Context string `json:"context,omitempty"`
EngineFacts EngineFacts `json:"engine_facts"`
}
input := Input{
Task: "size",
Basket: "market | venue | side | price | stake | fair | resolves | source | theme | liquidity\nFed cuts at the December 2026 FOMC | Kalshi | YES | 62c | 3000 | 71% | 2026-12-10 | FOMC statement | rates | 12000\nFed cuts at the December 2026 FOMC | Polymarket | NO | 41c | 1200 | 33% | 2026-12-10 | FOMC statement | rates | 40000\nCPI YoY above 3.0% for November 2026 | Kalshi | YES | 0.48 | 2500 | 44% | 2026-12-10 | BLS CPI release | rates | 6000\nIncumbent wins the 2026 GA runoff | Polymarket | YES | +150 | 1800 | | | | politics | 900",
Bankroll: "25000",
Currency: "USD",
KellyFraction: "half",
PerLegCapPct: "5",
ThemeCapPct: "15",
Context: "Personal account, no leverage.",
EngineFacts: EngineFacts{
Format: "pipe table", Currency: "USD", Bankroll: 25000,
KellyFraction: "half", PerLegCapPct: 5, ThemeCapPct: 15,
Totals: map[string]any{"leg_count": 4, "total_stake": 8500,
"worst_single_theme": "rates", "worst_single_theme_loss": 6700},
Legs: []Leg{{Ref: "L1", Market: "Fed cuts at the December 2026 FOMC",
Venue: "Kalshi", Side: "YES", Theme: "rates", Quote: "62c",
ImpliedPct: 62, FairPct: 71, EdgePts: 9, Stake: 3000,
CapStatus: "over", Resolves: "2026-12-10", Liquidity: 12000}},
Themes: []map[string]any{{"theme": "rates", "legs": 3, "stake": 6700,
"refs": []string{"L1", "L2", "L3"}}},
Calendar: []map[string]any{{"month": "2026-12", "legs": 3, "stake": 6700}},
Flags: []Flag{{Key: "CAP-BREACH@L1", ID: "CAP-BREACH", Severity: "high",
Legs: []string{"L1"}, Label: "Stake breaches the per-leg cap"}},
},
}
body, _ := json.Marshal(input) // this JSON is the whole request body
raw, err := call("estimate", body, "")
if err != nil {
panic(err)
}
var quote struct {
Model string `json:"model_alias"`
Hold int `json:"hold_credits"`
MinCredits int `json:"min_credits"`
Sponsored bool `json:"sponsor_enabled"`
}
_ = json.Unmarshal(raw, "e)
fmt.Println(quote.Model, quote.Hold, quote.MinCredits) // estimate is free
// The request body is the input object itself -- no wrapper key. Kept as a JSON
// string here so the sample has no dependencies; the engine_facts block is
// abbreviated to one leg and one flag.
// (A Java text block would read better; concatenation keeps the sample
// pasteable into any source file without escaping surprises.)
String BASKET = "market | venue | side | price | stake | fair | resolves | source | theme | liquidity\nFed cuts at the December 2026 FOMC | Kalshi | YES | 62c | 3000 | 71% | 2026-12-10 | FOMC statement | rates | 12000\nFed cuts at the December 2026 FOMC | Polymarket | NO | 41c | 1200 | 33% | 2026-12-10 | FOMC statement | rates | 40000\nCPI YoY above 3.0% for November 2026 | Kalshi | YES | 0.48 | 2500 | 44% | 2026-12-10 | BLS CPI release | rates | 6000\nIncumbent wins the 2026 GA runoff | Polymarket | YES | +150 | 1800 | | | | politics | 900";
String INPUT = "{"
+ "\"task\": \"size\","
+ "\"basket\": \"" + BASKET + "\","
+ "\"bankroll\": \"25000\","
+ "\"currency\": \"USD\","
+ "\"kelly_fraction\": \"half\","
+ "\"per_leg_cap_pct\": \"5\","
+ "\"theme_cap_pct\": \"15\","
+ "\"context\": \"Personal account, no leverage.\","
+ "\"engine_facts\": {"
+ " \"format\": \"pipe table\", \"currency\": \"USD\", \"bankroll\": 25000,"
+ " \"kelly_fraction\": \"half\", \"per_leg_cap_pct\": 5, \"theme_cap_pct\": 15,"
+ " \"totals\": {\"leg_count\": 4, \"total_stake\": 8500,"
+ " \"worst_single_theme\": \"rates\","
+ " \"worst_single_theme_loss\": 6700},"
+ " \"legs\": [{\"ref\": \"L1\","
+ " \"market\": \"Fed cuts at the December 2026 FOMC\","
+ " \"venue\": \"Kalshi\", \"side\": \"YES\", \"theme\": \"rates\","
+ " \"quote\": \"62c\", \"implied_pct\": 62, \"fair_pct\": 71,"
+ " \"edge_pts\": 9, \"stake\": 3000, \"cap_status\": \"over\","
+ " \"resolves\": \"2026-12-10\", \"liquidity\": 12000}],"
+ " \"legs_omitted\": 3,"
+ " \"themes\": [{\"theme\": \"rates\", \"legs\": 3, \"stake\": 6700,"
+ " \"refs\": [\"L1\",\"L2\",\"L3\"]}],"
+ " \"calendar\": [{\"month\": \"2026-12\", \"legs\": 3, \"stake\": 6700}],"
+ " \"flags\": [{\"key\": \"CAP-BREACH@L1\", \"id\": \"CAP-BREACH\","
+ " \"severity\": \"high\", \"legs\": [\"L1\"],"
+ " \"label\": \"Stake breaches the per-leg cap\"}],"
+ " \"flags_omitted\": 0"
+ "}}";
// estimate creates no job and charges nothing.
System.out.println(call("estimate", INPUT, null));
// {"ok":true,"data":{"model_alias":"gpt-terra","hold_credits":2480,
// "min_credits":340,"sponsor_enabled":false}}
# The request body is the input object itself -- no wrapper key.
INPUT = {
"task" => "size",
"basket" => "market | venue | side | price | stake | fair | resolves | source | theme | liquidity\nFed cuts at the December 2026 FOMC | Kalshi | YES | 62c | 3000 | 71% | 2026-12-10 | FOMC statement | rates | 12000\nFed cuts at the December 2026 FOMC | Polymarket | NO | 41c | 1200 | 33% | 2026-12-10 | FOMC statement | rates | 40000\nCPI YoY above 3.0% for November 2026 | Kalshi | YES | 0.48 | 2500 | 44% | 2026-12-10 | BLS CPI release | rates | 6000\nIncumbent wins the 2026 GA runoff | Polymarket | YES | +150 | 1800 | | | | politics | 900",
"bankroll" => "25000",
"currency" => "USD",
"kelly_fraction" => "half",
"per_leg_cap_pct" => "5",
"theme_cap_pct" => "15",
"context" => "Personal account, no leverage.",
"engine_facts" => {
"format" => "pipe table",
"currency" => "USD",
"bankroll" => 25_000,
"kelly_fraction" => "half",
"per_leg_cap_pct" => 5,
"theme_cap_pct" => 15,
"totals" => {
"leg_count" => 4, "priced_legs" => 4, "legs_with_fair_value" => 3,
"total_stake" => 8_500, "total_expected_value" => 331.2,
"kelly_stake_total" => 2_604.17, "at_risk_pct_of_bankroll" => 34,
"worst_single_theme_loss" => 6_700, "worst_single_theme" => "rates"
},
"legs" => [
{ "ref" => "L1", "market" => "Fed cuts at the December 2026 FOMC",
"venue" => "Kalshi", "side" => "YES", "theme" => "rates",
"quote" => "62c", "implied_pct" => 62, "fair_pct" => 71,
"edge_pts" => 9, "stake" => 3_000, "stake_pct_of_bankroll" => 12,
"kelly_full_pct" => 23.68, "kelly_stake_at_fraction" => 2_960.53,
"cap_status" => "over", "resolves" => "2026-12-10",
"days_to_resolution" => 113, "resolution_source" => "FOMC statement",
"liquidity" => 12_000, "notes" => "" }
# ... one entry per leg, same refs and same order as your own table
],
"legs_omitted" => 0,
"themes" => [
{ "theme" => "rates", "legs" => 3, "stake" => 6_700, "refs" => %w[L1 L2 L3] },
{ "theme" => "politics", "legs" => 1, "stake" => 1_800, "refs" => %w[L4] }
],
"calendar" => [{ "month" => "2026-12", "legs" => 3, "stake" => 6_700 }],
"flags" => [
{ "key" => "CAP-BREACH@L1", "id" => "CAP-BREACH", "severity" => "high",
"legs" => %w[L1], "label" => "Stake breaches the per-leg cap" },
{ "key" => "NEG-EDGE@L3", "id" => "NEG-EDGE", "severity" => "critical",
"legs" => %w[L3], "label" => "Negative edge as stated" }
],
"flags_omitted" => 0
}
}
quote = call("estimate", INPUT) # free: no job, no charge
puts "hold=#{quote['hold_credits']} min=#{quote['min_credits']} #{quote['model_alias']}"
<?php
// The request body is the input object itself -- no wrapper key.
$INPUT = [
"task" => "size",
"basket" => "market | venue | side | price | stake | fair | resolves | source | theme | liquidity\nFed cuts at the December 2026 FOMC | Kalshi | YES | 62c | 3000 | 71% | 2026-12-10 | FOMC statement | rates | 12000\nFed cuts at the December 2026 FOMC | Polymarket | NO | 41c | 1200 | 33% | 2026-12-10 | FOMC statement | rates | 40000\nCPI YoY above 3.0% for November 2026 | Kalshi | YES | 0.48 | 2500 | 44% | 2026-12-10 | BLS CPI release | rates | 6000\nIncumbent wins the 2026 GA runoff | Polymarket | YES | +150 | 1800 | | | | politics | 900",
"bankroll" => "25000",
"currency" => "USD",
"kelly_fraction" => "half",
"per_leg_cap_pct" => "5",
"theme_cap_pct" => "15",
"context" => "Personal account, no leverage.",
"engine_facts" => [
"format" => "pipe table",
"currency" => "USD",
"bankroll" => 25000,
"kelly_fraction" => "half",
"per_leg_cap_pct" => 5,
"theme_cap_pct" => 15,
"totals" => [
"leg_count" => 4, "priced_legs" => 4, "legs_with_fair_value" => 3,
"total_stake" => 8500, "total_expected_value" => 331.2,
"kelly_stake_total" => 2604.17, "at_risk_pct_of_bankroll" => 34,
"worst_single_theme_loss" => 6700, "worst_single_theme" => "rates",
],
"legs" => [[
"ref" => "L1", "market" => "Fed cuts at the December 2026 FOMC",
"venue" => "Kalshi", "side" => "YES", "theme" => "rates",
"quote" => "62c", "implied_pct" => 62, "fair_pct" => 71,
"edge_pts" => 9, "stake" => 3000, "cap_status" => "over",
"resolves" => "2026-12-10", "liquidity" => 12000,
]],
"legs_omitted" => 3,
"themes" => [[
"theme" => "rates", "legs" => 3, "stake" => 6700,
"refs" => ["L1", "L2", "L3"],
]],
"calendar" => [["month" => "2026-12", "legs" => 3, "stake" => 6700]],
"flags" => [[
"key" => "CAP-BREACH@L1", "id" => "CAP-BREACH", "severity" => "high",
"legs" => ["L1"], "label" => "Stake breaches the per-leg cap",
]],
"flags_omitted" => 0,
],
];
$quote = call("estimate", $INPUT); // free: no job, no charge
printf("hold=%d min=%d %s\n", $quote["hold_credits"], $quote["min_credits"],
$quote["model_alias"]);
// The request body is the input object itself -- no wrapper key. Anonymous
// objects serialise straight to it.
var INPUT = new
{
task = "size",
basket = "market | venue | side | price | stake | fair | resolves | source | theme | liquidity\nFed cuts at the December 2026 FOMC | Kalshi | YES | 62c | 3000 | 71% | 2026-12-10 | FOMC statement | rates | 12000\nFed cuts at the December 2026 FOMC | Polymarket | NO | 41c | 1200 | 33% | 2026-12-10 | FOMC statement | rates | 40000\nCPI YoY above 3.0% for November 2026 | Kalshi | YES | 0.48 | 2500 | 44% | 2026-12-10 | BLS CPI release | rates | 6000\nIncumbent wins the 2026 GA runoff | Polymarket | YES | +150 | 1800 | | | | politics | 900",
bankroll = "25000",
currency = "USD",
kelly_fraction = "half",
per_leg_cap_pct = "5",
theme_cap_pct = "15",
context = "Personal account, no leverage.",
engine_facts = new
{
format = "pipe table",
currency = "USD",
bankroll = 25000,
kelly_fraction = "half",
per_leg_cap_pct = 5,
theme_cap_pct = 15,
totals = new
{
leg_count = 4, priced_legs = 4, legs_with_fair_value = 3,
total_stake = 8500, total_expected_value = 331.2,
kelly_stake_total = 2604.17, at_risk_pct_of_bankroll = 34,
worst_single_theme_loss = 6700, worst_single_theme = "rates",
},
legs = new[]
{
new
{
r_ef = "L1", market = "Fed cuts at the December 2026 FOMC",
venue = "Kalshi", side = "YES", theme = "rates", quote = "62c",
implied_pct = 62, fair_pct = 71, edge_pts = 9, stake = 3000,
cap_status = "over", resolves = "2026-12-10", liquidity = 12000,
},
},
legs_omitted = 3,
themes = new[] { new { theme = "rates", legs = 3, stake = 6700 } },
calendar = new[] { new { month = "2026-12", legs = 3, stake = 6700 } },
flags = new[]
{
new
{
key = "CAP-BREACH@L1", id = "CAP-BREACH", severity = "high",
legs = new[] { "L1" }, label = "Stake breaches the per-leg cap",
},
},
flags_omitted = 0,
},
};
// Note: "ref" is a C# keyword, so serialise the leg ref with a JsonPropertyName
// attribute on a real DTO rather than the r_ef placeholder above.
var quote = await OddsDesk.Call("estimate", INPUT); // free: no job, no charge
Console.WriteLine(quote.GetProperty("hold_credits").GetInt32());
Console.WriteLine(quote.GetProperty("min_credits").GetInt32());
5. Run it, then poll
POST /run takes the same bare input object and returns a job_id; poll
GET /jobs/{job_id} until status is succeeded or
failed. The worksheet is the string at data.output.output. The terminal job
also carries charged_credits — the real price — and the
truncated flag.
Always send an Idempotency-Key header. It is not formally required by
the endpoint and it is required in practice: derive it from the input the way the web app does, a
content hash plus an attempt counter (odds-desk:<hash>:a<attempt>). A
retried request carrying the same key returns the same job instead of billing a second run, which is
what makes a retry safe after a network blip. Replaying a key with a different body is a 409
conflict — and because the body is the input object itself, switching
task from plan to risk over the same basket is a different
body. Bump the attempt suffix whenever the input actually changed.
Poll with a delay of a second or two. A tight loop earns a 429 rate_limited and gets you
the answer no sooner.
# Derive the key from the input so a retry after a network blip replays the same
# job instead of billing a second run. Same body + same key = same job.
KEY="odds-desk:$(printf '%s' "$INPUT" | shasum -a 256 | cut -c1-16):a1"
JOB=$(call run "$INPUT" "$KEY" | unwrap \
| python3 -c 'import sys,json;print(json.load(sys.stdin)["job_id"])')
# Poll until the job reaches a terminal status. Do not tight-loop: 429 is real.
while :; do
OUT=$(call "jobs/$JOB")
STATUS=$(printf '%s' "$OUT" \
| python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["status"])')
[ "$STATUS" = "succeeded" ] && break
[ "$STATUS" = "failed" ] && echo "$OUT" && exit 1
sleep 2
done
# The terminal job looks like this -- the worksheet is a STRING at
# data.output.output, so it needs unwrapping twice:
# {"ok":true,"data":{"job_id":"job_...","status":"succeeded",
# "output":{"output":"{\"lane\":\"size\",\"basket_name\":\"Dec 2026 rates ...\"}"},
# "charged_credits":612,"truncated":false}}
printf '%s' "$OUT" \
| python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["output"]["output"])'
import hashlib
import time
# Derive the key from the input: a retry with the same key returns the SAME job
# instead of billing a second run. Bump :a2 only when the input really changed.
digest = hashlib.sha256(json.dumps(INPUT, sort_keys=True).encode()).hexdigest()[:16]
key = "odds-desk:%s:a1" % digest
started = call("run", INPUT, idempotency_key=key)
job_id = started["job_id"]
while True:
job = call("jobs/" + job_id)
if job["status"] == "succeeded":
break
if job["status"] == "failed":
raise RuntimeError(job.get("error"))
time.sleep(2)
# Unwrap twice: the envelope, then the JSON string the model produced.
sheet = json.loads(job["output"]["output"])
print(sheet["lane"], sheet["posture"], len(sheet["legs"]), "legs",
len(sheet["findings"]), "findings")
print("charged", job.get("charged_credits"), "truncated", job.get("truncated"))
import { createHash } from "node:crypto";
// Derive the key from the input: a retry with the same key returns the SAME job
// instead of billing a second run. Bump :a2 only when the input really changed.
const digest = createHash("sha256").update(JSON.stringify(INPUT)).digest("hex").slice(0, 16);
const key = `odds-desk:${digest}:a1`;
const started = await call("run", INPUT, key);
let job = started;
while (job.status !== "succeeded" && job.status !== "failed") {
await new Promise((r) => setTimeout(r, 2000));
job = await call(`jobs/${started.job_id}`);
}
if (job.status === "failed") throw new Error(JSON.stringify(job.error));
// Unwrap twice: the envelope, then the JSON string the model produced.
const sheet = JSON.parse(job.output.output);
console.log(sheet.lane, sheet.posture, sheet.legs.length, "legs");
console.log("charged", job.charged_credits, "truncated", job.truncated);
// Derive the key from the input: a retry with the same key returns the SAME job
// instead of billing a second run.
sum := sha256.Sum256(body)
key := fmt.Sprintf("odds-desk:%x:a1", sum[:8])
startedRaw, err := call("run", body, key)
if err != nil {
panic(err)
}
var started struct {
JobID string `json:"job_id"`
}
_ = json.Unmarshal(startedRaw, &started)
type job struct {
Status string `json:"status"`
ChargedCredits int `json:"charged_credits"`
Truncated bool `json:"truncated"`
Output struct {
Output string `json:"output"`
} `json:"output"`
}
var j job
for {
jobRaw, err := call("jobs/"+started.JobID, nil, "")
if err != nil {
panic(err)
}
_ = json.Unmarshal(jobRaw, &j)
if j.Status == "succeeded" || j.Status == "failed" {
break
}
time.Sleep(2 * time.Second)
}
if j.Status == "failed" {
panic("run failed")
}
// Unwrap twice: the envelope, then the JSON string the model produced.
var sheet Sheet // the struct from step 7
_ = json.Unmarshal([]byte(j.Output.Output), &sheet)
fmt.Println(sheet.Lane, sheet.Posture, len(sheet.Legs), j.ChargedCredits, j.Truncated)
// Derive the key from the input: a retry with the same key returns the SAME job
// instead of billing a second run.
var sha = MessageDigest.getInstance("SHA-256").digest(INPUT.getBytes(UTF_8));
var hex = new StringBuilder();
for (int i = 0; i < 8; i++) hex.append(String.format("%02x", sha[i]));
String key = "odds-desk:" + hex + ":a1";
String startedBody = call("run", INPUT, key);
// {"ok":true,"data":{"job_id":"job_...","status":"queued"}}
String jobId = startedBody.split("\"job_id\":\"")[1].split("\"")[0];
String jobBody;
String status;
do {
Thread.sleep(2000);
jobBody = call("jobs/" + jobId, null, null);
status = jobBody.contains("\"status\":\"succeeded\"") ? "succeeded"
: jobBody.contains("\"status\":\"failed\"") ? "failed" : "running";
} while (status.equals("running"));
if (status.equals("failed")) throw new IllegalStateException(jobBody);
System.out.println(jobBody);
// data.output.output is the worksheet as a JSON STRING -- parse it with a real
// JSON library, then walk the contract described in step 7.
require "digest"
# Derive the key from the input: a retry with the same key returns the SAME job
# instead of billing a second run.
digest = Digest::SHA256.hexdigest(JSON.generate(INPUT))[0, 16]
key = "odds-desk:#{digest}:a1"
started = call("run", INPUT, key)
job = nil
loop do
job = call("jobs/#{started['job_id']}")
break if %w[succeeded failed].include?(job["status"])
sleep 2
end
raise "run failed: #{job['error']}" if job["status"] == "failed"
# Unwrap twice: the envelope, then the JSON string the model produced.
sheet = JSON.parse(job["output"]["output"])
puts "#{sheet['lane']} #{sheet['posture']} #{sheet['legs'].length} legs"
puts "charged #{job['charged_credits']} truncated #{job['truncated']}"
<?php
// Derive the key from the input: a retry with the same key returns the SAME job
// instead of billing a second run.
$key = "odds-desk:" . substr(hash("sha256", json_encode($INPUT)), 0, 16) . ":a1";
$started = call("run", $INPUT, $key);
do {
sleep(2);
$job = call("jobs/" . $started["job_id"]);
} while (!in_array($job["status"], ["succeeded", "failed"], true));
if ($job["status"] === "failed") {
throw new RuntimeException("run failed: " . json_encode($job["error"] ?? null));
}
// Unwrap twice: the envelope, then the JSON string the model produced.
$sheet = json_decode($job["output"]["output"], true);
printf("%s %s %d legs, charged %d, truncated %s\n",
$sheet["lane"], $sheet["posture"], count($sheet["legs"]),
$job["charged_credits"], $job["truncated"] ? "yes" : "no");
// Derive the key from the input: a retry with the same key returns the SAME job
// instead of billing a second run.
var json = JsonSerializer.Serialize(INPUT);
var hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(json)))
.ToLowerInvariant()[..16];
var key = $"odds-desk:{hash}:a1";
var started = await OddsDesk.Call("run", INPUT, key);
var jobId = started.GetProperty("job_id").GetString();
JsonElement job;
string status;
do
{
await Task.Delay(2000);
job = await OddsDesk.Call($"jobs/{jobId}");
status = job.GetProperty("status").GetString();
} while (status != "succeeded" && status != "failed");
if (status == "failed") throw new Exception("run failed");
// Unwrap twice: the envelope, then the JSON string the model produced.
var sheetJson = job.GetProperty("output").GetProperty("output").GetString();
var sheet = JsonSerializer.Deserialize<JsonElement>(sheetJson);
Console.WriteLine(sheet.GetProperty("lane").GetString());
Console.WriteLine(sheet.GetProperty("posture").GetString());
Console.WriteLine(job.GetProperty("charged_credits").GetInt32());
6. Or stream it
POST /run-stream is the same call over server-sent events, with the same bare input
object as the body and the same Idempotency-Key discipline. Each delta
event carries {"text": "..."}, a chunk of the worksheet JSON; a job event
arrives first with the job_id; the final done event carries
status, charged_credits and truncated. An error
event carries {code, message, job_id} instead.
One thing to know before you write the reader: on an idempotent replay the endpoint
answers with a plain JSON envelope rather than a stream, because there is nothing left to generate.
Check the response content-type for text/event-stream before you start
parsing frames, and fall back to reading data if it is missing.
The practical tip for a progress display: do not try to parse the partial JSON. Watch for key names
arriving in the accumulating text. The appearance of "legs", then
"checks", then "actions", then "coverage_check", then
"summary" is the order the worksheet is written in, and substring matching on the quoted
key name is enough to advance a stage label. It costs nothing and never throws.
# Server-sent events. Same bare input object as the body, same Idempotency-Key
# discipline; the only additions are the Accept header and -N to stop buffering.
curl -N -X POST "$BASE/run-stream" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-H "Accept: text/event-stream" \
-d "$INPUT"
# event: job {"job_id":"job_..."}
# event: delta {"text":"{\"lane\":\"size\",\"basket_name\":\"Dec 2026 rates"}
# event: delta {"text":" and one runoff\",\"posture\":\"caution\","}
# event: done {"job_id":"job_...","status":"succeeded","charged_credits":612,
# "truncated":false}
#
# On an idempotent replay the endpoint answers with plain JSON instead of a
# stream -- check the response content-type before you parse events.
# Server-sent events: the worksheet arrives in chunks, so a UI can show progress.
req = urllib.request.Request(
BASE + "/run-stream", data=json.dumps(INPUT).encode(), method="POST")
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Content-Type", "application/json")
req.add_header("Idempotency-Key", key)
req.add_header("Accept", "text/event-stream")
raw = ""
done = {}
event = None
stage = "reading the basket"
with urllib.request.urlopen(req) as stream:
for line in stream:
line = line.decode().rstrip("\n")
if line.startswith("event: "):
event = line[7:]
elif line.startswith("data: ") and event == "delta":
raw += json.loads(line[6:]).get("text", "")
# Watching for key names is enough to drive a progress display --
# cheaper and far more robust than parsing partial JSON.
if '"summary"' in raw:
stage = "closing the worksheet"
elif '"coverage_check"' in raw:
stage = "reconciling the engine flags"
elif '"actions"' in raw:
stage = "writing the actions"
elif '"checks"' in raw:
stage = "working the lane checks"
elif '"legs"' in raw:
stage = "walking the legs"
elif line.startswith("data: ") and event == "done":
done = json.loads(line[6:])
sheet = json.loads(raw[raw.index("{"):raw.rindex("}") + 1])
print(stage, sheet["posture"], done.get("charged_credits"), done.get("truncated"))
// Server-sent events. Same bare input object as the body.
const res = await fetch(`${BASE}/run-stream`, {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
"Idempotency-Key": key,
Accept: "text/event-stream",
},
body: JSON.stringify(INPUT),
});
// An idempotent replay answers with plain JSON, not a stream.
if (!(res.headers.get("content-type") || "").includes("text/event-stream")) {
const replay = (await res.json()).data;
console.log("replayed job", replay.job_id);
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let raw = "";
let done = null;
for (;;) {
const chunk = await reader.read();
if (chunk.done) break;
buffer += decoder.decode(chunk.value, { stream: true });
let idx;
while ((idx = buffer.indexOf("\n\n")) >= 0) {
const frame = buffer.slice(0, idx);
buffer = buffer.slice(idx + 2);
let name = "message";
let 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") raw += payload.text || "";
else if (name === "done") done = payload;
}
}
const sheet = JSON.parse(raw.slice(raw.indexOf("{"), raw.lastIndexOf("}") + 1));
console.log(sheet.posture, done?.charged_credits, done?.truncated);
// Server-sent events, read frame by frame with bufio.
streamReq, _ := http.NewRequest(http.MethodPost, base+"/run-stream", bytes.NewReader(body))
streamReq.Header.Set("Authorization", "Bearer "+token)
streamReq.Header.Set("Content-Type", "application/json")
streamReq.Header.Set("Idempotency-Key", key)
streamReq.Header.Set("Accept", "text/event-stream")
streamRes, err := http.DefaultClient.Do(streamReq)
if err != nil {
panic(err)
}
defer streamRes.Body.Close()
var accumulated strings.Builder
var doneEvent struct {
Status string `json:"status"`
ChargedCredits int `json:"charged_credits"`
Truncated bool `json:"truncated"`
}
scanner := bufio.NewScanner(streamRes.Body)
scanner.Buffer(make([]byte, 0, 1024*1024), 4*1024*1024)
event := ""
for scanner.Scan() {
line := scanner.Text()
switch {
case strings.HasPrefix(line, "event: "):
event = strings.TrimSpace(line[7:])
case strings.HasPrefix(line, "data: ") && event == "delta":
var d struct {
Text string `json:"text"`
}
if json.Unmarshal([]byte(line[6:]), &d) == nil {
accumulated.WriteString(d.Text)
}
case strings.HasPrefix(line, "data: ") && event == "done":
_ = json.Unmarshal([]byte(line[6:]), &doneEvent)
}
}
text := accumulated.String()
text = text[strings.Index(text, "{") : strings.LastIndex(text, "}")+1]
var streamed Sheet
_ = json.Unmarshal([]byte(text), &streamed)
fmt.Println(streamed.Posture, doneEvent.ChargedCredits, doneEvent.Truncated)
// Server-sent events, line by line. The bare input object is still the body.
var streamReq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.header("Idempotency-Key", key)
.header("Accept", "text/event-stream")
.POST(HttpRequest.BodyPublishers.ofString(INPUT))
.build();
var stream = HTTP.send(streamReq, HttpResponse.BodyHandlers.ofLines());
var accumulated = new StringBuilder();
String doneLine = null;
String event = null;
for (String line : (Iterable<String>) stream.body()::iterator) {
if (line.startsWith("event: ")) {
event = line.substring(7).trim();
} else if (line.startsWith("data: ") && "delta".equals(event)) {
// {"text":"..."} -- decode with a JSON library in real code.
accumulated.append(line.substring(6));
} else if (line.startsWith("data: ") && "done".equals(event)) {
doneLine = line.substring(6);
}
}
System.out.println(doneLine);
// {"job_id":"job_...","status":"succeeded","charged_credits":612,"truncated":false}
System.out.println(accumulated.length() + " bytes of delta text");
# Server-sent events. Net::HTTP yields the body in chunks; split on blank lines.
uri = URI("#{BASE}/run-stream")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = key
req["Accept"] = "text/event-stream"
req.body = JSON.generate(INPUT)
buffer = ""
raw = ""
done = {}
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(req) do |res|
res.read_body do |chunk|
buffer << chunk
while (idx = buffer.index("\n\n"))
frame = buffer.slice!(0, idx + 2)
event = frame[/^event:\s*(\S+)/, 1]
data = frame.scan(/^data:\s?(.*)$/).flatten.join
next if data.empty?
payload = JSON.parse(data)
raw << (payload["text"] || "") if event == "delta"
done = payload if event == "done"
end
end
end
end
sheet = JSON.parse(raw[raw.index("{")..raw.rindex("}")])
puts "#{sheet['posture']} charged=#{done['charged_credits']} truncated=#{done['truncated']}"
<?php
// Server-sent events via a curl write callback.
$raw = "";
$done = [];
$buffer = "";
$ch = curl_init(BASE . "/run-stream");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($INPUT));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . TOKEN,
"Content-Type: application/json",
"Idempotency-Key: " . $key,
"Accept: text/event-stream",
]);
curl_setopt($ch, CURLOPT_WRITEFUNCTION,
function ($ch, $chunk) use (&$raw, &$done, &$buffer) {
$buffer .= $chunk;
while (($idx = strpos($buffer, "\n\n")) !== false) {
$frame = substr($buffer, 0, $idx);
$buffer = substr($buffer, $idx + 2);
$event = "message";
$data = "";
foreach (explode("\n", $frame) as $line) {
if (str_starts_with($line, "event:")) {
$event = trim(substr($line, 6));
} elseif (str_starts_with($line, "data:")) {
$data .= trim(substr($line, 5));
}
}
if ($data === "") {
continue;
}
$payload = json_decode($data, true);
if ($event === "delta") {
$raw .= $payload["text"] ?? "";
} elseif ($event === "done") {
$done = $payload;
}
}
return strlen($chunk);
});
curl_exec($ch);
curl_close($ch);
$sheet = json_decode(substr($raw, strpos($raw, "{"),
strrpos($raw, "}") - strpos($raw, "{") + 1), true);
printf("%s charged=%d truncated=%s\n", $sheet["posture"],
$done["charged_credits"] ?? 0, ($done["truncated"] ?? false) ? "yes" : "no");
// Server-sent events, read line by line off the response stream.
var streamReq = new HttpRequestMessage(HttpMethod.Post,
"https://api.skillsafe.ai/v1/app-api/run-stream");
streamReq.Headers.Add("Authorization", "Bearer " + TOKEN);
streamReq.Headers.Add("Idempotency-Key", key);
streamReq.Headers.Add("Accept", "text/event-stream");
streamReq.Content = new StringContent(
JsonSerializer.Serialize(INPUT), Encoding.UTF8, "application/json");
using var http = new HttpClient();
using var streamRes = await http.SendAsync(
streamReq, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await streamRes.Content.ReadAsStreamAsync());
var accumulated = new StringBuilder();
JsonElement done = default;
string evt = null;
while (!reader.EndOfStream)
{
var line = await reader.ReadLineAsync();
if (line is null) continue;
if (line.StartsWith("event: "))
{
evt = line[7..].Trim();
}
else if (line.StartsWith("data: ") && evt == "delta")
{
var payload = JsonSerializer.Deserialize<JsonElement>(line[6..]);
accumulated.Append(payload.GetProperty("text").GetString());
}
else if (line.StartsWith("data: ") && evt == "done")
{
done = JsonSerializer.Deserialize<JsonElement>(line[6..]);
}
}
var text = accumulated.ToString();
text = text[text.IndexOf('{')..(text.LastIndexOf('}') + 1)];
var streamed = JsonSerializer.Deserialize<JsonElement>(text);
Console.WriteLine(streamed.GetProperty("posture").GetString());
Console.WriteLine(done.GetProperty("charged_credits").GetInt32());
7. Parse the result
data.output.output is a string holding one JSON object, so you unwrap
twice: once for the envelope, once for the reply. Take everything from the first { to
the last } before parsing — that one slice is what makes a caller robust against a
stray code fence or a trailing newline, and it is the same thing the web app does.
Here is an abbreviated size worksheet for the basket above, structurally complete. Two
of the four legs are shown; a real reply carries one entry per leg in
engine_facts.legs, with the same ref in the same order.
{
"lane": "size",
"basket_name": "December 2026 rates, plus one runoff",
"posture": "blocked",
"verdict": "The rates theme carries $6,700 against a $3,750 theme cap, so the group has to shrink before any single leg's size is the question.",
"exec_summary": "Kelly is a ceiling here, not a target. On the stated fair values, half Kelly allows $2,961 on L1 against a planned $3,000, but the per-leg cap of 5% of a $25,000 bankroll is $1,250, so the cap binds first. L1, L2 and L3 all settle on the same December policy path and are one bet at $6,700, which is 26.8% of bankroll against a stated 15% theme cap. L3 is negative-edge on the stated 44% against a 48% quote, and L4 carries no fair value at all, so neither is sizeable as written.",
"legs": [
{
"ref": "L1",
"market": "Fed cuts at the December 2026 FOMC",
"headline": "Cap binds well below the Kelly allowance",
"detail": "Planned $3,000 against a half-Kelly figure of $2,961 on the stated 71% fair value, a ratio of 1.01. The binding constraint is not Kelly but the stated 5% per-leg cap, which is $1,250. Quoted depth is $12,000, so a $1,250 fill is comfortable.",
"severity": "high",
"stake_note": "Planned $3,000 against half Kelly of $2,961 (1.01x) and a per-leg cap of $1,250.",
"suggested_band": "between $0 and $1,250, which is the stated per-leg cap; half Kelly on your stated 71% would have allowed $2,961",
"cap_status": "over",
"concentration_note": "Shares the rates theme and the 2026-12-10 date with L2 and L3; the three cannot be sized independently."
},
{
"ref": "L3",
"market": "CPI YoY above 3.0% for November 2026",
"headline": "Negative edge on the stated fair value",
"detail": "The stated fair value of 44% sits below the 48% quote, a -4.0 point edge on the caller's own numbers. Expected value on a $2,500 stake is negative. There is no Kelly fraction to take a share of.",
"severity": "critical",
"stake_note": "Planned $2,500 against a Kelly stake of $0 at any fraction.",
"suggested_band": "$0 - the stated fair value is below the quote",
"cap_status": "within",
"concentration_note": "Also a rates leg resolving on 2026-12-10, so it adds to the theme total it cannot justify."
}
],
"findings": [
{
"id": "F-001",
"severity": "critical",
"title": "Rates theme is 26.8% against a 15% cap",
"detail": "L1, L2 and L3 carry $6,700 of the $25,000 bankroll and all three resolve off the same December policy path on the same day. The stated theme cap is $3,750.",
"legs": ["L1", "L2", "L3"],
"mitigation": "Cut the theme total to $3,750 or below, or restate the theme cap and say why the correlation is acceptable."
},
{
"id": "F-002",
"severity": "high",
"title": "L4 has no stated fair value",
"detail": "Without a fair probability there is no edge, no expected value and no Kelly size, so the $1,800 planned stake does not follow from anything in the input.",
"legs": ["L4"],
"mitigation": "State a fair probability for the YES side of the runoff market, or remove the leg."
}
],
"checks": [
{ "check": "Bankroll, currency and Kelly fraction are all stated", "status": "pass",
"evidence": "$25,000, USD, half Kelly", "requirement": "All three present in the input." },
{ "check": "Every leg carries a stated fair probability", "status": "fail",
"evidence": "L4 has no fair value", "requirement": "A stated probability for the side named on every leg." },
{ "check": "Each Kelly figure follows from the stated edge at the stated fraction", "status": "pass",
"evidence": "L1: 23.68% full Kelly, $2,961 at half on $25,000", "requirement": "Engine figures reproduced without contradiction." },
{ "check": "No leg exceeds the per-leg cap", "status": "fail",
"evidence": "L1 at 12% and L3 at 10% against a 5% cap", "requirement": "Every leg at or under $1,250." },
{ "check": "No theme exceeds the theme cap", "status": "fail",
"evidence": "rates at $6,700, 26.8%", "requirement": "Theme totals at or under $3,750." },
{ "check": "Total stake sits inside the bankroll", "status": "pass",
"evidence": "$8,500 of $25,000", "requirement": "Total stake under bankroll." },
{ "check": "Every negative-edge leg is sized at zero", "status": "fail",
"evidence": "L3 planned at $2,500 on a -4.0 point edge", "requirement": "A $0 band on any leg whose fair value is below its quote." }
],
"actions": [
{ "id": "A-001", "stage": "before-entry",
"action": "Reduce the rates theme total to $3,750 or below, or restate the theme cap with the reason.",
"trigger": "Before any leg in the rates theme is entered.",
"evidence": "THEME-CONC@L1+L2+L3" },
{ "id": "A-002", "stage": "before-entry",
"action": "State a fair probability for L4 or drop the leg.",
"trigger": "Before L4 is entered.", "evidence": "FAIR-MISSING@L4" },
{ "id": "A-003", "stage": "at-entry",
"action": "Work each fill against the stated per-leg cap of $1,250 rather than the planned stake.",
"trigger": "At order entry on L1 and L3.", "evidence": "CAP-BREACH@L1" }
],
"coverage_check": [
{ "key": "CAP-BREACH@L1", "addressed": true,
"note": "L1's band is the $1,250 cap; A-003 carries it into entry." },
{ "key": "NEG-EDGE@L3", "addressed": true,
"note": "L3's band is $0 and check 7 fails on it." },
{ "key": "FAIR-MISSING@L4", "addressed": true, "note": "F-002 and A-002." },
{ "key": "THEME-CONC@L1+L2+L3", "addressed": true,
"note": "F-001 and A-001; it is also what sets the posture to blocked." }
],
"assumptions": [
"The bankroll of $25,000 is the whole account available to this basket, not a sleeve of a larger one.",
"L1 and L2 are the same underlying market at two venues rather than two different questions."
],
"open_questions": [
"Is the 15% theme cap meant to apply to the resolution date as well as the theme label?",
"Is L2 held as a hedge on L1 or as an independent view?"
],
"evidence_gaps": [
"Whether the quoted depth on L4 is still $900, which would be checked on the venue's order book at entry.",
"Whether the CPI market resolves on the first print or on a revision, which would be checked in the venue's own rules page."
],
"summary": "Nothing here is a recommendation to take or avoid any of these positions. On the numbers as typed, the theme total and two unsizeable legs are what stand between this basket and a coherent set of sizes; the per-leg arithmetic is secondary to that."
}
Then the assertions worth keeping in your own code, because they are the contract and not style:
laneechoes thetaskyou sent. If it does not, the lane was guessed and the first sentence ofexec_summarysays which one it picked.checkshas exactly six entries forread, seven forsize, seven forplanand eight forrisk, in a fixed per-lane order. Render by index; do not search by name. Two runs over the same basket are then diffable row by row.legshas one entry perengine_facts.legsentry, sameref, same order. A leg with nothing to say still gets an entry atseverity: "none".- Only the lane's own leg keys are present. Test for a key's presence rather than for an empty string — the other lanes' keys are omitted, not blanked.
coverage_checkhas one entry perengine_facts.flags[].key, exactly once, in the order sent, and carries no keys you did not send.findingsmay legitimately be[]. An empty findings array on a clean basket is a correct answer, not a parse failure — do not treat it as one and do not prompt for padding.actionscarries three to twelve entries orderedbefore-entry,at-entry,while-open,at-resolution. Every one is something a person does by hand.postureis one of three values, and it is the single field a gate should branch on.
One thing not to build: a check that the reply recommends something. It will not. Every band is phrased as a consequence of the numbers you sent, every action is conditional on your own decision to proceed, and eligibility is flagged rather than assured. If your product needs a recommendation, that decision belongs to your user, not to this API.
# The worksheet is a JSON STRING inside the envelope, so unwrap twice.
SHEET=$(printf '%s' "$OUT" \
| python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["output"]["output"])')
printf '%s' "$SHEET" | python3 -c '
import sys, json
s = json.load(sys.stdin)
# Fixed check counts per lane -- assert them, they are the contract.
expected = {"read": 6, "size": 7, "plan": 7, "risk": 8}[s["lane"]]
assert len(s["checks"]) == expected, "wrong number of checks for this lane"
print(s["lane"], s["posture"], "-", s["verdict"])
print(s["exec_summary"])
for leg in s["legs"]:
print(leg["ref"], leg["severity"], leg["headline"])
for f in s["findings"]:
print(f["id"], f["severity"], f["title"], "legs:", ",".join(f["legs"]))
for c in s["checks"]:
print(("[%s]" % c["status"]).ljust(10), c["check"])
for a in s["actions"]:
print(a["id"], a["stage"], a["action"])
# Every engine flag key comes back exactly once, in order.
print(len(s["coverage_check"]), "coverage entries")
'
# `sheet` came from step 5 or 6. Everything below is the output contract.
LANE_CHECKS = {"read": 6, "size": 7, "plan": 7, "risk": 8}
LANE_KEYS = {
"read": ("resolves_on", "ambiguity", "implied_read", "movers"),
"size": ("stake_note", "suggested_band", "cap_status", "concentration_note"),
"plan": ("entry_condition", "invalidation", "exit", "monitor"),
"risk": ("risk_type", "data_quality", "settlement_risk"),
}
assert sheet["lane"] in LANE_CHECKS, "unknown lane"
assert sheet["lane"] == INPUT["task"], "the reply answered a different lane"
assert len(sheet["checks"]) == LANE_CHECKS[sheet["lane"]], "wrong check count"
assert sheet["posture"] in ("clear", "caution", "blocked")
# One legs entry per engine leg, same refs, same order.
sent = [leg["ref"] for leg in INPUT["engine_facts"]["legs"]]
got = [leg["ref"] for leg in sheet["legs"]]
assert got == sent, "leg refs drifted: %r != %r" % (got, sent)
# Every engine flag key is reconciled exactly once, and nothing else appears.
flag_keys = [f["key"] for f in INPUT["engine_facts"]["flags"]]
covered = [c["key"] for c in sheet["coverage_check"]]
assert covered == flag_keys, "coverage_check does not match the flags sent"
print(sheet["basket_name"], "-", sheet["posture"])
print(sheet["verdict"])
print(sheet["exec_summary"])
for leg in sheet["legs"]:
extra = {k: leg[k] for k in LANE_KEYS[sheet["lane"]] if k in leg}
print(leg["ref"], leg["severity"], leg["headline"], extra)
for finding in sheet["findings"]: # may legitimately be []
print(finding["id"], finding["severity"], finding["title"],
"->", finding["mitigation"])
for check in sheet["checks"]: # fixed per-lane order: render by index
print("%-8s %s" % (check["status"], check["check"]))
for action in sheet["actions"]: # 3 to 12, in stage order
print(action["id"], action["stage"], action["trigger"], action["action"])
for entry in sheet["coverage_check"]:
print(entry["key"], entry["addressed"], entry["note"])
for bucket in ("assumptions", "open_questions", "evidence_gaps"):
for line in sheet[bucket]:
print(bucket, "-", line)
print(sheet["summary"])
// `sheet` came from step 5 or 6. Everything below is the output contract.
const LANE_CHECKS = { read: 6, size: 7, plan: 7, risk: 8 };
const LANE_KEYS = {
read: ["resolves_on", "ambiguity", "implied_read", "movers"],
size: ["stake_note", "suggested_band", "cap_status", "concentration_note"],
plan: ["entry_condition", "invalidation", "exit", "monitor"],
risk: ["risk_type", "data_quality", "settlement_risk"],
};
if (sheet.lane !== INPUT.task) throw new Error("the reply answered a different lane");
if (sheet.checks.length !== LANE_CHECKS[sheet.lane]) {
throw new Error(`expected ${LANE_CHECKS[sheet.lane]} checks, got ${sheet.checks.length}`);
}
if (!["clear", "caution", "blocked"].includes(sheet.posture)) {
throw new Error(`unknown posture ${sheet.posture}`);
}
// One legs entry per engine leg, same refs, same order.
const sent = INPUT.engine_facts.legs.map((l) => l.ref).join(",");
const got = sheet.legs.map((l) => l.ref).join(",");
if (sent !== got) throw new Error(`leg refs drifted: ${got} != ${sent}`);
// Every engine flag key is reconciled exactly once, and nothing else appears.
const flagKeys = INPUT.engine_facts.flags.map((f) => f.key).join(",");
const covered = sheet.coverage_check.map((c) => c.key).join(",");
if (flagKeys !== covered) throw new Error("coverage_check does not match the flags sent");
console.log(`${sheet.basket_name} - ${sheet.posture}`);
console.log(sheet.verdict);
console.log(sheet.exec_summary);
for (const leg of sheet.legs) {
const extra = Object.fromEntries(
LANE_KEYS[sheet.lane].filter((k) => k in leg).map((k) => [k, leg[k]]));
console.log(leg.ref, leg.severity, leg.headline, extra);
}
for (const f of sheet.findings) console.log(f.id, f.severity, f.title, f.mitigation);
for (const c of sheet.checks) console.log(c.status.padEnd(8), c.check, c.requirement);
for (const a of sheet.actions) console.log(a.id, a.stage, a.trigger, a.action);
for (const e of sheet.coverage_check) console.log(e.key, e.addressed, e.note);
for (const key of ["assumptions", "open_questions", "evidence_gaps"]) {
for (const line of sheet[key]) console.log(key, "-", line);
}
console.log(sheet.summary);
// The whole contract as one struct. Lane-specific leg keys are pointers so an
// omitted key stays nil rather than becoming an empty string.
type Sheet struct {
Lane string `json:"lane"`
BasketName string `json:"basket_name"`
Posture string `json:"posture"`
Verdict string `json:"verdict"`
ExecSummary string `json:"exec_summary"`
Legs []struct {
Ref string `json:"ref"`
Market string `json:"market"`
Headline string `json:"headline"`
Detail string `json:"detail"`
Severity string `json:"severity"`
// read
ResolvesOn *string `json:"resolves_on,omitempty"`
Ambiguity *string `json:"ambiguity,omitempty"`
ImpliedRead *string `json:"implied_read,omitempty"`
Movers []string `json:"movers,omitempty"`
// size
StakeNote *string `json:"stake_note,omitempty"`
SuggestedBand *string `json:"suggested_band,omitempty"`
CapStatus *string `json:"cap_status,omitempty"`
ConcentrationNote *string `json:"concentration_note,omitempty"`
// plan
EntryCondition *string `json:"entry_condition,omitempty"`
Invalidation *string `json:"invalidation,omitempty"`
Exit *string `json:"exit,omitempty"`
Monitor *string `json:"monitor,omitempty"`
// risk
RiskType *string `json:"risk_type,omitempty"`
DataQuality *string `json:"data_quality,omitempty"`
SettlementRisk *string `json:"settlement_risk,omitempty"`
} `json:"legs"`
Findings []struct {
ID string `json:"id"`
Severity string `json:"severity"`
Title string `json:"title"`
Detail string `json:"detail"`
Legs []string `json:"legs"`
Mitigation string `json:"mitigation"`
} `json:"findings"`
Checks []struct {
Check string `json:"check"`
Status string `json:"status"`
Evidence string `json:"evidence"`
Requirement string `json:"requirement"`
} `json:"checks"`
Actions []struct {
ID string `json:"id"`
Stage string `json:"stage"`
Action string `json:"action"`
Trigger string `json:"trigger"`
Evidence string `json:"evidence"`
} `json:"actions"`
CoverageCheck []struct {
Key string `json:"key"`
Addressed bool `json:"addressed"`
Note string `json:"note"`
} `json:"coverage_check"`
Assumptions []string `json:"assumptions"`
OpenQuestions []string `json:"open_questions"`
EvidenceGaps []string `json:"evidence_gaps"`
Summary string `json:"summary"`
}
laneChecks := map[string]int{"read": 6, "size": 7, "plan": 7, "risk": 8}
if want := laneChecks[sheet.Lane]; len(sheet.Checks) != want {
panic(fmt.Sprintf("expected %d checks for lane %s, got %d",
want, sheet.Lane, len(sheet.Checks)))
}
// Every engine flag key is reconciled exactly once, in the order it was sent.
if len(sheet.CoverageCheck) != len(input.EngineFacts.Flags) {
panic("coverage_check does not match the flags sent")
}
for i, entry := range sheet.CoverageCheck {
if entry.Key != input.EngineFacts.Flags[i].Key {
panic("coverage_check key out of order: " + entry.Key)
}
}
fmt.Println(sheet.BasketName, sheet.Posture, sheet.Verdict)
for _, leg := range sheet.Legs {
fmt.Println(leg.Ref, leg.Severity, leg.Headline)
}
for _, c := range sheet.Checks {
fmt.Printf("%-8s %s\n", c.Status, c.Check)
}
// With Jackson: bind the envelope, then bind the inner STRING as its own tree.
var mapper = new ObjectMapper();
JsonNode job = mapper.readTree(jobBody).get("data");
JsonNode sheet = mapper.readTree(job.get("output").get("output").asText());
Map<String, Integer> laneChecks = Map.of("read", 6, "size", 7, "plan", 7, "risk", 8);
String lane = sheet.get("lane").asText();
int wanted = laneChecks.getOrDefault(lane, -1);
if (sheet.get("checks").size() != wanted) {
throw new IllegalStateException(
"expected " + wanted + " checks for lane " + lane
+ ", got " + sheet.get("checks").size());
}
System.out.println(sheet.get("basket_name").asText() + " - "
+ sheet.get("posture").asText());
System.out.println(sheet.get("verdict").asText());
System.out.println(sheet.get("exec_summary").asText());
for (JsonNode leg : sheet.get("legs")) {
System.out.printf("%s %s %s%n", leg.get("ref").asText(),
leg.get("severity").asText(), leg.get("headline").asText());
// Only this lane's keys are present -- has() before get().
if (leg.has("suggested_band")) {
System.out.println(" band: " + leg.get("suggested_band").asText());
}
}
for (JsonNode f : sheet.get("findings")) {
System.out.printf("%s %s %s -> %s%n", f.get("id").asText(),
f.get("severity").asText(), f.get("title").asText(),
f.get("mitigation").asText());
}
for (JsonNode c : sheet.get("checks")) {
System.out.printf("%-8s %s%n", c.get("status").asText(), c.get("check").asText());
}
for (JsonNode a : sheet.get("actions")) {
System.out.printf("%s %s %s%n", a.get("id").asText(), a.get("stage").asText(),
a.get("action").asText());
}
for (JsonNode e : sheet.get("coverage_check")) {
System.out.printf("%s %s %s%n", e.get("key").asText(),
e.get("addressed").asBoolean(), e.get("note").asText());
}
System.out.println(sheet.get("summary").asText());
# `sheet` came from step 5 or 6. Everything below is the output contract.
LANE_CHECKS = { "read" => 6, "size" => 7, "plan" => 7, "risk" => 8 }.freeze
LANE_KEYS = {
"read" => %w[resolves_on ambiguity implied_read movers],
"size" => %w[stake_note suggested_band cap_status concentration_note],
"plan" => %w[entry_condition invalidation exit monitor],
"risk" => %w[risk_type data_quality settlement_risk]
}.freeze
lane = sheet["lane"]
raise "unknown lane #{lane}" unless LANE_CHECKS.key?(lane)
raise "the reply answered a different lane" unless lane == INPUT["task"]
raise "wrong check count" unless sheet["checks"].length == LANE_CHECKS[lane]
raise "unknown posture" unless %w[clear caution blocked].include?(sheet["posture"])
# One legs entry per engine leg, and every flag key reconciled exactly once.
sent_refs = INPUT["engine_facts"]["legs"].map { |l| l["ref"] }
raise "leg refs drifted" unless sheet["legs"].map { |l| l["ref"] } == sent_refs
sent_keys = INPUT["engine_facts"]["flags"].map { |f| f["key"] }
raise "coverage mismatch" unless sheet["coverage_check"].map { |c| c["key"] } == sent_keys
puts "#{sheet['basket_name']} - #{sheet['posture']}"
puts sheet["verdict"]
puts sheet["exec_summary"]
sheet["legs"].each do |leg|
extra = LANE_KEYS[lane].select { |k| leg.key?(k) }.map { |k| "#{k}=#{leg[k]}" }
puts "#{leg['ref']} #{leg['severity']} #{leg['headline']} #{extra.join(' ')}"
end
sheet["findings"].each { |f| puts "#{f['id']} #{f['severity']} #{f['title']} -> #{f['mitigation']}" }
sheet["checks"].each { |c| puts format("%-8s %s", c["status"], c["check"]) }
sheet["actions"].each { |a| puts "#{a['id']} #{a['stage']} #{a['trigger']} #{a['action']}" }
sheet["coverage_check"].each { |e| puts "#{e['key']} #{e['addressed']} #{e['note']}" }
%w[assumptions open_questions evidence_gaps].each do |bucket|
sheet[bucket].each { |line| puts "#{bucket} - #{line}" }
end
puts sheet["summary"]
<?php
// `$sheet` came from step 5 or 6. Everything below is the output contract.
const LANE_CHECKS = ["read" => 6, "size" => 7, "plan" => 7, "risk" => 8];
const LANE_KEYS = [
"read" => ["resolves_on", "ambiguity", "implied_read", "movers"],
"size" => ["stake_note", "suggested_band", "cap_status", "concentration_note"],
"plan" => ["entry_condition", "invalidation", "exit", "monitor"],
"risk" => ["risk_type", "data_quality", "settlement_risk"],
];
$lane = $sheet["lane"];
if (($sheet["checks"] ? count($sheet["checks"]) : 0) !== LANE_CHECKS[$lane]) {
throw new RuntimeException("wrong number of checks for lane " . $lane);
}
if ($lane !== $INPUT["task"]) {
throw new RuntimeException("the reply answered a different lane");
}
// Every engine flag key is reconciled exactly once, in the order it was sent.
$sentKeys = array_column($INPUT["engine_facts"]["flags"], "key");
$covered = array_column($sheet["coverage_check"], "key");
if ($sentKeys !== $covered) {
throw new RuntimeException("coverage_check does not match the flags sent");
}
echo $sheet["basket_name"], " - ", $sheet["posture"], PHP_EOL;
echo $sheet["verdict"], PHP_EOL;
echo $sheet["exec_summary"], PHP_EOL;
foreach ($sheet["legs"] as $leg) {
echo $leg["ref"], " ", $leg["severity"], " ", $leg["headline"], PHP_EOL;
foreach (LANE_KEYS[$lane] as $k) {
if (array_key_exists($k, $leg)) {
echo " ", $k, ": ", is_array($leg[$k]) ? implode("; ", $leg[$k]) : $leg[$k], PHP_EOL;
}
}
}
foreach ($sheet["findings"] as $f) {
echo $f["id"], " ", $f["severity"], " ", $f["title"], " -> ", $f["mitigation"], PHP_EOL;
}
foreach ($sheet["checks"] as $c) {
printf("%-8s %s\n", $c["status"], $c["check"]);
}
foreach ($sheet["actions"] as $a) {
echo $a["id"], " ", $a["stage"], " ", $a["action"], PHP_EOL;
}
foreach ($sheet["coverage_check"] as $e) {
echo $e["key"], " ", $e["addressed"] ? "yes" : "no", " ", $e["note"], PHP_EOL;
}
echo $sheet["summary"], PHP_EOL;
// `sheet` came from step 5 or 6. Everything below is the output contract.
var laneChecks = new Dictionary<string, int>
{
["read"] = 6, ["size"] = 7, ["plan"] = 7, ["risk"] = 8,
};
var laneKeys = new Dictionary<string, string[]>
{
["read"] = new[] { "resolves_on", "ambiguity", "implied_read", "movers" },
["size"] = new[] { "stake_note", "suggested_band", "cap_status", "concentration_note" },
["plan"] = new[] { "entry_condition", "invalidation", "exit", "monitor" },
["risk"] = new[] { "risk_type", "data_quality", "settlement_risk" },
};
var lane = sheet.GetProperty("lane").GetString();
var checks = sheet.GetProperty("checks").EnumerateArray().ToList();
if (checks.Count != laneChecks[lane])
{
throw new Exception($"expected {laneChecks[lane]} checks for {lane}, got {checks.Count}");
}
Console.WriteLine($"{sheet.GetProperty("basket_name").GetString()} - "
+ sheet.GetProperty("posture").GetString());
Console.WriteLine(sheet.GetProperty("verdict").GetString());
Console.WriteLine(sheet.GetProperty("exec_summary").GetString());
foreach (var leg in sheet.GetProperty("legs").EnumerateArray())
{
Console.WriteLine($"{leg.GetProperty("ref").GetString()} "
+ $"{leg.GetProperty("severity").GetString()} "
+ leg.GetProperty("headline").GetString());
// Only this lane's keys are present -- TryGetProperty, never assume.
foreach (var k in laneKeys[lane])
{
if (leg.TryGetProperty(k, out var v)) Console.WriteLine($" {k}: {v}");
}
}
foreach (var c in checks)
{
Console.WriteLine($"{c.GetProperty("status").GetString(),-8} "
+ c.GetProperty("check").GetString());
}
foreach (var a in sheet.GetProperty("actions").EnumerateArray())
{
Console.WriteLine($"{a.GetProperty("id").GetString()} "
+ $"{a.GetProperty("stage").GetString()} "
+ a.GetProperty("action").GetString());
}
foreach (var e in sheet.GetProperty("coverage_check").EnumerateArray())
{
Console.WriteLine($"{e.GetProperty("key").GetString()} "
+ $"{e.GetProperty("addressed").GetBoolean()} "
+ e.GetProperty("note").GetString());
}
Console.WriteLine(sheet.GetProperty("summary").GetString());
The output contract
One JSON object, and the same envelope for all four lanes — so one parser covers every lane and the only per-lane branching is which leg keys you read and how many checks you expect.
| key | type | meaning |
|---|---|---|
lane | string | The lane that answered: read, size, plan or risk. Compare it to the task you sent. |
basket_name | string | A short name for this basket, drawn from what the legs have in common. Useful as a title and as a grouping key across runs. |
posture | enum | clear, caution or blocked. blocked when a critical engine flag or a critical finding stands in the way of entering the basket as written; caution when it is workable but something material is unresolved; clear only when nothing above low is outstanding for this lane. The one value a gate should branch on. |
verdict | string | One sentence naming the single thing that decides the posture. |
exec_summary | string | Two to four sentences a desk head could read and act on without the rest. |
legs | object[] | One entry per leg in engine_facts.legs, same ref and same order. Common keys: ref, market, headline (six to twelve words), detail (two to four sentences), severity. Then the lane's own keys — see the table below. |
findings | object[] | {id, severity, title, detail, legs[], mitigation}. Ids are F-001, F-002, sequential and zero-padded to three digits. legs[] holds the refs it applies to; mitigation is the concrete change that closes it. May be []. |
checks | object[] | {check, status, evidence, requirement}. Six entries for read, seven for size, seven for plan, eight for risk — always in the lane's fixed order. evidence is the specific thing in the basket that decided it; requirement is what would make it pass. |
actions | object[] | {id, stage, action, trigger, evidence}. Three to twelve entries, ids A-001 upward, ordered by stage. trigger is the observable condition or date that starts it; evidence names the leg refs or the engine flag key it came from. |
coverage_check | object[] | {key, addressed, note}. One entry per engine_facts.flags[].key, exactly once, in the order sent. addressed: false means deliberately set aside, with the reason in note. |
assumptions | string[] | What had to be assumed because the paste did not say. An explicit constraint in context is recorded here rather than argued with. |
open_questions | string[] | Questions only the caller can answer, whose answers would change the worksheet. |
evidence_gaps | string[] | What would need to be looked up, and where it would be checked. This is where the absence of a browser is made explicit instead of papered over. |
summary | string | One closing paragraph, introducing no new facts. |
All eight array fields are always present, even when empty. Numbers carry their unit: points for
probability differences, the input currency for money, days for time.
The lane-specific leg keys
| lane | keys on each legs[] entry | what they hold |
|---|---|---|
read |
resolves_on, ambiguity, implied_read, movers[] |
resolves_on is the settlement condition in plain words; if the paste does not say, it says exactly that and the leg's severity is at least high. ambiguity is the specific reading two reasonable people would disagree on — a threshold with no operator, a date with no timezone, "announced" versus "completed". implied_read puts the quote's implied probability next to your stated fair value with the gap in points, attributing the fair value to you. movers[] is two to five dated, checkable items, each naming where it would be checked; never a news claim and never a prediction. |
size |
stake_note, suggested_band, cap_status, concentration_note |
stake_note is the planned stake against the Kelly stake with the currency and the ratio. suggested_band is a range phrased as a consequence of your inputs, never a single correct number; a negative-edge leg's band is exactly zero, and a leg with no stated fair probability gets no band at all. cap_status is within, at or over. concentration_note names what the leg shares with the others — theme, venue, resolution date, or the same underlying event under a different question. |
plan |
entry_condition, invalidation, exit, monitor |
entry_condition is the observable condition plus a price limit in the same notation you pasted — cents if you wrote cents — and it says the fill is worked in tranches when the stake exceeds quoted depth. invalidation is the dated or measurable event that means the stated thesis is wrong. exit names how the position ends: held to settlement, closed at a stated level, or reduced on a stated date. monitor is what is checked, when, and where. |
risk |
risk_type, data_quality, settlement_risk |
risk_type is the dominant category: settlement, data-quality, liquidity, concentration, compliance, operational or none. data_quality is what is wrong or unverifiable about the leg's inputs. settlement_risk is how the leg could resolve in a way you do not expect — an ambiguous rule, a revisable statistic, a source that can be delayed, a market that can void. |
The enums
| field | values | notes |
|---|---|---|
posture | clear, caution, blocked | Set for this lane: a basket can be clear for read and blocked for size, because the lanes are outstanding on different things. |
legs[].severity | none, low, medium, high, critical | none is a real value and means the leg is fine in this lane, not that the leg was skipped. |
findings[].severity | critical, high, medium, low | No none here: a finding exists because something is wrong. |
checks[].status | pass, fail, partial, unknown | unknown is a legitimate answer and is preferred over a guess — a paste with no jurisdiction stated cannot prove eligibility. partial means the practice is present but incomplete. |
actions[].stage | before-entry, at-entry, while-open, at-resolution | The array is ordered by stage, so it renders as a timeline without sorting. |
legs[].cap_status | within, at, over | size lane only; copied from engine_facts.legs[].cap_status when you sent one. |
The per-lane checks, in order
These are fixed. Render by index, diff by index, and assert the count — a wrong count is a malformed reply, not a variant.
read (6)
1. Every leg names what it resolves on
2. The resolution source is an authority that can be checked on the day
3. Each market question has a single reading
4. Every resolution date is stated and still in the future
5. The stated fair value is distinguishable from the market price
6. Nothing in this read came from outside the paste
size (7)
1. Bankroll, currency and Kelly fraction are all stated
2. Every leg carries a stated fair probability
3. Each Kelly figure follows from the stated edge at the stated fraction
4. No leg exceeds the per-leg cap
5. No theme exceeds the theme cap
6. Total stake sits inside the bankroll
7. Every negative-edge leg is sized at zero
plan (7)
1. Order prerequisites are named for every venue in the basket
2. Every enterable leg has an entry condition with an explicit price limit
3. Every leg has an invalidation trigger
4. Exit or settlement handling is stated per leg
5. The monitoring calendar covers every resolution date in the basket
6. Quoted depth is respected by the stated entry method
7. Nothing in the plan executes without the user acting
risk (8)
1. Jurisdiction and eligibility are addressed or explicitly unknown
2. Resolution-source integrity and manipulation surface are assessed per leg
3. Input data quality is assessed for every leg
4. Privacy of the pasted material is assessed
5. Venue and counterparty concentration is quantified
6. Execution and settlement risk is stated per leg
7. Automation authority limits are reviewed or recorded as not applicable
8. A go / hold / revise gate is stated with its condition
The worksheet never echoes a secret. If the paste carries an account id, an API key or a position tied
to a named person, the risk lane names the field and says to remove it — the value itself does
not appear in detail, evidence or summary.
Truncation and partial results
When the balance sits between min_credits and hold_credits, the run is not
refused: it executes with a reduced output cap and comes back with truncated: true on
the finished job and on the streaming done event. What you hold then is a
prefix of the worksheet, not the worksheet. The realistic failure looks like this:
legs is complete, checks is complete, and coverage_check,
evidence_gaps and summary are missing or cut mid-string.
Check the flag before you treat a reply as complete, and note that a truncated reply fails the coverage contract by construction — the flags that would have been reconciled at the end are simply not there. Do not report that as a modelling failure.
Render what parsed, and tell the user it was cut short. That is the whole rule. Show the legs and the checks you actually have, label the worksheet as incomplete, and say what is missing. Do not present a clipped answer as a complete one: on this app that is not a cosmetic problem, because a sizing worksheet missing its theme reconciliation reads exactly like a sizing worksheet that found no theme problem.
The right recovery is a retry, not a repair. Resubmit with a retry_note asking for
fewer, denser findings and shorter detail fields, and with the attempt suffix on the
Idempotency-Key incremented so the new body is not read as a replay of the old key.
Repairing truncated JSON by appending closing braces produces something that parses and is not what
the model meant.