← First Frost
API
Tokens

Drive First Frost from your own code

The metered reply is available over HTTP. It takes one cold week - the units, where your things are, each night with its forecast low, sky, wind and the page's ground estimate, each thing outside with its kind, threshold and the nights it crosses it, and the page's figures - and returns one JSON object: a verdict, a title and summary, a note for tonight, an alert rule per thing, a card per night, a kit list and what to watch.

The division of labour first: the page owns the arithmetic and the rule; the model owns the words; the page re-checks the reply. The ground estimates, the thresholds and the verdict rule come from frostkit.js, which you can load in node to build the input exactly as the page does (see Build the input yourself).

Get a token on the token page (step 2 below also shows how to mint a guest token in code).

Base URL and the envelope

Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response uses one envelope:

{ "ok": true,  "data":  { ... } }
{ "ok": false, "error": { "code": "...", "message": "...", "status": 402, "details": { ... } } }

The token is minted for this app (the guest call takes {"slug":"first-frost"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or a required field (units, unit_symbol, nights, items, figures) is missing.
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: the reply is metered and needs a personal token.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. Retry with the same Idempotency-Key so a retry never double-bills.

The verdict rule

There is one task, so no task field is sent. The verdict is decided by a fixed rule on the page's figures, checked in this order - the first that applies wins:

  1. act_tonight - some thing's act_on contains the first night.
  2. act_this_week - some thing's act_on is not empty.
  3. all_clear - everything else.

A thing's act_on lists the nights whose ground estimate is at or below its threshold. The ground estimate is the forecast low plus rough adjustments, in °C (double them in °F): clear -2, partly cloudy -1, cloudy 0; calm -1, light wind 0, breezy +1, windy +2; low spot -1, open garden 0, sheltered courtyard or balcony +1. Thresholds: frost-tender 2°C (36°F), car glass 1°C (34°F), half-hardy and unrecognised 0°C (32°F), water -1°C (30°F), pots -2°C (28°F), hardy -4°C (25°F). These are estimates, not a forecast. The declared input schema carries scalars only, so the array- and object-valued fields are JSON-encoded strings.

Input fields

FieldTypeNotes
units, unit_symbolstringC or F, and °C or °F: the unit of every temperature. Required.
spot, spot_labelstringopen, low, sheltered or balcony, and its plain label.
nightsstring (JSON)Up to 7 nights, in order, the first is tonight: [{night, low, sky, wind, ground, risk}]; risk is hard freeze, frost, frost possible or no frost. Required.
itemsstring (JSON)Up to 8 things outside, in order: [{item, kind, kind_label, threshold, act_on}]; kind is tender, half, hardy, water, pot, car or unsure. Required.
figuresstring (JSON){verdict, first_alert, coldest_night, coldest_ground, frost_nights, items_hit, items_total, nights_total}, every value a string. Required.
extrastringYour note (a limit, like a planter too heavy to lift), or empty; past 400 characters the opening and closing are kept with the middle marked [...].
clip_note, retry_notestringWhat the page cut, when it cut something; and only on a reformat retry, what was wrong.

The reply

The job's output.output is a string holding one JSON object: verdict (act_tonight | act_this_week | all_clear), title, summary, tonight, rules (one {item, when, action, undo} per thing, in order, the name copied exactly, each when stating its threshold), nights (one {night, card} per night, in order), kit (up to 6 supplies; empty when all clear) and watch_note. The page re-checks every reply: the verdict against the rule, one rule per thing and one card per night, every temperature one of the input's lows, ground estimates or thresholds and in the input's unit, no number that is not in the input, no weekday, month, season or place the input does not name, no first person, and no internal field names in the prose.

Worked examples

Replies produced against the app's prompt for the four sample weeks (made up; the full replies ship in example-result.js). Together they cover every verdict and every kind of thing.

Back garden - act tonight, an open garden in Celsius

Input:

{
 "units": "C",
 "unit_symbol": "°C",
 "spot": "open",
 "spot_label": "an open garden or yard",
 "nights": "[{\"night\":\"Tue\",\"low\":3,\"sky\":\"clear\",\"wind\":\"calm\",\"ground\":0,\"risk\":\"frost\"},{\"night\":\"Wed\",\"low\":1,\"sky\":\"clear\",\"wind\":\"calm\",\"ground\":-2,\"risk\":\"frost\"},{\"night\":\"Thu\",\"low\":4,\"sky\":\"cloudy\",\"wind\":\"breezy\",\"ground\":5,\"risk\":\"no frost\"},{\"night\":\"Fri\",\"low\":6,\"sky\":\"cloudy\",\"wind\":\"windy\",\"ground\":8,\"risk\":\"no frost\"},{\"night\":\"Sat\",\"low\":2,\"sky\":\"partly cloudy\",\"wind\":\"calm\",\"ground\":0,\"risk\":\"frost\"},{\"night\":\"Sun\",\"low\":-1,\"sky\":\"clear\",\"wind\":\"calm\",\"ground\":-4,\"risk\":\"hard freeze\"},{\"night\":\"Mon\",\"low\":1,\"sky\":\"clear\",\"wind\":\"light wind\",\"ground\":-1,\"risk\":\"frost\"}]",
 "items": "[{\"item\":\"Dahlias in the border\",\"kind\":\"tender\",\"kind_label\":\"a frost-tender plant (any frost can kill it)\",\"threshold\":2,\"act_on\":[\"Tue\",\"Wed\",\"Sat\",\"Sun\",\"Mon\"]},{\"item\":\"Potted lemon tree\",\"kind\":\"tender\",\"kind_label\":\"a frost-tender plant (any frost can kill it)\",\"threshold\":2,\"act_on\":[\"Tue\",\"Wed\",\"Sat\",\"Sun\",\"Mon\"]},{\"item\":\"Salad bed\",\"kind\":\"half\",\"kind_label\":\"a half-hardy plant (copes with a touch of frost, not a real one)\",\"threshold\":0,\"act_on\":[\"Tue\",\"Wed\",\"Sat\",\"Sun\",\"Mon\"]},{\"item\":\"Garden hose\",\"kind\":\"water\",\"kind_label\":\"water outside (hoses, taps and pipes can freeze and split)\",\"threshold\":-1,\"act_on\":[\"Wed\",\"Sun\",\"Mon\"]},{\"item\":\"Outdoor tap\",\"kind\":\"water\",\"kind_label\":\"water outside (hoses, taps and pipes can freeze and split)\",\"threshold\":-1,\"act_on\":[\"Wed\",\"Sun\",\"Mon\"]},{\"item\":\"Terracotta pots\",\"kind\":\"pot\",\"kind_label\":\"a pot or planter (wet clay or glaze can crack in a hard frost)\",\"threshold\":-2,\"act_on\":[\"Wed\",\"Sun\"]}]",
 "figures": "{\"verdict\":\"act_tonight\",\"first_alert\":\"Tue\",\"coldest_night\":\"Sun\",\"coldest_ground\":\"-4\",\"frost_nights\":\"5\",\"items_hit\":\"6\",\"items_total\":\"6\",\"nights_total\":\"7\"}",
 "extra": ""
}

Reply:

{
 "verdict": "act_tonight",
 "title": "Cover the dahlias and lemon tree before the frost",
 "summary": "Sun is the coldest night, with a ground estimate of -4°C, and frost risk continues through the week. Something on this list needs doing before dark tonight.",
 "tonight": "Before dark, cover the dahlias and salad bed with horticultural fleece or sheets, and bring the potted lemon tree indoors or somewhere sheltered. Uncover the dahlias and salad bed in the morning once the frost has gone.",
 "rules": [
  {
   "item": "Dahlias in the border",
   "when": "Any night the ground estimate is 2°C or lower.",
   "action": "Cover the plants with horticultural fleece, old sheets or cardboard, kept off the leaves.",
   "undo": "Uncover in the morning once the frost has gone."
  },
  {
   "item": "Potted lemon tree",
   "when": "Any night the ground estimate is 2°C or lower.",
   "action": "Bring it indoors or somewhere sheltered before dark.",
   "undo": "Put it back outside once the frost has gone."
  },
  {
   "item": "Salad bed",
   "when": "Any night the ground estimate is 0°C or lower.",
   "action": "Cover the bed with horticultural fleece, old sheets or cardboard, kept off the leaves.",
   "undo": "Uncover in the morning once the frost has gone."
  },
  {
   "item": "Garden hose",
   "when": "Any night the ground estimate is -1°C or lower.",
   "action": "Disconnect, drain and coil it, then put it away.",
   "undo": "Leave it drained and stored until the cold spell is over."
  },
  {
   "item": "Outdoor tap",
   "when": "Any night the ground estimate is -1°C or lower.",
   "action": "Turn it off at the indoor valve, drain it, then wrap it with an insulating cover.",
   "undo": "Leave it drained until the cold spell is over."
  },
  {
   "item": "Terracotta pots",
   "when": "Any night the ground estimate is -2°C or lower.",
   "action": "Group them against a house wall and lift them off the ground on bricks or pot feet.",
   "undo": "Move them back once the cold spell is over."
  }
 ],
 "nights": [
  {
   "night": "Tue",
   "card": "Frost expected, ground estimate 0°C: cover the dahlias and salad bed, and bring the lemon tree indoors or somewhere sheltered."
  },
  {
   "night": "Wed",
   "card": "Frost expected, ground estimate -2°C: cover the dahlias and salad bed, bring the lemon tree inside, drain the hose and tap, and move the pots to a wall."
  },
  {
   "night": "Thu",
   "card": "No frost expected, ground estimate 5°C. Nothing on this list needs doing."
  },
  {
   "night": "Fri",
   "card": "No frost expected, ground estimate 8°C. Nothing on this list needs doing."
  },
  {
   "night": "Sat",
   "card": "Frost expected, ground estimate 0°C: cover the dahlias and salad bed, and bring the lemon tree indoors or somewhere sheltered."
  },
  {
   "night": "Sun",
   "card": "Hard freeze expected, ground estimate -4°C: cover the dahlias and salad bed, bring the lemon tree inside, drain the hose and tap, and move the pots to a wall."
  },
  {
   "night": "Mon",
   "card": "Frost expected, ground estimate -1°C: cover the dahlias and salad bed, bring the lemon tree inside, and drain the hose and tap."
  }
 ],
 "kit": [
  "Horticultural fleece or old sheets",
  "Pot feet or bricks",
  "An insulating tap cover"
 ],
 "watch_note": "These ground estimates are rough rules of thumb: check the forecast again before Sun, since a clear, calm night can run colder than forecast."
}

Valley yard - act tonight, a low spot in Fahrenheit with a hard freeze and planters too heavy to lift

Input:

{
 "units": "F",
 "unit_symbol": "°F",
 "spot": "low",
 "spot_label": "a low spot where cold air settles",
 "nights": "[{\"night\":\"Tonight\",\"low\":30,\"sky\":\"clear\",\"wind\":\"calm\",\"ground\":22,\"risk\":\"hard freeze\"},{\"night\":\"Fri\",\"low\":27,\"sky\":\"clear\",\"wind\":\"light wind\",\"ground\":21,\"risk\":\"hard freeze\"},{\"night\":\"Sat\",\"low\":33,\"sky\":\"cloudy\",\"wind\":\"breezy\",\"ground\":33,\"risk\":\"frost possible\"},{\"night\":\"Sun\",\"low\":35,\"sky\":\"partly cloudy\",\"wind\":\"light wind\",\"ground\":31,\"risk\":\"frost\"},{\"night\":\"Mon\",\"low\":39,\"sky\":\"cloudy\",\"wind\":\"windy\",\"ground\":41,\"risk\":\"no frost\"},{\"night\":\"Tue\",\"low\":34,\"sky\":\"clear\",\"wind\":\"calm\",\"ground\":26,\"risk\":\"hard freeze\"},{\"night\":\"Wed\",\"low\":36,\"sky\":\"partly cloudy\",\"wind\":\"breezy\",\"ground\":34,\"risk\":\"frost possible\"}]",
 "items": "[{\"item\":\"Chard and lettuce\",\"kind\":\"half\",\"kind_label\":\"a half-hardy plant (copes with a touch of frost, not a real one)\",\"threshold\":32,\"act_on\":[\"Tonight\",\"Fri\",\"Sun\",\"Tue\"]},{\"item\":\"Kale\",\"kind\":\"hardy\",\"kind_label\":\"a hardy plant (only a hard freeze hurts it)\",\"threshold\":25,\"act_on\":[\"Tonight\",\"Fri\"]},{\"item\":\"Drip irrigation line\",\"kind\":\"water\",\"kind_label\":\"water outside (hoses, taps and pipes can freeze and split)\",\"threshold\":30,\"act_on\":[\"Tonight\",\"Fri\",\"Tue\"]},{\"item\":\"Car windshield\",\"kind\":\"car\",\"kind_label\":\"car glass (frosts over on cold, clear nights)\",\"threshold\":34,\"act_on\":[\"Tonight\",\"Fri\",\"Sat\",\"Sun\",\"Tue\",\"Wed\"]},{\"item\":\"Ceramic planters\",\"kind\":\"pot\",\"kind_label\":\"a pot or planter (wet clay or glaze can crack in a hard frost)\",\"threshold\":28,\"act_on\":[\"Tonight\",\"Fri\",\"Tue\"]}]",
 "figures": "{\"verdict\":\"act_tonight\",\"first_alert\":\"Tonight\",\"coldest_night\":\"Fri\",\"coldest_ground\":\"21\",\"frost_nights\":\"6\",\"items_hit\":\"5\",\"items_total\":\"5\",\"nights_total\":\"7\"}",
 "extra": "The ceramic planters are too heavy to lift."
}

Reply:

{
 "verdict": "act_tonight",
 "title": "Cover plants, pots and the windshield before the freeze",
 "summary": "Fri has the coldest ground estimate at 21°F, and the week ahead brings frost risk on most nights. Something needs doing before dark tonight to protect everything listed.",
 "tonight": "Tonight: cover the chard and lettuce, kale and ceramic planters where they stand, drain the drip irrigation line, and cover the car windshield. Uncover the plants, planters and windshield in the morning once frost has gone.",
 "rules": [
  {
   "item": "Chard and lettuce",
   "when": "Any night the ground estimate is 32°F or lower",
   "action": "Cover with horticultural fleece, old sheets, blankets or cardboard, kept off the leaves.",
   "undo": "Uncover in the morning once the frost has gone."
  },
  {
   "item": "Kale",
   "when": "Any night the ground estimate is 25°F or lower",
   "action": "Cover with horticultural fleece, old sheets, blankets or cardboard, kept off the leaves.",
   "undo": "Uncover in the morning once the frost has gone."
  },
  {
   "item": "Drip irrigation line",
   "when": "Any night the ground estimate is 30°F or lower",
   "action": "Drain the drip line to empty it.",
   "undo": "Leave it drained until the cold spell is over."
  },
  {
   "item": "Car windshield",
   "when": "Any night the ground estimate is 34°F or lower",
   "action": "Cover the windshield with a windscreen cover, cardboard or an old sheet.",
   "undo": "Remove the cover in the morning."
  },
  {
   "item": "Ceramic planters",
   "when": "Any night the ground estimate is 28°F or lower",
   "action": "Cover or wrap the planters where they stand; do not try to move them.",
   "undo": "Uncover or unwrap them once the frost has gone."
  }
 ],
 "nights": [
  {
   "night": "Tonight",
   "card": "Cover the chard and lettuce, kale and planters where they stand, drain the drip line, and cover the windshield before dark."
  },
  {
   "night": "Fri",
   "card": "Cover the chard and lettuce, kale and planters where they stand, drain the drip line, and cover the windshield again."
  },
  {
   "night": "Sat",
   "card": "Cover the windshield; frost is possible on this night."
  },
  {
   "night": "Sun",
   "card": "Cover the chard and lettuce, and cover the windshield; frost is expected."
  },
  {
   "night": "Mon",
   "card": "Nothing needs doing; no frost is expected this night."
  },
  {
   "night": "Tue",
   "card": "Cover the chard and lettuce and the planters where they stand, drain the drip line again, and cover the windshield."
  },
  {
   "night": "Wed",
   "card": "Cover the windshield again; frost is possible on this night."
  }
 ],
 "kit": [
  "Horticultural fleece",
  "Old sheets or blankets",
  "Cardboard sheets",
  "A windscreen cover"
 ],
 "watch_note": "These ground estimates are rough rules of thumb. Check the forecast again before Fri, the coldest night, since a clear, calm night can run colder than forecast."
}

Balcony - act later this week, two frost-possible nights

Input:

{
 "units": "F",
 "unit_symbol": "°F",
 "spot": "balcony",
 "spot_label": "a balcony or roof terrace above the ground",
 "nights": "[{\"night\":\"Thu\",\"low\":46,\"sky\":\"cloudy\",\"wind\":\"light wind\",\"ground\":48,\"risk\":\"no frost\"},{\"night\":\"Fri\",\"low\":44,\"sky\":\"partly cloudy\",\"wind\":\"light wind\",\"ground\":44,\"risk\":\"no frost\"},{\"night\":\"Sat\",\"low\":41,\"sky\":\"clear\",\"wind\":\"breezy\",\"ground\":41,\"risk\":\"no frost\"},{\"night\":\"Sun\",\"low\":38,\"sky\":\"clear\",\"wind\":\"calm\",\"ground\":34,\"risk\":\"frost possible\"},{\"night\":\"Mon\",\"low\":37,\"sky\":\"partly cloudy\",\"wind\":\"calm\",\"ground\":35,\"risk\":\"frost possible\"},{\"night\":\"Tue\",\"low\":42,\"sky\":\"cloudy\",\"wind\":\"breezy\",\"ground\":46,\"risk\":\"no frost\"},{\"night\":\"Wed\",\"low\":45,\"sky\":\"cloudy\",\"wind\":\"windy\",\"ground\":51,\"risk\":\"no frost\"}]",
 "items": "[{\"item\":\"Basil pots\",\"kind\":\"tender\",\"kind_label\":\"a frost-tender plant (any frost can kill it)\",\"threshold\":36,\"act_on\":[\"Sun\",\"Mon\"]},{\"item\":\"Geraniums in window boxes\",\"kind\":\"tender\",\"kind_label\":\"a frost-tender plant (any frost can kill it)\",\"threshold\":36,\"act_on\":[\"Sun\",\"Mon\"]},{\"item\":\"Rosemary\",\"kind\":\"hardy\",\"kind_label\":\"a hardy plant (only a hard freeze hurts it)\",\"threshold\":25,\"act_on\":[]},{\"item\":\"Small olive tree\",\"kind\":\"hardy\",\"kind_label\":\"a hardy plant (only a hard freeze hurts it)\",\"threshold\":25,\"act_on\":[]}]",
 "figures": "{\"verdict\":\"act_this_week\",\"first_alert\":\"Sun\",\"coldest_night\":\"Sun\",\"coldest_ground\":\"34\",\"frost_nights\":\"2\",\"items_hit\":\"2\",\"items_total\":\"4\",\"nights_total\":\"7\"}",
 "extra": ""
}

Reply:

{
 "verdict": "act_this_week",
 "title": "Cover the basil and geraniums before Sun and Mon",
 "summary": "The coldest night is Sun, with a ground estimate of 34°F, and the week does bring a frost risk. Nothing needs doing tonight, but get ready before Sun, when protection is needed.",
 "tonight": "Nothing needs doing tonight; the first night that needs action is Sun.",
 "rules": [
  {
   "item": "Basil pots",
   "when": "Any night the ground estimate is 36°F or lower.",
   "action": "Bring the pots indoors or somewhere sheltered before dark.",
   "undo": "Bring back out once the frost risk has passed."
  },
  {
   "item": "Geraniums in window boxes",
   "when": "Any night the ground estimate is 36°F or lower.",
   "action": "Cover it with horticultural fleece, an old sheet or cardboard before dark.",
   "undo": "Take the cover off in the morning once the frost has gone."
  },
  {
   "item": "Rosemary",
   "when": "Any night the ground estimate is 25°F or lower.",
   "action": "Cover it with horticultural fleece or an old sheet before a hard freeze.",
   "undo": "Take the cover off once the frost has gone."
  },
  {
   "item": "Small olive tree",
   "when": "Any night the ground estimate is 25°F or lower.",
   "action": "Cover it with horticultural fleece or an old sheet before a hard freeze.",
   "undo": "Take the cover off once the frost has gone."
  }
 ],
 "nights": [
  {
   "night": "Thu",
   "card": "No frost expected on Thu; nothing needs doing for the plants."
  },
  {
   "night": "Fri",
   "card": "No frost expected on Fri; nothing needs doing."
  },
  {
   "night": "Sat",
   "card": "No frost expected on Sat; nothing needs doing."
  },
  {
   "night": "Sun",
   "card": "Frost is possible on Sun; bring Basil pots indoors or somewhere sheltered, and cover Geraniums in window boxes with fleece or a sheet."
  },
  {
   "night": "Mon",
   "card": "Frost is possible on Mon too; bring Basil pots indoors or somewhere sheltered, and keep Geraniums in window boxes covered."
  },
  {
   "night": "Tue",
   "card": "No frost expected on Tue; nothing needs doing."
  },
  {
   "night": "Wed",
   "card": "No frost expected on Wed; nothing needs doing."
  }
 ],
 "kit": [
  "horticultural fleece or an old sheet"
 ],
 "watch_note": "These ground estimates are rough rules of thumb; check the forecast again before Sun, since a clear, calm night can run colder than expected."
}

Courtyard - all clear, one night close to a threshold

Input:

{
 "units": "C",
 "unit_symbol": "°C",
 "spot": "sheltered",
 "spot_label": "a sheltered courtyard or a bed against a house wall",
 "nights": "[{\"night\":\"Mon\",\"low\":8,\"sky\":\"cloudy\",\"wind\":\"breezy\",\"ground\":10,\"risk\":\"no frost\"},{\"night\":\"Tue\",\"low\":7,\"sky\":\"cloudy\",\"wind\":\"light wind\",\"ground\":8,\"risk\":\"no frost\"},{\"night\":\"Wed\",\"low\":9,\"sky\":\"cloudy\",\"wind\":\"windy\",\"ground\":12,\"risk\":\"no frost\"},{\"night\":\"Thu\",\"low\":6,\"sky\":\"partly cloudy\",\"wind\":\"light wind\",\"ground\":6,\"risk\":\"no frost\"},{\"night\":\"Fri\",\"low\":5,\"sky\":\"clear\",\"wind\":\"calm\",\"ground\":3,\"risk\":\"no frost\"},{\"night\":\"Sat\",\"low\":7,\"sky\":\"partly cloudy\",\"wind\":\"breezy\",\"ground\":8,\"risk\":\"no frost\"},{\"night\":\"Sun\",\"low\":8,\"sky\":\"cloudy\",\"wind\":\"calm\",\"ground\":8,\"risk\":\"no frost\"}]",
 "items": "[{\"item\":\"Pelargoniums\",\"kind\":\"tender\",\"kind_label\":\"a frost-tender plant (any frost can kill it)\",\"threshold\":2,\"act_on\":[]},{\"item\":\"Tomato plants in grow bags\",\"kind\":\"tender\",\"kind_label\":\"a frost-tender plant (any frost can kill it)\",\"threshold\":2,\"act_on\":[]},{\"item\":\"Water butt\",\"kind\":\"water\",\"kind_label\":\"water outside (hoses, taps and pipes can freeze and split)\",\"threshold\":-1,\"act_on\":[]},{\"item\":\"Glazed pots\",\"kind\":\"pot\",\"kind_label\":\"a pot or planter (wet clay or glaze can crack in a hard frost)\",\"threshold\":-2,\"act_on\":[]}]",
 "figures": "{\"verdict\":\"all_clear\",\"first_alert\":\"\",\"coldest_night\":\"Fri\",\"coldest_ground\":\"3\",\"frost_nights\":\"0\",\"items_hit\":\"0\",\"items_total\":\"4\",\"nights_total\":\"7\"}",
 "extra": ""
}

Reply:

{
 "verdict": "all_clear",
 "title": "No frost action needed this week",
 "summary": "The coldest night is Fri, with a ground estimate of 3°C, and the week shows no frost risk. Nothing you listed needs protecting on any night entered.",
 "tonight": "Nothing needs doing tonight, or on any of the other nights entered this week.",
 "rules": [
  {
   "item": "Pelargoniums",
   "when": "Any night the ground estimate is 2°C or lower",
   "action": "Cover with horticultural fleece, old sheets, blankets or cardboard, kept off the leaves.",
   "undo": "Uncover in the morning once the frost has gone."
  },
  {
   "item": "Tomato plants in grow bags",
   "when": "Any night the ground estimate is 2°C or lower",
   "action": "Cover with horticultural fleece, old sheets, blankets or cardboard, kept off the leaves.",
   "undo": "Uncover in the morning once the frost has gone."
  },
  {
   "item": "Water butt",
   "when": "Any night the ground estimate is -1°C or lower",
   "action": "Drain the water butt so it cannot freeze and split.",
   "undo": "Leave it drained until the cold spell is over."
  },
  {
   "item": "Glazed pots",
   "when": "Any night the ground estimate is -2°C or lower",
   "action": "Bring them indoors or somewhere sheltered, or group them against a house wall on bricks.",
   "undo": "Bring them back once the frost has gone."
  }
 ],
 "nights": [
  {
   "night": "Mon",
   "card": "No frost risk, and the ground estimate of 10°C means nothing needs doing."
  },
  {
   "night": "Tue",
   "card": "No frost risk, and the ground estimate of 8°C means nothing needs doing."
  },
  {
   "night": "Wed",
   "card": "No frost risk, and the ground estimate of 12°C means nothing needs doing."
  },
  {
   "night": "Thu",
   "card": "No frost risk, and the ground estimate of 6°C means nothing needs doing."
  },
  {
   "night": "Fri",
   "card": "No frost risk, and the ground estimate of 3°C means nothing needs doing."
  },
  {
   "night": "Sat",
   "card": "No frost risk, and the ground estimate of 8°C means nothing needs doing."
  },
  {
   "night": "Sun",
   "card": "No frost risk, and the ground estimate of 8°C means nothing needs doing."
  }
 ],
 "kit": [],
 "watch_note": "The ground estimates are rough rules of thumb. Check the forecast again before Fri, the coldest night, since a clear, calm night can run colder than forecast."
}

Build the input yourself

Load the script in node (global.window = global; require("./frostkit.js")), then:

const K = FrostKit;
const form = K.form({ units: "C", spot: "open", start: "2026-10-27",
  nights: "Tue 3 clear calm\nWed 1 partly cloudy light wind\nThu 4 cloudy breezy",
  items: "Dahlias in the border\nOutdoor tap\nTerracotta pots", extra: "" });
const r = K.read(form);                            // r.nights (ground, risk), r.items (kind, threshold, act_on), r.fig.verdict
const built = K.buildInput(form);
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 reply
// K.csv(built, form), K.ics(built, reply, form)  // the bed as a table and the reminders

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://first-frost.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"}
}

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":"first-frost"}' | jq -r '.data.token')

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}' 

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 - {"units": "C", "unit_symbol": "°C", "spot": "open", "spot_label": "an open garden or yard", "nights": "[...]", "items": "[...]", "figures": "{...}", "extra": ""}
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}' 

Step 5 — POST /run, then poll the job

Send an Idempotency-Key built from the input: a retry with the same key never bills twice. The page uses first-frost:plan:<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="first-frost: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}' 

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: first-frost:plan:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json