← Last In, First Out
API

Drive Last In, First Out from your own code

The one metered task, load, is available over HTTP. It takes a car, the stops in driving order and the things to carry - each already given its size, weight, stop, flags, spot in the boot and reason by the page - plus the page's plan. It returns loading notes: the verdict, a headline, one line a thing in the order to load it, a line for each stop, up to three things worth a second look and one tip.

The division of labour first: the page plans; the model words it. The page reads the list, counts the litres against the car (typical boot sizes - estimates - or your own figure, with 80% counted as usable), orders the load (the last stop in first; within a stop heavy first, fragile last), places every thing (against the back seats, in the middle or by the opening; on the floor or on top), moves things needed on the way to the cabin, splits off a second trip when the car is too small, and picks the verdict and the tip. It sends all of that as things and facts; the model never changes them. After the reply, the page lists anything that does not match. A packing aid, not professional 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": "..." } }

The token is minted for this app (the guest call takes {"slug":"last-in-first-out"}), 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 (task, trip, things, facts) 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 loading notes are metered and needs a personal sign-in.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. Retry with the same Idempotency-Key so a retry never double-bills.

The task field, the input and the rules

Every run sends task: load (the one task is load); a missing or different one is still answered as load. All fields are strings.

FieldRequiredWhat it holds
taskyesAlways load.
tripyeswhat the trip is | car: ... | back seats: .... The trip is not named when empty (up to 60 characters); the car part names the car type and its boot litres with the seats up and folded, marked as estimates; back seats are people riding, they stay up or free, they can fold.
thingsyesOne thing a line, as typed: N. name | size | weight | stop S: stop name | flags | spot: ... | reason: .... Size is S, M, L, XL (about 10, 30, 70 and 150 litres), litres or centimetres; weight is light, medium or heavy; flags are fragile, upright, on the way or no flags. Two to twenty-four things.
factsyesThe page's plan: the verdict, the stops in driving order, the usable space, the load in litres and how full, the load order (first in to last in), what comes out at each stop, the second trip, the cabin, the heavy things away from the back seats, what the tip is about and the safety line. Build it with LoadKit.buildInput.
retry_notenoOnly on a reformat retry: why the previous reply was unusable.

The verdict, checked in this order: two-trips when the load is bigger than the space the car can give in one go (folded seats count only when the back seats are free); else fold-the-seats when it does not fit with the back seats up but fits folded; else wedge-the-heavy when a heavy thing for an earlier stop cannot sit against the back seats; else straight-in.

The reply

One JSON object, no prose around it:

{"task": "load", "verdict": "straight-in", "headline": "...", "steps": [{"n": 3, "say": "..."}], "stops": [{"stop": 1, "say": "..."}], "worth_a_look": [{"n": 2, "kind": "fragile", "why": "..."}], "tip": "..."}
FieldWhat it holds
verdicttwo-trips, fold-the-seats, wedge-the-heavy or straight-in - always the page's.
headlineOne sentence for the driver that matches the verdict: the things that wait for the second trip, folding the back seats first, the heavy thing that rides low and wedged, or that it all goes straight in.
stepsOne {"n": int, "say": "..."} per thing in the page's load order, in that order, and nothing from the cabin or the second trip. say starts with a verb, names the thing, says where it goes in the words of its spot and gives the page's reason, in at most 30 words.
stopsOne {"stop": int, "say": "..."} per stop where something comes out, in driving order: the stop's name, then every thing that comes out there, boot things first and cabin things after.
worth_a_lookZero to three {"n": int, "kind": "fragile | upright | heavy", "why": "..."} for boot things whose own name names glass, china, a liquid that pours, stone, books and the like while their line does not already say so.
tipOne sentence for the driver about what the page says the tip is about (a heavy thing away from the back seats, upright things, fragile things on top, things for the way, or filling the gaps), naming the things it concerns.

