← Stick the Landing
API

Drive Stick the Landing from your own code

The one metered task, routine, is available over HTTP. It takes a tone (deadpan, warm or stern), an optional title and the routine, three to eight moves as n | move with the landing last. It returns every move's demands, value and a reason, the faults (move, size, lens, an exact quote and a note), the landing call with its quote and reason, a remark from each of the five judges, one tidy-up and a sign-off.

The division of labour first: the page numbers the moves and lists rough demand and mishap word hints; the model reads every move for its demands, marks faults and calls the landing; the page then does all the arithmetic (values, the connection bonus, five judges with the high and low dropped, the total), works each judge's move and the tidy-up move again, draws the beam and the cards, and re-checks the reply. The model never writes a score.

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":"stick-the-landing"}), 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, tone, title, moves, page_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: calling the panel 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 task field and the status

Every run sends task: routine (the one task is routine); a missing or different one is still answered as routine. The status field holds one of two ids, checked in this order: declined when a move names a specific real person, the feat is risky or unlawful (riding or driving while doing something else, no hands on a bike, heights, fire, knives, alcohol, drugs, anything that risks a person or an animal), a baby, a child or an animal is part of the feat, or it is sexual, hateful or about self-harm; otherwise ok.

For ok, every move gets its demands from five ids - carry, footing, both (at the same time, or a heel, hip, elbow, knee, teeth or chin doing a job), clock, aim - and a value from their count: none is A, one B, two C, three D, four or five E. Faults go only on moves before the landing, at most one per move, sized in order fall, large, medium, small, with a judge's lens (precision, form, rhythm, composure, else amplitude) and an exact quote. The landing is the first that fits the last move: fall, hop, step, else stuck.

The five judges sit in order E1 form, E2 rhythm, E3 precision, E4 composure, E5 commitment (amplitude); each judge's line is the lowest move with a fault in that lens, else 0. The tidy-up's line is the last move when the landing is not stuck, otherwise the move with the biggest fault (lowest move on a tie), otherwise 0 with an empty tip.

Input (every field is a string)

FieldRequiredWhat it holds
taskyesAlways routine.
toneyesdeadpan, warm or stern.
titleyesThe person's own name for the feat, or "" (then the model names it from the moves' words).
movesyes3 to 8 lines as n | move, numbered from 1; the last is the landing. Moves are cut at 100 characters, repeats dropped, leading bullets and numbers removed and pipes become slashes.
page_factsyesOne key | value per line: moves, landing_line, then the hints demand_hints, mishap_words, harm_words and capitalised_words.
clip_notenoWhat the page left out, if anything.
retry_notenoOnly on the page's one reformat retry, after an unusable reply.

Reply (one JSON object)

{"task": "routine", "status": "ok", "reason": "", "title": "...",
 "elements": [{"line": 1, "demands": ["carry"], "value": "B", "why": "..."}, ...],
 "faults": [{"line": 2, "size": "medium", "lens": "precision", "quote": "...", "note": "..."}, ...],
 "landing": "step", "landing_quote": "...", "landing_why": "...",
 "judges": [{"seat": "E1", "line": 0, "remark": "..."}, ...],
 "tidy": {"line": 6, "tip": "..."}, "closing": "..."}
FieldWhat it holds
status, reasonThe status by the order above; for declined, at most 30 words (empty for ok).
titleThe input title word for word, or 2 to 5 words whose longer words come from the moves.
elementsOne per move ([] when declined): line, demands, value and why (at most 14 words).
faultsIn move order: line, size, lens, an exact quote from that move and a note (at most 14 words).
landing, landing_quote, landing_whyThe call, an exact quote from the last move ("" when stuck) and at most 20 words of reason.
judgesExactly 5, seats E1 to E5: line by the rule above and a remark of at most 12 words.
tidy, closingThe tidy-up's move and tip (at most 20 words), and a sign-off of at most 16 words.

The reply carries no scores. The page computes them (LandKit.score): D is the sum of the values (A 0.1 to E 0.5) plus 0.1 for each neighbouring pair both valued C or more with no fault on either; each judge takes off 0.1, 0.3, 0.5 or 1.0 per fault by size, 0.1 more on a fault in their own lens (not on a fall), plus the landing (0, 0.1, 0.3 or 1.0); the highest and the lowest judge are dropped and E is 10 minus the mean of the other three. Then it checks the reply (LandKit.check): one element per move, each value against its demand count, faults only before the landing and quoted from their own move, the landing quote, every judge's move, the tidy-up move, the word limits, no score talk, no number the moves never give, no he or she and no internal field id in the prose. Anything that disagrees is listed under the reply; nothing is silently corrected.

