← Left at the Scarecrow
Scarecrow API
Tokens

Drive Left at the Scarecrow from your own code

The one metered task, passport, is available over HTTP. It takes a fact list (one fact per line), a theme, who will walk the maze and how many checkpoint posts it has, and returns a verdict (ready to print, short of facts, or no usable facts), one question per post - each with the right answer taken from a fact line and two wrong answers - the lines it left out with a reason, any spare facts, a maze name and the entrance and exit signs.

The division of labour first: the page owns the maze, the posts, the turns and the line count; the model owns which lines are facts and the words; the page pins each right answer to its post's right turn and re-checks the reply. All of it comes from mazekit.js, which you can run yourself (see Build the input yourself). The model never sees the maze, so one reply fits any maze with the same number of posts.

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":"left-at-the-scarecrow"}), 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, ages, posts, 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: writing a passport 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 verdict

Every run sends task: passport; a missing or different one is still answered as passport. The model sorts every line into usable facts and skipped lines (opinions, instructions, greetings, questions, vague lines, repeats, private details, unkind lines). With U usable lines and P posts, the verdict is checked in a fixed order and the first that applies wins:

The page recomputes the rule from the reply's own count (MazeKit.check) and flags a reply that disagrees.

Input fields

Every value is a string (objects are never sent). app.js builds it with MazeKit.buildInput(form).input.

FieldRequiredWhat it holds
taskyesAlways passport.
themenoWhat the maze is about, at most 80 characters, or (no theme given).
agesyes5-7, 8-11 or 12+ - sets the question and answer lengths.
postsyesThe number of checkpoint posts, 3 to 10 (at most 8 on the small field).
factsyesNumbered lines, 1. ..., one fact per line. At most 24 lines of at most 240 characters are sent; bullets and old numbers are stripped and the lines renumbered.
page_countnofact lines L; posts P - the page's own count.
clip_notenoPresent only when lines were dropped or cut.
retry_notenoSent only on the one automatic reformat retry.

The reply contract

The reply is one JSON object in job.output.output. The page parses it with MazeKit.parseReply and MazeKit.normalize.

{"task": "passport", "verdict": "ready | short | unusable", "verdict_reason": "...", "maze_name": "...", "welcome": "...",
 "stations": [{"line": 1, "question": "...", "answer": "...", "wrong": ["...", "..."], "reveal": "..."}],
 "spare_lines": [7], "skipped_lines": [{"line": 2, "why": "an opinion"}], "finish": "..."}
FieldRule
stationsStation 1 goes on post 1, and so on. The question uses only its fact line; the answer keeps the line's words; two wrong answers, different from each other and from the answer, and plainly false. No turns or directions - the page adds them.
spare_lines, skipped_linesTogether with the stations' line values, every line from 1 to L appears exactly once.
maze_name, welcome, finishBuilt only from the theme and the facts - no invented names, places, dates or prizes.

The page then pins each station to its post (MazeKit.signs): the right answer on the turn that leads on, the wrong answers on the dead-end turns in order (a two-way fork uses only the first wrong answer). It checks the reply (MazeKit.check): the verdict against the reply's own count, every line accounted for exactly once, line numbers in range, two different wrong answers per post, a right answer that shares a word with its fact line, no talk of turns, and no internal field names in the wording.

Worked examples - one per verdict

These are the app's three free samples: the exact input app.js sends and a reply to it, unedited.

Pumpkin patch, ages 8-11 - ready

Input:

{
 "task": "passport",
 "theme": "Pumpkin patch",
 "ages": "8-11",
 "posts": "5",
 "facts": "1. Pumpkins are a kind of squash.\n2. Pumpkin patches are the most fun place to be!\n3. A pumpkin is about 90 percent water.\n4. Pumpkins grow on long vines that spread along the ground.\n5. Pumpkin flowers are bright yellow.\n6. Bees carry pollen between pumpkin flowers so the fruit can grow.\n7. Pumpkin seeds can be roasted and eaten.",
 "page_count": "fact lines 7; posts 5"
}

Reply:

{
 "task": "passport",
 "verdict": "ready",
 "verdict_reason": "Six of the seven lines are facts with one clear answer, enough for all five posts.",
 "maze_name": "Pumpkin Patch Maze",
 "welcome": "Welcome to the Pumpkin Patch maze, where every right answer about pumpkins shows you the way on!",
 "stations": [
  {
   "line": 1,
   "question": "A pumpkin is a kind of what?",
   "answer": "Squash",
   "wrong": [
    "Melon",
    "Cucumber"
   ],
   "reveal": "Pumpkins are a kind of squash."
  },
  {
   "line": 3,
   "question": "About what percent of a pumpkin is water?",
   "answer": "90 percent",
   "wrong": [
    "50 percent",
    "75 percent"
   ],
   "reveal": "A pumpkin is about 90 percent water."
  },
  {
   "line": 4,
   "question": "What do pumpkins grow on?",
   "answer": "Long vines",
   "wrong": [
    "Short stems",
    "Tall trees"
   ],
   "reveal": "Pumpkins grow on long vines that spread along the ground."
  },
  {
   "line": 5,
   "question": "What color are pumpkin flowers?",
   "answer": "Bright yellow",
   "wrong": [
    "Deep red",
    "Soft blue"
   ],
   "reveal": "Pumpkin flowers are bright yellow."
  },
  {
   "line": 6,
   "question": "What do bees carry between pumpkin flowers?",
   "answer": "Pollen",
   "wrong": [
    "Leaves",
    "Sand"
   ],
   "reveal": "Bees carry pollen between pumpkin flowers so the fruit can grow."
  }
 ],
 "spare_lines": [
  7
 ],
 "skipped_lines": [
  {
   "line": 2,
   "why": "an opinion"
  }
 ],
 "finish": "You made it through the Pumpkin Patch maze - great job answering all about pumpkins!"
}