The page reads the reply with ReplyKit.parseReply and ReplyKit.normalize, then re-checks it against its own plan and lists, under "The page's check", a verdict that differs from the page's, a thing left out of the steps, loaded twice or out of order, a cabin or second-trip thing loaded now, a stop with no line or a line that misses its things, a worth-a-look note for a thing already marked, a number or a day the list never gave, and a page label used in a sentence.

Worked examples - every verdict

These are the free samples on the page: the exact input the page sends for each and the loading notes that replay for free. Together they cover every verdict - two trips, fold the seats, wedge the heavy and straight in.

Saturday drop-offs - wedge-the-heavy

Input:

{
 "task": "load",
 "trip": "Saturday drop-offs | car: Family hatchback, boot about 380 litres with the back seats up and 1250 folded (typical figures - estimates) | back seats: people riding, they stay up",
 "things": "1. Birthday cake tin | M (about 30 litres) | light | stop 1: Mum's | fragile | spot: by the boot opening, on top | reason: fragile\n2. Box of books | 40x30x30 cm (about 36 litres) | heavy (18 kg) | stop 2: charity shop | no flags | spot: in the middle, on the floor | reason: heavy, but comes out second, at charity shop, before the last stop\n3. Bag of clothes | L (about 70 litres) | light | stop 2: charity shop | no flags | spot: in the middle, on top | reason: comes out second, at charity shop\n4. Glass vase | S (about 10 litres) | light | stop 2: charity shop | no flags | spot: in the middle, on top | reason: comes out second, at charity shop\n5. Cool box | L (about 70 litres) | heavy | stop 3: home | upright | spot: against the back seats, on the floor, standing up | reason: heavy\n6. Potted fern | M (about 30 litres) | medium (weight not given) | stop 1: Mum's | no flags | spot: by the boot opening, on the floor | reason: comes out first, at Mum's\n7. Water bottles | S (about 10 litres) | medium (weight not given) | stop 3: home | on the way | spot: in the cabin, within reach | reason: needed on the way",
 "facts": "verdict: wedge-the-heavy\nstops in driving order: 1 Mum's; 2 charity shop; 3 home\nspace: back seats up, about 304 litres you can use (80% of the figure, for the gaps)\nload: 246 litres in the boot, 81% of the space you can use\nload order, first in to last in: 5, 2, 3, 4, 6, 1\ntake out at each stop: 1 Mum's: 6, 1; 2 charity shop: 2, 3, 4; 3 home: 5 (cabin: 7)\nsecond trip: none\ncabin, on the way: 7\nheavy away from the back seats: 2\ntip about: a heavy thing riding away from the back seats\nsafety: Heavy loose things keep moving in a sudden stop: keep them low against the back seats, never stack above the shelf or block the rear window, and stay under the car's load limit in the handbook."
}

Reply:

{
 "task": "load",
 "verdict": "wedge-the-heavy",
 "headline": "The box of books rides low and wedged, away from the back seats, because its stop, the charity shop, comes before the last stop, home.",
 "steps": [
  {
   "n": 5,
   "say": "Stand the cool box against the back seats, on the floor. It is heavy."
  },
  {
   "n": 2,
   "say": "Set the box of books in the middle, on the floor. It is heavy, but it comes out second, at the charity shop, before the last stop."
  },
  {
   "n": 3,
   "say": "Lay the bag of clothes in the middle, on top. It comes out second, at the charity shop."
  },
  {
   "n": 4,
   "say": "Slide the glass vase in the middle, on top. It comes out second, at the charity shop."
  },
  {
   "n": 6,
   "say": "Put the potted fern by the boot opening, on the floor. It comes out first, at Mum's."
  },
  {
   "n": 1,
   "say": "Set the birthday cake tin by the boot opening, on top. It is fragile."
  }
 ],
 "stops": [
  {
   "stop": 1,
   "say": "At Mum's, take out the potted fern and the birthday cake tin. They are nearest the boot opening by then."
  },
  {
   "stop": 2,
   "say": "At the charity shop, take out the box of books, the bag of clothes and the glass vase. They are nearest the boot opening by then."
  },
  {
   "stop": 3,
   "say": "At home, take out the cool box, nearest the boot opening by then, and bring in the water bottles, in the cabin."
  }
 ],
 "worth_a_look": [
  {
   "n": 4,
   "kind": "fragile",
   "why": "its name says glass"
  }
 ],
 "tip": "Keep the box of books flat on the floor and wedged so it cannot slide."
}

