Drive Raise a Glass from your own code
Both metered lanes are available over HTTP. write takes the occasion, who is at the table, a tone, a target length and a few true details, and returns ready (a toast in five beats: open, story, bridge, thanks, raise) or needs-details (questions to answer first). tune takes a draft split into numbered sentences and returns ready, trim or rework, with every cut and edit itemised and the tuned toast in five beats. A natural use is a family group-chat bot that collects a line from each person during November and drafts the toast the night before.
The division of labour first: the page owns the clock, the flags and the cake; the model writes the words; the page re-checks the reply. The detail reader, the draft splitter and the clipping come from toastkit.js, which runs in node as well as in the browser. Speaking time is never asked of the model - ToastKit.secs(words) estimates it at 130 words a minute, and ToastKit.targetWords(seconds) turns a 30, 45, 60 or 90 second target into a word budget. ToastKit.check holds each reply to the input: five beats in order, ids that exist, cut sentences really gone from the toast, length against the target, and no capitalised name, number or pronoun that the input does not hold.
Get a token on the token page (step 2 below also shows how to mint a guest token in code).
Base URL and the envelope
Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response uses one envelope:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "...", "status": 402, "details": { ... } } }
The token is minted for this app (the guest call takes {"slug":"raise-a-glass"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or a required field (task, target_seconds) is missing. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: both lanes are metered and need a personal token. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
The task field and the verdicts
Every run sends task: write or tune. A missing or unknown task becomes tune when a draft is present, otherwise write. Verdicts are decided by ordered, non-overlapping rules - the first that applies wins:
| Task | Order | Verdict | When |
|---|---|---|---|
| write | 1 | needs-details | Fewer than two usable details (a specific thing someone did, said, made or brought, a moment, a first, a reason to thank them). No beats; asks holds two or three questions. |
| write | 2 | ready | Otherwise: five beats within 15% of target_words. |
| tune | 1 | rework | A sentence teases someone about body, weight, looks, age, money, work troubles, love life, divorce, religion, politics, health or drinking, or touches avoid - or no sentence holds a specific detail. |
| tune | 2 | trim | More than 15% over target_words, or a sentence apologises for the toast, is an inside joke, repeats itself or lists names with no reasons. |
| tune | 3 | ready | Otherwise. No notes; the draft's sentences come back unchanged in five beats. |
The declared input schema carries scalars only, so the array-valued fields (details, draft, page_flags) travel JSON-encoded as strings (ToastKit.toWire does this). Every value is a string.
Input fields
| Field | Type | Notes |
|---|---|---|
task | string | write or tune. Required. |
occasion | string | As typed - "Thanksgiving dinner", "Friendsgiving". Up to 80 characters. |
speaker, table | string | Who gives the toast; the names at the table, commas (up to 24). Both may be empty. |
tone | string | warm, funny, heartfelt or short-and-sweet. |
target_seconds, target_words | string | 30, 45, 60 or 90; the word budget at 130 words a minute (65, 98, 130, 195). target_seconds is required. |
details | string (JSON) | write: [{id, who, text}] - ids D1.., up to 12 details of up to 240 characters; who from a "Name:" prefix. |
draft, draft_words, page_flags | string (JSON), string, string (JSON) | tune: [{id, text, words}] - ids S1.., up to 40 sentences; the total words; [{ref, flag}] with flags apology, inside-joke, tease:<word>, avoid:<word>, long - the page's guesses, which the model judges for itself. |
avoid | string | Topics to stay away from, commas; may be empty. |
clip_note, retry_note | string | What the page cut, when it cut something; and only on a reformat retry, what was wrong. |
The reply
The job's output.output is a string holding one JSON object: task, verdict, title, summary, beats ([{kind, text, uses}] - exactly five, kind in the order open, story, bridge, thanks, raise, uses the detail or sentence ids it draws on; [] for needs-details), notes (tune: [{ref, action, why, edit}], action cut or edit; write: []) and asks (needs-details only). Parse with ToastKit.parseReply, then normalize(reply, task), then check(reply, form), which returns the page's notes.
Worked examples
Replies produced against the app's prompt for the five sample tables (made-up people; long fields shortened; the full replies ship in example-result.js). Together they cover every verdict of both lanes.
write - Thanksgiving dinner, four true details, 60 s: ready
Input:
{
"task": "write",
"occasion": "Thanksgiving dinner",
"speaker": "Maya",
"table": "Maya, Grandpa Lou, Priya, Sam, Theo",
"tone": "warm",
"target_seconds": "60",
"target_words": "130",
"details": "[{\"id\":\"D1\",\"who\":\"Grandpa Lou\",\"text\":\"drove up through the snow to be here\"},{\"id\":\"D2\",\"who\":\"Priya\",\"text\":\"baked her first ever pie for tonight - lopsided, and proud of it\"},{\"id\":\"D3\",\"who\":\"Sam\",\"text\":\"started as a volunteer firefighter this spring\"},{\"id\":\"D4\",\"who\":\"Theo\",\"text\":\"drew a name card for every seat at the table\"}]",
"avoid": ""
}
Reply:
{
"task": "write",
"verdict": "ready",
"title": "A Thanksgiving toast for this table",
"summary": "This gives you a five-beat toast built on four specific details: Grandpa Lou driving through the snow, Priya baking her first pie, Sam starting as a volunteer firefighter, and Theo making name cards for every seat. It begins by gathering the table, centers on Grandpa Lou's drive through the snow, thanks Priya, Sam, and Theo by name, and ends with everyone raising a glass. There are enough specific details here to build the whole toast without guessing at anything.",
"beats": [
{
"kind": "open",
"text": "We're gathered around this table tonight for Thanksgiving dinner, and I'm grateful every one of you is here with us.",
"uses": []
},
{
"kind": "story",
"text": "Grandpa Lou, you drove up through the snow just to be here with us tonight.",
"uses": [
"D1"
]
},
{
"kind": "bridge",
"text": "That's what this table is built on: people who show up for each other, through the snow or anything else.",
"uses": [
"D1"
]
},
{
"kind": "thanks",
"text": "Priya, thank you for baking your first ever pie for tonight, lopsided and proud of it. Sam, thank you for starting as a volunteer firefighter this spring. And Theo, thank you for drawing a name card for every seat at this table.",
"uses": [
"D2",
"D3",
"D4"
]
},
{
"kind": "raise",
"text": "So tonight, let's raise a glass together. To this table, and to everyone who shows up for it.",
"uses": []
}
],
"notes": [],
"asks": []
}
write - Friendsgiving, "everyone is great" and logistics: needs-details
Input:
{
"task": "write",
"occasion": "Friendsgiving",
"speaker": "",
"table": "Jess, Omar, Kai, Lena",
"tone": "funny",
"target_seconds": "30",
"target_words": "65",
"details": "[{\"id\":\"D1\",\"who\":\"\",\"text\":\"everyone is great\"},{\"id\":\"D2\",\"who\":\"\",\"text\":\"thanks for coming\"},{\"id\":\"D3\",\"who\":\"\",\"text\":\"dinner at 7\"},{\"id\":\"D4\",\"who\":\"\",\"text\":\"Omar\"}]",
"avoid": ""
}
Reply:
{
"task": "write",
"verdict": "needs-details",
"title": "A toast for Friendsgiving",
"summary": "This toast isn't ready yet because none of the details given are specific enough to build a story or thanks around. Right now there's a general compliment, a thank-you with no reason, a dinner time, and a bare name. A few concrete moments or reasons would give the toast something to say.",
"beats": [],
"notes": [],
"asks": [
"What's something specific someone made, brought, or did that you could mention?",
"Is there a particular moment you'd like the toast to picture?",
"Who do you most want to thank, and what's the specific reason?"
]
}
tune - a kind holiday-lunch draft, on time for 45 s: ready
Input:
{
"task": "tune",
"occasion": "our family's holiday lunch",
"speaker": "Mira",
"table": "Nana Rose, Dev, Mira, Jun",
"tone": "heartfelt",
"target_seconds": "45",
"target_words": "98",
"draft": "[{\"id\":\"S1\",\"text\":\"Before we eat, I want to say something.\",\"words\":8},{\"id\":\"S2\",\"text\":\"This year Nana Rose taught me to make the dumplings we are about to eat.\",\"words\":15},{\"id\":\"S3\",\"text\":\"They came out in every shape you can imagine, and Nana said every one of them was perfect.\",\"words\":18},{\"id\":\"S4\",\"text\":\"That is what this table does - it makes r ... (709 characters)",
"draft_words": "90",
"page_flags": "[]",
"avoid": ""
}
Reply:
{
"task": "tune",
"verdict": "ready",
"title": "To every shape at this table",
"summary": "Your draft already lands: the story about learning Nana Rose's dumplings, in every shape, carries into thanks that name Dev and Jun, and the close already raises a glass before its 'To' line. Nothing here needs cutting or rewording, so the beats below are your own sentences, grouped in order.",
"beats": [
{
"kind": "open",
"text": "Before we eat, I want to say something.",
"uses": [
"S1"
]
},
{
"kind": "story",
"text": "This year Nana Rose taught me to make the dumplings we are about to eat. They came out in every shape you can imagine, and Nana said every one of them was perfect.",
"uses": [
"S2",
"S3"
]
},
{
"kind": "bridge",
"text": "That is what this table does - it makes room for every shape.",
"uses": [
"S4"
]
},
{
"kind": "thanks",
"text": "Thank you, Nana, for the recipe and the patience. Thank you, Dev and Jun, for driving through the night to be here.",
"uses": [
"S5",
"S6"
]
},
{
"kind": "raise",
"text": "So please raise a glass. To Nana Rose, and to every shape at this table!",
"uses": [
"S7",
"S8"
]
}
],
"notes": [],
"asks": []
}
tune - a Friendsgiving draft, long, with an apology and an inside joke: trim
Input:
{
"task": "tune",
"occasion": "Friendsgiving",
"speaker": "",
"table": "Bea, Carlos, Dana, Eli, Fern",
"tone": "funny",
"target_seconds": "30",
"target_words": "65",
"draft": "[{\"id\":\"S1\",\"text\":\"Okay, I'm not good at speeches, so bear with me.\",\"words\":10},{\"id\":\"S2\",\"text\":\"I just want to say a few words before the food gets cold.\",\"words\":13},{\"id\":\"S3\",\"text\":\"Bea, you organised this whole thing in a group chat of forty-three messages and somehow nobody brought two salads.\",\"words\":20},{\"id\":\"S4\",\"text\":\"Carlos, you carved the ... (928 characters)",
"draft_words": "118",
"page_flags": "[{\"ref\":\"S1\",\"flag\":\"apology\"},{\"ref\":\"S6\",\"flag\":\"inside-joke\"}]",
"avoid": ""
}
Reply:
{
"task": "tune",
"verdict": "trim",
"title": "A toast for Bea's Friendsgiving",
"summary": "This adds a short opening line and a short bridge line, since the draft had nothing to open on or to turn the story into meaning, and keeps everything else the draft already does well: Bea's chaotic group chat, the thank-yous for Carlos, Dana, Eli and Fern, and the closing toast to Bea's group chat and everyone at the table. It also drops the opening apology, the stalling line about the food, an aside not everyone at the table would follow, the general thank-you line now covered by name, and the trailing sign-off, leaving a toast that's short and easy to say.",
"beats": [
{
"kind": "open",
"text": "Happy Friendsgiving, everyone.",
"uses": []
},
{
"kind": "story",
"text": "Bea, you organised this whole thing in a group chat of forty-three messages and somehow nobody brought two salads.",
"uses": [
"S3"
]
},
{
"kind": "bridge",
"text": "That's this table for you.",
"uses": []
},
{
"kind": "thanks",
"text": "Carlos, you carved the bird like a surgeon. Dana, Eli and Fern, you brought the folding chairs, the playlist and the pie.",
"uses": [
"S4",
"S5"
]
},
{
"kind": "raise",
"text": "Raise your glasses. To Bea's group chat and to all of us!",
"uses": [
"S9",
"S10"
]
}
],
"notes": [
{
"ref": "S1",
"action": "cut",
"why": "it apologises for the toast, which isn't needed",
"edit": ""
},
{
"ref": "S2",
"action": "cut",
"why": "a stalling line with no detail to keep",
"edit": ""
},
{
"ref": "S6",
"action": "cut",
"why": "an aside the whole table wouldn't get",
"edit": ""
},
"... (5 in all)"
],
"asks": []
}
tune - a Thanksgiving draft that teases about a diet and a vote: rework
Input:
{
"task": "tune",
"occasion": "Thanksgiving dinner",
"speaker": "",
"table": "Uncle Ray, Aunt June, Kim, Leo",
"tone": "funny",
"target_seconds": "45",
"target_words": "98",
"draft": "[{\"id\":\"S1\",\"text\":\"Happy Thanksgiving, everybody.\",\"words\":3},{\"id\":\"S2\",\"text\":\"Uncle Ray finally fits in his chair after that diet, so that's something to be thankful for.\",\"words\":17},{\"id\":\"S3\",\"text\":\"Aunt June made the stuffing from Grandma's recipe again, and it tasted like every Thanksgiving I remember.\",\"words\":17},{\"id\":\"S4\",\"text\":\"And let's be h ... (545 characters)",
"draft_words": "61",
"page_flags": "[{\"ref\":\"S2\",\"flag\":\"tease:finally fits\"},{\"ref\":\"S4\",\"flag\":\"tease:voted\"}]",
"avoid": "politics"
}
Reply:
{
"task": "tune",
"verdict": "rework",
"title": "A toast for Thanksgiving dinner",
"summary": "This toast keeps Aunt June's stuffing as its story and turns Kim's table-setting into a direct thank-you; since the draft had no line turning that story into what it means for the table, a short one was added before the thanks. Two lines had to go: the one about Uncle Ray's chair teased him, and the one about Leo's vote touched a topic you asked to avoid. Because of that, this toast needed more than small changes to be ready.",
"beats": [
{
"kind": "open",
"text": "Happy Thanksgiving, everybody.",
"uses": [
"S1"
]
},
{
"kind": "story",
"text": "Aunt June made the stuffing from Grandma's recipe again, and it tasted like every Thanksgiving I remember.",
"uses": [
"S3"
]
},
{
"kind": "bridge",
"text": "That's what this table gives us.",
"uses": []
},
{
"kind": "thanks",
"text": "Thank you, Kim, for setting the table with the good plates.",
"uses": [
"S5"
]
},
{
"kind": "raise",
"text": "So let's raise a glass. To family!",
"uses": [
"S6"
]
}
],
"notes": [
{
"ref": "S2",
"action": "cut",
"why": "This teases Uncle Ray about his weight.",
"edit": ""
},
{
"ref": "S4",
"action": "cut",
"why": "This brings up politics, a topic you asked to avoid.",
"edit": ""
},
{
"ref": "S5",
"action": "edit",
"why": "Turns the detail into a direct thank-you.",
"edit": "Thank you, Kim, for setting the table with the good plates."
}
],
"asks": []
}
Build the input yourself
Load the script in node (global.window = global; require("./toastkit.js")), then:
const K = ToastKit;
const form = K.form({ lane: "tune", occasion: "Thanksgiving dinner", table: "Ray, June, Kim", length: "45", avoid: "politics",
draft: "Happy Thanksgiving, everybody. Kim set the table with the good plates. To family!" });
console.log(K.flags(form), K.lean(form), K.draft(form).secs); // the flags, the page's lean, the estimated seconds
const wire = K.toWire(K.buildInput("tune", form).input); // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output), "tune");
// const { notes } = K.check(reply, form); // the page's re-check of the reply
// K.secs(K.beatWords(reply)) // the tuned toast's estimated seconds
Step 1 — a tiny client helper
One function that sends JSON and unwraps the envelope. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here" # from https://raise-a-glass.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://raise-a-glass.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"]
// raise-a-glass.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://raise-a-glass.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://raise-a-glass.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
}
// RaiseAGlass.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 RaiseAGlass {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://raise-a-glass.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://raise-a-glass.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://raise-a-glass.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://raise-a-glass.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":"raise-a-glass"}' | 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": "raise-a-glass"})["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: "raise-a-glass" })).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": "raise-a-glass"}, 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", "raise-a-glass"), 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: "raise-a-glass" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "raise-a-glass"])["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 = "raise-a-glass" }))!["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, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.
# input.json: the run input object - {"task": "write", "occasion": "Thanksgiving dinner", "speaker": "Maya", "tone": "warm", "target_seconds": "60", "target_words": "130", "details": "[{\"id\":\"D1\",\"who\":\"Grandpa Lou\",\"text\":\"drove up through the snow to be here\"},{\"id\":\"D2\",\"who\":\"Priya\",\"text\":\"baked a pie for the first time - lopsided, and proud of it\"}, ...]"}
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": "write", "occasion": "Thanksgiving dinner", "speaker": "Maya", "tone": "warm", "target_seconds": "60", "target_words": "130", "details": "[{\"id\":\"D1\",\"who\":\"Grandpa Lou\",\"text\":\"drove up through the snow to be here\"},{\"id\":\"D2\",\"who\":\"Priya\",\"text\":\"baked a pie for the first time - lopsided, and proud of it\"}, ...]"}
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: "write", occasion, table, tone, target_seconds, details, ... (see the field table)}
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 raise-a-glass:<task>:<inputHash>:a<attempt> (the lanes are write and tune), and its one reformat retry (with retry_note added) is attempt 2, a new key and a second run. The job's output.output is the reply text; truncated: true means the balance cut it short.
KEY="raise-a-glass:write:$(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 reply contract
echo "$J" | jq '.data | {charged_credits, truncated}'
import hashlib, time
key = "raise-a-glass:" + 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 reply as one JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("verdict"))
import { createHash } from "node:crypto";
const key = `raise-a-glass:${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 := "raise-a-glass:" + 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 = "raise-a-glass:" + 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 = "raise-a-glass:#{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["title"]].inspect
$key = "raise-a-glass:" . $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["title"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"raise-a-glass:{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
The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.
# 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: raise-a-glass:write:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json
rev = json.load(open("input.json")) # the same run input as input.json: {"task": "write", "occasion": "Thanksgiving dinner", "speaker": "Maya", "tone": "warm", "target_seconds": "60", "target_words": "130", "details": "[{\"id\":\"D1\",\"who\":\"Grandpa Lou\",\"text\":\"drove up through the snow to be here\"},{\"id\":\"D2\",\"who\":\"Priya\",\"text\":\"baked a pie for the first time - lopsided, and proud of it\"}, ...]"}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "raise-a-glass:write:" + 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("input.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": `raise-a-glass:write:${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("input.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", "raise-a-glass:write:"+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("input.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", "raise-a-glass:write:" + 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("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "raise-a-glass:write:#{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("input.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: raise-a-glass:write:" . 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("input.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", $"raise-a-glass:write:{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);