Drive Yes, And from your own code
The one metered task, notes, is available over HTTP. It takes the audience suggestion, the page's facts about the scene (the players, the line count, each player's questions and the lines that open like a denial) and the scene itself as numbered lines. It returns one verdict (blocking, no_game, game_dropped or found_the_game), what every line offered and what happened to it, the base reality, the first unusual thing and the game, the lines where the game came back, a listening moment, three notes, three suggested next lines and a closing line.
The division of labour first: the page reads the scene into lines, counts and cue words; the model reads the meaning; the page then re-checks the reply against the scene - one entry per line, each fate decided by a later line from another player, the verdict against the order, real players in the next lines. A playful practice aid, not a teacher's assessment.
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":"yes-and"}), 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, suggestion, page_facts, scene) 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: the coach's notes are metered and needs 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: notes (the one task is notes); a missing or different one is still answered as notes. The verdict field holds one of four ids, by a fixed check order over the reply's own fields that stops at the first that fits: blocking when any line's offer is blocked; no_game when no unusual thing is found; game_dropped when the game comes back fewer than 2 times; otherwise found_the_game. The page recomputes the verdict from the reply's own fields and flags any mismatch.
Input (every field is a string)
| Field | Required | What it holds |
|---|---|---|
task | yes | Always notes. |
suggestion | yes | The audience suggestion, up to 60 characters, or none given. |
page_facts | yes | One key | ... per line: players (count and names), lines (count sent), questions (per player, with line numbers), denial_openers (line numbers whose first words look like a denial, or none) and verdict_rule. |
scene | yes | One n | player | text per line. A scene over 40 lines sends lines 1 to 30 and the last 10, with a [... lines a to b cut ...] marker. |
clip_note | no | What the page shortened, if anything (a long scene, or lines over 280 characters). |
retry_note | no | Only on the page's one reformat retry, after an unusable reply. |
Reply (one JSON object)
{"task": "notes", "verdict": "found_the_game", "verdict_reason": "...",
"base_reality": {"who": "...", "where": "...", "what": "..."},
"first_unusual": {"line": 3, "what": "..."}, "game": "...",
"lines": [{"n": 1, "offer": "...", "fate": "built", "by": 2}], "game_moves": [5, 7],
"listening": {"line": 5, "underneath": "...", "reply_line": 6, "picked_up": false},
"notes": [{"line": 7, "note": "..."}], "next_beats": [{"speaker": "Priya", "line": "..."}], "closing": "..."}
| Field | What it holds |
|---|---|
verdict, verdict_reason | The verdict id by the check order above; at most 30 words on what decided it. |
lines | Exactly one entry per line sent: n, the offer (at most 12 words, or "" for a bare acknowledgement), its fate (none, blocked, built, accepted or ignored, by its own check order) and by, the later line by another player that decided it (0 for ignored and none). |
base_reality | Who, where and what, at most 12 words each, from the scene's own words. |
first_unusual, game, game_moves | The first line that breaks from the ordinary (line 0 when none), the game in at most 20 words, and the later lines where it came back. |
listening | One line, what was underneath it (at most 20 words, quoting the line), the next reply by another player and whether that reply picked it up. |
notes | Exactly 3, each tied to a line, at most 25 words; note 1 is on the moment that decided the verdict. |
next_beats, closing | Exactly 3 suggested next lines, in the speaker's own words, for real players and never one player twice in a row; a closing line of at most 20 words. |
The page then checks the reply (SceneKit.check): the verdict against its own fields, one entry per line, every fate decided by a later line from another player, game moves after the first unusual line, the listening reply line, three notes on real lines with note 1 on the deciding moment, a questions note when a player asked 3 or more, three next lines for real players that give words rather than describe them, pronouns, stray numbers, leaked field ids and the word limits. It also reconciles its own cue words: a line that opens like a denial but is not read as a block is flagged for a second look.
Worked examples
These are the four free samples on the page: the exact input and a stand-in model's reply, unedited. Together they cover all four verdicts - a lighthouse keeper scared of the dark (found the game), a leaning wedding cake that keeps getting denied (blocking), an ordinary chat at a bus stop (no game yet), and a museum guide who asks the paintings' permission once and never again (game dropped).
The lighthouse keeper - found the game
Input:
{
"task": "notes",
"suggestion": "lighthouse",
"page_facts": "players | 2 | Mo; Priya\nlines | 9\nquestions | Mo 0; Priya 0\ndenial_openers | none\nverdict_rule | blocking when any line's offer is blocked; else no_game when no unusual thing is found; else game_dropped when the game is played again fewer than 2 times; else found_the_game",
"scene": "1 | Mo | Priya, the new bulb for the lighthouse came today. Biggest one they make.\n2 | Priya | Great. Let's get it up the stairs before it gets dark.\n3 | Mo | Before dark, yes. I'm scared of the dark. I sleep with every light on.\n4 | Priya | A lighthouse keeper who is scared of the dark. That explains all the night-lights in the hall.\n5 | Mo | Nine night-lights in the hall, and one inside my pillow for emergencies.\n6 | Priya | And you picked the job with the biggest light on the whole coast.\n7 | Mo | The bigger the bulb, the braver I feel. This new one should make me fearless.\n8 | Priya | Then we fit it at noon, when it is brightest, and you stand right next to it.\n9 | Mo | Noon, with every curtain open and the night-lights on, just in case."
}
Reply:
{
"task": "notes",
"verdict": "found_the_game",
"verdict_reason": "No line is blocked, an unusual fear of the dark appears at line 3, and the game returns 6 times after that, so found the game.",
"base_reality": {
"who": "Mo is the lighthouse keeper; Priya helps fit the new bulb.",
"where": "At the lighthouse, climbing the stairs with the new bulb.",
"what": "Fitting the lighthouse's new, brightest bulb before dark, then at noon."
},
"first_unusual": {
"line": 3,
"what": "A lighthouse keeper who is scared of the dark."
},
"game": "If Mo fears the dark, then every corner of Mo's life needs backup light, even at the brightest moment.",
"lines": [
{
"n": 1,
"offer": "A new, biggest bulb has arrived for the lighthouse.",
"fate": "built",
"by": 2
},
{
"n": 2,
"offer": "Proposes carrying the bulb up the stairs before dark.",
"fate": "built",
"by": 3
},
{
"n": 3,
"offer": "Mo reveals being scared of the dark, sleeping with every light on.",
"fate": "built",
"by": 4
},
{
"n": 4,
"offer": "Points out the irony of a dark-fearing lighthouse keeper's night-lights.",
"fate": "built",
"by": 5
},
{
"n": 5,
"offer": "Specifies nine night-lights in the hall, one hidden in the pillow.",
"fate": "built",
"by": 6
},
{
"n": 6,
"offer": "Highlights the irony of choosing the coast's biggest light job.",
"fate": "built",
"by": 7
},
{
"n": 7,
"offer": "Claims bigger bulbs make Mo braver; expects to feel fearless.",
"fate": "built",
"by": 8
},
{
"n": 8,
"offer": "Proposes fitting the bulb at noon, right next to Mo.",
"fate": "built",
"by": 9
},
{
"n": 9,
"offer": "Insists on open curtains and night-lights even at noon, just in case.",
"fate": "none",
"by": 0
}
],
"game_moves": [
4,
5,
6,
7,
8,
9
],
"listening": {
"line": 3,
"underneath": "Underneath the joke, Mo admits real fear, saying \"I'm scared of the dark,\" wanting it taken seriously.",
"reply_line": 4,
"picked_up": false
},
"notes": [
{
"line": 9,
"note": "Line 9 works because Mo still needs every light on even at noon, proving the fear outlasts the brightest bulb."
},
{
"line": 4,
"note": "Priya's line 4 instantly turns Mo's fear into the lighthouse's funniest detail, building fast."
},
{
"line": 3,
"note": "Line 4 answers the words but skips the real fear under \"I'm scared of the dark,\" worth a beat of real care."
}
],
"next_beats": [
{
"speaker": "Priya",
"line": "Then I will keep the new bulb shining all night, right by your pillow."
},
{
"speaker": "Mo",
"line": "Then I will fit a second new bulb in the hall too, just in case."
},
{
"speaker": "Priya",
"line": "Then the whole coast will run on our brightest bulbs, day and night."
}
],
"closing": "A strong game already alive in these words; next time, let it grow even bigger."
}
The leaning cake - blocking
Input:
{
"task": "notes",
"suggestion": "wedding cake",
"page_facts": "players | 2 | Dee; Rob\nlines | 10\nquestions | Dee 0; Rob 1 (line 10)\ndenial_openers | 2, 4, 6\nverdict_rule | blocking when any line's offer is blocked; else no_game when no unusual thing is found; else game_dropped when the game is played again fewer than 2 times; else found_the_game",
"scene": "1 | Dee | Rob, the wedding starts in an hour and the cake is leaning to the left.\n2 | Rob | No, it's not leaning. That's just how the table is.\n3 | Dee | The table is level. I checked it with my phone. Help me hold up the top tier.\n4 | Rob | We don't have a top tier. It's a one-tier cake.\n5 | Dee | Fine. Help me hold up the one tier.\n6 | Rob | I can't, I'm the groom. I'm not allowed to see the cake before the wedding.\n7 | Dee | Then close your eyes and push where I tell you.\n8 | Rob | Okay, eyes closed. Tell me where.\n9 | Dee | Left hand on the bottom. Gently. It's straightening.\n10 | Rob | It feels warm. Is the cake supposed to feel warm?"
}
Reply:
{
"task": "notes",
"verdict": "blocking",
"verdict_reason": "4 lines are blocked: 1, 2, 3 and 5, so the scene is blocking.",
"base_reality": {
"who": "Dee is preparing the cake; Rob is the groom.",
"where": "Near the wedding cake, shortly before the wedding begins.",
"what": "Fixing the cake together before the wedding begins."
},
"first_unusual": {
"line": 6,
"what": "Rob is not allowed to see the cake before the wedding."
},
"game": "If Rob cannot see the cake, every task with it must happen by touch.",
"lines": [
{
"n": 1,
"offer": "Says the wedding starts in an hour and the cake leans left.",
"fate": "blocked",
"by": 2
},
{
"n": 2,
"offer": "Denies the cake is leaning and blames the table instead.",
"fate": "blocked",
"by": 3
},
{
"n": 3,
"offer": "Says the table is level; asks for help with the top tier.",
"fate": "blocked",
"by": 4
},
{
"n": 4,
"offer": "Denies a top tier exists and says the cake is one-tier.",
"fate": "accepted",
"by": 5
},
{
"n": 5,
"offer": "Concedes and asks Rob to help hold up the one-tier cake.",
"fate": "blocked",
"by": 6
},
{
"n": 6,
"offer": "Refuses to help; Rob is the groom who cannot see the cake.",
"fate": "built",
"by": 7
},
{
"n": 7,
"offer": "Tells Rob to close eyes and push as guided.",
"fate": "accepted",
"by": 8
},
{
"n": 8,
"offer": "",
"fate": "none",
"by": 0
},
{
"n": 9,
"offer": "Directs where to push gently and says the cake is straightening.",
"fate": "ignored",
"by": 0
},
{
"n": 10,
"offer": "Reports the cake feels warm and asks if that is normal.",
"fate": "none",
"by": 0
}
],
"game_moves": [
7,
8,
10
],
"listening": {
"line": 1,
"underneath": "Dee is anxious about time and wants reassurance or help, not an argument about \"leaning\".",
"reply_line": 2,
"picked_up": false
},
"notes": [
{
"line": 2,
"note": "Line 2 denies the cake is leaning; saying \"yes, and let's fix it together\" would accept Dee's offer instead."
},
{
"line": 7,
"note": "Dee turns the \"close your eyes\" idea into a game, finally building on an offer instead of blocking it."
},
{
"line": 6,
"note": "Line 6 blocks Dee's request with \"I can't\"; accepting by guiding Rob's hands would keep the help offer alive."
}
],
"next_beats": [
{
"speaker": "Rob",
"line": "You're right, it's leaning! Let's shift the tier so it balances evenly."
},
{
"speaker": "Dee",
"line": "Good, now let's steady the bottom so the whole cake holds."
},
{
"speaker": "Rob",
"line": "There, it's balanced now - we saved the cake together."
}
],
"closing": "Next time, try accepting the first offer before building anything new."
}
The bus stop - no game yet
Input:
{
"task": "notes",
"suggestion": "bus stop",
"page_facts": "players | 2 | Ana; Jules\nlines | 11\nquestions | Ana 5 (lines 1, 3, 5, 7, 9); Jules 0\ndenial_openers | none\nverdict_rule | blocking when any line's offer is blocked; else no_game when no unusual thing is found; else game_dropped when the game is played again fewer than 2 times; else found_the_game",
"scene": "1 | Ana | Is this the stop for the number 12?\n2 | Jules | Yes, the 12 stops here. It should come in about five minutes.\n3 | Ana | Have you been waiting long?\n4 | Jules | About ten minutes. I am going to the library.\n5 | Ana | Oh, nice. What are you reading at the moment?\n6 | Jules | A book about gardens. I have a small garden at home.\n7 | Ana | Do you grow vegetables?\n8 | Jules | Some tomatoes and a few beans.\n9 | Ana | That sounds nice. Is it hard to grow tomatoes?\n10 | Jules | They just need sun and water, and a little patience.\n11 | Ana | Here comes the bus."
}
Reply:
{
"task": "notes",
"verdict": "no_game",
"verdict_reason": "Nothing in the scene breaks from ordinary reality, so it is no game yet; the players simply trade real facts about buses, libraries and tomatoes.",
"base_reality": {
"who": "Ana and Jules, two people waiting for the bus together.",
"where": "At a bus stop, waiting for a bus to arrive.",
"what": "Making conversation about waiting, a library book and growing tomatoes."
},
"first_unusual": {
"line": 0,
"what": ""
},
"game": "",
"lines": [
{
"n": 1,
"offer": "Asks whether this is the right stop for a particular bus.",
"fate": "built",
"by": 2
},
{
"n": 2,
"offer": "Confirms the stop and says the bus is coming soon.",
"fate": "accepted",
"by": 11
},
{
"n": 3,
"offer": "Asks how long Jules has already been waiting.",
"fate": "built",
"by": 4
},
{
"n": 4,
"offer": "Reveals that Jules is on the way to the library.",
"fate": "built",
"by": 5
},
{
"n": 5,
"offer": "Asks what Jules is reading at the moment.",
"fate": "built",
"by": 6
},
{
"n": 6,
"offer": "Reveals the book is about gardens and Jules has one at home.",
"fate": "built",
"by": 7
},
{
"n": 7,
"offer": "Asks whether Jules grows vegetables in that garden.",
"fate": "built",
"by": 8
},
{
"n": 8,
"offer": "Reveals Jules grows tomatoes and beans.",
"fate": "built",
"by": 9
},
{
"n": 9,
"offer": "Asks whether tomatoes are hard to grow.",
"fate": "built",
"by": 10
},
{
"n": 10,
"offer": "Explains tomatoes need sun, water and patience.",
"fate": "ignored",
"by": 0
},
{
"n": 11,
"offer": "Announces the bus is arriving, ending their talk.",
"fate": "none",
"by": 0
}
],
"game_moves": [],
"listening": {
"line": 9,
"underneath": "Ana's \"that sounds nice\" shows real interest, not just a plain yes or no question.",
"reply_line": 10,
"picked_up": true
},
"notes": [
{
"line": 6,
"note": "Line 6's book about gardens could have turned odd, but the scene kept things ordinary."
},
{
"line": 9,
"note": "Ana asks question after question; a statement here would add a new fact instead of just asking."
},
{
"line": 8,
"note": "Jules keeps adding real detail, like the tomatoes and beans, instead of answering briefly."
}
],
"next_beats": [
{
"speaker": "Jules",
"line": "My tomatoes only grow this well because I read them a few pages every night."
},
{
"speaker": "Ana",
"line": "I'm not surprised the whole bus seems to listen whenever you read out loud."
},
{
"speaker": "Jules",
"line": "Maybe that is why the bus always seems to know when I bring a garden book."
}
],
"closing": "Next time, let one small unusual detail slip in early and see where the game could go."
}
The museum guide - game dropped
Input:
{
"task": "notes",
"suggestion": "museum",
"page_facts": "players | 3 | Kit; Lou; Bea\nlines | 11\nquestions | Kit 0; Lou 1 (line 6); Bea 1 (line 8)\ndenial_openers | none\nverdict_rule | blocking when any line's offer is blocked; else no_game when no unusual thing is found; else game_dropped when the game is played again fewer than 2 times; else found_the_game",
"scene": "1 | Kit | Welcome to the museum. I'm your guide for the tour this morning.\n2 | Lou | Thanks. My sister and I have come all the way across town for this.\n3 | Kit | Wonderful. Before we start, I need to ask the paintings if they're ready for visitors.\n4 | Bea | Of course. We can wait right here by the entrance.\n5 | Kit | First stop is the main hall. It was built a long time ago.\n6 | Lou | The floor is beautiful. Is it marble?\n7 | Kit | Yes, all marble. Please walk slowly, it gets slippery.\n8 | Bea | We'll walk slowly. Where is the gift shop?\n9 | Kit | Down the stairs on the left, next to the cafe.\n10 | Lou | Let's look at the gift shop after the tour, Bea.\n11 | Bea | Good idea. And then lunch in the cafe."
}
Reply:
{
"task": "notes",
"verdict": "game_dropped",
"verdict_reason": "Game dropped: the odd paintings idea surfaced once at line 3 and came back 0 times after that.",
"base_reality": {
"who": "Kit is the museum guide; Lou and Bea are visitors",
"where": "A museum, including the entrance, main hall, gift shop and cafe",
"what": "Kit guides Lou and Bea on a museum tour"
},
"first_unusual": {
"line": 3,
"what": "Kit says paintings must be asked if they are ready"
},
"game": "If paintings have moods, Kit must constantly check and report what they want",
"lines": [
{
"n": 1,
"offer": "Welcomes everyone to the museum and introduces the morning tour",
"fate": "accepted",
"by": 2
},
{
"n": 2,
"offer": "Lou and a sister traveled across town for the tour",
"fate": "ignored",
"by": 0
},
{
"n": 3,
"offer": "Paintings must be asked if they are ready for visitors",
"fate": "accepted",
"by": 4
},
{
"n": 4,
"offer": "Offers to wait by the entrance while Kit checks",
"fate": "ignored",
"by": 0
},
{
"n": 5,
"offer": "Main hall is the first stop and very old",
"fate": "built",
"by": 6
},
{
"n": 6,
"offer": "Floor is beautiful; asks whether it is marble",
"fate": "built",
"by": 7
},
{
"n": 7,
"offer": "Confirms marble floor and warns it gets slippery",
"fate": "accepted",
"by": 8
},
{
"n": 8,
"offer": "Agrees to walk slowly and asks about the gift shop",
"fate": "built",
"by": 9
},
{
"n": 9,
"offer": "Gift shop is downstairs next to the cafe",
"fate": "built",
"by": 10
},
{
"n": 10,
"offer": "Proposes visiting the gift shop after the tour",
"fate": "built",
"by": 11
},
{
"n": 11,
"offer": "Agrees, and proposes lunch in the cafe afterward",
"fate": "none",
"by": 0
}
],
"game_moves": [],
"listening": {
"line": 2,
"underneath": "Wants the trip to feel worthwhile after coming \"all the way across town\" for this",
"reply_line": 3,
"picked_up": false
},
"notes": [
{
"line": 4,
"note": "This is where the paintings idea could have continued, but the scene moved on to waiting instead."
},
{
"line": 9,
"note": "Good build: answering exactly where the gift shop is, adding the cafe right next to it."
},
{
"line": 2,
"note": "The sister and the trip across town are never mentioned again after this line."
}
],
"next_beats": [
{
"speaker": "Kit",
"line": "I just heard the painting in the main hall say it's not ready yet."
},
{
"speaker": "Lou",
"line": "Then let's ask the marble floor if it agrees with the painting."
},
{
"speaker": "Bea",
"line": "Now the cafe says it wants to serve lunch to the paintings too."
}
],
"closing": "A charming odd idea appeared once - a next scene could let it return again and again."
}
Build the input yourself
Load the engine in node (global.window = global; require("./scenekit.js")), then:
const K = SceneKit;
const built = K.buildInput({ suggestion: "lighthouse", scene: "Mo: The new bulb came today.\nPriya: Great. Let's get it upstairs.\nMo: I'm scared of the dark.\nPriya: A keeper scared of the dark!" });
built.scene.players; // ["Mo", "Priya"]
built.prescan.denial; // line numbers that open like a denial
const wire = K.toWire(built.input); // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// K.verdictFrom(reply) // the verdict the reply's own fields give
// K.check(reply, built).notes // the page's re-check of the reply
// K.md(built, reply) // the notes as Markdown
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://yes-and.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://yes-and.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"]
// yes-and.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://yes-and.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://yes-and.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
}
// YesAnd.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 YesAnd {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://yes-and.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://yes-and.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://yes-and.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://yes-and.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":"yes-and"}' | 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": "yes-and"})["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: "yes-and" })).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": "yes-and"}, 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", "yes-and"), 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: "yes-and" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "yes-and"])["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 = "yes-and" }))!["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": "notes", "suggestion": "lighthouse", "page_facts": "players | 2 | Mo; Priya\n...", "scene": "1 | Mo | ..."} - see the field table
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": "notes", "suggestion": "lighthouse", "page_facts": "players | 2 | Mo; Priya\n...", "scene": "1 | Mo | ..."} - see the field table
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: "notes", suggestion, page_facts, scene} (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 yes-and:<task>:<inputHash>:a<attempt> (the one task is notes), 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="yes-and:notes:$(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 = "yes-and:" + 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 = `yes-and:${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 := "yes-and:" + 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 = "yes-and:" + 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 = "yes-and:#{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 = "yes-and:" . $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 = $"yes-and:{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: yes-and:notes:$(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": "notes", "suggestion": "lighthouse", "page_facts": "players | 2 | Mo; Priya\n...", "scene": "1 | Mo | ..."} - see the field table
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "yes-and:notes:" + 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": `yes-and:notes:${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", "yes-and:notes:"+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", "yes-and:notes:" + 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" => "yes-and:notes:#{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: yes-and:notes:" . 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", $"yes-and:notes:{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);