A camping weekend - fold-the-seats

Input:

{
 "task": "load",
 "trip": "Camping weekend | car: Small hatchback, boot about 280 litres with the back seats up and 950 folded (typical figures - estimates) | back seats: free, they can fold",
 "things": "1. Tent | XL (about 150 litres) | medium (12 kg) | stop 2: campsite | no flags | spot: against the back seats, on the floor | reason: comes out last, at campsite\n2. Sleeping bags | L (about 70 litres) | light | stop 2: campsite | no flags | spot: against the back seats, on top | reason: comes out last, at campsite\n3. Food crate | M (about 30 litres) | heavy | stop 2: campsite | no flags | spot: against the back seats, on the floor | reason: heavy\n4. Camping stove | S (about 10 litres) | medium | stop 2: campsite | no flags | spot: against the back seats, on the floor | reason: comes out last, at campsite\n5. Lantern | S (about 10 litres) | light | stop 2: campsite | fragile | spot: against the back seats, on top | reason: fragile\n6. Borrowed casserole dish | S (about 10 litres) | medium | stop 1: Gran's | no flags | spot: by the boot opening, on the floor | reason: comes out first, at Gran's\n7. Snacks | S (about 10 litres) | light | stop 2: campsite | on the way | spot: in the cabin, within reach | reason: needed on the way",
 "facts": "verdict: fold-the-seats\nstops in driving order: 1 Gran's; 2 campsite\nspace: back seats folded, about 760 litres you can use (80% of the figure, for the gaps)\nload: 280 litres in the boot, 37% of the space you can use\nload order, first in to last in: 3, 1, 4, 2, 5, 6\ntake out at each stop: 1 Gran's: 6; 2 campsite: 3, 1, 4, 2, 5 (cabin: 7)\nsecond trip: none\ncabin, on the way: 7\nheavy away from the back seats: none\ntip about: fragile things riding on top\nsafety: Heavy loose things keep moving in a sudden stop: keep them low against the back seats, never stack above the shelf or block the rear window, and stay under the car's load limit in the handbook."
}

Reply:

{
 "task": "load",
 "verdict": "fold-the-seats",
 "headline": "Fold the back seats down before loading - with the seats up, the load will not fit.",
 "steps": [
  {
   "n": 3,
   "say": "Set the food crate on the floor against the back seats - it is heavy."
  },
  {
   "n": 1,
   "say": "Lay the tent on the floor against the back seats - it comes out last, at the campsite."
  },
  {
   "n": 4,
   "say": "Put the camping stove on the floor against the back seats - it comes out last, at the campsite."
  },
  {
   "n": 2,
   "say": "Lay the sleeping bags on top, against the back seats - they come out last, at the campsite."
  },
  {
   "n": 5,
   "say": "Set the lantern on top, against the back seats - it is fragile."
  },
  {
   "n": 6,
   "say": "Put the borrowed casserole dish on the floor by the boot opening - it comes out first, at Gran's."
  }
 ],
 "stops": [
  {
   "stop": 1,
   "say": "At Gran's, take out the borrowed casserole dish - it is nearest the boot opening by then."
  },
  {
   "stop": 2,
   "say": "At the campsite, take out the food crate, the tent, the camping stove, the sleeping bags and the lantern - they are nearest the boot opening by then. Then take the snacks from the cabin."
  }
 ],
 "worth_a_look": [],
 "tip": "Nothing should go on top of the lantern, since it is fragile and rides on top."
}

Clearing the garage - two-trips

Input:

