Drive Daylight Snacks from your own code
The one metered task, plan, is available over HTTP. It takes a place, the date the plan starts, a weekday schedule in plain lines, a weekly goal, what you like doing outside, what gets in the way, and the page's own daylight numbers. It returns a menu of sun snacks (short daylight outings), a week of punches that fits the reachable windows, three backups for missed days, a note on the darkest week and a first step.
The division of labour first: the page owns the sun's times, the reachable windows, the darkest week, the punch count and the verdict rule; the model owns the menu and the wording; the page re-checks the reply. Build page_facts with the page's own script (see Build the input yourself) rather than by hand.
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":"daylight-snacks"}), 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, place, plan_from, schedule, goal, likes, limits, 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 plan 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 verdicts
Every run sends task: plan (the one task is plan); a missing or different one is still answered as plan. The verdict is a rule on N, the daylight minutes reachable on a workday of the darkest week (the workday_total line of page_facts), checked in this order: days-off when N is under 20, squeeze from 20 to 59, plenty at 60 or more.
Input fields (every value is a string)
| Field | Required | What it holds |
|---|---|---|
task | yes | Always plan. |
place | yes | The place label, e.g. London, UK, or Latitude 52.37, longitude 4.9. |
plan_from | yes | The first day of the plan, YYYY-MM-DD. |
schedule | yes | Plain lines: workdays and days off, getting up and bedtime, work hours, the commute (indoors, or outdoors on foot or by bike) and the lunch break. |
goal | yes | Sun snacks wanted per week, 3 to 21. |
likes / limits | yes | What you like doing outside and what gets in the way (at most 300 characters each), or (not given). |
page_facts | yes | The page's numbers, one key | ... per line: place, plan_from, this_week, darkest_week, optional clock_change, work_days, days_off, four workday_window lines (morning, commute, lunch, evening), workday_total, dayoff_window, snacks_that_fit and target_snacks. |
clip_note | no | What the page cut, if anything. |
retry_note | no | Only on the automatic reformat retry. |
The reply
One JSON object, in output.output of the finished job:
{"task": "plan", "title": "...", "verdict": "plenty | squeeze | days-off", "verdict_reason": "...",
"snacks": [{"id": "s1", "name": "...", "minutes": 15, "window": "morning | commute | lunch | evening | dayoff", "how": "..."}],
"week": [{"day": "mon", "snack": "s1"}],
"backup": ["...", "...", "..."], "darkest_week": "...", "first_step": "..."}
| Field | What it holds |
|---|---|
verdict / verdict_reason | The rule above, and one sentence (at most 30 words) quoting N and the windows that hold it. |
snacks | 2 to 8 distinct ideas (never more than target_snacks; none when it is 0), each with a window that has at least 10 minutes, 10 to 60 minutes that fit the window, a name (at most 6 words) and how (at most 22). |
week | Exactly target_snacks punches in day order; workdays take workday windows (each at most once a day), days off take day-off snacks within the day's daylight, at most 3 a day, every snack used. |
backup | Exactly three ideas (at most 18 words) for a day when every window is missed. |
darkest_week / first_step / title | How the darkest week differs, quoting both weeks' sun times (at most 35 words); one small first step (at most 20); a playful title (at most 8). |
The page then checks the reply (DayKit.check): the verdict against the rule and the quoted minutes, every snack inside its window, workday and day-off windows on the right days, the punch count, three backups, the word limits, clock times that appear in the schedule or the sun's times, and no weekday, season, he or she, health-claim word or internal field name in the text. Daylight Snacks is a daylight planner, not medical advice.
Worked examples, one per verdict
These are the three free samples on the page: the exact input and a stand-in model's reply, unedited. Together they cover plenty, squeeze and days-off.
Lisbon, from home - plenty
Input:
{
"task": "plan",
"place": "Lisbon, Portugal",
"plan_from": "2026-10-19",
"schedule": "Works mon, tue, wed, thu, fri; days off sat, sun\nUp at 07:30, bed at 23:30\nWork 09:00-17:30\nNo commute\nLunch break 13:00 for 60 min",
"goal": "12",
"likes": "Coffee on the balcony, walking along the river, a bike ride on a free day",
"limits": "Calls most afternoons from 15:00",
"page_facts": "place | Lisbon, Portugal | latitude 38.72 | time zone Europe/Lisbon\nplan_from | 2026-10-19\nthis_week | 2026-10-19 | sunrise 07:49 | sunset 18:54 | daylight 11 h 05 min\ndarkest_week | 2027-01-04 | sunrise 07:55 | sunset 17:27 | daylight 9 h 33 min\nclock_change | week of 2026-10-26 | clocks go back 1 hour\nwork_days | mon tue wed thu fri\ndays_off | sat sun\nworkday_window | morning | 65 min | 07:55-09:00\nworkday_window | commute | 0 min | no commute\nworkday_window | lunch | 60 min | 13:00-14:00\nworkday_window | evening | 0 min | in the dark\nworkday_total | 125 min\ndayoff_window | dayoff | 572 min | 07:55-17:27\nsnacks_that_fit | 16\ntarget_snacks | 12"
}
Reply:
{
"task": "plan",
"title": "Your Lisbon Daylight Snack Lineup",
"verdict": "plenty",
"verdict_reason": "125 minutes of workday daylight land in the morning and lunch windows, with none in the commute or evening.",
"snacks": [
{
"id": "s1",
"name": "Balcony Coffee In Daylight",
"minutes": 20,
"window": "morning",
"how": "Bring your coffee out to the balcony while the early light is there."
},
{
"id": "s2",
"name": "River Walk Before Calls",
"minutes": 45,
"window": "lunch",
"how": "Walk along the river during your lunch break, finishing before your afternoon calls begin."
},
{
"id": "s3",
"name": "Day Off Bike Ride",
"minutes": 45,
"window": "dayoff",
"how": "Take a bike ride while the daylight lasts on your day off."
}
],
"week": [
{
"day": "mon",
"snack": "s1"
},
{
"day": "mon",
"snack": "s2"
},
{
"day": "tue",
"snack": "s1"
},
{
"day": "tue",
"snack": "s2"
},
{
"day": "wed",
"snack": "s1"
},
{
"day": "wed",
"snack": "s2"
},
{
"day": "thu",
"snack": "s1"
},
{
"day": "thu",
"snack": "s2"
},
{
"day": "fri",
"snack": "s1"
},
{
"day": "fri",
"snack": "s2"
},
{
"day": "sat",
"snack": "s3"
},
{
"day": "sun",
"snack": "s3"
}
],
"backup": [
"Sit near the brightest spot indoors for a few minutes to catch some daylight.",
"Step outside the doorway for a short stretch to catch the last of the light.",
"Add extra time on your next day off to make up the missed daylight."
],
"darkest_week": "Sunrise shifts from 07:49 to 07:55 and sunset from 18:54 to 17:27, with clocks going back 1 hour the week of 2026-10-26.",
"first_step": "On the first day, enjoy coffee on the balcony in the early light."
}
London, by train - squeeze
Input:
{
"task": "plan",
"place": "London, UK",
"plan_from": "2026-10-26",
"schedule": "Works mon, tue, wed, thu, fri; days off sat, sun\nUp at 06:45, bed at 23:00\nWork 09:00-17:30\nCommute 45 min each way, indoors (car, bus or train)\nLunch break 12:30 for 30 min",
"goal": "10",
"likes": "Walking, a park bench with a sandwich, the allotment on days off",
"limits": "Two kids to get to school before the train",
"page_facts": "place | London, UK | latitude 51.51 | time zone Europe/London\nplan_from | 2026-10-26\nthis_week | 2026-10-26 | sunrise 06:43 | sunset 16:46 | daylight 10 h 03 min\ndarkest_week | 2026-12-28 | sunrise 08:06 | sunset 15:58 | daylight 7 h 52 min\nwork_days | mon tue wed thu fri\ndays_off | sat sun\nworkday_window | morning | 9 min | 08:06-08:15\nworkday_window | commute | 0 min | indoors\nworkday_window | lunch | 30 min | 12:30-13:00\nworkday_window | evening | 0 min | in the dark\nworkday_total | 39 min\ndayoff_window | dayoff | 472 min | 08:06-15:58\nsnacks_that_fit | 11\ntarget_snacks | 10"
}
Reply:
{
"task": "plan",
"title": "Your London Daylight, Snack by Snack",
"verdict": "squeeze",
"verdict_reason": "Your workday daylight comes to 39 minutes, all of it in the morning and lunch windows.",
"snacks": [
{
"id": "s1",
"name": "Lunch bench with a sandwich",
"minutes": 15,
"window": "lunch",
"how": "Take your sandwich to a bench during your break and sit outside."
},
{
"id": "s2",
"name": "A short lunch walk",
"minutes": 10,
"window": "lunch",
"how": "Use part of your break for a short, easy walk outside."
},
{
"id": "s3",
"name": "Time at the allotment",
"minutes": 45,
"window": "dayoff",
"how": "Spend time at the allotment, for as long as suits you."
},
{
"id": "s4",
"name": "A longer walk outside",
"minutes": 30,
"window": "dayoff",
"how": "Take a longer walk outside, wherever feels good, with no rush."
},
{
"id": "s5",
"name": "A few minutes standing outside",
"minutes": 20,
"window": "dayoff",
"how": "Step outside and stand in the light for a few minutes, no agenda."
}
],
"week": [
{
"day": "mon",
"snack": "s1"
},
{
"day": "tue",
"snack": "s2"
},
{
"day": "wed",
"snack": "s1"
},
{
"day": "thu",
"snack": "s2"
},
{
"day": "fri",
"snack": "s1"
},
{
"day": "sat",
"snack": "s3"
},
{
"day": "sat",
"snack": "s4"
},
{
"day": "sat",
"snack": "s5"
},
{
"day": "sun",
"snack": "s4"
},
{
"day": "sun",
"snack": "s3"
}
],
"backup": [
"Sit by the brightest window you can find for a few minutes.",
"Stand near an open door or window for a short stretch.",
"Add the missed time to your next day off outside."
],
"darkest_week": "This week the sun rises 06:43 and sets 16:46; in the darkest week it rises 08:06 and sets 15:58, giving noticeably less daylight.",
"first_step": "On your first day, take your sandwich to a bench at lunch."
}
Helsinki, 08:00 to 16:30 - days-off
Input:
{
"task": "plan",
"place": "Helsinki, Finland",
"plan_from": "2026-10-26",
"schedule": "Works mon, tue, wed, thu, fri; days off sat, sun\nUp at 06:30, bed at 22:30\nWork 08:00-16:30\nCommute 30 min each way, indoors (car, bus or train)\nLunch break 11:30 for 15 min",
"goal": "8",
"likes": "Cross-country skiing when there is snow, a sauna evening",
"limits": "Lunch is usually eaten at the desk",
"page_facts": "place | Helsinki, Finland | latitude 60.17 | time zone Europe/Helsinki\nplan_from | 2026-10-26\nthis_week | 2026-10-26 | sunrise 07:27 | sunset 16:42 | daylight 9 h 15 min\ndarkest_week | 2026-12-21 | sunrise 09:24 | sunset 15:13 | daylight 5 h 49 min\nwork_days | mon tue wed thu fri\ndays_off | sat sun\nworkday_window | morning | 0 min | in the dark\nworkday_window | commute | 0 min | indoors\nworkday_window | lunch | 15 min | 11:30-11:45\nworkday_window | evening | 0 min | in the dark\nworkday_total | 15 min\ndayoff_window | dayoff | 349 min | 09:24-15:13\nsnacks_that_fit | 11\ntarget_snacks | 8"
}
Reply:
{
"task": "plan",
"title": "Chasing Helsinki's Last Light",
"verdict": "days-off",
"verdict_reason": "Your lunch window holds all 15 minutes of workday daylight; morning, commute and evening hold none.",
"snacks": [
{
"id": "s1",
"name": "Quick Midday Fresh-Air Break",
"minutes": 10,
"window": "lunch",
"how": "Step outside for a few minutes before eating lunch at your desk."
},
{
"id": "s2",
"name": "Cross-Country Ski Loop",
"minutes": 60,
"window": "dayoff",
"how": "Head out for a cross-country ski loop if there is snow."
},
{
"id": "s3",
"name": "Slow Outdoor Loop",
"minutes": 45,
"window": "dayoff",
"how": "Take a slow loop outside, breathing in the daylight at your own pace."
},
{
"id": "s4",
"name": "Walk Then Sauna Evening",
"minutes": 30,
"window": "dayoff",
"how": "Take a walk in the daylight, then settle into a sauna evening."
}
],
"week": [
{
"day": "mon",
"snack": "s1"
},
{
"day": "tue",
"snack": "s1"
},
{
"day": "wed",
"snack": "s1"
},
{
"day": "thu",
"snack": "s1"
},
{
"day": "fri",
"snack": "s1"
},
{
"day": "sat",
"snack": "s2"
},
{
"day": "sat",
"snack": "s3"
},
{
"day": "sun",
"snack": "s4"
}
],
"backup": [
"Stand outside for a few minutes at some point today.",
"Spend a few minutes near natural light indoors instead.",
"Add the missed time to your next day off outing."
],
"darkest_week": "By the darkest week, sunrise shifts from 07:27 to 09:24 and sunset moves from 16:42 to 15:13, narrowing the daylight further.",
"first_step": "On your first day, step outside briefly during lunch before eating at your desk."
}
Build the input yourself
Load the script in node (global.window = global; require("./daykit.js")), then:
const K = DayKit;
const built = K.buildInput({ placeId: "london", start: "2026-10-26", workDays: ["mon", "tue", "wed", "thu", "fri"],
wake: "06:45", bed: "23:00", workStart: "09:00", workEnd: "17:30", commute: "45", commuteMode: "indoor",
lunchStart: "12:30", lunchMin: "30", goal: "10", likes: "Walking" });
built.verdict; // "squeeze" - 39 reachable minutes on a workday of the darkest week
built.weeks[built.dark].date; // "2026-12-28"
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 plan
// K.ics(built, reply) // the punches as weekly calendar events
// Custom place: { placeId: "custom", lat: "52.37", lon: "4.9", tz: "Europe/Amsterdam", ... }
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://daylight-snacks.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://daylight-snacks.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"]
// daylight-snacks.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://daylight-snacks.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://daylight-snacks.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
}
// DaylightSnacks.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 DaylightSnacks {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://daylight-snacks.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://daylight-snacks.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://daylight-snacks.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://daylight-snacks.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":"daylight-snacks"}' | 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": "daylight-snacks"})["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: "daylight-snacks" })).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": "daylight-snacks"}, 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", "daylight-snacks"), 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: "daylight-snacks" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "daylight-snacks"])["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 = "daylight-snacks" }))!["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", "place": "London, UK", "plan_from": "2026-10-26", "schedule": "Works mon, tue, wed, thu, fri; days off sat, sun\nUp at 06:45, bed at 23:00\nWork 09:00-17:30\nCommute 45 min each way, indoors (car, bus or train)\nLunch break 12:30 for 30 min", "goal": "10", "likes": "Walking, a park bench with a sandwich, the allotment on days off", "limits": "Two kids to get to school before the train", "page_facts": "place | London, UK | latitude 51.51 | time zone Europe/London\nplan_from | 2026-10-26\nthis_week | 2026-10-26 | sunrise 06:43 | sunset 16:46 | daylight 10 h 03 min\ndarkest_week | 2026-12-28 | sunrise 08:06 | sunset 15:58 | daylight 7 h 52 min\nwork_days | mon tue wed thu fri\ndays_off | sat sun\nworkday_window | morning | 9 min | 08:06-08:15\nworkday_window | commute | 0 min | indoors\nworkday_window | lunch | 30 min | 12:30-13:00\nworkday_window | evening | 0 min | in the dark\nworkday_total | 39 min\ndayoff_window | dayoff | 472 min | 08:06-15:58\nsnacks_that_fit | 11\ntarget_snacks | 10"}
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", "place": "London, UK", "plan_from": "2026-10-26", "schedule": "Works mon, tue, wed, thu, fri; days off sat, sun\nUp at 06:45, bed at 23:00\nWork 09:00-17:30\nCommute 45 min each way, indoors (car, bus or train)\nLunch break 12:30 for 30 min", "goal": "10", "likes": "Walking, a park bench with a sandwich, the allotment on days off", "limits": "Two kids to get to school before the train", "page_facts": "place | London, UK | latitude 51.51 | time zone Europe/London\nplan_from | 2026-10-26\nthis_week | 2026-10-26 | sunrise 06:43 | sunset 16:46 | daylight 10 h 03 min\ndarkest_week | 2026-12-28 | sunrise 08:06 | sunset 15:58 | daylight 7 h 52 min\nwork_days | mon tue wed thu fri\ndays_off | sat sun\nworkday_window | morning | 9 min | 08:06-08:15\nworkday_window | commute | 0 min | indoors\nworkday_window | lunch | 30 min | 12:30-13:00\nworkday_window | evening | 0 min | in the dark\nworkday_total | 39 min\ndayoff_window | dayoff | 472 min | 08:06-15:58\nsnacks_that_fit | 11\ntarget_snacks | 10"}
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", place, plan_from, schedule, goal, likes, limits, 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 daylight-snacks:<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="daylight-snacks: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 = "daylight-snacks:" + 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 = `daylight-snacks:${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 := "daylight-snacks:" + 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 = "daylight-snacks:" + 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 = "daylight-snacks:#{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 = "daylight-snacks:" . $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 = $"daylight-snacks:{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: daylight-snacks: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", "place": "London, UK", "plan_from": "2026-10-26", "schedule": "Works mon, tue, wed, thu, fri; days off sat, sun\nUp at 06:45, bed at 23:00\nWork 09:00-17:30\nCommute 45 min each way, indoors (car, bus or train)\nLunch break 12:30 for 30 min", "goal": "10", "likes": "Walking, a park bench with a sandwich, the allotment on days off", "limits": "Two kids to get to school before the train", "page_facts": "place | London, UK | latitude 51.51 | time zone Europe/London\nplan_from | 2026-10-26\nthis_week | 2026-10-26 | sunrise 06:43 | sunset 16:46 | daylight 10 h 03 min\ndarkest_week | 2026-12-28 | sunrise 08:06 | sunset 15:58 | daylight 7 h 52 min\nwork_days | mon tue wed thu fri\ndays_off | sat sun\nworkday_window | morning | 9 min | 08:06-08:15\nworkday_window | commute | 0 min | indoors\nworkday_window | lunch | 30 min | 12:30-13:00\nworkday_window | evening | 0 min | in the dark\nworkday_total | 39 min\ndayoff_window | dayoff | 472 min | 08:06-15:58\nsnacks_that_fit | 11\ntarget_snacks | 10"}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "daylight-snacks: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": `daylight-snacks: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", "daylight-snacks: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", "daylight-snacks: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" => "daylight-snacks: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: daylight-snacks: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", $"daylight-snacks: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);