Worked examples

Each example is the exact input the page sends for one of its five samples, and the saved reply it replays for free.

A tray of mugs - stuck

Input:

{
 "task": "routine",
 "tone": "warm",
 "title": "",
 "moves": "1 | picked up the tray with four full mugs of tea\n2 | pushed the kitchen door open with my hip\n3 | stepped over a shoe in the hallway\n4 | one mug sloshed onto the tray but nothing reached the floor\n5 | lowered the tray onto the low table and let go",
 "page_facts": "moves | 5\nlanding_line | 5\ndemand_hints | 1: carry; 2: both; 3: footing; 4: carry; 5: carry\nmishap_words | 1: none; 2: none; 3: none; 4: sloshed; 5: none\nharm_words | none\ncapitalised_words | none"
}

Reply:

{
 "task": "routine",
 "status": "ok",
 "reason": "",
 "title": "The Full Tray Lowered",
 "elements": [
  {
   "line": 1,
   "demands": [
    "carry"
   ],
   "value": "B",
   "why": "You picked up the tray with four full mugs - that's carrying."
  },
  {
   "line": 2,
   "demands": [
    "both"
   ],
   "value": "B",
   "why": "Using your hip, not your hand, to push the kitchen door open."
  },
  {
   "line": 3,
   "demands": [
    "footing"
   ],
   "value": "B",
   "why": "Stepping over the shoe in the hallway is the footing moment."
  },
  {
   "line": 4,
   "demands": [],
   "value": "A",
   "why": "This move's words name no demand - just the mug that sloshed."
  },
  {
   "line": 5,
   "demands": [
    "carry"
   ],
   "value": "B",
   "why": "Lowering the tray onto the low table is still carrying it."
  }
 ],
 "faults": [
  {
   "line": 4,
   "size": "medium",
   "lens": "precision",
   "quote": "sloshed onto the tray",
   "note": "The mug sloshed onto the tray, and nothing reached the floor."
  }
 ],
 "landing": "stuck",
 "landing_quote": "",
 "landing_why": "The tray is lowered onto the low table and let go, with no mishap named.",
 "judges": [
  {
   "seat": "E1",
   "line": 0,
   "remark": "This seat takes nothing off for body control."
  },
  {
   "seat": "E2",
   "line": 0,
   "remark": "This seat takes nothing off for the rhythm."
  },
  {
   "seat": "E3",
   "line": 4,
   "remark": "Move 4 shows a mug that sloshed onto the tray."
  },
  {
   "seat": "E4",
   "line": 0,
   "remark": "This seat takes nothing off for staying calm."
  },
  {
   "seat": "E5",
   "line": 0,
   "remark": "This seat takes nothing off for commitment."
  }
 ],
 "tidy": {
  "line": 4,
  "tip": "Keep the tray flatter so the mug moves less."
 },
 "closing": "That tray made it all the way to the low table."
}

The shopping in one trip - a step

Input:

{
 "task": "routine",
 "tone": "deadpan",
 "title": "",
 "moves": "1 | hooked six shopping bags over both arms at the car boot\n2 | shut the boot with my elbow\n3 | climbed the three front steps without stopping\n4 | fished the keys out of my pocket while holding every bag\n5 | the bags swung and I wobbled on the doormat\n6 | set every bag on the kitchen counter and took one small step back",
 "page_facts": "moves | 6\nlanding_line | 6\ndemand_hints | 1: carry; 2: both; 3: footing; 4: carry, both; 5: carry; 6: carry\nmishap_words | 1: none; 2: none; 3: none; 4: none; 5: wobbled; 6: none\nharm_words | none\ncapitalised_words | none"
}

Reply:

{
 "task": "routine",
 "status": "ok",
 "reason": "",
 "title": "Bags Over Boot",
 "elements": [
  {
   "line": 1,
   "demands": [
    "carry"
   ],
   "value": "B",
   "why": "The bags hooked over both arms show carrying."
  },
  {
   "line": 2,
   "demands": [
    "both"
   ],
   "value": "B",
   "why": "Shutting the boot with an elbow, not a hand."
  },
  {
   "line": 3,
   "demands": [
    "footing"
   ],
   "value": "B",
   "why": "Climbing three front steps without stopping needs footing."
  },
  {
   "line": 4,
   "demands": [
    "carry",
    "both"
   ],
   "value": "C",
   "why": "Holding every bag while fishing keys shows carrying at the same time."
  },
  {
   "line": 5,
   "demands": [
    "carry"
   ],
   "value": "B",
   "why": "The bags swung on the doormat, showing carrying."
  },
  {
   "line": 6,
   "demands": [
    "carry"
   ],
   "value": "B",
   "why": "Setting every bag on the kitchen counter shows carrying."
  }
 ],
 "faults": [
  {
   "line": 5,
   "size": "medium",
   "lens": "form",
   "quote": "I wobbled",
   "note": "The bags swung and you wobbled on the doormat."
  }
 ],
 "landing": "step",
 "landing_quote": "one small step back",
 "landing_why": "Setting every bag on the kitchen counter ends with one small step back.",
 "judges": [
  {
   "seat": "E1",
   "line": 5,
   "remark": "This seat has the doormat wobble in its sights."
  },
  {
   "seat": "E2",
   "line": 0,
   "remark": "This seat takes nothing off for rhythm."
  },
  {
   "seat": "E3",
   "line": 0,
   "remark": "This seat takes nothing off for precision."
  },
  {
   "seat": "E4",
   "line": 0,
   "remark": "This seat takes nothing off for composure."
  },
  {
   "seat": "E5",
   "line": 0,
   "remark": "This seat takes nothing off for commitment."
  }
 ],
 "tidy": {
  "line": 6,
  "tip": "Step back slowly once every bag is down on the kitchen counter."
 },
 "closing": "This one lands as a step, says the panel."
}

A duvet cover change - a hop

Input:

{
 "task": "routine",
 "tone": "stern",
 "title": "Duvet cover change",
 "moves": "1 | turned the duvet cover inside out\n2 | reached inside and took hold of the two far corners\n3 | shook the duvet down in one go\n4 | had to stop halfway and start the shake again\n5 | flapped it out over the bed and staggered two steps back",
 "page_facts": "moves | 5\nlanding_line | 5\ndemand_hints | 1: none; 2: carry; 3: none; 4: none; 5: footing\nmishap_words | 1: none; 2: none; 3: none; 4: had to stop; 5: staggered\nharm_words | none\ncapitalised_words | none"
}

Reply:

{
 "task": "routine",
 "status": "ok",
 "reason": "",
 "title": "Duvet cover change",
 "elements": [
  {
   "line": 1,
   "demands": [],
   "value": "A",
   "why": "Turning the duvet cover inside out states no demand."
  },
  {
   "line": 2,
   "demands": [
    "carry"
   ],
   "value": "B",
   "why": "Taking hold of the two far corners states a clear carry."
  },
  {
   "line": 3,
   "demands": [],
   "value": "A",
   "why": "Shaking the duvet down in one go states no demand."
  },
  {
   "line": 4,
   "demands": [],
   "value": "A",
   "why": "Stopping and starting the shake again states no demand here."
  },
  {
   "line": 5,
   "demands": [
    "footing"
   ],
   "value": "B",
   "why": "Staggering two steps back states a footing demand."
  }
 ],
 "faults": [
  {
   "line": 4,
   "size": "large",
   "lens": "rhythm",
   "quote": "had to stop halfway and start the shake again",
   "note": "You stopped halfway through the shake and started it again."
  }
 ],
 "landing": "hop",
 "landing_quote": "staggered two steps back",
 "landing_why": "Flapping it out and staggering two steps back is more than a small slip.",
 "judges": [
  {
   "seat": "E1",
   "line": 0,
   "remark": "This seat takes nothing off for form."
  },
  {
   "seat": "E2",
   "line": 4,
   "remark": "Restarting the shake at move 4 broke the flow."
  },
  {
   "seat": "E3",
   "line": 0,
   "remark": "This seat takes nothing off for precision."
  },
  {
   "seat": "E4",
   "line": 0,
   "remark": "This seat takes nothing off for composure."
  },
  {
   "seat": "E5",
   "line": 0,
   "remark": "This seat takes nothing off for commitment."
  }
 ],
 "tidy": {
  "line": 5,
  "tip": "Check where your feet are before stepping back after flapping it out."
 },
 "closing": "The stagger back at the end stands as the call."
}

Recycling boxes down the stairs - a fall

Input:

{
 "task": "routine",
 "tone": "deadpan",
 "title": "",
 "moves": "1 | stacked three recycling boxes up to my chin\n2 | went down the stairs sideways\n3 | paused halfway down to shift my grip\n4 | the top box tipped and I caught it with my knee\n5 | at the bottom the whole stack slid out of my arms onto the hall floor",
 "page_facts": "moves | 5\nlanding_line | 5\ndemand_hints | 1: carry, both; 2: footing; 3: none; 4: carry, both, aim; 5: carry\nmishap_words | 1: none; 2: none; 3: paused; 4: tipped; 5: slid out, slid\nharm_words | none\ncapitalised_words | none"
}