{
 "task": "load",
 "trip": "Clearing the garage | car: Estate, boot about 550 litres with the back seats up and 1600 folded (typical figures - estimates) | back seats: people riding, they stay up",
 "things": "1. Old paint tins | M (about 30 litres) | heavy | stop 1: tip | no flags | spot: by the boot opening, on the floor | reason: heavy, but comes out first, at tip, before the last stop\n2. Broken chair | XL (about 150 litres) | medium | stop 1: tip | no flags | spot: by the boot opening, on the floor | reason: comes out first, at tip\n3. Garden waste sacks | XL (about 150 litres) | medium | stop 1: tip | no flags | spot: by the boot opening, on the floor | reason: comes out first, at tip\n4. Box of toys | L (about 70 litres) | light | stop 2: charity shop | no flags | spot: in the middle, on the floor | reason: comes out second, at charity shop\n5. Table lamp | M (about 30 litres) | light | stop 2: charity shop | fragile | spot: in the middle, on top | reason: fragile\n6. Toolbox | M (about 30 litres) | heavy | stop 3: Dad's | no flags | spot: second trip | reason: waits for the second trip\n7. Bike helmet | S (about 10 litres) | light | stop 3: Dad's | no flags | spot: against the back seats, on the floor | reason: comes out last, at Dad's",
 "facts": "verdict: two-trips\nstops in driving order: 1 tip; 2 charity shop; 3 Dad's\nspace: back seats up, about 440 litres you can use (80% of the figure, for the gaps)\nload: 470 litres in the boot; trip one carries 440 litres, 100% of the space you can use\nload order, first in to last in: 7, 4, 5, 1, 2, 3\ntake out at each stop: 1 tip: 1, 2, 3; 2 charity shop: 4, 5; 3 Dad's: 7\nsecond trip: 6\ncabin, on the way: none\nheavy away from the back seats: 1\ntip about: a heavy thing riding away from the back seats\nsafety: Heavy loose things keep moving in a sudden stop: keep them low against the back seats, never stack above the shelf or block the rear window, and stay under the car's load limit in the handbook."
}

Reply:

{
 "task": "load",
 "verdict": "two-trips",
 "headline": "This takes two trips, and the toolbox waits for the second trip.",
 "steps": [
  {
   "n": 7,
   "say": "Put the bike helmet on the floor against the back seats. It comes out last, at Dad's."
  },
  {
   "n": 4,
   "say": "Set the box of toys on the floor in the middle. It comes out second, at the charity shop."
  },
  {
   "n": 5,
   "say": "Stand the table lamp in the middle, on top. It is fragile."
  },
  {
   "n": 1,
   "say": "Lay the old paint tins on the floor by the boot opening. They are heavy, but come out first, at the tip, before the last stop."
  },
  {
   "n": 2,
   "say": "Put the broken chair on the floor by the boot opening. It comes out first, at the tip."
  },
  {
   "n": 3,
   "say": "Slide the garden waste sacks onto the floor by the boot opening. They come out first, at the tip."
  }
 ],
 "stops": [
  {
   "stop": 1,
   "say": "At the tip, take out the old paint tins, the broken chair and the garden waste sacks, now nearest the boot opening."
  },
  {
   "stop": 2,
   "say": "At the charity shop, take out the box of toys and the table lamp, now nearest the boot opening."
  },
  {
   "stop": 3,
   "say": "At Dad's, take out the bike helmet, now nearest the boot opening."
  }
 ],
 "worth_a_look": [
  {
   "n": 1,
   "kind": "upright",
   "why": "names paint, which can spill"
  }
 ],
 "tip": "Keep the old paint tins flat on the floor and wedged in so they cannot slide."
}

A picnic in the park - straight-in

Input:

