Drive Nap Ticket from your own code
The one metered task, plan, is available over HTTP. It takes the time a person can lie down, the time they must be up, their bedtime, how they slept, what comes next, whether coffee is fine, a short note in their own words, and the page's fare board (four fares priced with fixed rules of thumb). It returns one fare (quiet_rest, full_cycle, coffee_nap or power_nap), the lie-down and alarm times copied from the board, two to four steps for before and after, what to do if sleep does not come, a caution when driving is next, a line about tonight and a playful announcement.
The division of labour first: the page owns the clock arithmetic, which fares fit and the rule applied to the times; the model owns reading the note (does it describe trouble sleeping at night?) and all the wording; the page then re-checks the reply against its own fare board. Not medical advice.
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":"nap-ticket"}), 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, free_from, back_by, bedtime, last_night, next, coffee, note, page_facts) 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: a conductor's ticket is 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 fares
Every run sends task: plan (the one task is plan); a missing or different one is still answered as plan. The fare (the verdict) follows a fixed check order, stopping at the first that fits: quiet_rest when no nap fare fits the times, or when the note says the person often has trouble falling or staying asleep at night (the model's judgement - one bad night with an outside cause does not count); full_cycle when it fits and last night was under 6 hours or a late night or night shift is next; coffee_nap when it fits and driving or focused work or study is next, or a power nap does not fit; otherwise power_nap.
Input (every field is a string)
| Field | Required | What it holds |
|---|---|---|
task | yes | Always plan. |
free_from, back_by, bedtime | yes | 24-hour HH:MM: when they can lie down, when they must be up and functioning, and when they next go to bed for the night (times wrap past midnight). |
last_night | yes | under 5 hours, 5 to 6 hours, 6 to 7 hours, 7 hours or more or not sure. |
next | yes | focused work or study, driving, time with people or being active or a late night or a night shift. |
coffee | yes | a coffee right before is fine or no caffeine. |
note | yes | The person's own words, at least 4; over 1,500 characters it keeps its start and end with the middle marked as cut. |
page_facts | yes | One key | ... per line: window, one line per fare (quiet_rest, power_nap, coffee_nap, full_cycle) saying whether it fits and its clock times, trouble_sleeping_hint, drive_wait (only when driving is next), page_pick and verdict_rule. Build it with NapKit.buildInput (below) so the fares match the page's. |
clip_note | no | What the page shortened, if anything. |
retry_note | no | Only on the page's one reformat retry, after an unusable reply. |
Reply (one JSON object)
{"task": "plan", "title": "...", "verdict": "power_nap", "verdict_reason": "...", "lie_down": "15:00", "alarm": "15:30",
"before": ["...", "..."], "wake": ["...", "..."], "if_awake": "...", "caution": "...", "tonight": "...", "announcement": "..."}
| Field | What it holds |
|---|---|
verdict, verdict_reason | The fare id by the check order above; at most 30 words on what decided it. |
lie_down, alarm | Copied from the chosen fare's line in page_facts (for quiet rest, the timer time). |
before, wake | Two to four steps each, at most 16 words per step; a coffee nap starts with the coffee, and a nap fare names its clear-headed time. |
if_awake | At most 25 words on what to do if sleep does not come. |
caution | At most 30 words, or empty; when driving is next it says to wait at least 15 minutes after waking and not to drive while drowsy. |
tonight, announcement, title | A line about tonight naming the bedtime (at most 25 words), a playful conductor's line (at most 22) and a title of at most 8 words. |
The page then checks the reply (NapKit.check): the fare against its rule and whether it fits, the lie-down and alarm times against the board, the coffee step, the step counts, the driving wait, the bedtime in the tonight line, every number in the prose, names the note never uses, any sleep aid, alcohol or supplement, leaked field ids and the word limits.
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 fares - awake until 2 most nights (quiet rest, though a power nap fits), up three times with a baby with two hours free (full cycle), a slump before a study block with coffee allowed (coffee nap), and fading after yard work with a drive ahead and no caffeine (power nap).
Awake until 2 most nights - quiet rest
Input:
{
"task": "plan",
"free_from": "13:30",
"back_by": "14:30",
"bedtime": "23:00",
"last_night": "5 to 6 hours",
"next": "focused work or study",
"coffee": "no caffeine",
"note": "I lie awake until 2 most nights and then drag through the afternoon. Big report due by five.",
"page_facts": "window | 60 minutes from 13:30 to 14:30; bedtime 23:00 is 9 h 30 min after 13:30\nquiet_rest | fits: lie down 13:30, timer 13:50 (eyes closed, no need to sleep)\npower_nap | fits: lie down 13:30, alarm 14:00 (about 20 minutes asleep), clear-headed by about 14:10; ends 9 h before bedtime\ncoffee_nap | does not fit: no caffeine chosen\nfull_cycle | does not fit: needs 115 minutes, the window is 60\ntrouble_sleeping_hint | the note seems to mention trouble sleeping at night (a hint only - read the note)\npage_pick | power_nap (the rule applied without reading the note)\nverdict_rule | check in order and stop at the first that fits: quiet_rest when no nap fare fits, or when the note says the person often has trouble falling or staying asleep at night; full_cycle when full_cycle fits and last night was under 6 hours or a late night or night shift is next; coffee_nap when coffee_nap fits and either driving or focused work or study is next, or power_nap does not fit; otherwise power_nap"
}
Reply:
{
"task": "plan",
"title": "Quiet Rest Before the Big Report",
"verdict": "quiet_rest",
"verdict_reason": "Your note says you lie awake until 2 most nights, which is ongoing trouble sleeping, so quiet rest beats the power nap that fits your window.",
"lie_down": "13:30",
"alarm": "13:50",
"before": [
"Set your timer for 13:50.",
"Silence your notifications.",
"Dim or block the light.",
"Loosen any tight clothing, lie down, and take a few slow breaths."
],
"wake": [
"Get up as soon as the timer sounds.",
"Step into bright light or daylight.",
"Drink a glass of water.",
"Take a short walk or stretch before getting back to your report."
],
"if_awake": "Sleep isn't the goal here; resting with eyes closed still helps. Keep the timer where it is, and don't stretch this past it.",
"caution": "",
"tonight": "Keep tonight's bedtime at 23:00 as planned; a short quiet rest now won't get in the way.",
"announcement": "Next stop: quiet rest, 13:30 to 13:50. Eyes closed, no sleep required, back on track for your report."
}
Up three times with a baby, two hours free - full cycle
Input:
{
"task": "plan",
"free_from": "13:00",
"back_by": "15:00",
"bedtime": "22:30",
"last_night": "under 5 hours",
"next": "time with people or being active",
"coffee": "no caffeine",
"note": "Up three times in the night with a teething baby. My partner has her until three, then we're going to the park.",
"page_facts": "window | 120 minutes from 13:00 to 15:00; bedtime 22:30 is 9 h 30 min after 13:00\nquiet_rest | fits: lie down 13:00, timer 13:20 (eyes closed, no need to sleep)\npower_nap | fits: lie down 13:00, alarm 13:30 (about 20 minutes asleep), clear-headed by about 13:40; ends 9 h before bedtime\ncoffee_nap | does not fit: no caffeine chosen\nfull_cycle | fits: lie down 13:00, alarm 14:40 (about 90 minutes asleep), clear-headed by about 14:55; ends 7 h 50 min before bedtime\ntrouble_sleeping_hint | none found in the note (a hint only - read the note)\npage_pick | full_cycle (the rule applied without reading the note)\nverdict_rule | check in order and stop at the first that fits: quiet_rest when no nap fare fits, or when the note says the person often has trouble falling or staying asleep at night; full_cycle when full_cycle fits and last night was under 6 hours or a late night or night shift is next; coffee_nap when coffee_nap fits and either driving or focused work or study is next, or power_nap does not fit; otherwise power_nap"
}
Reply:
{
"task": "plan",
"title": "Recharge Before Park Time With Baby",
"verdict": "full_cycle",
"verdict_reason": "The full cycle fits the window, and last night was under 5 hours, so a full cycle nap is the call over a shorter power nap.",
"lie_down": "13:00",
"alarm": "14:40",
"before": [
"Silence notifications so the next 90 minutes stay undisturbed.",
"Dim or block out the light around you.",
"Set your alarm for 14:40, now your partner has her until three.",
"Lie down, loosen anything tight, and take a few slow breaths."
],
"wake": [
"Get up as soon as the 14:40 alarm sounds, no snoozing.",
"Step into bright light or daylight right away.",
"Drink a glass of water to help you feel alert.",
"Any grogginess should lift by about 14:55, in time for the park."
],
"if_awake": "If sleep doesn't come, just rest with your eyes closed - that still helps. Leave the alarm where it is and don't stretch the nap longer.",
"caution": "",
"tonight": "Keep tonight's bedtime at 22:30, no matter how the day with the baby and park goes.",
"announcement": "Next stop: a 90-minute full cycle from 13:00 to 14:40, then on to the park with the baby."
}
A slump before a study block, coffee allowed - coffee nap
Input:
{
"task": "plan",
"free_from": "14:00",
"back_by": "14:40",
"bedtime": "23:30",
"last_night": "6 to 7 hours",
"next": "focused work or study",
"coffee": "a coffee right before is fine",
"note": "Afternoon slump before a long study block for my statistics exam. My eyes keep closing over my notes.",
"page_facts": "window | 40 minutes from 14:00 to 14:40; bedtime 23:30 is 9 h 30 min after 14:00\nquiet_rest | fits: lie down 14:00, timer 14:20 (eyes closed, no need to sleep)\npower_nap | fits: lie down 14:00, alarm 14:30 (about 20 minutes asleep), clear-headed by about 14:40; ends 9 h before bedtime\ncoffee_nap | fits: coffee at 14:00, lie down 14:05, alarm 14:25 (about 10 minutes asleep), clear-headed by about 14:35; ends 9 h 5 min before bedtime\nfull_cycle | does not fit: needs 115 minutes, the window is 40\ntrouble_sleeping_hint | none found in the note (a hint only - read the note)\npage_pick | coffee_nap (the rule applied without reading the note)\nverdict_rule | check in order and stop at the first that fits: quiet_rest when no nap fare fits, or when the note says the person often has trouble falling or staying asleep at night; full_cycle when full_cycle fits and last night was under 6 hours or a late night or night shift is next; coffee_nap when coffee_nap fits and either driving or focused work or study is next, or power_nap does not fit; otherwise power_nap"
}
Reply:
{
"task": "plan",
"title": "Beat the Statistics Exam Slump",
"verdict": "coffee_nap",
"verdict_reason": "A coffee nap fits your 40-minute window, and focused study is next, so a quick coffee before a short nap sets you up well.",
"lie_down": "14:05",
"alarm": "14:25",
"before": [
"Drink a regular cup of coffee quickly, right away at 14:00, before lying down.",
"Set your alarm for 14:25 so you wake on time.",
"Set your notes aside and silence notifications before lying down.",
"Dim the lights and lie down for your nap at 14:05."
],
"wake": [
"Get up as soon as the 14:25 alarm goes off.",
"Step into bright daylight or switch on a bright light to feel alert.",
"Drink a glass of water before returning to your notes.",
"Any grogginess should lift by about 14:35, clear-headed for study."
],
"if_awake": "If sleep doesn't come, just rest with your eyes closed - it still helps. Leave the alarm as set; don't stretch the nap longer.",
"caution": "",
"tonight": "Keep tonight's bedtime at 23:30 as planned - this short nap won't get in the way.",
"announcement": "Next stop: a 14:05 coffee nap, arriving clear-headed by 14:35 for your statistics exam."
}
Fading after yard work, driving next, no caffeine - power nap
Input:
{
"task": "plan",
"free_from": "15:00",
"back_by": "15:45",
"bedtime": "22:00",
"last_night": "7 hours or more",
"next": "driving",
"coffee": "no caffeine",
"note": "Long morning of yard work and I'm fading. Driving to pick up my sister from the station at four.",
"page_facts": "window | 45 minutes from 15:00 to 15:45; bedtime 22:00 is 7 h after 15:00\nquiet_rest | fits: lie down 15:00, timer 15:20 (eyes closed, no need to sleep)\npower_nap | fits: lie down 15:00, alarm 15:30 (about 20 minutes asleep), clear-headed by about 15:40; ends 6 h 30 min before bedtime\ncoffee_nap | does not fit: no caffeine chosen\nfull_cycle | does not fit: needs 115 minutes, the window is 45\ntrouble_sleeping_hint | none found in the note (a hint only - read the note)\ndrive_wait | after waking, wait at least 15 minutes before driving, and do not drive while still drowsy\npage_pick | power_nap (the rule applied without reading the note)\nverdict_rule | check in order and stop at the first that fits: quiet_rest when no nap fare fits, or when the note says the person often has trouble falling or staying asleep at night; full_cycle when full_cycle fits and last night was under 6 hours or a late night or night shift is next; coffee_nap when coffee_nap fits and either driving or focused work or study is next, or power_nap does not fit; otherwise power_nap"
}
Reply:
{
"task": "plan",
"title": "Yard Work Recovery Nap Before Your Drive",
"verdict": "power_nap",
"verdict_reason": "Full cycle needs far more than your 45-minute window, and no caffeine rules out a coffee nap, so a power nap fits best.",
"lie_down": "15:00",
"alarm": "15:30",
"before": [
"Set your alarm for 15:30, right where you'll hear it.",
"Dim the light, silence notifications, and lie down to rest.",
"Loosen any tight clothing and take a few slow breaths."
],
"wake": [
"Get up as soon as the alarm goes off.",
"Step into bright light or daylight to help you wake.",
"Have a glass of water before you get moving.",
"Any grogginess should lift by about 15:40."
],
"if_awake": "If sleep doesn't come, just rest with your eyes closed; it still helps. Keep the alarm set, and don't stretch the nap longer.",
"caution": "Wait at least 15 minutes after waking before you drive, and don't drive while still drowsy.",
"tonight": "This nap won't get in the way of tonight; keep your bedtime at 22:00 as planned.",
"announcement": "Next stop: 15:00 for lie-down, 15:30 for wake-up, right on time for your sister's station pickup at four."
}
Build the input yourself
Load the script in node (global.window = global; require("./napkit.js")), then:
const K = NapKit;
const built = K.buildInput({ free_from: "15:00", back_by: "15:45", bedtime: "22:00", last_night: "h7_plus", next: "drive", coffee: "no",
note: "Long morning of yard work and I'm fading. Driving to pick up my sister from the station at four." });
built.fs.power_nap; // { fits: true, lie_down: "15:00", alarm: "15:30", clear_by: "15:40", stops: [...] } - the free fare board
built.pick; // "power_nap" - the rule applied without reading the note
const wire = K.toWire(built.input); // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// K.check(reply, built).notes // the page's re-check of the ticket
// K.ics(built, reply, reply.title) // a calendar event from lying down to the alarm
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://nap-ticket.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://nap-ticket.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"]
// nap-ticket.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://nap-ticket.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://nap-ticket.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
}
// NapTicket.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 NapTicket {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://nap-ticket.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://nap-ticket.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://nap-ticket.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://nap-ticket.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":"nap-ticket"}' | 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": "nap-ticket"})["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: "nap-ticket" })).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": "nap-ticket"}, 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", "nap-ticket"), 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: "nap-ticket" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "nap-ticket"])["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 = "nap-ticket" }))!["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": "plan", "free_from": "15:00", "back_by": "15:45", "bedtime": "22:00", "note": "...", "page_facts": "...", ...} - 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": "plan", "free_from": "15:00", "back_by": "15:45", "bedtime": "22:00", "note": "...", "page_facts": "...", ...} - 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: "plan", free_from, back_by, bedtime, last_night, next, coffee, note, page_facts} (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 nap-ticket:<task>:<inputHash>:a<attempt> (the one task is plan), 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="nap-ticket:plan:$(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 = "nap-ticket:" + 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("title"))
import { createHash } from "node:crypto";
const key = `nap-ticket:${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.title);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "nap-ticket:" + 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 = "nap-ticket:" + 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 = "nap-ticket:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["title"]].inspect
$key = "nap-ticket:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", $reply["title"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"nap-ticket:{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: nap-ticket:plan:$(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": "plan", "free_from": "15:00", "back_by": "15:45", "bedtime": "22:00", "note": "...", "page_facts": "...", ...} - 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": "nap-ticket:plan:" + 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": `nap-ticket:plan:${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", "nap-ticket:plan:"+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", "nap-ticket:plan:" + 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" => "nap-ticket:plan:#{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: nap-ticket:plan:" . 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", $"nap-ticket:plan:{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);