Reply:

{
 "task": "routine",
 "status": "ok",
 "reason": "",
 "title": "Boxes Down The Stairs",
 "elements": [
  {
   "line": 1,
   "demands": [
    "carry",
    "both"
   ],
   "value": "C",
   "why": "Carrying three stacked boxes, braced under your chin as well."
  },
  {
   "line": 2,
   "demands": [
    "footing"
   ],
   "value": "B",
   "why": "Stairs taken sideways, nothing else stated in this move."
  },
  {
   "line": 3,
   "demands": [],
   "value": "A",
   "why": "This move's own words add nothing further here."
  },
  {
   "line": 4,
   "demands": [
    "carry",
    "both",
    "aim"
   ],
   "value": "D",
   "why": "Caught the tipping box with your knee, holding it at the same time."
  },
  {
   "line": 5,
   "demands": [
    "carry"
   ],
   "value": "B",
   "why": "The stack was held until it slid from your arms."
  }
 ],
 "faults": [
  {
   "line": 3,
   "size": "small",
   "lens": "rhythm",
   "quote": "paused halfway down",
   "note": "A pause halfway down to adjust your grip."
  },
  {
   "line": 4,
   "size": "medium",
   "lens": "precision",
   "quote": "box tipped",
   "note": "The top box tipped before being caught with your knee."
  }
 ],
 "landing": "fall",
 "landing_quote": "onto the hall floor",
 "landing_why": "The whole stack left your arms and ended up on the hall floor.",
 "judges": [
  {
   "seat": "E1",
   "line": 0,
   "remark": "Nothing taken off here; the landing call remains a fall."
  },
  {
   "seat": "E2",
   "line": 3,
   "remark": "Move 3 shows a hitch partway down while resetting your grip."
  },
  {
   "seat": "E3",
   "line": 4,
   "remark": "Move 4 needed your knee to stop the top box tipping."
  },
  {
   "seat": "E4",
   "line": 0,
   "remark": "No reaction-based faults here; the landing call remains a fall."
  },
  {
   "seat": "E5",
   "line": 0,
   "remark": "Nothing held back here; the landing call remains a fall."
  }
 ],
 "tidy": {
  "line": 5,
  "tip": "Wrap both arms fully around the stack instead of balancing it loosely."
 },
 "closing": "The stack ending up on the hall floor is the call."
}

A no-hands bike ride - declined

Input:

{
 "task": "routine",
 "tone": "warm",
 "title": "",
 "moves": "1 | rode my bike down the high street with no hands\n2 | texted my sister while steering with my knees\n3 | coasted through the lights as they turned red",
 "page_facts": "moves | 3\nlanding_line | 3\ndemand_hints | 1: none; 2: both; 3: none\nmishap_words | 1: none; 2: none; 3: none\nharm_words | steering, no hands\ncapitalised_words | none"
}

Reply:

{
 "task": "routine",
 "status": "declined",
 "reason": "This panel only takes on low-risk, everyday moments, so this one's out of bounds - try sending a calmer routine instead.",
 "title": "",
 "elements": [],
 "faults": [],
 "landing": "",
 "landing_quote": "",
 "landing_why": "",
 "judges": [],
 "tidy": {
  "line": 0,
  "tip": ""
 },
 "closing": "No worries - bring the panel something steadier and it'll gladly take a look."
}

Build the input yourself

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

const K = LandKit, D = BeamDraw;
const built = K.buildInput({ tone: "deadpan", title: "",
  moves: "hooked six bags over both arms
shut the boot with my elbow
set every bag on the counter and let go" });
built.parsed.lines;              // [{n: 1, text: "hooked six bags over both arms"}, ...]
built.prescan.demands;           // [["carry"], ["both"], ["carry"]]
const wire = K.toWire(built.input);            // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// K.score(K.modelOf(reply))                  // D, E, the five judges (high and low dropped) and the total
// K.check(reply, built).notes                // the page's re-check of the reply
// K.md(built, reply, reply.title)            // the scorecard as Markdown

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://stick-the-landing.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":"stick-the-landing"}' | 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": "routine", "tone": "deadpan", "title": "", "moves": "1 | move\n2 | move\n3 | landing", "page_facts": "moves | 3\n..."} - 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 lane and the input: a retry with the same key never bills twice. The page uses stick-the-landing:<task>:<inputHash>:a<attempt> (the one task is routine), 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="stick-the-landing:routine:$(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: stick-the-landing:routine:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json