Drive Offer Forge from your own code
The page does its free work in the browser: it reads an offer against the six components of a complete offer (core
deliverable, bonus stack, guarantee, scarcity/urgency, name, price + payment structure) and the four levers of the
value equation (dream outcome, perceived likelihood, time delay, effort and sacrifice), grades each with line
numbers, works out the money (headline price, payment-plan premium, value claims and the stack-to-price ratio) and
raises flags OF-01..OF-21. Its two paid lanes are available over HTTP. You send one JSON object with
task set to "diagnose" or "draft", and the model replies with one JSON
object. The easiest way to build the facts string is the page itself:
Offerkit.buildInput(lane, Offerkit.analyze(offer, {btype}), {note, component, diagnosisHandoff}) in
offerkit.js, which also runs under Node.
Base URL: https://api.skillsafe.ai/v1/app-api. Every call sends
Content-Type: application/json and, once you have a token,
Authorization: Bearer <token>. Both lanes run on gpt-terra with a 10%
publisher markup. A run needs a signed-in (user) token and is metered: the platform holds
the estimate's hold_credits while the run is in flight and then charges only
what it used (charged_credits). /estimate is free. A plain-text summary is at
/llms.txt.
The response envelope
Every response is {"data": ..., "error": null} on success or {"data": null, "error": {"code", "message"}} on failure.
Error codes
| HTTP | code | meaning |
|---|---|---|
| 400 | VALIDATION_ERROR | The body is not a JSON object, or a field is the wrong type. Every field of the run input is a string. |
| 401 | UNAUTHORIZED | No token, or an expired one: get a new one (step 2). |
| 402 | INSUFFICIENT_CREDITS | The balance is below the run's minimum. Top up at skillsafe.ai/account/billing/. |
| 403 | FORBIDDEN | A guest token tried a metered run: sign in for a personal token. |
| 404 | NOT_FOUND | Unknown job id. |
| 429 | RATE_LIMITED | Back off and retry with the same Idempotency-Key. |
The task field: two lanes
| task | what the model does |
|---|---|
"diagnose" | The offers skill's diagnostic loop: the offer restated in plain language; the four levers scored 1 to 10 with exact quotes (higher is better for the buyer); the binding constraint (the lowest lever); the six components rated strong, weak or missing; one component to fix first and the lever it moves; and the patterns the offer should drop (fake urgency, unconditioned or over-promising guarantees, bonus inflation, unanchored value claims, banned vocabulary, course-bro aesthetic, discounting to acquire). |
"draft" | The one changed component, ready to paste: what it replaces (an exact quote), the new text with [placeholders] for what only the seller knows, the lever it moves, its conditions and exposure, an honest lift range (whole percentages, at most 40), how to measure it, and what to keep. |
Input fields (all strings)
| field | when | content |
|---|---|---|
task | required | diagnose or draft |
offer | required | The offer, verbatim. Past 16,000 characters the page cuts the middle on a line boundary and leaves an [offer-forge: ...] marker. |
business_type | required | service, course, coaching, info, b2b, direct or saas - the type the facts were built with. |
facts | required | A JSON string: btype, btype_inferred, btype_why, summary, components (id, status: present|weak|missing, note, evidence with line, strength, label, quote), levers (id, status: shown|thin|absent, evidence, burden), binding_hint, money (headline_price, prices, plan, pay_in_full, plan_premium_pct, value_claims, stack_value, stack_to_price), patterns, numbers, flags (uid, id, sev, ref, name), notes. It is Offerkit.factsOf(A). |
note | optional | Your constraints, up to 1,500 characters ("we can refund at most half the fee"). |
component | draft only | core, bonus, guarantee, scarcity, name or price. Without it the run drafts the diagnosis's choice, or the weakest component in the scan. |
diagnosis_handoff | draft only | A JSON string of a diagnosis reply's business_type, binding_constraint{lever}, fix_first{component, lever, change}, components[{component, status}] and offer_plain, for the same offer. |
retry_note | optional | Only on a reformat retry: what was wrong with the previous reply. |
The reply contracts
Both lanes share an envelope: task, business_type, headline, questions[], summary.
task: "diagnose"
offer_plain{name, price, what_they_get, guarantee, deadline}, levers[{lever, score: 1-10, evidence[], why, raise}] (four, in order: dream, likelihood, time, effort), binding_constraint{lever, why}, components[{component, status: strong|weak|missing, evidence[], note}] (six, in order: core, bonus, guarantee, scarcity, name, price), fix_first{component, lever, why, change}, avoid[{pattern, quote, fix}].
task: "draft"
component, lever, source (given|diagnosis|picked from the scan), current{quote, problem}, draft{title, text}, why, conditions[], lift{low, high, basis}, measure{metric, window}, keep[].
Worked examples
These are the page's own bundled examples (invented companies), with the input as app.js builds it and the saved model reply the page replays for free.
Diagnosis: a course launch with a $13,988 bonus stack
Input (offer and facts shortened here):
{
"task": "diagnose",
"offer": "# Launch Your Course: The 6-Week Creator Accelerator\n\nDiscover the SECRET game-changing system top creators use to build a 6-figure course business. This is the revolutionary blueprint they don't want you to know about!!\n\n## Inside the program\n\n- 6 weekly modules (video lessons and worksheets)\n- Weekly live Q&A call\n- Private community\n\n## Plus these bonuses\n\n- Bonus 1: The Course Outline Template (worth $1,997)\n- Bonus 2: 50 Launch Email Swipes (worth $2,997)\n- Bonus 3: Lifetime access to the C... (847 characters in all)",
"business_type": "course",
"facts": "(a JSON string; decoded: business type course, components core present; bonus present; guarantee weak; scarcity weak; name present; price present, levers dream thin; likelihood absent; time shown; effort shown, binding hint likelihood, headline price $497, flags OF-08, OF-11, OF-15, OF-03, OF-10, OF-12, OF-13, OF-17, OF-20)",
"note": "First launch; about 2,000 people on my email list."
}
Reply:
{
"task": "diagnose",
"business_type": "course",
"offer_plain": {
"name": "Launch Your Course: The 6-Week Creator Accelerator",
"price": "$497 paid in full, or 3 payments of $197",
"what_they_get": "Six weekly modules of video lessons and worksheets, a weekly live Q&A call, a private community and four template and library bonuses.",
"guarantee": "Results 100% guaranteed, with no terms stated",
"deadline": "none"
},
"levers": [
{
"lever": "dream",
"score": 4,
"evidence": [
"top creators use to build a 6-figure course business"
],
"why": "The outcome is a vague income bracket borrowed from other creators, not what this buyer has at the end of week 6.",
"raise": "Name the week-6 result the program actually controls, such as a finished outline, a sales page and a launch email sequence sent to their list."
},
{
"lever": "likelihood",
"score": 2,
"evidence": [
"Results 100% guaranteed."
],
"why": "There is no student result, testimonial or credential anywhere, and an unconditioned 100% guarantee reads as hype rather than proof.",
"raise": "Replace the blanket guarantee with a conditional one tied to doing the work, and add any real result from past students or your own launches."
},
{
"lever": "time",
"score": 5,
"evidence": [
"The 6-Week Creator Accelerator",
"6 weekly modules (video lessons and worksheets)"
],
"why": "Six weeks is stated, but it is the length of the program, not when the buyer sees a result.",
"raise": "Say what exists at the end of each week, so the buyer can see th
... (shortened here; the page shows it whole)
Draft: the same offer, carrying the diagnosis
Input (with the diagnosis handed over):
{
"task": "draft",
"offer": "# Launch Your Course: The 6-Week Creator Accelerator\n\nDiscover the SECRET game-changing system top creators use to build a 6-figure course business. This is the revolutionary blueprint they don't want you to know about!!\n\n## Inside the program\n\n- 6 weekly modules (video lessons and worksheets)\n- Weekly live Q&A call\n- Private community\n\n## Plus these bonuses\n\n- Bonus 1: The Course Outline Template (worth $1,997)\n- Bonus 2: 50 Launch Email Swipes (worth $2,997)\n- Bonus 3: Lifetime access to the C... (847 characters in all)",
"business_type": "course",
"facts": "(a JSON string; decoded: business type course, components core present; bonus present; guarantee weak; scarcity weak; name present; price present, levers dream thin; likelihood absent; time shown; effort shown, binding hint likelihood, headline price $497, flags OF-08, OF-11, OF-15, OF-03, OF-10, OF-12, OF-13, OF-17, OF-20)",
"note": "First launch; about 2,000 people on my email list.",
"diagnosis_handoff": "{\"business_type\":\"course\",\"binding_constraint\":{\"lever\":\"likelihood\"},\"fix_first\":{\"component\":\"guarantee\",\"lever\":\"likelihood\",\"change\":\"Replace \\\"Results 100% guaranteed.\\\" with a refund tied to doing the work: complete the modules and attend the calls within a set window, and if you do not have a finished course out..."
}
Reply:
{
"task": "draft",
"business_type": "course",
"component": "guarantee",
"lever": "likelihood",
"source": "diagnosis",
"current": {
"quote": "Results 100% guaranteed.",
"problem": "It promises results with no terms: no window, no definition of what counts as not working, and no stated remedy. Buyers read it as a slogan, so it adds no belief and exposes you to refund claims you never defined."
},
"draft": {
"title": "The Do-the-Work Guarantee",
"text": "Do the work and finish with a course you can sell, or get your money back.\n\nHere is the deal. Within 14 days of the final module, you will have a finished course outline and a draft sales page for your course. To qualify, complete all 6 weekly modules and their worksheets, and attend or watch the replay of at least 4 of the 6 live Q&A calls.\n\nIf you did that and you do not have a finished outline and a draft sales page, email [support email] within 14 days of the last module with your completed worksheets. We will first offer you a free 1-on-1 review call to get you there. If you would rather have the money, we refund every payment you made, up to the full $497, within [number] business days. No argument.\n\nThe guarantee covers the work this program teaches. It does not promise a number of sales, because that depends on your audience and your launch."
},
"why": "Tying the refund to a concrete deliverable the buyer controls (an outline and a sales page) makes the promise believable and shows the seller expects the program to work. It raises perceived likelihood without overpromising sales results.",
"con
... (shortened here; the page shows it whole)
Draft: a cloud-cost audit, with the component chosen by hand
Input:
{
"task": "draft",
"offer": "# Cloud Cost Audit for SaaS teams\n\nSpanwick Ops audits your AWS bill and hands you a ranked list of savings with the change requests already written.\n\nWhat you get: a two-week audit of one AWS organisation, a savings report ranked by dollars per month and effort, and pull requests for the top five changes. We have done 40 audits for SaaS companies between $2M and $30M ARR; the median saving found was 23% of monthly spend.\n\nPrice: $9,500 fixed fee, invoiced at kickoff.\n\nMost prospects agree the ... (630 characters in all)",
"business_type": "b2b",
"facts": "(a JSON string; decoded: business type b2b, components core present; bonus missing; guarantee missing; scarcity missing; name present; price present, levers dream shown; likelihood shown; time shown; effort shown, binding hint likelihood, headline price $9,500, flags OF-05, OF-06, OF-07)",
"note": "We can afford to refund at most half the fee.",
"component": "guarantee"
}
Reply:
{
"task": "draft",
"business_type": "b2b",
"component": "guarantee",
"lever": "likelihood",
"source": "given",
"current": {
"quote": "Right now we have no answer and no refunds.",
"problem": "The CFO's one objection, what if the audit finds less than the fee, has no answer, so the buyer carries all the risk of a $9,500 fee paid at kickoff."
},
"draft": {
"title": "The Fee-Back Floor",
"text": "If the audit doesn't find at least the fee, you get half of it back.\n\nWhen we hand over the savings report at the end of the two weeks, we total the monthly savings it identifies and multiply by 12. If that annual figure is less than $9,500, we refund half the fee within [number] days of delivering the report. You keep the report and the pull requests either way.\n\nWho qualifies: one AWS organisation, with the read-only access we request granted within [number] business days of kickoff and one engineer available for a 30-minute walkthrough. Savings count when the report ranks them with a dollar-per-month estimate, whether or not you choose to ship the change.\n\nWe offer this because we have done 40 audits for SaaS companies between $2M and $30M ARR, and the median saving found was 23% of monthly spend."
},
"why": "It turns the CFO's question into a stated floor: the downside is capped at half the fee, so the 40-audit track record becomes something the buyer can bet on rather than just read.",
"conditions": [
"The refund is half the fee, the cap in your note; it must never pay more than that.",
"Fee-back only works if the savings estimate method is written do
... (shortened here; the page shows it whole)
The page re-checks the reply; the API does not
The page holds every reply to account (recon.js): for a diagnosis, four levers scored once with whole numbers, the binding constraint on the lowest score, no score of 7 or more without a quote, six components rated once, every quote in the offer word for word, no fake urgency or unconditioned guarantee rated strong, the fix on a weak or missing component, and no price or benchmark the offer does not contain; for a draft, the component asked for, the replaced quote word for word, the drafted text re-run through the page's own rules (banned words, fake urgency, guarantees without terms, unanchored values, discounts) and a lift range inside 0 to 40%. Over the API you get the model's reply as it is; run the same checks yourself with Recon.reconcile in recon.js, or paste the same offer into the page.
Step 1 — a tiny client helper
One function that sends JSON and unwraps the envelope.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here" # from https://offer-forge.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://offer-forge.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// offer-forge.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://offer-forge.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "paste-your-token-here" // from https://offer-forge.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// OfferForge.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class OfferForge {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://offer-forge.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://offer-forge.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "paste-your-token-here"; // from https://offer-forge.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "paste-your-token-here"; // from https://offer-forge.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
Step 2 — a token
A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"offer-forge"}' | jq -r '.data.token')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "offer-forge"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "offer-forge" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "offer-forge"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "offer-forge"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "offer-forge" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "offer-forge"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "offer-forge" }))!["token"]!.GetValue<string>();
Step 3 — GET /me
Who the token is (subject_type: user or guest) and the credit balance.
api GET /me | jq '.data | {subject_type, credits}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["credits"]}");
Step 4 — POST /estimate
Free. The body is the run input itself (not wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model reads gpt-5.6-terra and model_alias gpt-terra.
# input.json: the run input object - {"task": "diagnose", "offer": "...", "business_type": "service", "facts": "..."}
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}'
inp = json.load(open("input.json")) # {"task": "diagnose", "offer": "...", "business_type": "service", "facts": "...", ...}
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task, offer, business_type, facts, ...}
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
Step 5 — POST /run, then poll the job
Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses offer-forge:<lane>:<inputHash>:a<attempt>. The job's output.output is the reply text; truncated: true means the balance cut it short.
KEY="offer-forge:diagnose:$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json # one JSON object, the lane's contract
echo "$J" | jq '.data | {charged_credits, truncated}'
import hashlib, time
key = "offer-forge:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the lane's JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("verdict"))
import { createHash } from "node:crypto";
const key = `offer-forge:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.verdict);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "offer-forge:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
fmt.Println(j["charged_credits"], j["truncated"], j["output"].(map[string]any)["output"])
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "offer-forge:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply);
require "digest"
key = "offer-forge:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["verdict"]].inspect
$key = "offer-forge:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", $reply["verdict"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"offer-forge:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>());
Console.WriteLine($"{j["charged_credits"]} {reply}");
Step 6 — POST /run-stream (draft)
The same run as Server-Sent Events. draft.json is a draft input, with diagnosis_handoff when it follows a diagnosis.
# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: offer-forge:draft:$(shasum -a 256 draft.json | cut -c1-16):a1" --data @draft.json
rev = json.load(open("draft.json")) # {"task": "draft", "offer": "...", "business_type": "...", "facts": "...", "component": "...", "diagnosis_handoff": "..."}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "offer-forge:draft:" + hashlib.sha256(json.dumps(rev).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const rev = JSON.parse(readFileSync("draft.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `offer-forge:draft:${createHash("sha256").update(JSON.stringify(rev)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("draft.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "offer-forge:draft:"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String rev = java.nio.file.Files.readString(java.nio.file.Path.of("draft.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "offer-forge:draft:" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("draft.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "offer-forge:draft:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$rev = file_get_contents("draft.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: offer-forge:draft:" . substr(hash("sha256", $rev), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var rev = File.ReadAllText("draft.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"offer-forge:draft:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);