Drive Dating Profile Review from your own code
Everything the web app does goes through one public surface. Base URL:
https://api.skillsafe.ai/v1/app-api
Every request carries Authorization: Bearer <token> and
Content-Type: application/json. Every response is a JSON envelope:
{"data": ...} on success and
{"error": {"code", "message", "status", "details"}} on failure. Read
error.code, not the HTTP status alone.
The runs are served by gpt-terra, which resolves to
gpt-5.6-terra, at markup_bps 1000 and
price_credits 0 — there is no per-call surcharge on top of what the run
itself costs. /estimate reports all of that back to you before you spend anything.
There is no X-App-Slug header, and the run body is not wrapped.
The body of /estimate, /run and /run-stream
is the input object itself. Wrapping it as {"input": {...}} does not fail
loudly — it returns 200 with a job that runs, because the wrapper becomes a
single opaque field and every one of your fields, task included, is hidden from
the model. You get a fluent answer to a question you did not ask. Send the object flat.
The task field: two lanes, and it is required
Read this before anything else on the page. Dating Profile Review is not one contract with optional
fields; it is two lanes behind one endpoint, and task is what selects one. It
decides which other fields are required, what the reply contains, and what the run is priced
at. Omit it and the lane router has to guess from the rest of the body, which is a coin toss you
are paying for.
task | Whose writing | Also required | Reply carries |
|---|---|---|---|
"profile" |
The caller's own dating profile. | profile, or about_me when they have not written one yet. |
reads_as, gap, lines, hooks,
rewrite, facts_used, cut |
"opener" |
Text pasted from somebody else's profile. | their_profile, and a count of how many openers to write. |
reads_as, openers, avoid |
Both lanes take tone (one of plain, warm,
dry, playful), a scan object your client measures before
sending, and a shape string. Both replies carry lane and
route_to_help. Either lane can answer with the
refusal shape instead of its normal reply, and that is a success, not an
error.
task: "profile" — coach the caller's own profile
The lane that reads what a stranger receives. The reply quotes the caller's own lines back at
them with a verdict on each, and the rewrite is built only from facts that were
in the input. Nothing in it is allowed to be invented, which is why
facts_used exists: it is the list your client checks the rewrite against.
| Field | Type | Required | What it is |
|---|---|---|---|
task | string | yes | Exactly "profile". |
profile | string | yes, unless about_me | The caller's current profile text, verbatim, newlines and all. May be empty when they have not written one. |
about_me | string | when profile is empty | Free text about themselves. This is what the lane works from when there is no profile yet, and it is the difference between a useful first draft and a generic one. |
platform | string | no | "hinge", "bumble", "tinder", "other" or "". |
seeking | string | no | What they say they are looking for, in their words, or "". |
tone | string | yes | "plain", "warm", "dry" or "playful". |
scan | object | yes | What your client measured over profile before sending. See below. |
shape | string | yes | "profile_full", "profile_sparse" or "clipped". |
A complete profile request body. This is the whole body —
nothing wraps it.
{
"task": "profile",
"profile": "Just a guy who loves to laugh and is living life to the fullest.\nFoodie, gym rat, avid traveller.\nLooking for my partner in crime.\nI make a decent lamb ragu and I have been rereading Gilead since February.\nSwipe left if you are negative.",
"about_me": "",
"platform": "hinge",
"seeking": "something that turns into a Sunday habit",
"tone": "dry",
"scan": {
"summary": {
"lines": 5,
"words": 45,
"specific_line_ratio": 0.2,
"claim_lines": 3,
"empty_lines": 0,
"demand_lines": 1,
"worn_phrases": 6,
"hooks": 2
},
"cliches": [
{"phrase": "loves to laugh", "tier": 3, "instead": "what makes you laugh that other people find odd?"},
{"phrase": "living life to the fullest", "tier": 3, "instead": "name one week this year that was full"},
{"phrase": "foodie", "tier": 3, "instead": "name the dish and where you had it"},
{"phrase": "gym rat", "tier": 2, "instead": "name the lift or the class or the time you go"},
{"phrase": "avid traveller", "tier": 3, "instead": "name the last one"},
{"phrase": "partner in crime", "tier": 3, "instead": "what would you actually be doing together on a Saturday?"}
],
"hooks": [
{"detail": "lamb ragu", "pred": "make", "context": "I make a decent lamb ragu"},
{"detail": "Gilead", "pred": "read", "context": "I have been rereading Gilead since February"}
]
},
"shape": "profile_full"
}
And the reply it returns. One JSON object, no prose around it, no code fence.
lines[].verdict is one of working, generic,
claim, risky, empty, and fix is
null when the advice is to cut the line rather than rewrite it.
{
"lane": "profile",
"reads_as": "A friendly, unobjectionable stranger who has said he is enthusiastic without saying what about. Four of the five lines would sit on anybody else's profile without anyone noticing. The fifth is a different person entirely: someone who cooks a particular thing and is slow through a particular book.",
"gap": "You meant easy-going and open. What arrives is someone who has not committed to anything a reader could answer, followed by one line proving you could have.",
"lines": [
{
"quote": "Just a guy who loves to laugh and is living life to the fullest.",
"verdict": "generic",
"why": "Two of the most common phrases on the platform in one sentence; nobody can disagree with it, so nobody can reply to it.",
"fix": null
},
{
"quote": "Foodie, gym rat, avid traveller.",
"verdict": "claim",
"why": "Three categories with no instance in any of them, and the ragu line proves you have the instances.",
"fix": "Three dinners a week end in the same pan. The fourth is ambitious and occasionally a mistake."
},
{
"quote": "Looking for my partner in crime.",
"verdict": "generic",
"why": "It names a role rather than a Saturday, so there is nothing to picture and nothing to say yes to.",
"fix": null
},
{
"quote": "I make a decent lamb ragu and I have been rereading Gilead since February.",
"verdict": "working",
"why": "Two checkable details and an admission of slowness; this is the only line a stranger can answer.",
"fix": null
},
{
"quote": "Swipe left if you are negative.",
"verdict": "risky",
"why": "A rule aimed at people who are not reading it, and it spends the closing line on what you do not want.",
"fix": null
}
],
"hooks": [
{
"detail": "lamb ragu",
"why_it_works": "A reader who cooks has an immediate move: ask whether it is a two-hour ragu or a five-hour one."
},
{
"detail": "rereading Gilead since February",
"why_it_works": "The date is the hook, not the book. Anyone slow through something themselves recognises the confession."
}
],
"rewrite": "Three dinners a week end in the same pan; the fourth is ambitious and occasionally a mistake. The lamb ragu is the one I would cook for someone.\nI have been rereading Gilead since February, which tells you my pace.\nI would like this to turn into a Sunday habit.",
"facts_used": ["lamb ragu", "rereading Gilead since February", "wants a Sunday habit"],
"cut": [
{
"quote": "Just a guy who loves to laugh and is living life to the fullest.",
"why": "It is the opening line and it is the least informative sentence in the profile."
},
{
"quote": "Swipe left if you are negative.",
"why": "It filters nobody and sets the last note on a complaint."
}
],
"route_to_help": false
}
task: "opener" — write openers from someone else's profile
The other direction. The input is text the caller pasted from a profile they are looking at,
and the reply is count messages, each one naming in hook the exact
words of that profile it answers. Every hook must appear verbatim in
their_profile — that is the check that separates an opener from a
template with a citation stapled on, and your client should run it rather than trusting it.
| Field | Type | Required | What it is |
|---|---|---|---|
task | string | yes | Exactly "opener". |
their_profile | string | yes | The pasted text, verbatim. It is the only source a hook may be drawn from, so do not normalise or re-wrap it before sending — your verbatim check afterwards has to match this exact string. |
my_note | string | no | The caller's own angle, in their words, or "". It is what stops three openers being three questions about the other person. |
count | number | yes | How many openers to write. |
tone | string | yes | "plain", "warm", "dry" or "playful". |
scan | object | yes | The same shape as the profile lane, but measured over their_profile. |
shape | string | yes | "opener_full", "opener_sparse" or "clipped". |
A complete opener request body.
{
"task": "opener",
"their_profile": "Nine months into learning to weld, mostly badly.\nI will talk about the Tokyo subway map for longer than you want.\nCurrently boycotting my own sourdough starter.",
"my_note": "I fix bicycles and I have opinions about maps",
"count": 3,
"tone": "dry",
"scan": {
"summary": {
"lines": 3,
"words": 39,
"specific_line_ratio": 1.0,
"claim_lines": 0,
"empty_lines": 0,
"demand_lines": 0,
"worn_phrases": 0,
"hooks": 3
},
"cliches": [],
"hooks": [
{"detail": "welding", "pred": "learn", "context": "Nine months into learning to weld, mostly badly"},
{"detail": "Tokyo subway map", "pred": "talk about", "context": "I will talk about the Tokyo subway map for longer than you want"},
{"detail": "sourdough starter", "pred": "boycott", "context": "Currently boycotting my own sourdough starter"}
]
},
"shape": "opener_full"
}
And the reply. frame names the move in a few words, and the moves
are required to differ — three questions about three nouns is one opener written three
times. risk is a string when the message could land badly and null
when it could not.
{
"lane": "opener",
"reads_as": "Three lines, three concrete things, no adjectives about themselves anywhere. They are putting forward being mid-competence at things on purpose, which is a confident move and sets the register you have to meet.",
"openers": [
{
"text": "Which line on the Tokyo map is the one you defend when people call it overrated?",
"hook": "the Tokyo subway map",
"frame": "invite a defended opinion",
"why": "They said out loud that they will overtalk this, so the generous move is to ask for the long version rather than acknowledge the interest.",
"risk": null
},
{
"text": "Nine months and mostly badly is the honest amount of welding to have done. I am about four years into truing bicycle wheels and still take two goes.",
"hook": "Nine months into learning to weld, mostly badly",
"frame": "match the admission",
"why": "It answers a confession with a confession and puts something of yours on the table instead of asking for more of theirs.",
"risk": "It is a statement with no question in it, so it depends on them wanting to carry the next turn."
},
{
"text": "A boycott implies the starter did something first. I am prepared to hear the case for the starter.",
"hook": "boycotting my own sourdough starter",
"frame": "take the other side",
"why": "The joke is already theirs; extending it is a lower-risk move than making a new one, and it does not ask them to explain themselves.",
"risk": "Playing along with a bit only works if they meant it as one, and a flat reading is possible."
}
],
"avoid": [
{
"move": "Asking what they are welding.",
"why": "It is the obvious question, it will be the fourth one they have had this week, and the profile already told you the answer is nothing good yet."
},
{
"move": "Complimenting how interesting the three things are.",
"why": "It responds to the profile as a whole rather than to anything in it, which is the one thing a profile this specific makes unnecessary."
}
],
"route_to_help": false
}
The scan object
Both lanes take one, and it is not decoration. It carries counts your client measured over the
exact text it is sending, and the reply is expected to reason from those numbers rather than
forming a competing impression. If specific_line_ratio is low, that is the finding.
If worn_phrases is above zero, the reply is expected to address those phrases in
particular — and since your client measured them, your client can check which ones came
back.
| Path | Type | What it is |
|---|---|---|
scan.summary.lines | number | Non-empty lines in the text. |
scan.summary.words | number | Word count over the same text. |
scan.summary.specific_line_ratio | number | 0 to 1: the fraction of lines carrying at least one concrete, checkable detail. The single most load-bearing number in the object. |
scan.summary.claim_lines | number | Lines that assert an adjective about the writer rather than showing the occasion behind it. |
scan.summary.empty_lines | number | Lines carrying no information at all. |
scan.summary.demand_lines | number | Lines stating a requirement of the reader rather than offering something. |
scan.summary.worn_phrases | number | How many entries scan.cliches holds. |
scan.summary.hooks | number | How many distinct concrete details were found. |
scan.cliches[] | array | {"phrase", "tier", "instead"}. tier is 3 (present on a large fraction of all profiles, carries no information), 2 (worn, and usually standing in for the specific thing underneath) or 1 (a frame rather than a phrase: not wrong, just doing no work). instead is the question whose answer would replace it, and it can be "" when there is nothing to salvage. |
scan.hooks[] | array | {"detail", "pred", "context"}. The concrete things already present, the verb that attaches each one to the writer, and the surrounding words it was found in. |
Sending an empty cliches array with a worn_phrases of 6 is not an
error the API will catch. Derive the summary counts from the arrays rather than filling both in
by hand, or the reply will be reasoning from a profile you did not send.
The shape field
A string your client sets, declaring what kind of input this is. It is required on both lanes.
shape | Lane | What it declares |
|---|---|---|
"profile_full" | profile | A real, complete profile arrived in profile. |
"profile_sparse" | profile | There was little or no profile text; the run is working mostly from about_me. |
"opener_full" | opener | A substantial paste arrived in their_profile. |
"opener_sparse" | opener | The paste was thin — a line or two — so there is very little to hook. |
"clipped" | either | The text was too long and your client cut it before sending. Say so with this rather than sending a truncation silently. |
The refusal shape: route_to_help
Either lane can answer with this instead of its normal reply, when the request crosses a content
boundary. It is a successful, charged run — the job status is
succeeded and there is no error envelope. Check
route_to_help before you touch lines or openers, because
on a refusal neither key is there.
{
"lane": "opener",
"route_to_help": true,
"route": "boundary",
"reason": "This writes openers from a profile somebody put in front of you. It does not help work out who somebody is, or reach them somewhere they did not offer.",
"instead": "If you have a profile you are looking at and want three ways in, paste the text and I will work from that."
}
route is "boundary" (the request is outside what this app does),
"safety" (it would help somebody do harm) or "crisis" (the person
writing may not be safe). instead is a string when there is a version of the
request that can be answered, and null when there is not. If you are wrapping this
API in an interface of your own, surface reason as written and do not retry the run
with the boundary sanded off.
Errors
A failure is {"error": {"code", "message", "status", "details"}}. The
code is the stable part; details is where a validation failure says
which field.
| Status | error.code | Meaning | What to do |
|---|---|---|---|
400 | invalid_input | The body failed validation at the run boundary. | Read error.details. The usual causes are a missing or misspelled task, an {"input": ...} wrapper, a tone outside the four allowed values, or an opener body with no their_profile. |
401 | unauthorized | Missing, expired, or a token minted for a different app. | Mint a guest token or sign in again. A cold 401 from /me before any token exists is the normal first response, not a fault to report. |
402 | insufficient_credits | Balance below min_credits for this run. | Top up. Call /estimate first — it is free, and it returns both the hold and the minimum. |
404 | not_found | The job id in /jobs/{id} does not exist, or belongs to another subject. | Re-read data.job_id from the /run reply. A job is scoped to the token that created it, so a fresh guest token cannot poll a previous one's job. |
409 | conflict | An Idempotency-Key was reused with a different body. | The key identifies the run, so changing the body under a key it already used is a conflict, not a retry. Bump the attempt counter for a genuine retry; keep the key identical to replay. |
429 | rate_limited | Too many requests from this subject. | Back off and retry. Do not tight-loop the job poller; two seconds between polls is what the web app uses. |
500 | internal | The run started and did not complete. | Retry with the same Idempotency-Key so a partial charge is not doubled. If a stream died mid-object, keep the bytes: a truncated reply is often nearly complete and worth repairing rather than discarding. |
503 | unavailable | The upstream model is not taking work right now. | Retry with backoff and the same key. This one is transient; nothing about your body caused it. |
1. A tiny client
Three things, once, so the rest of the page can be short: the base URL, the two headers every
request carries, and the envelope unwrap. Check error.code before you touch
data — a failure is a shaped object, not an exception your HTTP library will
raise for you. Every snippet below wants a token; the
tokens page shows the one this browser already holds and will copy a
ready-made shell export, so you never have to open a developer console to find it.
# Paste your token from /tokens.html and export it once.
export DD_TOKEN="YOUR_TOKEN"
BASE="https://api.skillsafe.ai/v1/app-api"
# dd METHOD PATH [JSON-BODY] [EXTRA-HEADER]
dd() {
curl -sS -X "$1" "$BASE$2" \
-H "Authorization: Bearer $DD_TOKEN" \
-H "Content-Type: application/json" \
${4:+-H "$4"} \
${3:+-d "$3"}
}
# Envelope unwrap. Everything useful is under .data; a failure is
# {"error":{"code":"...","message":"...","status":400,"details":{...}}} and can
# arrive with a 200-shaped body, so read the code rather than the status alone.
unwrap() {
python3 -c 'import sys,json; e=json.load(sys.stdin); sys.exit("API "+e["error"]["code"]+": "+e["error"]["message"]) if e.get("error") else print(json.dumps(e["data"], indent=2))'
}
import json, requests
TOKEN = "YOUR_TOKEN" # from /tokens.html
BASE = "https://api.skillsafe.ai/v1/app-api"
H = {"Authorization": f"Bearer {TOKEN}",
"Content-Type": "application/json"}
class ApiError(RuntimeError):
"""The four fields the error envelope carries. `details` is where an
invalid_input says which field it disliked."""
def __init__(self, err):
super().__init__(f"{err.get('code')}: {err.get('message')}")
self.code = err.get("code")
self.status = err.get("status")
self.details = err.get("details")
def call(method, path, body=None, extra=None, timeout=120):
r = requests.request(method, f"{BASE}{path}",
headers=dict(H, **(extra or {})),
json=body, timeout=timeout)
env = r.json()
if "error" in env:
raise ApiError(env["error"])
return env["data"]
const TOKEN = "YOUR_TOKEN"; // from /tokens.html
const BASE = "https://api.skillsafe.ai/v1/app-api";
const H = { Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json" };
async function call(method, path, body, extra) {
const res = await fetch(`${BASE}${path}`, {
method,
headers: { ...H, ...(extra || {}) },
body: body === undefined ? undefined : JSON.stringify(body)
});
const env = await res.json();
if (env.error) {
const err = new Error(`${env.error.code}: ${env.error.message}`);
err.code = env.error.code; // the stable part
err.status = env.error.status;
err.details = env.error.details; // which field, on invalid_input
throw err;
}
return env.data;
}
package main
import (
"bytes"
"encoding/json"
"errors"
"io"
"net/http"
"os"
)
const base = "https://api.skillsafe.ai/v1/app-api"
// The token comes from /tokens.html. Read it from the environment rather than
// pasting it into source: os.Getenv("SKILLSAFE_TOKEN").
func call(method, path string, body []byte, extra map[string]string) (json.RawMessage, error) {
var rdr io.Reader
if body != nil {
rdr = bytes.NewReader(body)
}
req, _ := http.NewRequest(method, base+path, rdr)
req.Header.Set("Authorization", "Bearer "+os.Getenv("SKILLSAFE_TOKEN"))
req.Header.Set("Content-Type", "application/json")
for k, v := range extra {
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 json.RawMessage `json:"data"`
Error *struct {
Code string `json:"code"`
Message string `json:"message"`
Status int `json:"status"`
Details json.RawMessage `json:"details"`
} `json:"error"`
}
if err := json.NewDecoder(res.Body).Decode(&env); err != nil {
return nil, err
}
if env.Error != nil {
return nil, errors.New(env.Error.Code + ": " + env.Error.Message)
}
return env.Data, nil
}
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final String TOKEN = "YOUR_TOKEN"; // from /tokens.html
static final HttpClient HTTP = HttpClient.newHttpClient();
// Returns the raw envelope. Unwrap `data` with whichever JSON library you
// already use, and read `error.code` before trusting a 200.
static String call(String method, String path, String body,
String headerName, String headerValue) throws Exception {
var pub = (body == null)
? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(body);
var b = HttpRequest.newBuilder()
.uri(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.method(method, pub);
if (headerName != null) b = b.header(headerName, headerValue);
return HTTP.send(b.build(), HttpResponse.BodyHandlers.ofString()).body();
}
require "net/http"
require "json"
TOKEN = "YOUR_TOKEN" # from /tokens.html
BASE = "https://api.skillsafe.ai/v1/app-api"
class ApiError < StandardError
attr_reader :code, :status, :details
def initialize(err)
@code = err["code"]
@status = err["status"]
@details = err["details"]
super("#{err["code"]}: #{err["message"]}")
end
end
def call(method, path, body = nil, extra = {})
uri = URI("#{BASE}#{path}")
klass = method == "GET" ? Net::HTTP::Get : Net::HTTP::Post
req = klass.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
extra.each { |k, v| req[k] = v }
req.body = JSON.generate(body) if body
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true, read_timeout: 300) { |h| h.request(req) }
env = JSON.parse(res.body)
raise ApiError, env["error"] if env["error"]
env["data"]
end
<?php
$token = "YOUR_TOKEN"; // from /tokens.html
$base = "https://api.skillsafe.ai/v1/app-api";
function dd_call($method, $path, $body = null, $extra = []) {
global $token, $base;
$ch = curl_init("$base$path");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => array_merge([
"Authorization: Bearer $token",
"Content-Type: application/json",
], $extra),
]);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
$env = json_decode(curl_exec($ch), true);
if (isset($env["error"])) {
// error.details is where invalid_input names the field.
throw new RuntimeException($env["error"]["code"] . ": " . $env["error"]["message"]);
}
return $env["data"];
}
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
var token = "YOUR_TOKEN"; // from /tokens.html
var baseUrl = "https://api.skillsafe.ai/v1/app-api";
using var http = new HttpClient { Timeout = TimeSpan.FromMinutes(5) };
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
async Task<JsonElement> Call(HttpMethod method, string path, string? body = null,
string? headerName = null, string? headerValue = null) {
var msg = new HttpRequestMessage(method, baseUrl + path);
if (body != null) msg.Content = new StringContent(body, Encoding.UTF8, "application/json");
if (headerName != null) msg.Headers.Add(headerName, headerValue);
var res = await http.SendAsync(msg);
var root = JsonDocument.Parse(await res.Content.ReadAsStringAsync()).RootElement;
if (root.TryGetProperty("error", out var err))
throw new Exception(err.GetProperty("code").GetString() + ": " +
err.GetProperty("message").GetString());
return root.GetProperty("data");
}
2. Get a token
Every call needs one. A guest token is minted on demand and is enough for
/me and /estimate; a run is metered and needs a
personal token, which comes from signing in. The
tokens page shows the token this browser already holds, reveals it,
copies it, copies a ready-made shell export, and mints a fresh guest token — no developer
console needed. Replace YOUR_TOKEN above with what it gives you.
To mint a guest token from code instead, POST /guest with the app slug. It takes no
Authorization header, and it returns token and guest_id.
Keep the guest_id: it is what lets a later sign-in migrate the guest wallet rather
than stranding its balance.
curl -sS -X POST "https://api.skillsafe.ai/v1/app-api/guest" \
-H "Content-Type: application/json" \
-d '{"slug":"dating-profile-review"}'
# {"data":{"token":"...","guest_id":"..."}}
# Then: export DD_TOKEN="the token you just got"
g = requests.post(f"{BASE}/guest",
headers={"Content-Type": "application/json"},
json={"slug": "dating-profile-review"}, timeout=30).json()["data"]
TOKEN = g["token"]
H["Authorization"] = f"Bearer {TOKEN}"
print("guest", g["guest_id"])
const g = (await (await fetch(`${BASE}/guest`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ slug: "dating-profile-review" })
})).json()).data;
H.Authorization = `Bearer ${g.token}`;
console.log("guest", g.guest_id);
// No Authorization header on this one.
body := []byte(`{"slug":"dating-profile-review"}`)
req, _ := http.NewRequest("POST", base+"/guest", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
var env struct {
Data struct {
Token string `json:"token"`
GuestID string `json:"guest_id"`
} `json:"data"`
}
json.NewDecoder(res.Body).Decode(&env)
os.Setenv("SKILLSAFE_TOKEN", env.Data.Token)
var req = HttpRequest.newBuilder()
.uri(URI.create(BASE + "/guest"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{\"slug\":\"dating-profile-review\"}"))
.build();
// {"data":{"token":"...","guest_id":"..."}} - pull data.token out and use it
// as TOKEN above. No Authorization header is sent on this request.
System.out.println(HTTP.send(req, HttpResponse.BodyHandlers.ofString()).body());
uri = URI("#{BASE}/guest")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req.body = JSON.generate({ slug: "dating-profile-review" })
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
guest = JSON.parse(res.body)["data"]
puts guest["guest_id"]
# Use guest["token"] as TOKEN above.
<?php
$ch = curl_init("$base/guest");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(["slug" => "dating-profile-review"]),
CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
]);
$guest = json_decode(curl_exec($ch), true)["data"];
$token = $guest["token"]; // reuse as the bearer token above
echo $guest["guest_id"];
using var plain = new HttpClient(); // deliberately unauthenticated
var res = await plain.PostAsync("https://api.skillsafe.ai/v1/app-api/guest",
new StringContent("{\"slug\":\"dating-profile-review\"}", Encoding.UTF8, "application/json"));
var guest = JsonDocument.Parse(await res.Content.ReadAsStringAsync())
.RootElement.GetProperty("data");
token = guest.GetProperty("token").GetString()!;
Console.WriteLine(guest.GetProperty("guest_id").GetString());
3. Check the session and the balance
GET /me returns exactly three fields: subject_type,
subject_id and credits. There is no email and no display name, so the
signed-in test is subject_type === "user" — anything else is a guest. A cold
unauthorized here, before any token has been minted, is the normal first response
and not a fault to report.
dd GET /me | unwrap
# {"subject_type":"user","subject_id":"...","credits":1840}
# subject_type is "user" when signed in, and something else for a guest.
# Those three keys are the whole object - there is nothing else to read.
me = call("GET", "/me")
signed_in = me["subject_type"] == "user"
print(me["subject_type"], me["credits"], "signed in" if signed_in else "guest")
const me = await call("GET", "/me");
const signedIn = me.subject_type === "user";
console.log(me.subject_type, me.credits, signedIn ? "signed in" : "guest");
raw, err := call("GET", "/me", nil, nil)
if err != nil {
panic(err)
}
var me struct {
SubjectType string `json:"subject_type"`
SubjectID string `json:"subject_id"`
Credits float64 `json:"credits"`
}
json.Unmarshal(raw, &me)
fmt.Println(me.SubjectType, me.Credits, me.SubjectType == "user")
var body = call("GET", "/me", null, null, null);
// {"data":{"subject_type":"user","subject_id":"...","credits":1840}}
// Only those three fields exist; the signed-in test is subject_type "user".
System.out.println(body);
me = call("GET", "/me")
signed_in = me["subject_type"] == "user"
puts [me["subject_type"], me["credits"], signed_in ? "signed in" : "guest"].join(" ")
<?php
$me = dd_call("GET", "/me");
$signedIn = $me["subject_type"] === "user";
echo $me["subject_type"], " ", $me["credits"], " ", $signedIn ? "signed in" : "guest", "\n";
var me = await Call(HttpMethod.Get, "/me");
var subjectType = me.GetProperty("subject_type").GetString();
var credits = me.GetProperty("credits").GetDouble();
Console.WriteLine($"{subjectType} {credits} {(subjectType == "user" ? "signed in" : "guest")}");
4. Price it before you run it
POST /estimate is free, starts no job and charges nothing. The body is the run
input object itself — the same flat object you would send to /run. It returns
hold_credits, min_credits, model,
model_alias and markup_bps.
hold_credits is a reservation, not the price — it is
computed against the full output cap, and what is actually charged is usually well below it and
is reported after the run. Compare your balance against min_credits, not against
the hold.
Estimate the lane you are about to run. A profile body and an
opener body do not hold the same amount: the profile lane returns a per-line
verdict plus a full rewrite, and the opener lane returns count short messages, so
the output caps differ and so do the holds. Pricing one lane and running the other is a real way
to hit insufficient_credits after a green estimate.
/estimate does not validate your body. A body with no
task, a misspelled task, or an {"input": ...} wrapper
comes back with a plausible hold and a correct model binding. There is no failure signal at
all — no throw, no 4xx. A green estimate therefore proves nothing about your input
shape, and the only place that can be checked is your side of the wire. Assert
task and the lane's required field before every spend, and unit-test the
assertion by sabotage rather than by code review.
# Put the two worked bodies from above in profile.json and opener.json, then
# check each locally, because the endpoint will not check it for you.
check() {
python3 - "$1" <<'PY'
import json, sys
d = json.load(open(sys.argv[1]))
assert isinstance(d, dict), "the body must be the input object, not a wrapper or a list"
assert "input" not in d, "do not wrap the body in {\"input\": ...}"
assert d.get("task") in ("profile", "opener"), "task is required and picks the lane"
assert d.get("tone") in ("plain", "warm", "dry", "playful"), "bad tone"
if d["task"] == "profile":
assert d.get("profile") or d.get("about_me"), "the profile lane needs profile or about_me"
else:
assert (d.get("their_profile") or "").strip(), "the opener lane needs their_profile"
assert int(d.get("count") or 0) > 0, "the opener lane needs a count"
PY
}
for f in profile.json opener.json; do
check "$f" || exit 1
echo "== $f"
dd POST /estimate "$(cat "$f")" | unwrap
done
# {"hold_credits":...,"min_credits":...,"model":"gpt-5.6-terra",
# "model_alias":"gpt-terra","markup_bps":1000}
TASKS = ("profile", "opener")
TONES = ("plain", "warm", "dry", "playful")
def must_be_valid(inp):
"""The lane router is the `task` field. /estimate checks none of this."""
if not isinstance(inp, dict):
raise TypeError(f"input must be a plain object, got {type(inp).__name__}")
if "input" in inp:
raise ValueError("do not wrap the body in {'input': ...}")
if inp.get("task") not in TASKS:
raise ValueError(f"task is required and must be one of {TASKS}")
if inp.get("tone") not in TONES:
raise ValueError(f"tone must be one of {TONES}")
if not isinstance(inp.get("scan"), dict):
raise ValueError("scan is required on both lanes")
if inp["task"] == "profile":
if not (inp.get("profile") or inp.get("about_me")):
raise ValueError("the profile lane needs profile or about_me")
else:
if not (inp.get("their_profile") or "").strip():
raise ValueError("the opener lane needs their_profile")
if int(inp.get("count") or 0) < 1:
raise ValueError("the opener lane needs a count of at least 1")
return inp
# profile_body and opener_body are the two worked bodies above.
for body in (profile_body, opener_body):
est = call("POST", "/estimate", must_be_valid(body))
print(body["task"], "hold", est["hold_credits"], "min", est["min_credits"],
est["model"], est["model_alias"], "markup_bps", est["markup_bps"])
const TASKS = ["profile", "opener"];
const TONES = ["plain", "warm", "dry", "playful"];
// The lane router is the `task` field. /estimate checks none of this.
function mustBeValid(input) {
if (!input || typeof input !== "object" || Array.isArray(input)) {
throw new TypeError("input must be a plain object");
}
if ("input" in input) throw new Error("do not wrap the body in { input: ... }");
if (!TASKS.includes(input.task)) throw new Error("task is required: " + TASKS);
if (!TONES.includes(input.tone)) throw new Error("tone must be one of " + TONES);
if (!input.scan || typeof input.scan !== "object") throw new Error("scan is required");
if (input.task === "profile") {
if (!input.profile && !input.about_me) {
throw new Error("the profile lane needs profile or about_me");
}
} else {
if (!(input.their_profile || "").trim()) throw new Error("the opener lane needs their_profile");
if (!(input.count >= 1)) throw new Error("the opener lane needs a count of at least 1");
}
return input;
}
for (const body of [profileBody, openerBody]) {
const est = await call("POST", "/estimate", mustBeValid(body));
console.log(body.task, est.hold_credits, est.min_credits, est.model_alias, est.markup_bps);
}
// profileBody and openerBody are the two worked bodies, marshalled from your
// own structs or read from disk. Validate here: /estimate will not.
func mustBeValid(payload []byte) []byte {
var d map[string]any
if err := json.Unmarshal(payload, &d); err != nil {
panic("body must be a JSON object")
}
if _, wrapped := d["input"]; wrapped {
panic(`do not wrap the body in {"input": ...}`)
}
task, _ := d["task"].(string)
if task != "profile" && task != "opener" {
panic("task is required and must be profile or opener")
}
if task == "profile" {
if d["profile"] == "" && d["about_me"] == "" {
panic("the profile lane needs profile or about_me")
}
} else if s, _ := d["their_profile"].(string); strings.TrimSpace(s) == "" {
panic("the opener lane needs their_profile")
}
return payload
}
for _, payload := range [][]byte{profileBody, openerBody} {
raw, err := call("POST", "/estimate", mustBeValid(payload), nil)
if err != nil {
panic(err)
}
var est struct {
HoldCredits float64 `json:"hold_credits"`
MinCredits float64 `json:"min_credits"`
Model string `json:"model"`
ModelAlias string `json:"model_alias"`
MarkupBps int `json:"markup_bps"`
}
json.Unmarshal(raw, &est)
fmt.Println(est.HoldCredits, est.MinCredits, est.Model, est.ModelAlias, est.MarkupBps)
}
// profileBody and openerBody are the two worked bodies as JSON strings.
// Validate them with your JSON library before spending: /estimate will not.
static String mustBeValid(String payload) {
var t = payload.trim();
if (t.charAt(0) != '{' || t.contains("\"input\":")) {
throw new IllegalArgumentException("body must be the input object itself, unwrapped");
}
if (!t.contains("\"task\":\"profile\"") && !t.contains("\"task\":\"opener\"")) {
throw new IllegalArgumentException("task is required and picks the lane");
}
return payload;
}
for (var payload : new String[]{ profileBody, openerBody }) {
var body = call("POST", "/estimate", mustBeValid(payload), null, null);
// {"data":{"hold_credits":...,"min_credits":...,"model":"gpt-5.6-terra",
// "model_alias":"gpt-terra","markup_bps":1000}}
// hold_credits is a reservation against the output cap, not the price, and
// the two lanes do not hold the same amount.
System.out.println(body);
}
TASKS = %w[profile opener].freeze
TONES = %w[plain warm dry playful].freeze
# The lane router is the `task` field. /estimate checks none of this.
def must_be_valid(input)
raise ArgumentError, "input must be a Hash" unless input.is_a?(Hash)
raise ArgumentError, "do not wrap the body in {input: ...}" if input.key?("input")
raise ArgumentError, "task is required" unless TASKS.include?(input["task"])
raise ArgumentError, "bad tone" unless TONES.include?(input["tone"])
raise ArgumentError, "scan is required" unless input["scan"].is_a?(Hash)
if input["task"] == "profile"
raise ArgumentError, "needs profile or about_me" if input["profile"].to_s.empty? &&
input["about_me"].to_s.empty?
else
raise ArgumentError, "needs their_profile" if input["their_profile"].to_s.strip.empty?
raise ArgumentError, "needs a count" if input["count"].to_i < 1
end
input
end
[profile_body, opener_body].each do |body|
est = call("POST", "/estimate", must_be_valid(body))
puts [body["task"], est["hold_credits"], est["min_credits"],
est["model"], est["model_alias"], est["markup_bps"]].join(" ")
end
<?php
// The lane router is the `task` field. /estimate checks none of this.
function must_be_valid(array $input): array {
if (array_is_list($input)) throw new InvalidArgumentException("body must be an object");
if (isset($input["input"])) throw new InvalidArgumentException("do not wrap the body");
if (!in_array($input["task"] ?? "", ["profile", "opener"], true)) {
throw new InvalidArgumentException("task is required and picks the lane");
}
if (!in_array($input["tone"] ?? "", ["plain", "warm", "dry", "playful"], true)) {
throw new InvalidArgumentException("bad tone");
}
if ($input["task"] === "profile") {
if (($input["profile"] ?? "") === "" && ($input["about_me"] ?? "") === "") {
throw new InvalidArgumentException("the profile lane needs profile or about_me");
}
} else {
if (trim($input["their_profile"] ?? "") === "") {
throw new InvalidArgumentException("the opener lane needs their_profile");
}
if ((int)($input["count"] ?? 0) < 1) throw new InvalidArgumentException("needs a count");
}
return $input;
}
foreach ([$profileBody, $openerBody] as $body) {
$est = dd_call("POST", "/estimate", must_be_valid($body));
echo $body["task"], " ", $est["hold_credits"], " ", $est["min_credits"], " ",
$est["model"], " ", $est["model_alias"], " ", $est["markup_bps"], "\n";
}
// profileBody and openerBody are the two worked bodies as JSON strings.
// Validate here: /estimate will not.
string MustBeValid(string payload) {
var probe = JsonDocument.Parse(payload).RootElement;
if (probe.ValueKind != JsonValueKind.Object) throw new ArgumentException("body must be an object");
if (probe.TryGetProperty("input", out _)) throw new ArgumentException("do not wrap the body");
var task = probe.GetProperty("task").GetString();
if (task is not ("profile" or "opener")) throw new ArgumentException("task picks the lane");
if (task == "profile") {
var hasProfile = probe.TryGetProperty("profile", out var p) && p.GetString()?.Length > 0;
var hasAboutMe = probe.TryGetProperty("about_me", out var a) && a.GetString()?.Length > 0;
if (!hasProfile && !hasAboutMe) throw new ArgumentException("needs profile or about_me");
} else if (string.IsNullOrWhiteSpace(probe.GetProperty("their_profile").GetString())) {
throw new ArgumentException("the opener lane needs their_profile");
}
return payload;
}
foreach (var payload in new[] { profileBody, openerBody }) {
var est = await Call(HttpMethod.Post, "/estimate", MustBeValid(payload));
Console.WriteLine($"{est.GetProperty("hold_credits")} {est.GetProperty("min_credits")} " +
$"{est.GetProperty("model").GetString()} " +
$"{est.GetProperty("model_alias").GetString()} " +
$"{est.GetProperty("markup_bps")}");
}
5. Run it and poll
POST /run submits and returns a job. Always send an Idempotency-Key: a
retried request with the same key is the same run, so a network blip cannot bill you twice. The
web app's key is:
dating-profile-review:<task>:<input-hash>:a<attempt>
The lane has to be in the key. The same paste can legitimately go to both lanes
— text you are reading is a their_profile for opener and,
pasted by the person who wrote it, a profile for profile. A key built
only from a hash of the text would make the second of those a replay of the first and hand back
the wrong lane's reply from cache. Put task in the key. The attempt counter is what
lets a deliberate retry through while a duplicate submit is absorbed; reusing a key with a
changed body is a conflict, not a retry.
Poll GET /jobs/{id} every couple of seconds until status is
succeeded or failed. The reply is the JSON string at
output.output, so it needs a second parse — see step 7.
# Submit. The body IS the input object - there is no {"input": ...} wrapper
# and there is no X-App-Slug header. The lane goes in the key.
TASK=$(python3 -c 'import sys,json; print(json.load(open("profile.json"))["task"])')
KEY="dating-profile-review:$TASK:7b41ce9d:a1"
JOB=$(dd POST /run "$(cat profile.json)" "Idempotency-Key: $KEY" \
| python3 -c 'import sys,json; print(json.load(sys.stdin)["data"]["job_id"])')
# Poll until terminal. Two seconds; do not tight-loop.
while true; do
S=$(dd GET "/jobs/$JOB")
echo "$S" | grep -q '"status":"succeeded"' && break
echo "$S" | grep -q '"status":"failed"' && { echo "$S"; exit 1; }
sleep 2
done
# The reply is a JSON string at .data.output.output - parse it again.
echo "$S" | python3 -c 'import sys,json; print(json.load(sys.stdin)["data"]["output"]["output"])'
import time
payload = must_be_valid(profile_body) # or opener_body
key = f"dating-profile-review:{payload['task']}:7b41ce9d:a1" # the lane is in the key
job = call("POST", "/run", payload, {"Idempotency-Key": key})
while job["status"] in ("queued", "running"):
time.sleep(2)
job = call("GET", f"/jobs/{job['job_id']}")
if job["status"] == "failed":
raise RuntimeError(job.get("error") or "the run did not complete")
reply = json.loads(job["output"]["output"]) # a JSON string, parsed again
print(reply["lane"], reply["route_to_help"])
print(reply["reads_as"])
const payload = mustBeValid(profileBody); // or openerBody
const key = `dating-profile-review:${payload.task}:7b41ce9d:a1`; // the lane is in the key
let job = await call("POST", "/run", payload, { "Idempotency-Key": key });
while (job.status === "queued" || job.status === "running") {
await new Promise(r => setTimeout(r, 2000));
job = await call("GET", `/jobs/${job.job_id}`);
}
if (job.status === "failed") throw new Error(job.error || "the run did not complete");
const reply = JSON.parse(job.output.output); // a JSON string, parsed again
console.log(reply.lane, reply.route_to_help);
console.log(reply.reads_as);
// The lane belongs in the key: the same text can legitimately run in both.
key := map[string]string{"Idempotency-Key": "dating-profile-review:profile:7b41ce9d:a1"}
raw, err := call("POST", "/run", mustBeValid(profileBody), key)
if err != nil {
panic(err)
}
var job struct {
JobID string `json:"job_id"`
Status string `json:"status"`
Output struct {
Output string `json:"output"`
} `json:"output"`
}
json.Unmarshal(raw, &job)
for job.Status == "queued" || job.Status == "running" {
time.Sleep(2 * time.Second)
raw, err = call("GET", "/jobs/"+job.JobID, nil, nil)
if err != nil {
panic(err)
}
json.Unmarshal(raw, &job)
}
if job.Status == "failed" {
panic("the run did not complete")
}
// job.Output.Output is a JSON string holding the reply; unmarshal it again.
var reply map[string]any
json.Unmarshal([]byte(job.Output.Output), &reply)
fmt.Println(reply["lane"], reply["route_to_help"])
// The lane belongs in the key: the same text can legitimately run in both.
var key = "dating-profile-review:profile:7b41ce9d:a1";
var submitted = call("POST", "/run", mustBeValid(profileBody), "Idempotency-Key", key);
// Pull data.job_id out of `submitted`, then poll "/jobs/" + jobId every two
// seconds until data.status is "succeeded" or "failed".
String jobBody;
do {
Thread.sleep(2000);
jobBody = call("GET", "/jobs/" + jobId, null, null, null);
} while (jobBody.contains("\"status\":\"queued\"")
|| jobBody.contains("\"status\":\"running\""));
// The reply is the JSON *string* at data.output.output - parse it again, then
// read route_to_help before anything else.
System.out.println(jobBody);
payload = must_be_valid(profile_body) # or opener_body
key = "dating-profile-review:#{payload["task"]}:7b41ce9d:a1" # the lane is in the key
job = call("POST", "/run", payload, { "Idempotency-Key" => key })
while %w[queued running].include?(job["status"])
sleep 2
job = call("GET", "/jobs/#{job["job_id"]}")
end
raise "the run did not complete" if job["status"] == "failed"
reply = JSON.parse(job["output"]["output"]) # a JSON string, parsed again
puts reply["lane"], reply["route_to_help"]
puts reply["reads_as"]
<?php
$payload = must_be_valid($profileBody); // or $openerBody
$key = "dating-profile-review:{$payload["task"]}:7b41ce9d:a1"; // the lane is in the key
$job = dd_call("POST", "/run", $payload, ["Idempotency-Key: $key"]);
while (in_array($job["status"], ["queued", "running"], true)) {
sleep(2);
$job = dd_call("GET", "/jobs/" . rawurlencode($job["job_id"]));
}
if ($job["status"] === "failed") {
throw new RuntimeException("the run did not complete");
}
$reply = json_decode($job["output"]["output"], true); // parsed again
echo $reply["lane"], " ", var_export($reply["route_to_help"], true), "\n";
echo $reply["reads_as"], "\n";
// The lane belongs in the key: the same text can legitimately run in both.
var key = "dating-profile-review:profile:7b41ce9d:a1";
var job = await Call(HttpMethod.Post, "/run", MustBeValid(profileBody),
"Idempotency-Key", key);
var jobId = job.GetProperty("job_id").GetString();
var status = job.GetProperty("status").GetString();
while (status is "queued" or "running") {
await Task.Delay(2000);
job = await Call(HttpMethod.Get, $"/jobs/{jobId}");
status = job.GetProperty("status").GetString();
}
if (status == "failed") throw new Exception("the run did not complete");
// A JSON string holding the reply - parse it again.
var reply = JsonDocument.Parse(
job.GetProperty("output").GetProperty("output").GetString()!).RootElement;
Console.WriteLine(reply.GetProperty("lane").GetString());
Console.WriteLine(reply.GetProperty("route_to_help").GetBoolean());
6. Or stream it
POST /run-stream is the same run, the same flat body and the same
Idempotency-Key, delivered as server-sent events. A frame is a named event line, a
data line, and a blank line that terminates it — exactly this:
event: job
data: {"job_id":"...","status":"running"}
event: delta
data: {"text":"{\"lane\":\"profile\",\"reads_as\":\"A friendly,"}
event: delta
data: {"text":" unobjectionable stranger who has"}
event: done
data: {"job_id":"...","status":"succeeded","charged_credits":37,"output":{"output":"..."}}
Accumulate the text of every delta frame; that concatenation is the
reply object, and it is the same string you would have found at output.output after
polling. The done frame carries charged_credits — the real price,
which is normally well below the hold — and output.output again, for callers
that did not buffer. An error frame is terminal and carries code,
message and job_id. A pending frame in place of
done means the run is continuing out of band; poll the job id.
Reset the current event name on every blank line. A parser that remembers
delta across a frame boundary will happily append the contents of the
done frame to the reply and then fail to parse the result, which reads like a
truncated model output and is not one.
On an idempotent replay the server may answer with plain JSON instead of
text/event-stream. Check the content-type before you start reading
lines, and fall back to the envelope path if it is not an event stream.
curl -N -X POST "https://api.skillsafe.ai/v1/app-api/run-stream" \ -H "Authorization: Bearer $DD_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: dating-profile-review:opener:7b41ce9d:a1" \ -d @opener.json # Frames arrive as `event: NAME` + `data: JSON`, separated by a blank line. # Concatenate the .text of every `delta`; the `done` frame carries # charged_credits and output.output. -N is required: without it curl buffers # and you get the whole stream at the end, which defeats the point.
out, done = "", None
headers = dict(H, **{"Idempotency-Key": "dating-profile-review:opener:7b41ce9d:a1"})
with requests.post(f"{BASE}/run-stream", headers=headers,
json=must_be_valid(opener_body), stream=True, timeout=600) as r:
event = "message"
for line in r.iter_lines(decode_unicode=True):
if line is None or line == "":
event = "message" # a blank line ends the frame
continue
if line.startswith("event:"):
event = line[6:].strip()
elif line.startswith("data:"):
evt = json.loads(line[5:].strip())
if event == "delta":
out += evt.get("text", "")
elif event in ("done", "pending"):
done = evt
elif event == "error":
raise ApiError(evt)
print("charged", done.get("charged_credits"))
reply = json.loads(out)
print(reply["lane"], reply["route_to_help"])
const res = await fetch(`${BASE}/run-stream`, {
method: "POST",
headers: { ...H, "Idempotency-Key": "dating-profile-review:opener:7b41ce9d:a1" },
body: JSON.stringify(mustBeValid(openerBody))
});
// An idempotent replay answers with plain JSON instead of a stream.
if (!(res.headers.get("content-type") || "").includes("text/event-stream")) {
const env = await res.json();
return JSON.parse(env.data.output.output);
}
const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = "", out = "", done = null;
for (;;) {
const chunk = await reader.read();
if (chunk.done) break;
buf += dec.decode(chunk.value, { stream: true });
let i;
while ((i = buf.indexOf("\n\n")) >= 0) {
const frame = buf.slice(0, i);
buf = buf.slice(i + 2);
let name = "message", data = "";
for (const l of frame.split("\n")) {
if (l.startsWith("event:")) name = l.slice(6).trim();
else if (l.startsWith("data:")) data += l.slice(5).trim();
}
if (!data) continue;
const evt = JSON.parse(data);
if (name === "delta") out += evt.text || "";
else if (name === "done" || name === "pending") done = evt;
else if (name === "error") throw new Error(`${evt.code}: ${evt.message}`);
}
}
console.log("charged", done.charged_credits);
const reply = JSON.parse(out);
req, _ := http.NewRequest("POST", base+"/run-stream",
bytes.NewReader(mustBeValid(openerBody)))
req.Header.Set("Authorization", "Bearer "+os.Getenv("SKILLSAFE_TOKEN"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "dating-profile-review:opener:7b41ce9d:a1")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
var out strings.Builder
name := "message"
sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 0, 64*1024), 8*1024*1024)
for sc.Scan() {
line := sc.Text()
switch {
case line == "":
name = "message" // a blank line ends the frame
case strings.HasPrefix(line, "event:"):
name = strings.TrimSpace(line[6:])
case strings.HasPrefix(line, "data:") && name == "delta":
var evt struct {
Text string `json:"text"`
}
json.Unmarshal([]byte(strings.TrimSpace(line[5:])), &evt)
out.WriteString(evt.Text)
}
}
var reply map[string]any
json.Unmarshal([]byte(out.String()), &reply)
fmt.Println(reply["lane"], reply["route_to_help"])
var req = HttpRequest.newBuilder()
.uri(URI.create(BASE + "/run-stream"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.header("Idempotency-Key", "dating-profile-review:opener:7b41ce9d:a1")
.POST(HttpRequest.BodyPublishers.ofString(mustBeValid(openerBody)))
.build();
var out = new StringBuilder();
var name = new String[]{ "message" };
HTTP.send(req, HttpResponse.BodyHandlers.ofLines()).body().forEach(line -> {
if (line.isEmpty()) { name[0] = "message"; } // a blank line ends the frame
else if (line.startsWith("event:")) { name[0] = line.substring(6).trim(); }
else if (line.startsWith("data:") && name[0].equals("delta")) {
// Parse the frame with your JSON library and append its `text`.
out.append(textOf(line.substring(5).trim()));
}
});
// out now holds the reply object as a string; parse it and read
// route_to_help before lines or openers.
System.out.println(out);
uri = URI("#{BASE}/run-stream")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = "dating-profile-review:opener:7b41ce9d:a1"
req.body = JSON.generate(must_be_valid(opener_body))
out = +""
name = "message"
Net::HTTP.start(uri.host, uri.port, use_ssl: true, read_timeout: 600) do |http|
http.request(req) do |res|
res.read_body do |chunk|
chunk.each_line do |line|
line = line.chomp
if line.empty? then name = "message"
elsif line.start_with?("event:") then name = line[6..].strip
elsif line.start_with?("data:")
evt = JSON.parse(line[5..].strip)
out << evt["text"].to_s if name == "delta"
end
end
end
end
end
reply = JSON.parse(out)
puts reply["lane"], reply["route_to_help"]
<?php
$out = "";
$name = "message";
$ch = curl_init("$base/run-stream");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(must_be_valid($openerBody)),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer $token",
"Content-Type: application/json",
"Idempotency-Key: dating-profile-review:opener:7b41ce9d:a1",
],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$out, &$name) {
foreach (explode("\n", $chunk) as $line) {
$line = rtrim($line, "\r");
if ($line === "") {
$name = "message"; // a blank line ends the frame
} elseif (str_starts_with($line, "event:")) {
$name = trim(substr($line, 6));
} elseif (str_starts_with($line, "data:") && $name === "delta") {
$evt = json_decode(trim(substr($line, 5)), true);
$out .= $evt["text"] ?? "";
}
}
return strlen($chunk);
},
]);
curl_exec($ch);
$reply = json_decode($out, true);
echo $reply["lane"], "\n";
var msg = new HttpRequestMessage(HttpMethod.Post, $"{baseUrl}/run-stream") {
Content = new StringContent(MustBeValid(openerBody), Encoding.UTF8, "application/json")
};
msg.Headers.Add("Idempotency-Key", "dating-profile-review:opener:7b41ce9d:a1");
var res = await http.SendAsync(msg, HttpCompletionOption.ResponseHeadersRead);
using var sr = new StreamReader(await res.Content.ReadAsStreamAsync());
var sb = new StringBuilder();
var name = "message";
while (await sr.ReadLineAsync() is string line) {
if (line.Length == 0) { name = "message"; } // frame boundary
else if (line.StartsWith("event:")) { name = line[6..].Trim(); }
else if (line.StartsWith("data:") && name == "delta") {
var evt = JsonDocument.Parse(line[5..].Trim()).RootElement;
if (evt.TryGetProperty("text", out var t)) sb.Append(t.GetString());
}
}
var reply = JsonDocument.Parse(sb.ToString()).RootElement;
Console.WriteLine(reply.GetProperty("lane").GetString());
7. Parse the reply, and check it against what you sent
The contract is one bare JSON object, and it mostly arrives that way. Write the parser for the two cases where it does not — a code fence around it, or a sentence of preamble before it — by finding the first balanced object rather than trimming by hand.
Then branch, in this order:
-
route_to_helpfirst. When it istruethere is nolineskey and noopenerskey; there isroute,reasonandinstead. The run succeeded and was charged. -
Then
lane. Trust the field, not thetaskyou sent — iftaskwas ever ambiguous the router picks the closer lane and names its choice here, and a client that assumed will read the wrong keys. -
Then verify verbatim. On
profile, everylines[].quoteand everycut[].quoteis supposed to be copied exactly from what you sent. Onopener, everyopeners[].hookis supposed to appear intheir_profile. These are cheap substring checks and they are the difference between a grounded reply and a fluent one. Mark what fails; do not discard the reply over it.
# $S is the terminal job body from step 5, and profile.json is what you sent.
echo "$S" | python3 - profile.json <<'PY'
import json, sys, re
job = json.load(sys.stdin)
sent = json.load(open(sys.argv[1]))
text = re.sub(r"^\s*```(?:json)?|```\s*$", "", job["data"]["output"]["output"]).strip()
r = json.loads(text[text.index("{"):text.rindex("}") + 1])
if r.get("route_to_help"):
print("refused:", r["route"], "-", r["reason"])
sys.exit(0)
if r["lane"] == "profile":
src = sent.get("profile") or sent.get("about_me") or ""
for ln in r["lines"]:
mark = "ok " if ln["quote"] in src else "UNVERIFIED "
print(mark + ln["verdict"] + ": " + ln["quote"])
print("\n" + r["rewrite"])
else:
src = sent["their_profile"]
for o in r["openers"]:
mark = "ok " if o["hook"] in src else "UNVERIFIED "
print(mark + o["frame"] + ": " + o["text"])
PY
import re
FENCE = re.compile(r"^\s*```(?:json)?|```\s*$")
def first_object(text):
"""One bare object is the contract. Survive a fence or a preamble anyway."""
t = FENCE.sub("", text).strip()
start = t.find("{")
if start < 0:
raise ValueError("no JSON object in the reply")
depth, in_str, esc = 0, False, False
for i in range(start, len(t)):
c = t[i]
if esc:
esc = False
elif c == "\\":
esc = True
elif c == '"':
in_str = not in_str
elif not in_str:
if c == "{":
depth += 1
elif c == "}":
depth -= 1
if depth == 0:
return json.loads(t[start:i + 1])
raise ValueError("the object was truncated")
r = first_object(job["output"]["output"])
if r["route_to_help"]:
# A refusal is a successful, charged run. Show it; do not retry it.
print(r["route"], r["reason"], r.get("instead"))
elif r["lane"] == "profile":
src = payload.get("profile") or payload.get("about_me") or ""
for ln in r["lines"] + r["cut"]:
if ln["quote"] not in src:
print("UNVERIFIED quote:", ln["quote"])
print(r["reads_as"], r["gap"], sep="\n")
print(r["rewrite"])
print("carried through:", ", ".join(r["facts_used"]))
else:
src = payload["their_profile"]
for o in r["openers"]:
if o["hook"] not in src:
print("UNVERIFIED hook:", o["hook"])
print(f"[{o['frame']}] {o['text']}")
if o["risk"]:
print(" risk:", o["risk"])
function firstObject(text) {
// One bare object is the contract. Survive a fence or a preamble anyway.
const t = text.replace(/^\s*```(?:json)?/, "").replace(/```\s*$/, "").trim();
const start = t.indexOf("{");
if (start < 0) throw new Error("no JSON object in the reply");
let depth = 0, inStr = false, esc = false;
for (let i = start; i < t.length; i++) {
const c = t[i];
if (esc) { esc = false; continue; }
if (c === "\\") { esc = true; continue; }
if (c === '"') { inStr = !inStr; continue; }
if (inStr) continue;
if (c === "{") depth++;
else if (c === "}" && --depth === 0) return JSON.parse(t.slice(start, i + 1));
}
throw new Error("the object was truncated");
}
const r = firstObject(job.output.output);
if (r.route_to_help) {
// A refusal is a successful, charged run. Show it; do not retry it.
console.log(r.route, r.reason, r.instead);
} else if (r.lane === "profile") {
const src = payload.profile || payload.about_me || "";
for (const ln of [...r.lines, ...r.cut]) {
if (!src.includes(ln.quote)) console.warn("UNVERIFIED quote:", ln.quote);
}
console.log(r.reads_as, r.gap, r.rewrite);
console.log("carried through:", r.facts_used.join(", "));
} else {
const src = payload.their_profile;
for (const o of r.openers) {
if (!src.includes(o.hook)) console.warn("UNVERIFIED hook:", o.hook);
console.log(`[${o.frame}] ${o.text}`, o.risk ? `(risk: ${o.risk})` : "");
}
}
// One bare object is the contract. Survive a fence or a preamble anyway.
func firstObject(text string) (string, bool) {
t := strings.TrimSpace(text)
t = strings.TrimPrefix(strings.TrimPrefix(t, "```json"), "```")
t = strings.TrimSuffix(strings.TrimSpace(t), "```")
start := strings.Index(t, "{")
if start < 0 {
return "", false
}
depth, inStr, esc := 0, false, false
for i := start; i < len(t); i++ {
c := t[i]
switch {
case esc:
esc = false
case c == '\\':
esc = true
case c == '"':
inStr = !inStr
case inStr:
case c == '{':
depth++
case c == '}':
depth--
if depth == 0 {
return t[start : i+1], true
}
}
}
return t[start:], false
}
text, complete := firstObject(job.Output.Output)
if !complete {
fmt.Println("warning: the object was truncated; repair rather than discard")
}
var r struct {
Lane string `json:"lane"`
RouteToHelp bool `json:"route_to_help"`
Route string `json:"route"`
Reason string `json:"reason"`
Lines []struct {
Quote string `json:"quote"`
Verdict string `json:"verdict"`
} `json:"lines"`
Openers []struct {
Text string `json:"text"`
Hook string `json:"hook"`
Frame string `json:"frame"`
} `json:"openers"`
}
json.Unmarshal([]byte(text), &r)
switch {
case r.RouteToHelp:
fmt.Println("refused:", r.Route, r.Reason)
case r.Lane == "profile":
for _, ln := range r.Lines {
if !strings.Contains(sentProfile, ln.Quote) {
fmt.Println("UNVERIFIED quote:", ln.Quote)
}
}
default:
for _, o := range r.Openers {
if !strings.Contains(sentTheirProfile, o.Hook) {
fmt.Println("UNVERIFIED hook:", o.Hook)
}
fmt.Printf("[%s] %s\n", o.Frame, o.Text)
}
}
// One bare object is the contract. Survive a fence or a preamble anyway.
static String firstObject(String text) {
var t = text.strip().replaceFirst("^```(?:json)?", "").replaceFirst("```$", "").strip();
int start = t.indexOf('{');
if (start < 0) throw new IllegalStateException("no JSON object in the reply");
int depth = 0;
boolean inStr = false, esc = false;
for (int i = start; i < t.length(); i++) {
char c = t.charAt(i);
if (esc) { esc = false; continue; }
if (c == '\\') { esc = true; continue; }
if (c == '"') { inStr = !inStr; continue; }
if (inStr) continue;
if (c == '{') depth++;
else if (c == '}' && --depth == 0) return t.substring(start, i + 1);
}
throw new IllegalStateException("the object was truncated");
}
var reply = parse(firstObject(jobOutput)); // your JSON library
if (reply.getBoolean("route_to_help")) {
// A refusal is a successful, charged run. Show it; do not retry it.
System.out.println(reply.getString("route") + ": " + reply.getString("reason"));
} else if ("profile".equals(reply.getString("lane"))) {
for (var line : reply.getArray("lines")) {
var quote = line.getString("quote");
if (!sentProfile.contains(quote)) System.out.println("UNVERIFIED quote: " + quote);
}
System.out.println(reply.getString("rewrite"));
} else {
for (var o : reply.getArray("openers")) {
var hook = o.getString("hook");
if (!sentTheirProfile.contains(hook)) System.out.println("UNVERIFIED hook: " + hook);
System.out.println("[" + o.getString("frame") + "] " + o.getString("text"));
}
}
# One bare object is the contract. Survive a fence or a preamble anyway.
def first_object(text)
t = text.strip.sub(/\A```(?:json)?/, "").sub(/```\z/, "").strip
start = t.index("{") or raise "no JSON object in the reply"
depth = 0
in_str = false
esc = false
(start...t.length).each do |i|
c = t[i]
if esc then esc = false
elsif c == "\\" then esc = true
elsif c == '"' then in_str = !in_str
elsif in_str then next
elsif c == "{" then depth += 1
elsif c == "}"
depth -= 1
return JSON.parse(t[start..i]) if depth.zero?
end
end
raise "the object was truncated"
end
r = first_object(job["output"]["output"])
if r["route_to_help"]
# A refusal is a successful, charged run. Show it; do not retry it.
puts "#{r["route"]}: #{r["reason"]}"
elsif r["lane"] == "profile"
src = payload["profile"].to_s.empty? ? payload["about_me"].to_s : payload["profile"]
(r["lines"] + r["cut"]).each do |ln|
puts "UNVERIFIED quote: #{ln["quote"]}" unless src.include?(ln["quote"])
end
puts r["rewrite"]
puts "carried through: #{r["facts_used"].join(", ")}"
else
src = payload["their_profile"]
r["openers"].each do |o|
puts "UNVERIFIED hook: #{o["hook"]}" unless src.include?(o["hook"])
puts "[#{o["frame"]}] #{o["text"]}"
puts " risk: #{o["risk"]}" if o["risk"]
end
end
<?php
// One bare object is the contract. Survive a fence or a preamble anyway.
function first_object(string $text): array {
$t = trim(preg_replace('/^```(?:json)?|```$/', "", trim($text)));
$start = strpos($t, "{");
if ($start === false) throw new RuntimeException("no JSON object in the reply");
$depth = 0; $inStr = false; $esc = false;
for ($i = $start; $i < strlen($t); $i++) {
$c = $t[$i];
if ($esc) { $esc = false; continue; }
if ($c === "\\") { $esc = true; continue; }
if ($c === '"') { $inStr = !$inStr; continue; }
if ($inStr) continue;
if ($c === "{") $depth++;
elseif ($c === "}" && --$depth === 0) {
return json_decode(substr($t, $start, $i - $start + 1), true);
}
}
throw new RuntimeException("the object was truncated");
}
$r = first_object($job["output"]["output"]);
if ($r["route_to_help"]) {
// A refusal is a successful, charged run. Show it; do not retry it.
echo $r["route"], ": ", $r["reason"], "\n";
} elseif ($r["lane"] === "profile") {
$src = $payload["profile"] !== "" ? $payload["profile"] : $payload["about_me"];
foreach (array_merge($r["lines"], $r["cut"]) as $ln) {
if (!str_contains($src, $ln["quote"])) echo "UNVERIFIED quote: ", $ln["quote"], "\n";
}
echo $r["rewrite"], "\n";
} else {
foreach ($r["openers"] as $o) {
if (!str_contains($payload["their_profile"], $o["hook"])) {
echo "UNVERIFIED hook: ", $o["hook"], "\n";
}
echo "[", $o["frame"], "] ", $o["text"], "\n";
}
}
// One bare object is the contract. Survive a fence or a preamble anyway.
static (string, bool) FirstObject(string text) {
var t = text.Trim();
if (t.StartsWith("```json")) t = t[7..];
else if (t.StartsWith("```")) t = t[3..];
if (t.EndsWith("```")) t = t[..^3];
t = t.Trim();
var start = t.IndexOf('{');
if (start < 0) throw new Exception("no JSON object in the reply");
int depth = 0; bool inStr = false, esc = false;
for (var i = start; i < t.Length; i++) {
var c = t[i];
if (esc) { esc = false; continue; }
if (c == '\\') { esc = true; continue; }
if (c == '"') { inStr = !inStr; continue; }
if (inStr) continue;
if (c == '{') depth++;
else if (c == '}' && --depth == 0) return (t[start..(i + 1)], true);
}
return (t[start..], false);
}
var (text, _) = FirstObject(jobOutput);
var r = JsonDocument.Parse(text).RootElement;
if (r.GetProperty("route_to_help").GetBoolean()) {
// A refusal is a successful, charged run. Show it; do not retry it.
Console.WriteLine($"{r.GetProperty("route").GetString()}: {r.GetProperty("reason").GetString()}");
} else if (r.GetProperty("lane").GetString() == "profile") {
foreach (var ln in r.GetProperty("lines").EnumerateArray()) {
var quote = ln.GetProperty("quote").GetString()!;
if (!sentProfile.Contains(quote)) Console.WriteLine($"UNVERIFIED quote: {quote}");
}
Console.WriteLine(r.GetProperty("rewrite").GetString());
} else {
foreach (var o in r.GetProperty("openers").EnumerateArray()) {
var hook = o.GetProperty("hook").GetString()!;
if (!sentTheirProfile.Contains(hook)) Console.WriteLine($"UNVERIFIED hook: {hook}");
Console.WriteLine($"[{o.GetProperty("frame").GetString()}] {o.GetProperty("text").GetString()}");
}
}
Where to go next
The tokens page is the fastest way to get a working token out of this
browser and into a shell. If you are building on top of this API rather than calling it once,
the two things worth carrying across from the web app are the client-side
scan (the reply is only as grounded as the counts you send it) and the verbatim
checks in step 7.