Drive TAM Desk from your own code
Everything the browser computes — both builds, the triangulation, the capacity test,
the unit economics, the sensitivity sweep and the memo — is deterministic and runs client-side, so the
API surface here is the metered judgement pass plus the storage the app saves its sizings to. Every request
goes to https://api.skillsafe.ai/v1/app-api and carries a bearer token.
Base URL, envelope and errors
Every response is a JSON envelope. Success carries data; failure carries error
with a stable code. Nothing else appears at the top level, so a client can branch on the presence
of error alone.
{ "ok": true, "data": { "...": "..." } }
{ "ok": false, "error": { "code": "insufficient_credits", "message": "...", "details": { } } }
| Code | HTTP | What it means | What to do |
|---|---|---|---|
unauthorized | 401 | No token, or the token was minted for another app. | Mint a guest token or sign in; see step 2. |
insufficient_credits | 402 | The balance is below min_credits. | Top up. /estimate is free, so check before you submit. |
validation_error | 400 | The input JSON is not the shape the app declares. | Compare against the input schema below. |
rate_limited | 429 | Too many requests. Similarity search is the tightest at 30/min per IP. | Back off and retry; do not tight-loop. |
payload_too_large | 413 | The run input exceeded 1 MB of JSON. | Clip the segment table on whole rows and keep the header. |
upstream_error | 502/529 | The model provider failed or was overloaded. | Retry with the SAME Idempotency-Key so a partial charge is not repeated. |
1. A tiny client
Every call is the same three lines: a bearer token, a JSON body, and the envelope unwrapped. Read the token
from your environment or your secret store — the snippets below use a "YOUR_TOKEN" placeholder,
and the token page will show you yours and copy a shell export for you, so you never
have to open a developer console.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="YOUR_TOKEN"
call() { # call <path> <json-body>
curl -sS -X POST "$BASE$1" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "$2"
}
import json, os, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = os.environ.get("SKILLSAFE_TOKEN", "YOUR_TOKEN")
def call(path, body=None, method="POST"):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(BASE + path, data=data, method=method)
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req) as r:
env = json.load(r)
if not env.get("ok"):
raise RuntimeError(env["error"]["code"] + ": " + env["error"]["message"])
return env["data"]
const BASE = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN";
async function call(path, body, method = "POST") {
const res = await fetch(BASE + path, {
method,
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json"
},
body: body === undefined ? undefined : JSON.stringify(body)
});
const env = await res.json();
if (!env.ok) throw new Error(`${env.error.code}: ${env.error.message}`);
return env.data;
}
package main
import (
"bytes"
"encoding/json"
"errors"
"net/http"
)
const base = "https://api.skillsafe.ai/v1/app-api"
const token = "YOUR_TOKEN"
type envelope struct {
OK bool `json:"ok"`
Data json.RawMessage `json:"data"`
Error *struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
func call(path string, body any) (json.RawMessage, error) {
b, _ := json.Marshal(body)
req, _ := http.NewRequest("POST", base+path, bytes.NewReader(b))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var env envelope
if err := json.NewDecoder(res.Body).Decode(&env); err != nil {
return nil, err
}
if !env.OK {
return nil, errors.New(env.Error.Code + ": " + env.Error.Message)
}
return env.Data, nil
}
import java.net.URI;
import java.net.http.*;
public class TamDesk {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final String TOKEN = "YOUR_TOKEN";
static final HttpClient HTTP = HttpClient.newHttpClient();
static String call(String path, String jsonBody) throws Exception {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
HttpResponse<String> res = HTTP.send(req, HttpResponse.BodyHandlers.ofString());
return res.body(); // a JSON envelope: { ok, data } or { ok, error }
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN"
def call(path, body = nil, method = :post)
uri = URI(BASE + path)
klass = method == :get ? Net::HTTP::Get : Net::HTTP::Post
req = klass.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req.body = JSON.dump(body) if body
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";
const TOKEN = "YOUR_TOKEN";
function call(string $path, ?array $body = null, string $method = "POST") {
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . TOKEN,
"Content-Type: application/json",
],
]);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
$env = json_decode(curl_exec($ch), true);
curl_close($ch);
if (empty($env["ok"])) {
throw new RuntimeException($env["error"]["code"] . ": " . $env["error"]["message"]);
}
return $env["data"];
}
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
var BASE = "https://api.skillsafe.ai/v1/app-api";
var TOKEN = "YOUR_TOKEN";
var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", TOKEN);
async Task<JsonElement> Call(string path, object? body = null)
{
var content = new StringContent(JsonSerializer.Serialize(body ?? new { }),
Encoding.UTF8, "application/json");
var res = await http.PostAsync(BASE + path, content);
var env = JsonDocument.Parse(await res.Content.ReadAsStringAsync()).RootElement;
if (!env.GetProperty("ok").GetBoolean())
throw new Exception(env.GetProperty("error").GetProperty("code").GetString());
return env.GetProperty("data");
}
2. A token
A guest token is minted per app and needs the slug in the body — an
X-App-Slug header returns 400. A guest can browse and estimate; running the metered pass needs a
signed-in personal token, which the token page will hand you along with a ready-made
shell export. Note that acl_read: "owner" scopes stored records to the calling subject, and every
/guest call mints a new subject — reuse one token across create and query or you
will read an empty collection.
curl -sS -X POST "$BASE/guest" \
-H "Content-Type: application/json" \
-d '{"slug":"tam-desk"}'
# -> { "ok": true, "data": { "token": "...", "subject_type": "guest" } }
import json, urllib.request
req = urllib.request.Request(
"https://api.skillsafe.ai/v1/app-api/guest",
data=json.dumps({"slug": "tam-desk"}).encode(),
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(req) as r:
TOKEN = json.load(r)["data"]["token"]
const res = await fetch("https://api.skillsafe.ai/v1/app-api/guest", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ slug: "tam-desk" })
});
const { data } = await res.json();
const TOKEN = data.token;
body := bytes.NewBufferString(`{"slug":"tam-desk"}`)
res, _ := http.Post(base+"/guest", "application/json", body)
defer res.Body.Close()
var env struct {
Data struct{ Token string } `json:"data"`
}
json.NewDecoder(res.Body).Decode(&env)
guestToken := env.Data.Token
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(BASE + "/guest"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{\"slug\":\"tam-desk\"}"))
.build();
String envelope = HTTP.send(req, HttpResponse.BodyHandlers.ofString()).body();
// envelope.data.token
uri = URI("https://api.skillsafe.ai/v1/app-api/guest")
res = Net::HTTP.post(uri, JSON.dump({ slug: "tam-desk" }),
"Content-Type" => "application/json")
TOKEN = JSON.parse(res.body)["data"]["token"]
<?php
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/guest");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode(["slug" => "tam-desk"]),
]);
$env = json_decode(curl_exec($ch), true);
$token = $env["data"]["token"];
var guestBody = new StringContent("{\"slug\":\"tam-desk\"}", Encoding.UTF8, "application/json");
var guestRes = await new HttpClient().PostAsync(
"https://api.skillsafe.ai/v1/app-api/guest", guestBody);
var guestEnv = JsonDocument.Parse(await guestRes.Content.ReadAsStringAsync()).RootElement;
var token = guestEnv.GetProperty("data").GetProperty("token").GetString();
3. Who am I, and what is the balance
GET /me is free and is what the app uses for its credit preflight: compare
credits against the min_credits that /estimate returns and refuse to
submit rather than collecting a 402 afterwards.
curl -sS "$BASE/me" -H "Authorization: Bearer $TOKEN"
# -> { "ok": true, "data": { "subject_type": "user", "credits": 250000 } }
me = call("/me", method="GET")
print(me["subject_type"], me["credits"])
const me = await call("/me", undefined, "GET");
console.log(me.subject_type, me.credits);
req, _ := http.NewRequest("GET", base+"/me", nil)
req.Header.Set("Authorization", "Bearer "+token)
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
HttpRequest me = HttpRequest.newBuilder()
.uri(URI.create(BASE + "/me"))
.header("Authorization", "Bearer " + TOKEN)
.GET()
.build();
System.out.println(HTTP.send(me, HttpResponse.BodyHandlers.ofString()).body());
me = call("/me", nil, :get)
puts me["subject_type"], me["credits"]
<?php
$me = call("/me", null, "GET");
echo $me["subject_type"], " ", $me["credits"], PHP_EOL;
var meReq = new HttpRequestMessage(HttpMethod.Get, BASE + "/me"); var meRes = await http.SendAsync(meReq); Console.WriteLine(await meRes.Content.ReadAsStringAsync());
4. The input this app accepts
Taken from buildInput() in app.js, not from intent. The facts object
is the whole client-side measurement and is what makes the model accountable: it is the authority on every
number, and the app re-checks the reply against it afterwards. If you are driving the API without running the
browser engine, send whatever of facts you can compute — but understand that the model is
instructed to review the drivers you list, so an empty facts.drivers yields an empty review.
{
"posture": "pre-seed | seed | series-a",
"note": "free-text steer, <= 500 chars",
"market_block": "the key: value market assumptions (middle-clipped at 3000 chars)",
"segments_excerpt": "the segment table, cut on WHOLE ROWS with the header kept (9000 chars)",
"gtm_block": "the go-to-market numbers (middle-clipped at 3000 chars)",
"sources_excerpt": "the driver: source lines (middle-clipped at 4000 chars)",
"facts": {
"market": "string",
"measured_at": "YYYY-MM-DD",
"verdict": "defensible | soft | contradictory",
"verdict_reason": "string",
"horizon_years": 5,
"claimed_som": 47350000,
"claimed_som_source": "top-down | bottom-up",
"triangulation": [
{ "level": "TAM", "top_down": 1550000000, "bottom_up": 1473300000,
"ratio": 1.052, "verdict": "converged | soft | contradictory | not-comparable" }
],
"reconcile": { "driver": "bu_acv", "current": 20809, "required": 132844,
"factor": 6.38, "alt_driver": "bu_accounts", "alt_required": 74487 },
"drivers": [
{ "id": "market_value", "label": "Total market value (top-down)", "value": 1550000000,
"unit": "amount | share | rate/yr | years | count",
"provenance": "sourced | stated | assumed | derived", "note": "string" }
],
"segments": [
{ "id": "seg_1", "name": "string", "accounts": 6100, "acv": 62000,
"reach": 0.72, "attach": 0.09, "tam": 378200000, "som": 24512400 }
],
"capacity": { "annual_capacity": 10080000, "method": "quota | funnel",
"years_to_som": 4.7, "verdict": "reachable | stretched | unreachable",
"reps_needed": 14 },
"unit_economics": { "ltv": 149447, "ltv_cac": 9.34, "ltv_cac_band": "healthy | thin | upside-down",
"payback_months": 11.7, "payback_band": "fast | acceptable | slow" },
"growth": { "implied_cagr": 0.42, "cagr_band": "ordinary | aggressive | heroic",
"som_share_of_sam": 0.065, "tam_future": 2612000000 },
"sensitivity": { "pct": 25, "tied": true,
"rows": [ { "id": "market_value", "label": "string", "swing": 23675000 } ] },
"checks": [ { "id": "triangulation", "level": "pass | warn | fail",
"label": "string", "detail": "string" } ],
"gaps": [ "string" ],
"warnings": [ "string" ],
"sourcing": { "sourced": 11, "stated": 3, "assumed": 2, "lines": 11 }
},
"current_datetime": "2026-08-08T09:00:00Z",
"retry_note": "optional - present only on the app's one reformat retry"
}
5. The output contract
Taken from normalizeModel() and reconcile() in sizekit.js. The model
returns exactly one JSON object with no fence and no prose. Unknown verdict values
normalise to needs-source, and entries with no driver_id are dropped, so a
malformed review is silently thinner rather than corrupting the render.
{
"title": "Market sizing review - North American construction project-management software",
"verdict_note": "one or two sentences",
"driver_reviews": [
{
"driver_id": "market_value",
"verdict": "defensible | needs-source | implausible",
"comment": "why, referring to the measured value",
"suggested_source": "required whenever verdict is needs-source"
}
],
"reconciliation": "which build to fix and why",
"recommended_driver": "an id from facts.drivers",
"recommended_value": "$45,000",
"investor_questions": [ "string" ],
"narrative": "120-250 words",
"risks": [ "string" ],
"unverified": [ "string" ]
}
What the app asserts about that reply
These are counted, not sampled, and the results are rendered next to the model's own words.
| Check | Assertion | Fails when |
|---|---|---|
driver_partition | driver_reviews is a partition of facts.drivers: every id reviewed exactly once. | An id is missing, reviewed twice, or is not a driver at all. |
sourcing_agreement | A driver whose provenance is sourced is never called needs-source. | The reply contradicts the provenance ledger. |
suggested_sources | Every needs-source verdict names a source that would settle it. | Warns when some do not. |
engine_gaps_excluded | Drivers the engine measured as absent are never in the partition, so the model is not blamed for a gap the browser already reported. | Never — this one is informational by construction. |
recommended_value | recommended_driver is a real driver and recommended_value is re-derived against the measurement. | The driver is unknown; warns when the value is not a parseable number. |
narrative | The memo paragraph exists and is long enough to carry the argument. | Absent (fail) or under 80 words (warn). |
6. Estimate first — it is free
/estimate creates no job and charges nothing. hold_credits is what will be
reserved, not the price: it prices the full output cap, and the settled
charged_credits is usually far lower. If the balance sits between min_credits and
hold_credits the run still executes with a reduced cap and comes back
"truncated": true.
curl -sS -X POST "$BASE/estimate" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
--data @input.json
# -> { "ok": true, "data": { "model": "gpt-5.6-terra", "model_alias": "gpt-terra",
# "markup_bps": 1000, "hold_credits": 2900, "min_credits": 120 } }
est = call("/estimate", payload)
print(est["model"], est["model_alias"], est["markup_bps"])
if me["credits"] < est["min_credits"]:
raise SystemExit("top up before running")
const est = await call("/estimate", payload);
if (me.credits < est.min_credits) throw new Error("top up before running");
console.log(`reserving up to ${est.hold_credits} credits on ${est.model}`);
data, err := call("/estimate", payload)
if err != nil {
log.Fatal(err)
}
var est struct {
Model string `json:"model"`
HoldCredits int `json:"hold_credits"`
MinCredits int `json:"min_credits"`
}
json.Unmarshal(data, &est)
String est = call("/estimate", inputJson);
System.out.println(est); // model, model_alias, markup_bps, hold_credits, min_credits
est = call("/estimate", payload)
abort("top up before running") if me["credits"] < est["min_credits"]
puts "reserving up to #{est["hold_credits"]} on #{est["model"]}"
<?php
$est = call("/estimate", $payload);
if ($me["credits"] < $est["min_credits"]) {
exit("top up before running\n");
}
var est = await Call("/estimate", payload);
Console.WriteLine(est.GetProperty("model").GetString());
Console.WriteLine(est.GetProperty("hold_credits").GetInt32());
7. Run it — with an Idempotency-Key
Pass a key derived from the input plus an attempt counter. The app uses
tam-desk:<fnv1a-of-inputs>:<length>:a<attempt>, and its one automatic reformat
retry reuses a key derived from the same input, so a malformed first reply can never double-bill. Do the same:
a network blip that makes you retry must not become a second charge.
curl -sS -X POST "$BASE/run" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: tam-desk:00328e8c:1358:a1" \
--data @input.json
# -> { "ok": true, "data": { "job_id": "job_...", "status": "queued" } }
curl -sS "$BASE/jobs/job_..." -H "Authorization: Bearer $TOKEN"
# poll until status is "succeeded" or "failed"
import time
req = urllib.request.Request(BASE + "/run", data=json.dumps(payload).encode(), method="POST")
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Content-Type", "application/json")
req.add_header("Idempotency-Key", "tam-desk:00328e8c:1358:a1")
with urllib.request.urlopen(req) as r:
job = json.load(r)["data"]
while True:
j = call("/jobs/" + job["job_id"], method="GET")
if j["status"] in ("succeeded", "failed"):
break
time.sleep(1.5)
result = json.loads(j["output"]["output"])
print(len(result["driver_reviews"]), "driver reviews")
const job = await fetch(`${BASE}/run`, {
method: "POST",
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json",
"Idempotency-Key": "tam-desk:00328e8c:1358:a1"
},
body: JSON.stringify(payload)
}).then(r => r.json()).then(e => e.data);
let j;
do {
await new Promise(r => setTimeout(r, 1500));
j = await call(`/jobs/${job.job_id}`, undefined, "GET");
} while (j.status !== "succeeded" && j.status !== "failed");
const result = JSON.parse(j.output.output);
b, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", base+"/run", bytes.NewReader(b))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "tam-desk:00328e8c:1358:a1")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
// then poll GET /jobs/{job_id} until status is terminal
HttpRequest run = HttpRequest.newBuilder()
.uri(URI.create(BASE + "/run"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.header("Idempotency-Key", "tam-desk:00328e8c:1358:a1")
.POST(HttpRequest.BodyPublishers.ofString(inputJson))
.build();
String job = HTTP.send(run, HttpResponse.BodyHandlers.ofString()).body();
// then poll GET /jobs/{job_id}
uri = URI(BASE + "/run")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = "tam-desk:00328e8c:1358:a1"
req.body = JSON.dump(payload)
job = JSON.parse(Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }.body)["data"]
loop do
j = call("/jobs/#{job["job_id"]}", nil, :get)
break if %w[succeeded failed].include?(j["status"])
sleep 1.5
end
<?php
$ch = curl_init(BASE . "/run");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . TOKEN,
"Content-Type: application/json",
"Idempotency-Key: tam-desk:00328e8c:1358:a1",
],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$job = json_decode(curl_exec($ch), true)["data"];
// then poll GET /jobs/{job_id}
var runMsg = new HttpRequestMessage(HttpMethod.Post, BASE + "/run")
{
Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json")
};
runMsg.Headers.Add("Idempotency-Key", "tam-desk:00328e8c:1358:a1");
var runRes = await http.SendAsync(runMsg);
// then poll GET /jobs/{job_id}
8. Streaming, if you want the progress
The SDK exposes runStream, and the app uses it to advance its staged progress card off real
signals — each field name appearing in the delta stream moves it on. Frame names arrive on the
event: line, and the payload on data:. The same
Idempotency-Key discipline applies.
curl -N -sS -X POST "$BASE/run-stream" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: tam-desk:00328e8c:1358:a1" \
--data @input.json
# event: job
# data: {"job_id":"job_..."}
# event: delta
# data: {"text":"{\"title\":\"Market sizing review"}
# event: done
# data: {"status":"succeeded","charged_credits":812,"truncated":false}
req = urllib.request.Request(BASE + "/run-stream", data=json.dumps(payload).encode())
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Content-Type", "application/json")
req.add_header("Idempotency-Key", "tam-desk:00328e8c:1358:a1")
raw, event = "", None
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().rstrip("\n")
if line.startswith("event:"):
event = line[6:].strip()
elif line.startswith("data:") and event == "delta":
raw += json.loads(line[5:])["text"]
result = json.loads(raw)
const res = await fetch(`${BASE}/run-stream`, {
method: "POST",
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json",
"Idempotency-Key": "tam-desk:00328e8c:1358:a1"
},
body: JSON.stringify(payload)
});
const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = "", raw = "", event = null;
for (;;) {
const { done, value } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
const lines = buf.split("\n");
buf = lines.pop();
for (const line of lines) {
if (line.startsWith("event:")) event = line.slice(6).trim();
else if (line.startsWith("data:") && event === "delta") raw += JSON.parse(line.slice(5)).text;
}
}
const result = JSON.parse(raw);
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(b))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "tam-desk:00328e8c:1358:a1")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
sc := bufio.NewScanner(res.Body)
var event, raw string
for sc.Scan() {
line := sc.Text()
switch {
case strings.HasPrefix(line, "event:"):
event = strings.TrimSpace(line[6:])
case strings.HasPrefix(line, "data:") && event == "delta":
var d struct{ Text string }
json.Unmarshal([]byte(line[5:]), &d)
raw += d.Text
}
}
HttpRequest stream = HttpRequest.newBuilder()
.uri(URI.create(BASE + "/run-stream"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.header("Idempotency-Key", "tam-desk:00328e8c:1358:a1")
.POST(HttpRequest.BodyPublishers.ofString(inputJson))
.build();
StringBuilder raw = new StringBuilder();
HTTP.send(stream, HttpResponse.BodyHandlers.ofLines())
.body()
.filter(l -> l.startsWith("data:"))
.forEach(l -> raw.append(l.substring(5)));
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = "tam-desk:00328e8c:1358:a1"
req.body = JSON.dump(payload)
raw, event = +"", nil
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|
event = line[6..].strip if line.start_with?("event:")
raw << JSON.parse(line[5..])["text"] if line.start_with?("data:") && event == "delta"
end
end
end
end
<?php
$raw = "";
$event = null;
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . TOKEN,
"Content-Type: application/json",
"Idempotency-Key: tam-desk:00328e8c:1358:a1",
],
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$raw, &$event) {
foreach (explode("\n", $chunk) as $line) {
if (str_starts_with($line, "event:")) {
$event = trim(substr($line, 6));
} elseif (str_starts_with($line, "data:") && $event === "delta") {
$raw .= json_decode(substr($line, 5), true)["text"] ?? "";
}
}
return strlen($chunk);
},
]);
curl_exec($ch);
var streamMsg = new HttpRequestMessage(HttpMethod.Post, BASE + "/run-stream")
{
Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json")
};
streamMsg.Headers.Add("Idempotency-Key", "tam-desk:00328e8c:1358:a1");
var streamRes = await http.SendAsync(streamMsg, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await streamRes.Content.ReadAsStreamAsync());
var raw = new StringBuilder();
string? evt = null, line;
while ((line = await reader.ReadLineAsync()) != null)
{
if (line.StartsWith("event:")) evt = line[6..].Trim();
else if (line.StartsWith("data:") && evt == "delta")
raw.Append(JsonDocument.Parse(line[5..]).RootElement.GetProperty("text").GetString());
}
9. Saving and finding past sizings
The app declares one collection, sizings, with acl_read: "owner" and
acl_write: "user". Record creation is POST
/collections/sizings/records. Every where entry must be an operator object
— the bare-value shorthand is rejected — and ordering uses a sort object;
order_by is silently ignored in favour of created_at desc.
// declared fields (queryable); the full document round-trips regardless
{ "name": "sizings", "acl_read": "owner", "acl_write": "user",
"fields": [
{ "name": "title", "type": "string" },
{ "name": "market", "type": "string" },
{ "name": "summary", "type": "string" },
{ "name": "verdict", "type": "string" },
{ "name": "som_usd", "type": "number" },
{ "name": "tam_ratio", "type": "number" },
{ "name": "check_fails", "type": "number" },
{ "name": "ran_at", "type": "timestamp" }
],
"embed": ["title", "market", "summary", "verdict"] }
# create
curl -sS -X POST "$BASE/collections/sizings/records" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"title":"Market sizing - construction PM software","market":"North American construction project-management software","verdict":"defensible","som_usd":47350000,"tam_ratio":1.052,"check_fails":0,"ran_at":"2026-08-08T09:00:00Z","summary":"top-down and bottom-up 1.05x apart (converged); SOM $47.35M; capacity reachable"}'
# exact filter - about a tenth the cost of a similarity query
curl -sS -X POST "$BASE/collections/sizings/query" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"where":{"verdict":{"eq":"contradictory"}},"sort":{"field":"ran_at","dir":"desc"},"limit":20}'
# semantic search - 30 requests/min per IP, so debounce it
curl -sS -X POST "$BASE/collections/sizings/similar" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"text":"the one where bottom-up was a thirtieth of the slide","limit":8}'
rec = call("/collections/sizings/records", {
"title": "Market sizing - construction PM software",
"market": "North American construction project-management software",
"verdict": "defensible",
"som_usd": 47350000,
"tam_ratio": 1.052,
"check_fails": 0,
"ran_at": "2026-08-08T09:00:00Z",
"summary": "top-down and bottom-up 1.05x apart (converged); SOM $47.35M; capacity reachable",
})
page = call("/collections/sizings/query", {
"where": {"verdict": {"eq": "contradictory"}},
"sort": {"field": "ran_at", "dir": "desc"},
"limit": 20,
})
hits = call("/collections/sizings/similar",
{"text": "the one where bottom-up was a thirtieth of the slide", "limit": 8})
// the SDK's similar() resolves to the record ARRAY; query() to { records }.
// Accept either shape rather than trusting one.
const recordsOf = (r) => (Array.isArray(r) ? r : (r && r.records) || []);
await call("/collections/sizings/records", {
title: "Market sizing - construction PM software",
market: "North American construction project-management software",
verdict: "defensible",
som_usd: 47350000,
tam_ratio: 1.052,
check_fails: 0,
ran_at: new Date().toISOString(),
summary: "top-down and bottom-up 1.05x apart (converged); SOM $47.35M"
});
const page = await call("/collections/sizings/query", {
where: { verdict: { eq: "contradictory" } },
sort: { field: "ran_at", dir: "desc" },
limit: 20
});
console.log(recordsOf(page).length);
rec := map[string]any{
"title": "Market sizing - construction PM software",
"market": "North American construction project-management software",
"verdict": "defensible",
"som_usd": 47350000,
"tam_ratio": 1.052,
"check_fails": 0,
"ran_at": time.Now().UTC().Format(time.RFC3339),
}
call("/collections/sizings/records", rec)
call("/collections/sizings/query", map[string]any{
"where": map[string]any{"verdict": map[string]any{"eq": "contradictory"}},
"sort": map[string]any{"field": "ran_at", "dir": "desc"},
"limit": 20,
})
call("/collections/sizings/records",
"{\"title\":\"Market sizing - construction PM software\"," +
"\"verdict\":\"defensible\",\"som_usd\":47350000," +
"\"ran_at\":\"2026-08-08T09:00:00Z\"}");
call("/collections/sizings/query",
"{\"where\":{\"verdict\":{\"eq\":\"contradictory\"}}," +
"\"sort\":{\"field\":\"ran_at\",\"dir\":\"desc\"},\"limit\":20}");
call("/collections/sizings/records", {
title: "Market sizing - construction PM software",
market: "North American construction project-management software",
verdict: "defensible",
som_usd: 47_350_000,
tam_ratio: 1.052,
check_fails: 0,
ran_at: Time.now.utc.iso8601
})
call("/collections/sizings/query", {
where: { verdict: { eq: "contradictory" } },
sort: { field: "ran_at", dir: "desc" },
limit: 20
})
<?php
call("/collections/sizings/records", [
"title" => "Market sizing - construction PM software",
"market" => "North American construction project-management software",
"verdict" => "defensible",
"som_usd" => 47350000,
"tam_ratio" => 1.052,
"check_fails" => 0,
"ran_at" => gmdate("c"),
]);
call("/collections/sizings/query", [
"where" => ["verdict" => ["eq" => "contradictory"]],
"sort" => ["field" => "ran_at", "dir" => "desc"],
"limit" => 20,
]);
await Call("/collections/sizings/records", new {
title = "Market sizing - construction PM software",
market = "North American construction project-management software",
verdict = "defensible",
som_usd = 47350000,
tam_ratio = 1.052,
check_fails = 0,
ran_at = DateTime.UtcNow.ToString("o")
});
await Call("/collections/sizings/query", new {
where = new { verdict = new { eq = "contradictory" } },
sort = new { field = "ran_at", dir = "desc" },
limit = 20
});
Rules worth knowing before you design around this
- Vectors are never backfilled. Only records written after
embedwas declared are searchable, and widening the embed set later does not re-index old rows. - Indexing is asynchronous — a similarity query fired immediately after a write can lag by seconds.
- 64 KB per document. The app spends that budget in priority order and marks a record it had to trim, so a restored sizing is honest about what it can no longer re-measure.
- Prefer
wheretosimilarwhenever an exact match would do: it is roughly an order of magnitude cheaper and its rate limit is four times looser.
10. What this API cannot do
The free engine does not live behind an endpoint. Both builds, the triangulation, the reconciling solve, the
capacity test, the unit economics, the sensitivity sweep, the memo, the CSV and the JSON are computed in
sizekit.js in the browser, with no network call and nothing charged. If you want them
server-side, the honest answer is to run that file — it is plain ES5, has no dependencies, and exposes
analyze, renderMemo, driversCsv, measurementJson,
reconcile and validateMemo on window.SizeKit.