Everything the web app does, you can do from your own code: paste an energy decision —
a bill with its tariff components, an interval or peak-demand summary, a supplier bid table,
a PPA or VPPA term sheet, a hedge position, a rate-case notice, a broker email thread —
and get it worked the way a senior energy procurement manager would work it. The scenario
named, the risk called, the single next move argued from the numbers on the page, and the
traps flagged: demand ratchets, PPA basis and curtailment, credit postings, riders,
unhedged index exposure into a scarcity-priced season. Useful for sweeping a portfolio of
accounts before a renewal window, screening a bid round before the analyst sees it, or
refusing to sign anything that comes back “Walk away”. Base URL
https://api.skillsafe.ai/v1/app-api. Start at the
token page, or go back to the app.
Every path below hangs off https://api.skillsafe.ai/v1/app-api. There are five
of them — /guest, /me, /estimate,
/run, /run-stream — plus /jobs/{job_id} for
polling. There is no /apps/{slug}/ segment anywhere: the app slug is bound to
the token once, at /guest.
Every response is {"ok": true, "data": {...}} or
{"ok": false, "error": {"code": "...", "message": "...", "details": {...}}}.
Check ok before you read data; the HTTP status and
error.code always agree.
| Status | Code | What to do |
|---|---|---|
| 400 | VALIDATION_ERROR | The input shape is wrong. error.details names the field — most often a missing situation, or an input object wrapped in {"input": ...} when it should be sent directly. |
| 401 | UNAUTHORIZED | Missing, malformed or expired token. Mint a new one from the token page or POST /guest. |
| 402 | PAYMENT_REQUIRED | The balance is below the run's hold. Call /estimate first and compare against /me. |
| 404 | NOT_FOUND | Wrong slug or job id. |
| 429 | RATE_LIMITED | Back off and retry with the same idempotency key. |
| 5xx | INTERNAL | Retry with the same idempotency key; a completed job is returned rather than re-billed. |
The object you send — and it is the input object directly, not wrapped in {"input": ...}. Only situation is required.
| Field | Type | Meaning |
|---|---|---|
situation | string | Required. The energy decision as pasted: bill line items and tariff components, an interval or peak-demand summary, a supplier bid table, a PPA or VPPA term sheet, a hedge position, a rate-case notice, a broker email thread. The browser client clips long input at 40,000 characters, keeping the head and the tail and announcing the cut in-band with a marker of the form [... N characters of the middle omitted ...], because a renewal worksheet carries its identifying header at the top and its live pressure — the bids, the deadline, the broker pushing — at the bottom. A trailing [situation truncated] marker tells the model it is not seeing everything, so it will not claim completeness it does not have. Send whatever you like from your own code; clip it the same way if you want the same behaviour. |
context | string | Optional, clipped at 6,000 characters. The market or ISO (or that supply is regulated and bundled), the facilities in scope, budget tolerance and how finance reacts to variance, any sustainability or REC target, and the decision actually needed. It materially changes the answer: the same bid table reads differently when a mid-year true-up is worse for the buyer than a higher locked price. |
facts | string | Optional. Deterministic arithmetic the browser computes from numbers the user typed, one line per computed quantity. It is a hint, not ground truth: the prompt tells the model to reconcile every figure against situation and to disbelieve facts where the two conflict. An API caller may compute and pass its own. Lines look like Load factor computed: 46% from 8,400,000 kWh over 365 days at a 2,100 kW peak and Demand charge computed: $58,800/month, $705,600/year at $28/kW on a 2,100 kW peak., plus peak-shave value with a simple payback banded under 5 / 5-8 / over 8 years, and a PPA strike-versus-forward spread annualized over contract volume net of a basis drag. |
retry_note | string | Optional, normally omitted, and not for humans. It is sent only on the single automatic retry that follows a reply which failed to parse, and it tells the model to answer again in the exact required shape — the five tag lines, then the six ## sections in order, every section line a - bullet, no code fence. It is a formatting instruction only: it can never change the scenario, the risk, the action or any number. The retry reuses an idempotency key derived from the same input with an incremented attempt counter, so it stays inside one idempotency family and cannot double-bill. |
One helper, reused by every step below. It sets the bearer header, sends JSON when there is
a body, passes an optional Idempotency-Key through, and raises on
ok: false so a failure never gets read as data.
# Every call below reuses these two. Get the token from the token page
# linked at the top - never paste it into a shared shell history.
export SKILLSAFE_TOKEN="YOUR_TOKEN"
export SKILLSAFE_BASE="https://api.skillsafe.ai/v1/app-api"
# Success: {"ok":true,"data":{...}}
# Failure: {"ok":false,"error":{"code":"...","message":"...","details":{...}}}
# So always check .ok before reading .data:
ss() {
out=$(curl -s "$@")
if [ "$(printf '%s' "$out" | jq -r .ok)" != "true" ]; then
printf '%s\n' "$out" | jq -r '.error.code + ": " + (.error.message // "")' >&2
return 1
fi
printf '%s' "$out" | jq .data
}
import json, os, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = os.environ.get("SKILLSAFE_TOKEN", "YOUR_TOKEN")
def call(method, path, json_body=None, headers=None):
data = json.dumps(json_body).encode() if json_body is not None else None
req = urllib.request.Request(BASE + path, data=data, method=method)
req.add_header("Authorization", "Bearer " + TOKEN)
if data: req.add_header("Content-Type", "application/json")
for k, v in (headers or {}).items(): req.add_header(k, v)
with urllib.request.urlopen(req) as r:
env = json.loads(r.read())
if not env.get("ok"):
raise RuntimeError(env["error"]["code"] + ": " + env["error"].get("message", ""))
return env["data"]
const BASE = "https://api.skillsafe.ai/v1/app-api";
// Read it from your own secret store; never hard-code a real token.
const TOKEN = "YOUR_TOKEN";
async function call(method, path, body, extraHeaders) {
const res = await fetch(BASE + path, {
method,
headers: {
Authorization: `Bearer ${TOKEN}`,
...(body ? { "Content-Type": "application/json" } : {}),
...(extraHeaders ?? {}),
},
body: body ? JSON.stringify(body) : undefined,
});
const env = await res.json();
if (!env.ok) throw new Error(`${env.error.code}: ${env.error.message ?? ""}`);
return env.data;
}
package main
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"time"
)
const base = "https://api.skillsafe.ai/v1/app-api"
func call(method, path string, body any, hdr map[string]string) (map[string]any, error) {
var rdr io.Reader
if body != nil {
b, _ := json.Marshal(body)
rdr = bytes.NewReader(b)
}
req, _ := http.NewRequest(method, base+path, rdr)
req.Header.Set("Authorization", "Bearer "+os.Getenv("SKILLSAFE_TOKEN"))
if body != nil { req.Header.Set("Content-Type", "application/json") }
for k, v := range hdr { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct {
OK bool `json:"ok"`
Data map[string]any `json:"data"`
Error struct{ Code, Message string } `json:"error"`
}
if err := json.NewDecoder(res.Body).Decode(&env); err != nil { return nil, err }
if !env.OK { return nil, fmt.Errorf("%s: %s", env.Error.Code, env.Error.Message) }
return env.Data, nil
}
import java.net.URI;
import java.net.http.*;
class WattDesk {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final String TOKEN = System.getenv().getOrDefault("SKILLSAFE_TOKEN", "YOUR_TOKEN");
static final HttpClient HTTP = HttpClient.newHttpClient();
static String call(String method, String path, String body, String idemKey) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN);
if (idemKey != null) b.header("Idempotency-Key", idemKey);
if (body != null) {
b.header("Content-Type", "application/json");
b.method(method, HttpRequest.BodyPublishers.ofString(body));
} else {
b.method(method, HttpRequest.BodyPublishers.noBody());
}
var res = HTTP.send(b.build(), HttpResponse.BodyHandlers.ofString());
return res.body(); // {"ok":true,"data":{...}} - decode with your JSON library
}
}
require "json"
require "net/http"
require "uri"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = ENV.fetch("SKILLSAFE_TOKEN", "YOUR_TOKEN")
def call(method, path, body = nil, headers = {})
uri = URI(BASE + path)
klass = { "GET" => Net::HTTP::Get, "POST" => Net::HTTP::Post }.fetch(method)
req = klass.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
headers.each { |k, v| req[k] = v }
if body
req["Content-Type"] = "application/json"
req.body = JSON.generate(body)
end
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
env = JSON.parse(res.body)
raise "#{env["error"]["code"]}: #{env["error"]["message"]}" unless env["ok"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = getenv("SKILLSAFE_TOKEN") ?: "YOUR_TOKEN";
function call($method, $path, $body = null, $extra = []) {
global $TOKEN;
$headers = array_merge(["Authorization: Bearer $TOKEN"], $extra);
if ($body !== null) $headers[] = "Content-Type: application/json";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $body === null ? null : json_encode($body),
]);
$env = json_decode(curl_exec($ch), true);
curl_close($ch);
if (empty($env["ok"])) throw new Exception($env["error"]["code"]);
return $env["data"];
}
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
const string Base = "https://api.skillsafe.ai/v1/app-api";
var token = Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN") ?? "YOUR_TOKEN";
var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
async Task<string> Call(string method, string path, string? body, string? idemKey = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (idemKey != null) req.Headers.Add("Idempotency-Key", idemKey);
if (body != null)
req.Content = new StringContent(body, Encoding.UTF8, "application/json");
var res = await http.SendAsync(req);
return await res.Content.ReadAsStringAsync(); // {"ok":true,"data":{...}}
}
Every call carries Authorization: Bearer <token>. Open the
token page to sign in, reveal your personal token and copy a
ready-made shell export — that is the supported way to get one, and it never asks you
to open the DevTools console. If you would rather script it,
POST /guest mints a guest token for a named slug. That is where the app slug is
bound, which is why no later path contains an /apps/{slug}/ segment. A guest
token works for reading and estimating; a signed-in token is needed to run.
# Unauthenticated. The slug is bound to the token here, once.
curl -s -X POST "$SKILLSAFE_BASE/guest" \
-H "Content-Type: application/json" \
-d '{"slug": "watt-desk"}'
# -> {"ok":true,"data":{"token":"...","expires_at":"..."}}
export SKILLSAFE_TOKEN="$(curl -s -X POST "$SKILLSAFE_BASE/guest" \
-H "Content-Type: application/json" \
-d '{"slug": "watt-desk"}' | jq -r .data.token)"
# A guest token, if you do not have a personal one from /tokens.html yet.
# The slug is bound to the token here - no later path repeats it.
guest = call("POST", "/guest", {"slug": "watt-desk"})
TOKEN = guest["token"]
print(guest["expires_at"])
// A guest token, if you do not have a personal one from /tokens.html yet.
// The slug is bound to the token here - no later path repeats it.
const guest = await call("POST", "/guest", { slug: "watt-desk" });
// Re-create the client with guest.token, or keep it in your secret store.
console.log(guest.expires_at);
// A guest token, if you do not have a personal one from /tokens.html yet.
// The slug is bound to the token here - no later path repeats it.
guest, err := call("POST", "/guest", map[string]any{"slug": "watt-desk"}, nil)
if err != nil { log.Fatal(err) }
os.Setenv("SKILLSAFE_TOKEN", guest["token"].(string))
fmt.Println(guest["expires_at"])
// A guest token, if you do not have a personal one from /tokens.html yet.
// The slug is bound to the token here - no later path repeats it.
String guest = WattDesk.call("POST", "/guest", "{\"slug\": \"watt-desk\"}", null);
System.out.println(guest); // {"ok":true,"data":{"token":"...","expires_at":"..."}}
# A guest token, if you do not have a personal one from /tokens.html yet.
# The slug is bound to the token here - no later path repeats it.
guest = call("POST", "/guest", { "slug" => "watt-desk" })
TOKEN = guest["token"]
puts guest["expires_at"]
<?php
// A guest token, if you do not have a personal one from /tokens.html yet.
// The slug is bound to the token here - no later path repeats it.
$guest = call("POST", "/guest", ["slug" => "watt-desk"]);
$TOKEN = $guest["token"];
echo $guest["expires_at"];
// A guest token, if you do not have a personal one from /tokens.html yet.
// The slug is bound to the token here - no later path repeats it.
var guest = await Call("POST", "/guest", "{\"slug\": \"watt-desk\"}");
Console.WriteLine(guest); // {"ok":true,"data":{"token":"...","expires_at":"..."}}
Confirms who the token belongs to and how many credits are available. Do this before a run: a 402 after submitting is avoidable.
curl -s -X GET "https://api.skillsafe.ai/v1/app-api/me" \
-H "Authorization: Bearer $SKILLSAFE_TOKEN"
me = call("GET", "/me")
print(me)
const me = await call("GET", "/me");
console.log(me);
me, err := call("GET", "/me", nil, nil)
if err != nil { log.Fatal(err) }
fmt.Println(me)
String me = WattDesk.call("GET", "/me", null, null);
System.out.println(me);
me = call("GET", "/me")
puts me
$me = call("GET", "/me");
print_r($me);
var me = await Call("GET", "/me", null);
Console.WriteLine(me);
Returns the credit hold a run would reserve, plus the resolved model and markup.
It creates no job and charges nothing, so it is safe to call on every
keystroke — the app debounces it at half a second and does exactly that. The response
carries model, model_alias, markup_bps,
hold_credits, min_credits and sponsor_enabled. Assert
on model_alias and markup_bps if you want a tripwire when the
deployment changes underneath you.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/estimate" \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"situation": "ELECTRIC SUPPLY RENEWAL - Brantley Forge Plant 2 (Muhlenberg PA), PPL zone / PJM, Rate LP-4 primary metered, billed on max 15-min demand.\nCurrent supply: KMES $0.0642/kWh fixed all-in, 36 mo, term ends 2026-09-30; supplier wants signed docs 30 days prior (2026-08-31).\n12-mo usage 8,400,000 kWh. 12-mo max billed demand 2,100 kW, set 7/16 in the 3-4pm interval. Demand rate $28.00/kW-mo (distribution + transmission combined), riders on top.\nRFP bids received 7/29-7/31: Allegheny Ridge fixed all-in 24 mo $0.0791/kWh; Nine Mile fixed all-in 36 mo $0.0774/kWh; Cardinal Hollow block-and-index 24 mo - 700 kW ATC block at $0.0728/kWh, balance settles at PJM real-time LMP (PPL zone) plus a $0.0041/kWh adder, block resizable once at 12 mo by +/- 200 kW, no LC required.\nThree shifts M-F; the press line and the melt shop both pull hard in the afternoon and nobody staggers them.", "context": "One site in the PPL zone of PJM, deregulated supply, distribution stays with the utility on LP-4. Finance will live with about 10% variance against the budget number but hates a mid-year surprise: a Q1 true-up is worse for me than a slightly higher locked price. No REC or scope 2 mandate has come down, so this is a pure cost and risk decision. Decide which of the three structures we take, and whether there is any reason to sign this week rather than after we have interval data and a chiller commissioning plan.", "facts": "Load factor computed: 46% from 8,400,000 kWh over 365 days at a 2,100 kW peak\nDemand charge computed: $58,800/month, $705,600/year at $28/kW on a 2,100 kW peak."}'
# -> {"ok":true,"data":{"model":"gpt-5.6-terra","model_alias":"gpt-terra",
# "markup_bps":1000,"hold_credits":...,"min_credits":...,"sponsor_enabled":false}}
SITUATION = (
"ELECTRIC SUPPLY RENEWAL - Brantley Forge Plant 2 (Muhlenberg PA), PPL zone / PJM, "
"Rate LP-4 primary metered, billed on max 15-min demand.\n"
"Current supply: KMES $0.0642/kWh fixed all-in, 36 mo, term ends 2026-09-30; supplier "
"wants signed docs 30 days prior (2026-08-31).\n"
"12-mo usage 8,400,000 kWh. 12-mo max billed demand 2,100 kW, set 7/16 in the 3-4pm "
"interval. Demand rate $28.00/kW-mo (distribution + transmission combined), riders on top.\n"
"RFP bids received 7/29-7/31: Allegheny Ridge fixed all-in 24 mo $0.0791/kWh; Nine Mile "
"fixed all-in 36 mo $0.0774/kWh; Cardinal Hollow block-and-index 24 mo - 700 kW ATC block "
"at $0.0728/kWh, balance settles at PJM real-time LMP (PPL zone) plus a $0.0041/kWh adder, "
"block resizable once at 12 mo by +/- 200 kW, no LC required.\n"
"Three shifts M-F; the press line and the melt shop both pull hard in the afternoon and "
"nobody staggers them."
)
# `facts` is arithmetic YOU compute, and it is a hint the model reconciles against
# `situation` - it is not ground truth, and it loses where the two disagree.
FACTS = (
"Load factor computed: 46% from 8,400,000 kWh over 365 days at a 2,100 kW peak\n"
"Demand charge computed: $58,800/month, $705,600/year at $28/kW on a 2,100 kW peak."
)
INPUT = {
"situation": SITUATION,
"context": (
"One site in the PPL zone of PJM, deregulated supply, distribution stays with the utility "
"on LP-4. Finance will live with about 10% variance against the budget number but hates a "
"mid-year surprise: a Q1 true-up is worse for me than a slightly higher locked price. No "
"REC or scope 2 mandate has come down, so this is a pure cost and risk decision. Decide "
"which of the three structures we take, and whether there is any reason to sign this week "
"rather than after we have interval data and a chiller commissioning plan."
),
"facts": FACTS,
}
est = call("POST", "/estimate", INPUT)
assert est["model_alias"] == "gpt-terra", est["model_alias"]
assert est["model"] == "gpt-5.6-terra", est["model"]
assert est["markup_bps"] == 1000, est["markup_bps"]
# hold_credits is what gets RESERVED, not what you pay. The actual charge
# comes back with the finished job and is normally well under the hold.
print("reserved:", est["hold_credits"], "min:", est["min_credits"],
"sponsored:", est["sponsor_enabled"])
const SITUATION = [
"ELECTRIC SUPPLY RENEWAL - Brantley Forge Plant 2 (Muhlenberg PA), PPL zone / PJM, Rate LP-4 primary metered, billed on max 15-min demand.",
"Current supply: KMES $0.0642/kWh fixed all-in, 36 mo, term ends 2026-09-30; supplier wants signed docs 30 days prior (2026-08-31).",
"12-mo usage 8,400,000 kWh. 12-mo max billed demand 2,100 kW, set 7/16 in the 3-4pm interval. Demand rate $28.00/kW-mo (distribution + transmission combined), riders on top.",
"RFP bids received 7/29-7/31: Allegheny Ridge fixed all-in 24 mo $0.0791/kWh; Nine Mile fixed all-in 36 mo $0.0774/kWh; Cardinal Hollow block-and-index 24 mo - 700 kW ATC block at $0.0728/kWh, balance settles at PJM real-time LMP (PPL zone) plus a $0.0041/kWh adder, block resizable once at 12 mo by +/- 200 kW, no LC required.",
"Three shifts M-F; the press line and the melt shop both pull hard in the afternoon and nobody staggers them.",
].join("\n");
// `facts` is arithmetic YOU compute, and it is a hint the model reconciles
// against `situation` - not ground truth, and it loses where the two disagree.
const FACTS = [
"Load factor computed: 46% from 8,400,000 kWh over 365 days at a 2,100 kW peak",
"Demand charge computed: $58,800/month, $705,600/year at $28/kW on a 2,100 kW peak.",
].join("\n");
const INPUT = {
situation: SITUATION,
context:
"One site in the PPL zone of PJM, deregulated supply, distribution stays with the utility on LP-4. " +
"Finance will live with about 10% variance against the budget number but hates a mid-year surprise: " +
"a Q1 true-up is worse for me than a slightly higher locked price. No REC or scope 2 mandate has " +
"come down, so this is a pure cost and risk decision. Decide which of the three structures we take, " +
"and whether there is any reason to sign this week rather than after we have interval data and a " +
"chiller commissioning plan.",
facts: FACTS,
};
const est = await call("POST", "/estimate", INPUT);
if (est.model_alias !== "gpt-terra") throw new Error(`alias moved: ${est.model_alias}`);
if (est.model !== "gpt-5.6-terra") throw new Error(`model moved: ${est.model}`);
if (est.markup_bps !== 1000) throw new Error(`markup moved: ${est.markup_bps}`);
// hold_credits is RESERVED, not charged. The real charge arrives with the job.
console.log("reserved", est.hold_credits, "min", est.min_credits, "sponsored", est.sponsor_enabled);
const situation = "ELECTRIC SUPPLY RENEWAL - Brantley Forge Plant 2 (Muhlenberg PA), PPL zone / PJM, Rate LP-4 primary metered, billed on max 15-min demand.\n" +
"Current supply: KMES $0.0642/kWh fixed all-in, 36 mo, term ends 2026-09-30; supplier wants signed docs 30 days prior (2026-08-31).\n" +
"12-mo usage 8,400,000 kWh. 12-mo max billed demand 2,100 kW, set 7/16 in the 3-4pm interval. Demand rate $28.00/kW-mo (distribution + transmission combined), riders on top.\n" +
"RFP bids received 7/29-7/31: Allegheny Ridge fixed all-in 24 mo $0.0791/kWh; Nine Mile fixed all-in 36 mo $0.0774/kWh; Cardinal Hollow block-and-index 24 mo - 700 kW ATC block at $0.0728/kWh, balance settles at PJM real-time LMP (PPL zone) plus a $0.0041/kWh adder, block resizable once at 12 mo by +/- 200 kW, no LC required.\n" +
"Three shifts M-F; the press line and the melt shop both pull hard in the afternoon and nobody staggers them."
// `facts` is a hint reconciled against `situation`, not ground truth.
const facts = "Load factor computed: 46% from 8,400,000 kWh over 365 days at a 2,100 kW peak\n" +
"Demand charge computed: $58,800/month, $705,600/year at $28/kW on a 2,100 kW peak."
input := map[string]any{
"situation": situation,
"context": "One site in the PPL zone of PJM, deregulated supply, distribution stays with the utility on LP-4. Finance will live with about 10% variance against the budget number but hates a mid-year surprise: a Q1 true-up is worse for me than a slightly higher locked price. No REC or scope 2 mandate has come down, so this is a pure cost and risk decision. Decide which of the three structures we take, and whether there is any reason to sign this week rather than after we have interval data and a chiller commissioning plan.",
"facts": facts,
}
est, err := call("POST", "/estimate", input, nil)
if err != nil { log.Fatal(err) }
if est["model_alias"] != "gpt-terra" { log.Fatalf("alias moved: %v", est["model_alias"]) }
// hold_credits is RESERVED, not charged.
fmt.Println(est["model"], est["markup_bps"], est["hold_credits"], est["sponsor_enabled"])
// Build the input with your JSON library rather than by hand; shown literally here.
String situation = "ELECTRIC SUPPLY RENEWAL - Brantley Forge Plant 2 (Muhlenberg PA), PPL zone / PJM, Rate LP-4 primary metered, billed on max 15-min demand.\\n"
+ "Current supply: KMES $0.0642/kWh fixed all-in, 36 mo, term ends 2026-09-30; supplier wants signed docs 30 days prior (2026-08-31).\\n"
+ "12-mo usage 8,400,000 kWh. 12-mo max billed demand 2,100 kW, set 7/16 in the 3-4pm interval. Demand rate $28.00/kW-mo (distribution + transmission combined), riders on top.\\n"
+ "RFP bids received 7/29-7/31: Allegheny Ridge fixed all-in 24 mo $0.0791/kWh; Nine Mile fixed all-in 36 mo $0.0774/kWh; Cardinal Hollow block-and-index 24 mo - 700 kW ATC block at $0.0728/kWh, balance settles at PJM real-time LMP (PPL zone) plus a $0.0041/kWh adder, block resizable once at 12 mo by +/- 200 kW, no LC required.\\n"
+ "Three shifts M-F; the press line and the melt shop both pull hard in the afternoon and nobody staggers them.";
// `facts` is a hint reconciled against `situation`, not ground truth.
String facts = "Load factor computed: 46% from 8,400,000 kWh over 365 days at a 2,100 kW peak\\n"
+ "Demand charge computed: $58,800/month, $705,600/year at $28/kW on a 2,100 kW peak.";
String context = "One site in the PPL zone of PJM, deregulated supply, distribution stays with the utility on LP-4. Finance will live with about 10% variance against the budget number but hates a mid-year surprise: a Q1 true-up is worse for me than a slightly higher locked price. No REC or scope 2 mandate has come down, so this is a pure cost and risk decision. Decide which of the three structures we take, and whether there is any reason to sign this week rather than after we have interval data and a chiller commissioning plan.";
String inputJson = toJson(Map.of("situation", situation, "context", context, "facts", facts));
String est = WattDesk.call("POST", "/estimate", inputJson, null);
// Assert model_alias == "gpt-terra", model == "gpt-5.6-terra", markup_bps == 1000.
// hold_credits is RESERVED, not charged.
System.out.println(est);
SITUATION = [
"ELECTRIC SUPPLY RENEWAL - Brantley Forge Plant 2 (Muhlenberg PA), PPL zone / PJM, Rate LP-4 primary metered, billed on max 15-min demand.",
"Current supply: KMES $0.0642/kWh fixed all-in, 36 mo, term ends 2026-09-30; supplier wants signed docs 30 days prior (2026-08-31).",
"12-mo usage 8,400,000 kWh. 12-mo max billed demand 2,100 kW, set 7/16 in the 3-4pm interval. Demand rate $28.00/kW-mo (distribution + transmission combined), riders on top.",
"RFP bids received 7/29-7/31: Allegheny Ridge fixed all-in 24 mo $0.0791/kWh; Nine Mile fixed all-in 36 mo $0.0774/kWh; Cardinal Hollow block-and-index 24 mo - 700 kW ATC block at $0.0728/kWh, balance settles at PJM real-time LMP (PPL zone) plus a $0.0041/kWh adder, block resizable once at 12 mo by +/- 200 kW, no LC required.",
"Three shifts M-F; the press line and the melt shop both pull hard in the afternoon and nobody staggers them.",
].join("\n")
# `facts` is a hint reconciled against `situation`, not ground truth.
FACTS = [
"Load factor computed: 46% from 8,400,000 kWh over 365 days at a 2,100 kW peak",
"Demand charge computed: $58,800/month, $705,600/year at $28/kW on a 2,100 kW peak.",
].join("\n")
INPUT = {
"situation" => SITUATION,
"context" => "One site in the PPL zone of PJM, deregulated supply, distribution stays with the utility on LP-4. Finance will live with about 10% variance against the budget number but hates a mid-year surprise: a Q1 true-up is worse for me than a slightly higher locked price. No REC or scope 2 mandate has come down, so this is a pure cost and risk decision. Decide which of the three structures we take, and whether there is any reason to sign this week rather than after we have interval data and a chiller commissioning plan.",
"facts" => FACTS,
}
est = call("POST", "/estimate", INPUT)
raise "alias moved: #{est["model_alias"]}" unless est["model_alias"] == "gpt-terra"
raise "model moved: #{est["model"]}" unless est["model"] == "gpt-5.6-terra"
raise "markup moved: #{est["markup_bps"]}" unless est["markup_bps"] == 1000
# hold_credits is RESERVED, not charged.
puts est["hold_credits"], est["min_credits"], est["sponsor_enabled"]
<?php
$SITUATION = implode("\n", [
"ELECTRIC SUPPLY RENEWAL - Brantley Forge Plant 2 (Muhlenberg PA), PPL zone / PJM, Rate LP-4 primary metered, billed on max 15-min demand.",
"Current supply: KMES \$0.0642/kWh fixed all-in, 36 mo, term ends 2026-09-30; supplier wants signed docs 30 days prior (2026-08-31).",
"12-mo usage 8,400,000 kWh. 12-mo max billed demand 2,100 kW, set 7/16 in the 3-4pm interval. Demand rate \$28.00/kW-mo (distribution + transmission combined), riders on top.",
"RFP bids received 7/29-7/31: Allegheny Ridge fixed all-in 24 mo \$0.0791/kWh; Nine Mile fixed all-in 36 mo \$0.0774/kWh; Cardinal Hollow block-and-index 24 mo - 700 kW ATC block at \$0.0728/kWh, balance settles at PJM real-time LMP (PPL zone) plus a \$0.0041/kWh adder, block resizable once at 12 mo by +/- 200 kW, no LC required.",
"Three shifts M-F; the press line and the melt shop both pull hard in the afternoon and nobody staggers them.",
]);
// `facts` is a hint reconciled against `situation`, not ground truth.
$FACTS = implode("\n", [
"Load factor computed: 46% from 8,400,000 kWh over 365 days at a 2,100 kW peak",
"Demand charge computed: \$58,800/month, \$705,600/year at \$28/kW on a 2,100 kW peak.",
]);
$INPUT = [
"situation" => $SITUATION,
"context" => "One site in the PPL zone of PJM, deregulated supply, distribution stays with the utility on LP-4. Finance will live with about 10% variance against the budget number but hates a mid-year surprise: a Q1 true-up is worse for me than a slightly higher locked price. No REC or scope 2 mandate has come down, so this is a pure cost and risk decision. Decide which of the three structures we take, and whether there is any reason to sign this week rather than after we have interval data and a chiller commissioning plan.",
"facts" => $FACTS,
];
$est = call("POST", "/estimate", $INPUT);
if ($est["model_alias"] !== "gpt-terra") throw new Exception("alias moved");
if ($est["model"] !== "gpt-5.6-terra") throw new Exception("model moved");
if ($est["markup_bps"] !== 1000) throw new Exception("markup moved");
// hold_credits is RESERVED, not charged.
print_r($est);
// Build the input with System.Text.Json rather than by hand; shown literally here.
var situation = string.Join("\n", new[] {
"ELECTRIC SUPPLY RENEWAL - Brantley Forge Plant 2 (Muhlenberg PA), PPL zone / PJM, Rate LP-4 primary metered, billed on max 15-min demand.",
"Current supply: KMES $0.0642/kWh fixed all-in, 36 mo, term ends 2026-09-30; supplier wants signed docs 30 days prior (2026-08-31).",
"12-mo usage 8,400,000 kWh. 12-mo max billed demand 2,100 kW, set 7/16 in the 3-4pm interval. Demand rate $28.00/kW-mo (distribution + transmission combined), riders on top.",
"RFP bids received 7/29-7/31: Allegheny Ridge fixed all-in 24 mo $0.0791/kWh; Nine Mile fixed all-in 36 mo $0.0774/kWh; Cardinal Hollow block-and-index 24 mo - 700 kW ATC block at $0.0728/kWh, balance settles at PJM real-time LMP (PPL zone) plus a $0.0041/kWh adder, block resizable once at 12 mo by +/- 200 kW, no LC required.",
"Three shifts M-F; the press line and the melt shop both pull hard in the afternoon and nobody staggers them.",
});
// `facts` is a hint reconciled against `situation`, not ground truth.
var facts = string.Join("\n", new[] {
"Load factor computed: 46% from 8,400,000 kWh over 365 days at a 2,100 kW peak",
"Demand charge computed: $58,800/month, $705,600/year at $28/kW on a 2,100 kW peak.",
});
var context = "One site in the PPL zone of PJM, deregulated supply, distribution stays with the utility on LP-4. Finance will live with about 10% variance against the budget number but hates a mid-year surprise: a Q1 true-up is worse for me than a slightly higher locked price. No REC or scope 2 mandate has come down, so this is a pure cost and risk decision. Decide which of the three structures we take, and whether there is any reason to sign this week rather than after we have interval data and a chiller commissioning plan.";
var inputJson = System.Text.Json.JsonSerializer.Serialize(new {
situation, context, facts
});
var est = await Call("POST", "/estimate", inputJson);
// Assert model_alias == "gpt-terra", model == "gpt-5.6-terra", markup_bps == 1000.
// hold_credits is RESERVED, not charged.
Console.WriteLine(est);
gpt-terra alias, which resolves to gpt-5.6-terra at
markup_bps 1000. hold_credits is the ceiling
reserved against the balance while the job runs — it is not the price. The
actual charge comes back with the finished job and is normally well under the hold; the
unused remainder is released. Read model and hold_credits from the
estimate rather than assuming either: the alias is the stable part, the resolved model is
not. When sponsor_enabled is true, a guest run costs nothing at all.
Creates a job and returns {"job_id": "..."} immediately; poll
GET /jobs/{job_id} until status is terminal
(succeeded, failed or cancelled), then read
data.output.output. The body is the input object directly. Always send
an Idempotency-Key: a retried request with the same key returns the original
job instead of billing twice.
# The key is a content hash of the input plus an attempt counter.
IDEM="watt-desk-$(printf '%s' "$SITUATION" | shasum -a 256 | cut -c1-16)-a1"
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/run" \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $IDEM" \
-d @input.json
# -> {"ok":true,"data":{"job_id":"job_..."}}
# input.json holds the SAME object shown under /estimate - sent directly,
# not wrapped in {"input": ...}.
# Then poll until the job is terminal:
curl -s -X GET "https://api.skillsafe.ai/v1/app-api/jobs/job_..." \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" \
| jq -r '.data.status, .data.output.output'
import hashlib, json, time
def idem_key(inp, attempt=1):
"""Content hash of the input plus an attempt counter, so a network retry
collapses server-side but a genuine re-run gets its own key."""
h = hashlib.sha256(json.dumps(inp, sort_keys=True).encode()).hexdigest()[:16]
return f"watt-desk-{h}-a{attempt}"
job = call("POST", "/run", INPUT, {"Idempotency-Key": idem_key(INPUT)})
job_id = job["job_id"]
while True:
j = call("GET", "/jobs/" + job_id)
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
raw = j["output"]["output"]
print(raw)
# If it does not parse (step 7), retry ONCE with retry_note and attempt=2 -
# the same idempotency family, so the reformat cannot double-bill.
# retry = dict(INPUT, retry_note="Your previous reply did not follow the required "
# "shape. Reply again with the five tag lines then the six ## sections "
# "in order, each containing only '- ' bullets. No code fence.")
# call("POST", "/run", retry, {"Idempotency-Key": idem_key(INPUT, 2)})
async function idemKey(input, attempt = 1) {
// Content hash of the input plus an attempt counter, so a network retry
// collapses server-side but a genuine re-run gets its own key.
const bytes = new TextEncoder().encode(JSON.stringify(input));
const digest = await crypto.subtle.digest("SHA-256", bytes);
const hex = [...new Uint8Array(digest)]
.map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, 16);
return `watt-desk-${hex}-a${attempt}`;
}
const { job_id } = await call("POST", "/run", INPUT, {
"Idempotency-Key": await idemKey(INPUT),
});
let j;
for (;;) {
j = await call("GET", `/jobs/${job_id}`);
if (["succeeded", "failed", "cancelled"].includes(j.status)) break;
await new Promise((r) => setTimeout(r, 2000));
}
const raw = j.output.output;
console.log(raw);
// If it does not parse (step 7), retry ONCE with retry_note and attempt 2 -
// same idempotency family, so the reformat cannot double-bill.
import (
"crypto/sha256"
"encoding/hex"
)
func idemKey(input map[string]any, attempt int) string {
b, _ := json.Marshal(input)
sum := sha256.Sum256(b)
return fmt.Sprintf("watt-desk-%s-a%d", hex.EncodeToString(sum[:])[:16], attempt)
}
job, err := call("POST", "/run", input, map[string]string{
"Idempotency-Key": idemKey(input, 1),
})
if err != nil { log.Fatal(err) }
jobID := job["job_id"].(string)
for {
j, err := call("GET", "/jobs/"+jobID, nil, nil)
if err != nil { log.Fatal(err) }
status, _ := j["status"].(string)
if status == "succeeded" || status == "failed" || status == "cancelled" {
out := j["output"].(map[string]any)
fmt.Println(out["output"])
break
}
time.Sleep(2 * time.Second)
}
import java.security.MessageDigest;
static String idemKey(String inputJson, int attempt) throws Exception {
var d = MessageDigest.getInstance("SHA-256").digest(inputJson.getBytes("UTF-8"));
var sb = new StringBuilder();
for (int i = 0; i < 8; i++) sb.append(String.format("%02x", d[i]));
return "watt-desk-" + sb + "-a" + attempt;
}
String job = WattDesk.call("POST", "/run", inputJson, idemKey(inputJson, 1));
String jobId = extractJobId(job); // decode {"ok":true,"data":{"job_id":"..."}}
String j;
while (true) {
j = WattDesk.call("GET", "/jobs/" + jobId, null, null);
if (isTerminal(j)) break; // status in succeeded | failed | cancelled
Thread.sleep(2000);
}
System.out.println(j); // data.output.output holds the plain-text assessment
require "digest"
def idem_key(input, attempt = 1)
# Content hash of the input plus an attempt counter.
h = Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]
"watt-desk-#{h}-a#{attempt}"
end
job = call("POST", "/run", INPUT, { "Idempotency-Key" => idem_key(INPUT) })
job_id = job["job_id"]
raw = nil
loop do
j = call("GET", "/jobs/#{job_id}")
if %w[succeeded failed cancelled].include?(j["status"])
raw = j["output"]["output"]
break
end
sleep 2
end
puts raw
<?php
function idem_key($input, $attempt = 1) {
// Content hash of the input plus an attempt counter.
$h = substr(hash("sha256", json_encode($input)), 0, 16);
return "watt-desk-$h-a$attempt";
}
$job = call("POST", "/run", $INPUT, ["Idempotency-Key: " . idem_key($INPUT)]);
$jobId = $job["job_id"];
$raw = null;
while (true) {
$j = call("GET", "/jobs/" . $jobId);
if (in_array($j["status"], ["succeeded", "failed", "cancelled"], true)) {
$raw = $j["output"]["output"];
break;
}
sleep(2);
}
echo $raw;
using System.Security.Cryptography;
static string IdemKey(string inputJson, int attempt = 1) {
// Content hash of the input plus an attempt counter.
var d = SHA256.HashData(Encoding.UTF8.GetBytes(inputJson));
return $"watt-desk-{Convert.ToHexString(d)[..16].ToLowerInvariant()}-a{attempt}";
}
var job = await Call("POST", "/run", inputJson, IdemKey(inputJson));
var jobId = ExtractJobId(job); // decode {"ok":true,"data":{"job_id":"..."}}
string j;
while (true) {
j = await Call("GET", $"/jobs/{jobId}", null);
if (IsTerminal(j)) break; // status in succeeded | failed | cancelled
await Task.Delay(2000);
}
Console.WriteLine(j); // data.output.output holds the plain-text assessment
The same job, delivered as server-sent events — this is what the web app uses.
delta events carry incremental text, job carries the job id, and
done carries the authoritative full output plus the actual
charged_credits. Trust done over the concatenated deltas, which
can drop the tail. done may also carry truncated: true, which
means the model hit its output ceiling and the assessment is cut short: surface that rather
than passing a clipped answer off as finished. Send the same
Idempotency-Key discipline here as on /run.
curl -N -X POST "https://api.skillsafe.ai/v1/app-api/run-stream" \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $IDEM" \
-d @input.json
# event: delta data: {"text":"SCENARIO: Procurement strategy..."}
# event: job data: {"job_id":"job_..."}
# event: done data: {"output":{"output":"SCENARIO: ..."},"charged_credits":1180,"truncated":false}
import json, urllib.request
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", idem_key(INPUT))
raw, charged, truncated = [], None, False
with urllib.request.urlopen(req) as r:
event = None
for line in r:
line = line.decode().rstrip("\n")
if line.startswith("event:"):
event = line[6:].strip()
elif line.startswith("data:"):
payload = json.loads(line[5:].strip())
if event == "delta":
raw.append(payload.get("text", ""))
elif event == "done":
raw = [payload["output"]["output"]] # authoritative
charged = payload.get("charged_credits")
truncated = bool(payload.get("truncated"))
raw = "".join(raw)
print(raw, charged, "CUT SHORT" if truncated else "")
const res = await fetch(`${BASE}/run-stream`, {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
"Idempotency-Key": await idemKey(INPUT),
},
body: JSON.stringify(INPUT),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "", raw = "", event = null, charged = null, truncated = false;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() ?? "";
for (const line of lines) {
if (line.startsWith("event:")) event = line.slice(6).trim();
else if (line.startsWith("data:")) {
const payload = JSON.parse(line.slice(5).trim());
if (event === "delta") raw += payload.text ?? "";
if (event === "done") {
raw = payload.output.output; // authoritative
charged = payload.charged_credits ?? null;
truncated = !!payload.truncated;
}
}
}
}
console.log(raw, charged, truncated ? "CUT SHORT" : "");
// SSE: read the body line by line rather than decoding it as one JSON document.
inputJSON, _ := json.Marshal(input)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(inputJSON))
req.Header.Set("Authorization", "Bearer "+os.Getenv("SKILLSAFE_TOKEN"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", idemKey(input, 1))
res, err := http.DefaultClient.Do(req)
if err != nil { log.Fatal(err) }
defer res.Body.Close()
sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 0, 64*1024), 4*1024*1024)
var event, raw string
var truncated bool
for sc.Scan() {
line := sc.Text()
switch {
case strings.HasPrefix(line, "event:"):
event = strings.TrimSpace(line[6:])
case strings.HasPrefix(line, "data:"):
var p struct {
Text string `json:"text"`
Truncated bool `json:"truncated"`
Output struct{ Output string `json:"output"` } `json:"output"`
}
json.Unmarshal([]byte(strings.TrimSpace(line[5:])), &p)
if event == "delta" { raw += p.Text }
if event == "done" { raw = p.Output.Output; truncated = p.Truncated } // authoritative
}
}
fmt.Println(raw, truncated)
// Stream the response body and split on SSE line prefixes.
var req = HttpRequest.newBuilder(URI.create(WattDesk.BASE + "/run-stream"))
.header("Authorization", "Bearer " + WattDesk.TOKEN)
.header("Content-Type", "application/json")
.header("Idempotency-Key", idemKey(inputJson, 1))
.POST(HttpRequest.BodyPublishers.ofString(inputJson))
.build();
var res = WattDesk.HTTP.send(req, HttpResponse.BodyHandlers.ofLines());
var raw = new StringBuilder();
final String[] event = { null };
res.body().forEach(line -> {
if (line.startsWith("event:")) event[0] = line.substring(6).trim();
else if (line.startsWith("data:") && "delta".equals(event[0])) {
// decode {"text":"..."} with your JSON library and append
raw.append(extractText(line.substring(5).trim()));
}
// on "done", replace raw with output.output - it is the authoritative copy -
// and read charged_credits and truncated off the same payload
});
System.out.println(raw);
require "net/http"
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = idem_key(INPUT)
req.body = JSON.generate(INPUT)
raw = +""
event = nil
truncated = false
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(req) do |res|
res.read_body do |chunk|
chunk.each_line do |line|
if line.start_with?("event:")
event = line[6..].strip
elsif line.start_with?("data:")
p = JSON.parse(line[5..].strip)
raw << p["text"].to_s if event == "delta"
if event == "done"
raw = p["output"]["output"] # authoritative
truncated = !!p["truncated"]
end
end
end
end
end
end
puts raw
warn "assessment was cut short" if truncated
<?php
$raw = "";
$event = null;
$truncated = false;
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $TOKEN",
"Content-Type: application/json",
"Idempotency-Key: " . idem_key($INPUT)],
CURLOPT_POSTFIELDS => json_encode($INPUT),
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$raw, &$event, &$truncated) {
foreach (explode("\n", $chunk) as $line) {
if (str_starts_with($line, "event:")) $event = trim(substr($line, 6));
elseif (str_starts_with($line, "data:")) {
$p = json_decode(trim(substr($line, 5)), true);
if ($event === "delta") $raw .= $p["text"] ?? "";
if ($event === "done") {
$raw = $p["output"]["output"]; // authoritative
$truncated = !empty($p["truncated"]);
}
}
}
return strlen($chunk);
},
]);
curl_exec($ch);
curl_close($ch);
echo $raw;
var req = new HttpRequestMessage(HttpMethod.Post, Base + "/run-stream");
req.Headers.Add("Idempotency-Key", IdemKey(inputJson));
req.Content = new StringContent(inputJson, Encoding.UTF8, "application/json");
var res = await http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var stream = await res.Content.ReadAsStreamAsync();
using var reader = new StreamReader(stream);
string? evt = null;
var raw = new StringBuilder();
while (!reader.EndOfStream) {
var line = await reader.ReadLineAsync();
if (line is null) continue;
if (line.StartsWith("event:")) evt = line[6..].Trim();
else if (line.StartsWith("data:") && evt == "delta") {
// decode {"text":"..."} with System.Text.Json and append
raw.Append(ExtractText(line[5..].Trim()));
}
// on "done", replace raw with output.output - it is the authoritative copy -
// and read charged_credits and truncated off the same payload
}
Console.WriteLine(raw);
data.output.output is plain text — no code fence around the response as a
whole — in exactly this shape: five tag lines, then six ## sections in
this order. This is what the app's parser decodes; a reply that breaks any rule below is
discarded and retried once with retry_note.
SCENARIO: <Procurement strategy | Demand charge | PPA evaluation | Rate review
| Budget and hedging | Market event | Sustainability
| Insufficient information>
RISK: <Low | Moderate | High | Severe>
ACTION: <Lock fixed price | Go block-and-index | Layer purchases | Stay on index
| Shave the peak | Sign with conditions | Walk away | Investigate first
| Insufficient information>
CONFIDENCE: <integer 0-100>
SUMMARY: <2 to 4 sentences, ends at the first blank line>
## Next moves
- <move, argued from a number on the page>
## The numbers
- <figure, with where it came from>
## Risk exposure
- <exposure and what it costs>
## Strategy
- <the structure and the term it argues for>
## Watch items
- <what to re-check, and when>
## Open questions
- <question>
SCENARIO: is the first line and
must be exactly one of the eight values. RISK: is one of Low,
Moderate, High, Severe. ACTION: is
exactly one of the nine values — there is one next move, not a menu.
CONFIDENCE: is a bare integer 0-100, no percent sign. SUMMARY: is
2 to 4 sentences, may wrap, and ends at the first blank line. All six ##
headings must appear, spelled exactly, in that order. Every line inside a section is a
- bullet, which may wrap onto indented continuation lines. A section with
nothing to report carries the single bullet - None.
Two consistency gates sit on top of the shape, and it is worth re-checking both on your
side: if RISK is High or Severe,
Risk exposure can never be - None.; and if ACTION is
Insufficient information, Open questions can never be
- None. A reply that breaks the shape or either gate is discarded and retried
once with the shape spelled out in retry_note, reusing the same idempotency
family with an incremented attempt counter so the reformat cannot double-bill.
# The reply is plain text, so parse it with awk and sed rather than jq.
curl -s -X GET "$SKILLSAFE_BASE/jobs/job_..." \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" \
| jq -r '.data.output.output' > assessment.txt
# The four single-line tags:
sed -n -E 's/^(SCENARIO|RISK|ACTION|CONFIDENCE): *(.*)$/\1=\2/p' assessment.txt
# SUMMARY runs from its tag to the first blank line:
awk '/^SUMMARY: /{f=1} f && NF==0{exit} f' assessment.txt
# The bullets of one section (stops at the next ## heading):
section() { awk -v h="## $1" '$0==h{f=1;next} /^## /{f=0} f && /^- /' assessment.txt; }
section "Next moves"
section "Risk exposure"
# All six headings must be present, spelled exactly, in this order:
grep -c '^## \(Next moves\|The numbers\|Risk exposure\|Strategy\|Watch items\|Open questions\)$' \
assessment.txt # -> 6
# Cross-rule: High or Severe risk may not have an empty Risk exposure section.
risk=$(sed -n -E 's/^RISK: *(.*)$/\1/p' assessment.txt)
case "$risk" in
High|Severe)
[ "$(section 'Risk exposure')" = "- None." ] && \
echo "contract violation: empty Risk exposure at $risk" >&2 ;;
esac
import re
SECTIONS = ["Next moves", "The numbers", "Risk exposure",
"Strategy", "Watch items", "Open questions"]
def parse(raw):
report = {}
for tag in ("SCENARIO", "RISK", "ACTION", "CONFIDENCE"):
m = re.search(r"^" + tag + r": *(.+)$", raw, re.M)
if not m:
raise ValueError("missing " + tag)
report[tag.lower()] = m.group(1).strip()
report["confidence"] = int(report["confidence"])
# SUMMARY may wrap; it ends at the first blank line.
m = re.search(r"^SUMMARY: *([\s\S]+?)(?:\n[ \t]*\n|\Z)", raw, re.M)
report["summary"] = " ".join(m.group(1).split()) if m else ""
sections, order = {}, []
for chunk in re.split(r"^## ", raw, flags=re.M)[1:]:
name, _, rest = chunk.partition("\n")
name = name.strip()
order.append(name)
# Bullets may wrap onto indented continuation lines.
bullets, cur = [], None
for ln in rest.splitlines():
if ln.startswith("- "):
cur = ln[2:].strip()
bullets.append(cur)
elif ln.strip() and bullets:
bullets[-1] += " " + ln.strip()
sections[name] = bullets
if order != SECTIONS:
raise ValueError("sections wrong or out of order: " + repr(order))
report["sections"] = sections
# The two cross-rules the app enforces:
if report["risk"] in ("High", "Severe") and sections["Risk exposure"] == ["None."]:
raise ValueError("High/Severe risk with an empty Risk exposure section")
if report["action"] == "Insufficient information" and sections["Open questions"] == ["None."]:
raise ValueError("Insufficient information with no Open questions")
return report
report = parse(raw)
print(report["scenario"], report["risk"], report["action"], report["confidence"])
for b in report["sections"]["Next moves"]:
print("-", b)
const SECTIONS = ["Next moves", "The numbers", "Risk exposure",
"Strategy", "Watch items", "Open questions"];
function parse(raw) {
const report = {};
for (const tag of ["SCENARIO", "RISK", "ACTION", "CONFIDENCE"]) {
const m = raw.match(new RegExp(`^${tag}: *(.+)$`, "m"));
if (!m) throw new Error(`missing ${tag}`);
report[tag.toLowerCase()] = m[1].trim();
}
report.confidence = parseInt(report.confidence, 10);
// SUMMARY may wrap; it ends at the first blank line.
const s = raw.match(/^SUMMARY: *([\s\S]+?)(?:\n[ \t]*\n|$)/m);
report.summary = s ? s[1].split(/\s+/).join(" ").trim() : "";
const sections = {}, order = [];
for (const chunk of raw.split(/^## /m).slice(1)) {
const nl = chunk.indexOf("\n");
const name = chunk.slice(0, nl < 0 ? undefined : nl).trim();
order.push(name);
const bullets = [];
for (const ln of (nl < 0 ? "" : chunk.slice(nl + 1)).split("\n")) {
if (ln.startsWith("- ")) bullets.push(ln.slice(2).trim());
// Bullets may wrap onto indented continuation lines.
else if (ln.trim() && bullets.length) bullets[bullets.length - 1] += " " + ln.trim();
}
sections[name] = bullets;
}
if (order.join("|") !== SECTIONS.join("|"))
throw new Error(`sections wrong or out of order: ${order.join(", ")}`);
report.sections = sections;
// The two cross-rules the app enforces:
const only = (k, v) => sections[k].length === 1 && sections[k][0] === v;
if (["High", "Severe"].includes(report.risk) && only("Risk exposure", "None."))
throw new Error("High/Severe risk with an empty Risk exposure section");
if (report.action === "Insufficient information" && only("Open questions", "None."))
throw new Error("Insufficient information with no Open questions");
return report;
}
const report = parse(raw);
console.log(report.scenario, report.risk, report.action, report.confidence);
console.log(report.sections["Next moves"]);
var sectionOrder = []string{"Next moves", "The numbers", "Risk exposure",
"Strategy", "Watch items", "Open questions"}
// Walk the text once: tag lines first, then ## sections of "- " bullets.
func parse(raw string) (map[string]string, map[string][]string, error) {
tags := map[string]string{}
sections := map[string][]string{}
var order []string
cur := ""
for _, ln := range strings.Split(raw, "\n") {
switch {
case strings.HasPrefix(ln, "## "):
cur = strings.TrimSpace(ln[3:])
order = append(order, cur)
sections[cur] = []string{}
case cur == "":
for _, t := range []string{"SCENARIO", "RISK", "ACTION", "CONFIDENCE", "SUMMARY"} {
if strings.HasPrefix(ln, t+": ") { tags[t] = strings.TrimSpace(ln[len(t)+2:]) }
}
case strings.HasPrefix(ln, "- "):
sections[cur] = append(sections[cur], strings.TrimSpace(ln[2:]))
case strings.TrimSpace(ln) != "" && len(sections[cur]) > 0:
sections[cur][len(sections[cur])-1] += " " + strings.TrimSpace(ln) // wrapped bullet
}
}
if strings.Join(order, "|") != strings.Join(sectionOrder, "|") {
return nil, nil, fmt.Errorf("sections wrong or out of order: %v", order)
}
// Cross-rules: High/Severe needs a real Risk exposure; Insufficient
// information needs real Open questions.
return tags, sections, nil
}
// Same walk: the five tag lines, then six ## sections of "- " bullets.
static final List<String> SECTIONS = List.of("Next moves", "The numbers",
"Risk exposure", "Strategy", "Watch items", "Open questions");
static Map<String, List<String>> parseSections(String raw) {
var sections = new LinkedHashMap<String, List<String>>();
String cur = null;
for (String ln : raw.split("\n", -1)) {
if (ln.startsWith("## ")) {
cur = ln.substring(3).trim();
sections.put(cur, new ArrayList<>());
} else if (cur != null && ln.startsWith("- ")) {
sections.get(cur).add(ln.substring(2).trim());
} else if (cur != null && !ln.isBlank() && !sections.get(cur).isEmpty()) {
var list = sections.get(cur); // wrapped bullet
list.set(list.size() - 1, list.get(list.size() - 1) + " " + ln.trim());
}
}
if (!new ArrayList<>(sections.keySet()).equals(SECTIONS))
throw new IllegalStateException("sections wrong or out of order: " + sections.keySet());
return sections;
}
// Tags: match ^(SCENARIO|RISK|ACTION|CONFIDENCE): (.+)$ with Pattern.MULTILINE,
// and take SUMMARY from its tag to the first blank line.
// Cross-rules: High/Severe needs a real Risk exposure; Insufficient
// information needs real Open questions.
SECTIONS = ["Next moves", "The numbers", "Risk exposure",
"Strategy", "Watch items", "Open questions"]
def parse(raw)
report = {}
%w[SCENARIO RISK ACTION CONFIDENCE].each do |tag|
m = raw[/^#{tag}: *(.+)$/, 1] or raise "missing #{tag}"
report[tag.downcase.to_sym] = m.strip
end
report[:confidence] = report[:confidence].to_i
report[:summary] = (raw[/^SUMMARY: *(.+?)(?:\n[ \t]*\n|\z)/m, 1] || "").split.join(" ")
sections = {}
order = []
raw.split(/^## /)[1..].to_a.each do |chunk|
name, rest = chunk.split("\n", 2)
name = name.strip
order << name
bullets = []
(rest || "").each_line do |ln|
ln = ln.chomp
if ln.start_with?("- ") then bullets << ln[2..].strip
elsif !ln.strip.empty? && bullets.any? then bullets[-1] += " " + ln.strip
end
end
sections[name] = bullets
end
raise "sections wrong or out of order: #{order}" unless order == SECTIONS
raise "empty Risk exposure at #{report[:risk]}" if
%w[High Severe].include?(report[:risk]) && sections["Risk exposure"] == ["None."]
raise "no Open questions" if
report[:action] == "Insufficient information" && sections["Open questions"] == ["None."]
report.merge(sections: sections)
end
<?php
const SECTIONS = ["Next moves", "The numbers", "Risk exposure",
"Strategy", "Watch items", "Open questions"];
function parse_report($raw) {
$report = [];
foreach (["SCENARIO", "RISK", "ACTION", "CONFIDENCE"] as $tag) {
if (!preg_match("/^$tag: *(.+)$/m", $raw, $m)) throw new Exception("missing $tag");
$report[strtolower($tag)] = trim($m[1]);
}
$report["confidence"] = (int) $report["confidence"];
$chunks = preg_split('/^## /m', $raw);
array_shift($chunks);
$sections = []; $order = [];
foreach ($chunks as $chunk) {
[$name, $rest] = array_pad(explode("\n", $chunk, 2), 2, "");
$name = trim($name); $order[] = $name;
$bullets = [];
foreach (explode("\n", $rest) as $ln) {
if (str_starts_with($ln, "- ")) $bullets[] = trim(substr($ln, 2));
elseif (trim($ln) !== "" && $bullets) $bullets[count($bullets) - 1] .= " " . trim($ln);
}
$sections[$name] = $bullets;
}
if ($order !== SECTIONS) throw new Exception("sections wrong or out of order");
// Cross-rules the app enforces:
if (in_array($report["risk"], ["High", "Severe"], true) && $sections["Risk exposure"] === ["None."])
throw new Exception("empty Risk exposure at " . $report["risk"]);
if ($report["action"] === "Insufficient information" && $sections["Open questions"] === ["None."])
throw new Exception("no Open questions");
$report["sections"] = $sections;
return $report;
}
using System.Text.RegularExpressions;
static readonly string[] Sections = {
"Next moves", "The numbers", "Risk exposure",
"Strategy", "Watch items", "Open questions"
};
static (Dictionary<string, string> Tags, Dictionary<string, List<string>> Body) Parse(string raw) {
var tags = new Dictionary<string, string>();
foreach (var tag in new[] { "SCENARIO", "RISK", "ACTION", "CONFIDENCE" }) {
var m = Regex.Match(raw, $"^{tag}: *(.+)$", RegexOptions.Multiline);
if (!m.Success) throw new InvalidOperationException($"missing {tag}");
tags[tag] = m.Groups[1].Value.Trim();
}
// SUMMARY runs to the first blank line.
var s = Regex.Match(raw, @"^SUMMARY: *([\s\S]+?)(?:\n[ \t]*\n|$)", RegexOptions.Multiline);
tags["SUMMARY"] = s.Success ? Regex.Replace(s.Groups[1].Value, @"\s+", " ").Trim() : "";
var body = new Dictionary<string, List<string>>();
var order = new List<string>();
string? cur = null;
foreach (var ln in raw.Split('\n')) {
if (ln.StartsWith("## ")) { cur = ln[3..].Trim(); order.Add(cur); body[cur] = new(); }
else if (cur != null && ln.StartsWith("- ")) body[cur].Add(ln[2..].Trim());
else if (cur != null && ln.Trim().Length > 0 && body[cur].Count > 0)
body[cur][^1] += " " + ln.Trim(); // wrapped bullet
}
if (!order.SequenceEqual(Sections))
throw new InvalidOperationException("sections wrong or out of order");
// Cross-rules: High/Severe needs a real Risk exposure; Insufficient
// information needs real Open questions.
return (tags, body);
}
facts that cannot be reconciled against situation is dropped
rather than repeated. Where the record does not support a call, the scenario and the action
are both Insufficient information and the questions that would settle it go in
Open questions. That is a real answer, not a failure — treat it as one.
/estimate is free and creates no job. /run and
/run-stream reserve hold_credits up front and charge only what the
run actually uses; the finished job and the done event both carry the real
charged_credits, and the unused reserve is released. Check
/me against the estimate before submitting and a 402
PAYMENT_REQUIRED never happens.
Send Idempotency-Key on every /run and
/run-stream. Derive it from a content hash of the input plus an attempt
counter, exactly as the samples above do: a network retry or a 429 backoff
replays the same key and collapses server-side into the original job, while a genuine
re-assessment of a changed situation hashes differently and gets its own key. The automatic
reformat retry — the one that carries retry_note — increments only
the attempt counter, so it stays inside the same idempotency family and cannot double-bill.