{
 "task": "load",
 "trip": "Picnic in the park | car: Small hatchback, boot about 280 litres with the back seats up and 950 folded (typical figures - estimates) | back seats: people riding, they stay up",
 "things": "1. Picnic basket | M (about 30 litres) | medium | stop 2: park | no flags | spot: against the back seats, on the floor | reason: comes out last, at park\n2. Folding chairs | L (about 70 litres) | light | stop 2: park | no flags | spot: against the back seats, on top | reason: comes out last, at park\n3. Blanket | M (about 30 litres) | light | stop 2: park | no flags | spot: against the back seats, on top | reason: comes out last, at park\n4. Cool bag | M (about 30 litres) | medium | stop 2: park | upright | spot: against the back seats, on the floor, standing up | reason: has to stay upright\n5. Borrowed drill | S (about 10 litres) | medium | stop 1: Sam's flat | no flags | spot: by the boot opening, on the floor | reason: comes out first, at Sam's flat\n6. Sun cream | S (about 10 litres) | light | stop 2: park | on the way | spot: in the cabin, within reach | reason: needed on the way",
 "facts": "verdict: straight-in\nstops in driving order: 1 Sam's flat; 2 park\nspace: back seats up, about 224 litres you can use (80% of the figure, for the gaps)\nload: 170 litres in the boot, 76% of the space you can use\nload order, first in to last in: 1, 4, 2, 3, 5\ntake out at each stop: 1 Sam's flat: 5; 2 park: 1, 4, 2, 3 (cabin: 6)\nsecond trip: none\ncabin, on the way: 6\nheavy away from the back seats: none\ntip about: keeping upright things standing\nsafety: Heavy loose things keep moving in a sudden stop: keep them low against the back seats, never stack above the shelf or block the rear window, and stay under the car's load limit in the handbook."
}

Reply:

{
 "task": "load",
 "verdict": "straight-in",
 "headline": "Everything fits with the back seats up, and each stop's things come out first.",
 "steps": [
  {
   "n": 1,
   "say": "Put the picnic basket against the back seats, on the floor. It comes out last, at the park."
  },
  {
   "n": 4,
   "say": "Stand the cool bag against the back seats, on the floor, standing up. It has to stay upright."
  },
  {
   "n": 2,
   "say": "Set the folding chairs against the back seats, on top. They come out last, at the park."
  },
  {
   "n": 3,
   "say": "Lay the blanket against the back seats, on top. It comes out last, at the park."
  },
  {
   "n": 5,
   "say": "Put the borrowed drill by the boot opening, on the floor. It comes out first, at Sam's flat."
  }
 ],
 "stops": [
  {
   "stop": 1,
   "say": "At Sam's flat, take out the borrowed drill. It is nearest the boot opening."
  },
  {
   "stop": 2,
   "say": "At the park, take out the picnic basket, the cool bag, the folding chairs and the blanket - they are nearest the boot opening now. Then take the sun cream from the cabin."
  }
 ],
 "worth_a_look": [],
 "tip": "Keep the cool bag standing, held firm by the things packed around it."
}

Build the input yourself

Load the engine in node (global.window = global; require("./loadkit.js"); require("./replykit.js")), then:

const K = LoadKit, R = ReplyKit;
const built = K.buildInput(K.form({ trip: "Library run", car: "small", boot: "", seats: "people", stops: "library, home",
  text: "Box of books | 18 kg | library
Shopping bags | M | light | home" }));  // built.ok is false, with built.note, until there are two things
const wire = K.toWire(built.input);   // POST this to /estimate and /run; built.analysis holds order, trip1, trip2, cabin, bands, verdict, fill, tip
// then: const reply = R.normalize(R.parseReply(job.output.output)); R.check(reply, built).notes lists any disagreement

Form keys: trip (up to 60 characters), car (small, family, estate, suv or mpv), boot (your own boot litres with the seats up, 100 to 3000, or empty for the typical figure), seats (people or free), stops (up to six, comma-separated, in driving order) and text (one thing a line, cells split by | or tabs, up to 3,000 characters). A thing with no size counts as M; with no stop it goes to the last stop.

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://last-in-first-out.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":"last-in-first-out"}' | 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 - {"task": "load", "trip": "Picnic in the park | car: Small hatchback, ...", "things": "1. Picnic basket | M (about 30 litres) | medium | stop 2: park | no flags | spot: ... | reason: ...", "facts": "...", ...}
# - see the field table
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 task and the input: a retry with the same key never bills twice. The page uses last-in-first-out:<task>:<inputHash>:a<attempt> (the one task is load), 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="last-in-first-out:load:$(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: last-in-first-out:load:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json