Grandad's 70th birthday - short

Input:

{
 "task": "passport",
 "theme": "Grandad's 70th birthday",
 "ages": "12+",
 "posts": "6",
 "facts": "1. Grandad was born in a village called Ashby.\n2. His first car was a green van.\n3. What cake should we get?\n4. He has kept bees for 30 years.\n5. Grandad's first car was a van painted green.\n6. Grandad is the best grandad in the world.\n7. Remember to bring the balloons.",
 "page_count": "fact lines 7; posts 6"
}

Reply:

{
 "task": "passport",
 "verdict": "short",
 "verdict_reason": "Three of the seven lines are facts with one clear answer, but there are six posts, so the extra posts print as blank signs.",
 "maze_name": "Grandad's 70th Birthday Maze",
 "welcome": "Step into Grandad's 70th birthday maze, where every right answer shows you the way on.",
 "stations": [
  {
   "line": 1,
   "question": "Which village was Grandad born in?",
   "answer": "Ashby",
   "wrong": [
    "Millbrook",
    "Oakford"
   ],
   "reveal": "Grandad was born in the village of Ashby."
  },
  {
   "line": 2,
   "question": "What was Grandad's first car?",
   "answer": "A green van",
   "wrong": [
    "A red car",
    "A blue truck"
   ],
   "reveal": "Grandad's first car was a green van."
  },
  {
   "line": 4,
   "question": "How many years has Grandad kept bees?",
   "answer": "30 years",
   "wrong": [
    "20 years",
    "15 years"
   ],
   "reveal": "Grandad has kept bees for 30 years."
  }
 ],
 "spare_lines": [],
 "skipped_lines": [
  {
   "line": 3,
   "why": "a question, not a fact"
  },
  {
   "line": 5,
   "why": "repeats line 2"
  },
  {
   "line": 6,
   "why": "an opinion"
  },
  {
   "line": 7,
   "why": "an instruction"
  }
 ],
 "finish": "Well done, you've made it through Grandad's birthday maze!"
}

Farm open day - unusable

Input:

{
 "task": "passport",
 "theme": "Farm open day",
 "ages": "5-7",
 "posts": "4",
 "facts": "1. Please wear your boots.\n2. Our apple juice is the tastiest around.\n3. Have a wonderful day!\n4. Which animal do you like best?",
 "page_count": "fact lines 4; posts 4"
}

Reply:

{
 "task": "passport",
 "verdict": "unusable",
 "verdict_reason": "None of the four lines are facts with one clear answer, so all four posts will print as blank signs.",
 "maze_name": "Farm Open Day Maze",
 "welcome": "Welcome to the farm open day maze, come on in!",
 "stations": [],
 "spare_lines": [],
 "skipped_lines": [
  {
   "line": 1,
   "why": "an instruction"
  },
  {
   "line": 2,
   "why": "an opinion"
  },
  {
   "line": 3,
   "why": "a greeting"
  },
  {
   "line": 4,
   "why": "a question, not a fact"
  }
 ],
 "finish": "Well done, you found your way through the farm open day maze!"
}

Build the input yourself

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

const K = MazeKit;
const form = K.form({ theme: "Apple orchard", ages: "5-7", posts: 4, size: "small", seed: "orchard",
  facts: "Apples grow on trees.\nApple blossoms are pink and white.\nBring a basket!" });
const lay = K.layout(form.size, form.seed, form.posts);   // the maze, its one true path, the forks and the posts
lay.posts.map(p => [p.no, p.turn, p.turns]);             // [[1, "right", ["straight", "right"]], [2, "right", ["left", "right"]], ...] - post, right turn, turns
const built = K.buildInput(form);                         // built.L = 3 lines, built.P = 4 posts
const wire = K.toWire(built.input);                       // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// K.signs(lay, reply)                                    // each post's options, the right answer on the right turn
// K.check(reply, built).notes                            // the page's re-check of the reply
// K.md(built, lay, reply)                                // the passport and answer key 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://left-at-the-scarecrow.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":"left-at-the-scarecrow"}' | 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": "passport", "theme": "Pumpkin patch", "ages": "8-11", "posts": "5", "facts": "1. Pumpkins are a kind of squash.\n2. Pumpkin patches are the most fun place to be!\n3. A pumpkin is about 90 percent water.\n4. Pumpkins grow on long vines that spread along the ground.\n5. Pumpkin flowers are bright yellow.\n6. Bees carry pollen between pumpkin flowers so the fruit can grow.\n7. Pumpkin seeds can be roasted and eaten.", "page_count": "fact lines 7; posts 5"}
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 left-at-the-scarecrow:<task>:<inputHash>:a<attempt> (the one task is passport), 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="left-at-the-scarecrow:passport:$(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: left-at-the-scarecrow:passport:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json