Driving Earnings Desk from your own code
Everything the web app does over HTTP, you can do. The base URL is
https://api.skillsafe.ai/v1/app-api. Every call takes
Authorization: Bearer <token> except the one that mints a token.
Earnings Desk is a reading tool for professionals working their own material. It is not investment advice, not a recommendation and not a research report. The prompt forbids the model from originating a rating, a price target or a buy/sell/hold call, and your integration should not present its output as one.
The envelope
Success is {"ok": true, "data": { ... }}. Failure is
{"error": {"code": "...", "message": "...", "details": { ... }}} with a matching
HTTP status. Read error.code, not the message, when you branch.
| code | status | what to do |
|---|---|---|
UNAUTHORIZED | 401 | No token, or a token this app does not accept. Mint a new one from the tokens page. |
FORBIDDEN | 403 | The token is valid but not for this app, or the call needs a personal token and you sent a guest one. |
VALIDATION_ERROR | 400 | The input object is malformed. error.details names the field. |
INSUFFICIENT_CREDITS | 402 | The balance cannot cover min_credits. Call /estimate first and compare with /me. |
RATE_LIMITED | 429 | Back off and retry. Do not tight-loop. |
JOB_FAILED | 5xx | The run reached a terminal failure; error.message carries the reason. |
The task field comes first
Earnings Desk is one app with four lanes over one work object, and task selects the
lane. Every lane takes the same input object and returns the same envelope; only the meaning of
the inner sections changes.
| task | what it produces | verdict values | artifact_json.kind | lines[] |
|---|---|---|---|---|
scorecard | the beat/miss table, the quality-of-earnings checks, and where the beat came from | clean-beat · low-quality-beat · mixed · in-line · miss | earnings_scorecard | required |
guidance | every guide, the move, and how much of a raise is only the beat flowing through | raised · reaffirmed · lowered · mixed · no-guide | guidance_bridge | required |
note | the desk note, with every figure sourced and the gaps marked rather than filled | ready-to-send · needs-numbers · needs-review | desk_note | optional |
thesis | each pillar of the stated case tested, with silence marked unsettled | strengthened · intact · under-pressure · broken | thesis_update | optional |
Step 1 — a tiny client
# Every call in this guide uses these two values.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="YOUR_TOKEN" # Replace "YOUR_TOKEN" with the token from the tokens page.
# The envelope is the same on every endpoint:
# success -> {"ok": true, "data": { ... }}
# failure -> {"error": {"code": "...", "message": "...", "details": {...}}}
call() { curl -s -X "$1" "$BASE$2" -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" ${3:+-d "$3"}; }
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # Replace "YOUR_TOKEN" with the token from the tokens page.
def call(method, path, body=None):
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:
payload = json.load(r)
# success is {"ok": true, "data": ...}; failure raises with {"error": ...}
return payload["data"]
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "YOUR_TOKEN"; // Replace "YOUR_TOKEN" with the token from the tokens page.
async function call(method, path, body, extraHeaders) {
const headers = Object.assign({ "Content-Type": "application/json" }, extraHeaders || {});
if (TOKEN) headers.Authorization = "Bearer " + TOKEN;
const res = await fetch(BASE + path, {
method,
headers,
body: body === undefined ? undefined : JSON.stringify(body),
});
const json = await res.json();
if (!res.ok) throw new Error(json.error ? json.error.message : res.statusText);
return json.data;
}
package main
import (
"bytes"
"encoding/json"
"io"
"net/http"
)
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "YOUR_TOKEN" // Replace "YOUR_TOKEN" with the token from the tokens page.
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(method, path string, body any) (json.RawMessage, error) {
var r io.Reader
if body != nil {
b, _ := json.Marshal(body)
r = bytes.NewReader(b)
}
req, _ := http.NewRequest(method, base+path, r)
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 e envelope
json.NewDecoder(res.Body).Decode(&e)
return e.Data, nil
}
import java.net.URI;
import java.net.http.*;
public class EarningsDesk {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "YOUR_TOKEN"; // Replace "YOUR_TOKEN" with the token from the tokens page.
static final HttpClient HTTP = HttpClient.newHttpClient();
static String call(String method, String path, String body) throws Exception {
HttpRequest.BodyPublisher pub = body == null
? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(body);
HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.method(method, pub)
.build();
// success {"ok":true,"data":...}, failure {"error":{"code","message"}}
return HTTP.send(req, HttpResponse.BodyHandlers.ofString()).body();
}
}
require "json"
require "net/http"
require "uri"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # Replace "YOUR_TOKEN" with the token from the tokens page.
def call(method, path, body = nil)
uri = URI(BASE + path)
klass = { "GET" => Net::HTTP::Get, "POST" => Net::HTTP::Post }.fetch(method)
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) }
payload = JSON.parse(res.body)
raise payload.dig("error", "message") || res.message unless res.is_a?(Net::HTTPSuccess)
payload["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "YOUR_TOKEN"; // Replace "YOUR_TOKEN" with the token from the tokens page.
function call(string $method, string $path, $body = null) {
global $TOKEN;
$ch = curl_init(BASE . $path);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . $TOKEN,
"Content-Type: application/json",
]);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
$payload = json_decode(curl_exec($ch), true);
curl_close($ch);
if (isset($payload["error"])) {
throw new RuntimeException($payload["error"]["message"]);
}
return $payload["data"];
}
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
static class EarningsDesk
{
const string Base = "https://api.skillsafe.ai/v1/app-api";
static string Token = "YOUR_TOKEN"; // Replace "YOUR_TOKEN" with the token from the tokens page.
static readonly HttpClient Http = new HttpClient();
static async Task<JsonElement> Call(HttpMethod method, string path, object body = null)
{
var req = new HttpRequestMessage(method, Base + path);
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token);
if (body != null)
req.Content = new StringContent(JsonSerializer.Serialize(body),
Encoding.UTF8, "application/json");
var res = await Http.SendAsync(req);
var doc = JsonDocument.Parse(await res.Content.ReadAsStringAsync());
if (doc.RootElement.TryGetProperty("error", out var err))
throw new Exception(err.GetProperty("message").GetString());
return doc.RootElement.GetProperty("data");
}
}
Step 2 — get a token
The guest endpoint needs no authorization and its body key is slug. A guest token is
enough for /me and /estimate; running a lane is metered and needs a
personal token from the tokens page.
# A guest token needs NO Authorization header. The body key is "slug".
curl -s -X POST "$BASE/guest" \
-H "Content-Type: application/json" \
-d '{"slug": "earnings-desk"}'
# -> {"ok":true,"data":{"token":"aut_...","subject_type":"guest", ...}}
# A guest can call /me and /estimate. Running a lane is metered and needs a
# personal token - get one from https://earnings-desk.skillsafe.ai/tokens.html
# The body key is "slug" - not "app_slug".
req = urllib.request.Request(BASE + "/guest",
data=json.dumps({"slug": "earnings-desk"}).encode(),
method="POST")
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req) as r:
TOKEN = json.load(r)["data"]["token"]
print(TOKEN[:12] + "...")
// The body key is "slug" - not "app_slug".
const res = await fetch(BASE + "/guest", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ slug: "earnings-desk" }),
});
TOKEN = (await res.json()).data.token;
// The body key is "slug" - not "app_slug".
raw, _ := call("POST", "/guest", map[string]string{"slug": "earnings-desk"})
var guest struct {
Token string `json:"token"`
}
json.Unmarshal(raw, &guest)
token = guest.Token
// The body key is "slug" - not "app_slug".
String guest = call("POST", "/guest", "{\"slug\":\"earnings-desk\"}");
// parse guest with your JSON library and read data.token into `token`
# The body key is "slug" - not "app_slug".
TOKEN = call("POST", "/guest", { "slug" => "earnings-desk" })["token"]
<?php
// The body key is "slug" - not "app_slug".
$guest = call("POST", "/guest", ["slug" => "earnings-desk"]);
$TOKEN = $guest["token"];
// The body key is "slug" - not "app_slug".
var guest = await Call(HttpMethod.Post, "/guest", new { slug = "earnings-desk" });
Token = guest.GetProperty("token").GetString();
Step 3 — who am I
curl -s "$BASE/me" -H "Authorization: Bearer $TOKEN"
# -> {"ok":true,"data":{"subject_type":"user","username":"...","credits":123456}}
# subject_type is "guest" or "user". Only a "user" can run a lane.
me = call("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await call("GET", "/me");
console.log(me.subject_type, me.credits);
raw, _ := call("GET", "/me", nil)
fmt.Println(string(raw))
System.out.println(call("GET", "/me", null));
me = call("GET", "/me")
puts me["subject_type"], me["credits"]
<?php
$me = call("GET", "/me");
echo $me["subject_type"], " ", $me["credits"], "\n";
var me = await Call(HttpMethod.Get, "/me");
Console.WriteLine(me.GetProperty("subject_type").GetString());
Step 4 — price the run (free)
/estimate creates no job and charges nothing. The request body is the input
object — it is not wrapped in a {"input": ...} field. Price each lane separately:
the four have different prompts and different output caps, so lane A's hold is not lane B's.
# /estimate is FREE. It creates no job and charges nothing.
# The body IS the input object - it is not wrapped in anything.
curl -s -X POST "$BASE/estimate" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"task": "scorecard", "release": "...", "estimates": "...", "period": "Q3 2025", "reading": "balanced", "masking_on": true, "prescan": {"readable": true, "flags": []}}'
# -> {"ok":true,"data":{"model":"gpt-5.6-terra","model_alias":"gpt-terra",
# "markup_bps":1000,"hold_credits":3200,"min_credits":400,
# "sponsor_enabled":false}}
#
# hold_credits is RESERVED, not charged. It prices the full output cap; the
# actual charge is usually far lower. Estimate each lane separately - the four
# lanes have different prompts and different caps.
est = call("POST", "/estimate", INPUT) # INPUT is the object below
print(est["model_alias"], est["hold_credits"], "reserved")
const est = await call("POST", "/estimate", INPUT);
console.log(est.model_alias, est.hold_credits, "reserved");
raw, _ := call("POST", "/estimate", input)
fmt.Println(string(raw))
System.out.println(call("POST", "/estimate", inputJson));
est = call("POST", "/estimate", input)
puts est["model_alias"], est["hold_credits"]
<?php
$est = call("POST", "/estimate", $input);
echo $est["model_alias"], " ", $est["hold_credits"], "\n";
var est = await Call(HttpMethod.Post, "/estimate", input);
Console.WriteLine(est.GetProperty("hold_credits").GetInt32());
The input object, field by field
| field | type | meaning |
|---|---|---|
task | string | The lane. One of scorecard, guidance, note, thesis. Missing or unrecognised, the model picks the closest lane and names its choice in summary. |
release | string | The results release. Required. Clipped in the MIDDLE at 26,000 characters, with the cut announced in-band. |
estimates | string | The estimate table as text. Optional; without it nothing can be called a beat or a miss. |
prior_guidance | string | The guide given last time. Optional; without it the bridge has nothing to move from. |
thesis | string | The pillars of the case. Required in substance by the thesis lane. |
notes | string | Anything the numbers do not say. |
period | string | The period the prescan read, e.g. Q3 2025, or unknown. |
reading | string | balanced, strict or fast. |
masking_on | boolean | Whether identities were replaced with placeholders in the browser. When true, the reply must never name a real company or person. |
redaction | array | One entry per masked identity: {kind, placeholder, length}. The literal value is never sent. |
prescan | object | The browser's own measurements: period, coverage, stats, compares[], guidance[], prior_guidance[], guidance_delta[] and flags[]. The model must answer every flags[].flag_id by id. |
upstream | string | The previous lane's output when a handoff button was used. |
A minimal but complete request body:
{
"task": "scorecard",
"release": "Q3 2025 results\n\n(in millions, except per share data)\n\n| Line item | Q3 2025 | Q3 2024 |\n| --- | --- | --- |\n| Total revenue | 1,284.6 | 1,088.2 |\n| Operating income | 214.0 | 150.1 |\n| Adjusted EPS | 0.87 | 0.61 |",
"estimates": "Metric,My estimate,Consensus\nRevenue,1250.0,1262.4\nAdjusted EPS,0.82,0.84",
"prior_guidance": "",
"thesis": "",
"notes": "",
"period": "Q3 2025",
"reading": "balanced",
"masking_on": true,
"redaction": [],
"prescan": {
"readable": true,
"period": "Q3 2025",
"flags": []
},
"upstream": ""
}
Step 5 — run it and poll
Always send an Idempotency-Key. Hash the lane, the input and an attempt counter; a
retry after a network blip must reuse the exact same key or it bills twice.
# Metered. Send an Idempotency-Key so a retry cannot double-bill.
KEY="earnings-desk:scorecard:$(printf %s "$RELEASE" | shasum | cut -c1-8):1"
JOB=$(curl -s -X POST "$BASE/run" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-d "$INPUT" | python3 -c 'import json,sys;print(json.load(sys.stdin)["data"]["job_id"])')
# Poll until terminal.
until curl -s "$BASE/jobs/$JOB" -H "Authorization: Bearer $TOKEN" \
| grep -q '"status":"succeeded"'; do sleep 2; done
curl -s "$BASE/jobs/$JOB" -H "Authorization: Bearer $TOKEN"
# -> data.output holds the JSON envelope described below.
import time
key = f"earnings-desk:{INPUT['task']}:{abs(hash(INPUT['release'])) % 10**8:08x}:1"
req = urllib.request.Request(BASE + "/run", data=json.dumps(INPUT).encode(), method="POST")
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Content-Type", "application/json")
req.add_header("Idempotency-Key", key) # a retry must reuse this exact key
with urllib.request.urlopen(req) as r:
job_id = json.load(r)["data"]["job_id"]
while True:
job = call("GET", "/jobs/" + job_id)
if job["status"] in ("succeeded", "failed"):
break
time.sleep(2)
result = json.loads(job["output"]) # the envelope described below
const key = `earnings-desk:${INPUT.task}:${hash(INPUT.release)}:1`;
const { job_id } = await call("POST", "/run", INPUT, { "Idempotency-Key": key });
let job;
do {
await new Promise((r) => setTimeout(r, 2000));
job = await call("GET", "/jobs/" + job_id);
} while (job.status !== "succeeded" && job.status !== "failed");
const result = JSON.parse(job.output);
// POST /run with an Idempotency-Key header, then poll GET /jobs/{id}
// until status is "succeeded" or "failed". job.output holds the envelope.
raw, _ := call("POST", "/run", input)
fmt.Println(string(raw))
// POST /run with header Idempotency-Key, then poll GET /jobs/{id}.
String job = call("POST", "/run", inputJson);
System.out.println(job);
job = call("POST", "/run", input) # add the Idempotency-Key header on req
loop do
status = call("GET", "/jobs/#{job['job_id']}")
break if %w[succeeded failed].include?(status["status"])
sleep 2
end
<?php
// Add "Idempotency-Key: ..." to the header array in call() for this request.
$job = call("POST", "/run", $input);
do {
sleep(2);
$status = call("GET", "/jobs/" . $job["job_id"]);
} while (!in_array($status["status"], ["succeeded", "failed"]));
// Add req.Headers.Add("Idempotency-Key", key) inside Call for this request.
var job = await Call(HttpMethod.Post, "/run", input);
var id = job.GetProperty("job_id").GetString();
Step 6 — stream it instead
/run-stream returns text/event-stream with four event names:
job, delta, done and error. A
delta frame's data is {"text": "..."} and the text is
cumulative — the whole output so far, not the increment.
# Server-sent events. Four event names: job, delta, done, error.
# A delta's data carries {"text": "<the whole output so far>"}.
curl -N -X POST "$BASE/run-stream" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-d "$INPUT"
# event: job
# data: {"job_id":"job_..."}
# event: delta
# data: {"text":"{\"lane\":\"scorecard\",..."}
# event: done
# data: {"job_id":"job_...","status":"succeeded","charged_credits":118,"output":"{...}"}
req = urllib.request.Request(BASE + "/run-stream",
data=json.dumps(INPUT).encode(), method="POST")
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Content-Type", "application/json")
req.add_header("Idempotency-Key", key)
event, buf = "message", ""
with urllib.request.urlopen(req) as r:
for raw in r:
line = raw.decode().rstrip("\n")
if line.startswith("event:"):
event = line[6:].strip()
elif line.startswith("data:"):
buf += line[5:].strip()
elif line == "":
if buf and event == "delta":
print(json.loads(buf)["text"][-80:]) # cumulative text
elif buf and event == "done":
result = json.loads(json.loads(buf)["output"])
buf = ""
const res = await fetch(BASE + "/run-stream", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + TOKEN,
"Idempotency-Key": key,
},
body: JSON.stringify(INPUT),
});
const reader = res.body.getReader();
const dec = new TextDecoder();
let buffer = "";
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buffer += dec.decode(value, { stream: true });
let i;
while ((i = buffer.indexOf("\n\n")) >= 0) {
const frame = buffer.slice(0, i);
buffer = buffer.slice(i + 2);
let name = "message", data = "";
frame.split("\n").forEach((l) => {
if (l.startsWith("event:")) name = l.slice(6).trim();
else if (l.startsWith("data:")) data += l.slice(5).trim();
});
if (name === "delta") render(JSON.parse(data).text);
if (name === "done") finish(JSON.parse(JSON.parse(data).output));
}
}
// POST /run-stream and read the response body line by line.
// Frames are separated by a blank line; "event:" names the frame and
// "data:" carries the JSON. delta -> {"text": "..."}; done -> {"output": "..."}.
// POST /run-stream with HttpResponse.BodyHandlers.ofLines() and parse
// "event:"/"data:" pairs. delta carries {"text": "..."}.
# Net::HTTP with read_body streams the SSE frames; split on a blank line,
# read "event:" and "data:", and JSON.parse the data.
<?php
// Set CURLOPT_WRITEFUNCTION and parse "event:"/"data:" frames as they arrive.
// Use HttpCompletionOption.ResponseHeadersRead and read the stream line by
// line, parsing "event:"/"data:" frames.
The output envelope
The reply is a single JSON object. Every key is always present: an empty section is
[], "" or {}, never null and never omitted.
Parse it with a tolerant reader — the web app recovers a truncated stream by walking the bracket
stack and rendering whatever sections completed.
| field | type | meaning |
|---|---|---|
lane | string | The lane the reply is for. |
title | string | Under 80 characters. |
verdict | string | One of the lane's own list. |
headline | string | One sentence, under 160 characters. |
summary | string | Two to four sentences. |
checks | array | {name, value, verdict, note}; verdict is good|weak|missing|risky|not-applicable. |
findings | array | {id, severity, metric, quote, why, so_what}; severity is critical|high|medium|low. |
lines | array | {metric, label, actual, expected, surprise, driver, note}, all strings. |
artifact | string | The lane's document, in Markdown. |
artifact_json | object | Carries a kind naming the lane's schema. |
coverage_check | array | {flag_id, status, note}; status is confirmed|cleared|not-applicable. Exactly one entry per prescan flag. |
questions | array | Strings. Non-empty in the thesis lane. |
confidence | string | high|medium|low. |
One worked example per lane
task: "scorecard"
Send the release and the estimates. You get the beat/miss table in lines[], the
quality-of-earnings questions in checks[], and
artifact_json.beat_quality of above-the-line,
below-the-line, mixed or not-determinable. Ask for
not-determinable to be respected: with no estimates it is the honest answer.
{
"task": "scorecard",
"release": "...",
"estimates": "Metric,Consensus\nRevenue,1262.4\nAdjusted EPS,0.84",
"period": "Q3 2025",
"reading": "balanced",
"masking_on": true,
"prescan": {
"readable": true,
"flags": []
}
}
task: "guidance"
Send prior_guidance as well, or the bridge has nothing to move from.
lines[] carries one row per guide and
artifact_json.guides[].flow_through_only marks a raise that is only the quarter's beat
flowing through.
{
"task": "guidance",
"release": "...",
"prior_guidance": "For the fourth quarter the company expects revenue of $1,300 million to $1,320 million.",
"period": "Q3 2025",
"masking_on": true,
"prescan": {
"readable": true,
"flags": []
}
}
task: "note"
The artifact is the product. Figures the input does not carry appear as
[not in the paste] rather than being estimated, and verdict is
needs-numbers when that happens. checks[] is the sourcing table: each row
names where in your input the claim came from.
{
"task": "note",
"release": "...",
"estimates": "...",
"period": "Q3 2025",
"masking_on": true,
"prescan": {
"readable": true,
"flags": []
}
}
task: "thesis"
Send thesis as pillars, one per line. checks[] comes back with one row
per pillar, and a pillar the release is silent on is missing, never good.
questions[] is non-empty in this lane by contract.
{
"task": "thesis",
"release": "...",
"thesis": "Pillar 1: growth stays above 15%.\nPillar 2: gross margin expands.",
"period": "Q3 2025",
"masking_on": true,
"prescan": {
"readable": true,
"flags": []
}
}
Answering the prescan
The web app sends its own measurements in prescan, and the prompt requires the model
to answer every prescan.flags[].flag_id exactly once in coverage_check.
If you build your own prescan, give each flag a stable flag_id and a
confidence of high or unknown:
high— you measured it against material that is present. Definite.unknown— you are asserting an absence across a document you may only have part of. The model is told not to upgrade this into a claim about what the company disclosed.
Sending flags with no confidence is safe — they are treated as high —
but it gives up the distinction, and that distinction is what stops a report saying a company
disclosed nothing when the user simply pasted an excerpt.
Rate limits and cost
/estimate,/meand/guestare free.- A run reserves
hold_creditsand chargescharged_credits, usually far less, because the hold prices the full output cap. - If the balance sits between
min_creditsandhold_creditsthe run still executes with a reduced cap and returns"truncated": true. Surface that rather than presenting a clipped answer as complete. - On
429, back off. Do not tight-loop.