← Fort to Scale
API

Drive Fort to Scale from your own code

The one metered task, notes, is available over HTTP. It takes a blanket fort the page has already planned from a parts list - the frame, the span, the headroom, the room, the roof, the walls, the holders and the verdict - and returns build notes: the verdict, a headline, five notes printed on the drawing (frame, roof, walls, holders, inside), a fix and a safety line.

The division of labour first: the page plans; the model words it. The page reads the list, tries every frame it allows (a sofa and chairs, two rows of chairs, or a table), sizes the span to give everyone a place, works out how far the sheet dips and the headroom left, picks the roof sheet and the wall sheets, counts the clips and weights, and gives the verdict. It sends all of that as parts and facts; the model never changes them. After the reply, the page lists anything that does not match. Sizes and allowances are estimates; a planning 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":"fort-to-scale"}), 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, fort, parts, 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 build notes are metered and need 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: notes (the one task is notes); a missing or different one is still answered as notes. All fields are strings.

FieldRequiredWhat it holds
taskyesAlways notes.
fortyestitle | how many people, how they will be inside. The title is A blanket fort when empty (up to 60 characters); the posture is sitting up, lounging on cushions or lying down.
partsyesOne thing a line: N. name | kind | size | xCOUNT | use. Kind is chair, sofa, table, sheet, cushion, clip, weight, light, other or hot thing; size is width and back height for chairs and sofas, length, depth and height for tables and the two sides for sheets, with (typical size, an estimate) when the list gave none; use is the page's choice - frame, roof, walls, holders, inside, spare or leave out. Two to twenty things.
factsyesThe page's plan, one line each: verdict, why (for rethink-frame), frame, size, headroom, room, roof, walls, holders, inside, spare, leave out, what would help and the safety line. Build it with FortKit.buildInput.
retry_notenoOnly on a reformat retry: why the previous reply was unusable.

The verdict, checked in this order: rethink-frame when the frame cannot give everyone a place or the headroom their posture needs; else more-fabric when the roof sheet is too small or part of the walls stays open; else more-clips when there are fewer clips and weights than the fort needs; else build-it.

The reply

One JSON object, no prose around it:

{"task": "notes", "verdict": "build-it", "headline": "...", "notes": [{"part": "frame", "say": "..."}, {"part": "roof", "say": "..."}, {"part": "walls", "say": "..."}, {"part": "holders", "say": "..."}, {"part": "inside", "say": "..."}], "fix": "...", "safety": "..."}
FieldWhat it holds
verdictrethink-frame, more-fabric, more-clips or build-it - always the page's.
headlineOne sentence for the builder that matches the verdict: the headroom or room that falls short, the roof size needed or the wall left open, how many more holders, or that frame, fabric and holders all work.
notesExactly five {"part": "...", "say": "..."} in the order frame, roof, walls, holders, inside. Each say starts with a verb, names that part's things and gives the page's numbers for it, in at most 30 words.
fixOne sentence: what would help, keeping its numbers - or, for build-it, to give each clip or weight a tug and keep the door end clear.
safetyOne sentence: the hot things to keep out of the fort, or battery or LED lights only and the door end clear.

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 missing, doubled or out-of-order note, a note that does not name its own things, a spare or hot thing brought into the notes, a safety line that misses a hot thing, a headline without the number that matters, 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 build notes that replay for free. Together they cover every verdict - rethink the frame, more fabric, more clips and build it.

Friday film fort - build-it

Input:

{
 "task": "notes",
 "fort": "Friday film fort | 2 people, lounging on cushions",
 "parts": "1. Sofa | sofa | 210 wide, back 88 cm high | x1 | frame\n2. Kitchen chairs | chair | 44 wide, back 90 cm high | x3 | frame\n3. King-size duvet cover | sheet | 260 x 220 cm | x1 | roof\n4. Single sheet | sheet | 260 x 180 cm | x1 | walls\n5. Picnic blanket | sheet | 150 x 130 cm | x1 | spare\n6. Bulldog clips | clip | x10 | holders\n7. Floor cushions | cushion | x4 | inside\n8. Battery lantern | light | x1 | inside",
 "facts": "verdict: build-it\nframe: sofa and chairs - the sofa, with a row of chairs facing away from the sofa and their backs toward the sofa back, and the roof sheet across from the sofa back to the chair backs; uses Sofa (part 1), Kitchen chairs x3 (part 2)\nsize: span 110 cm between the rows, 132 cm deep along them; tops 88 cm and 90 cm high\nheadroom: the sheet dips about 11 cm, leaving about 78 cm at the middle; lounging on cushions needs about 70 cm (estimates)\nroom: 2 people lounging on cushions, about 60 x 110 cm a person (estimates): room for 2\nroof: King-size duvet cover (part 3), 260 x 220 cm, long side across the span, 15 cm tucked over each row\nwalls: the two open ends need 160 cm of fabric (the door keeps 60 cm open); a wall sheet needs at least 94 cm on its short side to reach from the top to near the floor; Single sheet (part 4) closes 160 cm; all closed\nholders: need 10 (4 along each row for the roof + 2 for each wall sheet); have 10: Bulldog clips x10 (part 6) - clips on; enough\ninside: Floor cushions x4 (part 7), Battery lantern (part 8)\nspare: Picnic blanket (part 5)\nleave out: none\nwhat would help: none\nsafety: Nothing hot under the fabric: no candles, heaters or hot bulbs - battery or LED lights only. Keep the door end clear, never hang fabric from shelves or lamps that could tip, and never climb on the frame."
}

Reply:

{
 "task": "notes",
 "verdict": "build-it",
 "headline": "The sofa and chairs frame works, the fabric covers the roof and walls, and the clips are enough.",
 "notes": [
  {
   "part": "frame",
   "say": "Stand the Sofa with the Kitchen chairs facing away from it, backs toward the sofa back, span 110 cm, depth 132 cm."
  },
  {
   "part": "roof",
   "say": "Drape the King-size duvet cover long side across the span, with 15 cm tucked over each row."
  },
  {
   "part": "walls",
   "say": "Hang the Single sheet across 160 cm; the walls are all closed."
  },
  {
   "part": "holders",
   "say": "Clip the Bulldog clips on: you need 10 and have 10, enough."
  },
  {
   "part": "inside",
   "say": "Put the Floor cushions and Battery lantern inside the fort."
  }
 ],
 "fix": "Give each clip a tug and keep the door end clear before you get in.",
 "safety": "Use battery or LED lights only, and keep the door end clear."
}

Rainy-day reading den - more-fabric

Input:

{
 "task": "notes",
 "fort": "Rainy-day reading den | 2 people, lounging on cushions",
 "parts": "1. Dining chairs | chair | 45 wide, back 92 cm high | x6 | frame\n2. Double duvet cover | sheet | 200 x 200 cm | x1 | roof\n3. Fleece throw | sheet | 130 x 120 cm | x1 | walls\n4. Clothes pegs | clip | x12 | holders\n5. Cushions | cushion | x5 | inside\n6. Fairy lights (LED) | light | x1 | inside\n7. Tealight candle | hot thing | x1 | leave out",
 "facts": "verdict: more-fabric\nframe: two rows of chairs - two rows of chairs facing away from each other, backs toward the gap, with the roof sheet across the tops of the backs; uses Dining chairs x6 (part 1)\nsize: span 110 cm between the rows, 135 cm deep along them; tops 92 cm and 92 cm high\nheadroom: the sheet dips about 11 cm, leaving about 81 cm at the middle; lounging on cushions needs about 70 cm (estimates)\nroom: 2 people lounging on cushions, about 60 x 110 cm a person (estimates): room for 2\nroof: Double duvet cover (part 2), 200 x 200 cm, square, so either way across the span, 15 cm tucked over each row\nwalls: the two open ends need 160 cm of fabric (the door keeps 60 cm open); a wall sheet needs at least 97 cm on its short side to reach from the top to near the floor; Fleece throw (part 3) closes 130 cm; 30 cm still open\nholders: need 10 (4 along each row for the roof + 2 for each wall sheet); have 12: Clothes pegs x12 (part 4) - clips on; enough\ninside: Cushions x5 (part 5), Fairy lights (LED) (part 6)\nspare: none\nleave out: Tealight candle (part 7) - nothing hot under fabric\nwhat would help: one more sheet or blanket at least 97 cm on its short side, to close the last 30 cm\nsafety: Nothing hot under the fabric: no candles, heaters or hot bulbs - battery or LED lights only. Keep the door end clear, never hang fabric from shelves or lamps that could tip, and never climb on the frame."
}

Reply:

{
 "task": "notes",
 "verdict": "more-fabric",
 "headline": "The frame works, but the fabric falls short: 30 cm of wall stays open.",
 "notes": [
  {
   "part": "frame",
   "say": "Stand the Dining chairs in two rows, facing away from each other, backs toward the gap; span 110 cm, 135 cm deep."
  },
  {
   "part": "roof",
   "say": "Drape the Double duvet cover across the span, either way since it is square, tucking 15 cm over each row."
  },
  {
   "part": "walls",
   "say": "Drape the Fleece throw to close 130 cm of wall; 30 cm stays open."
  },
  {
   "part": "holders",
   "say": "Clip the Clothes pegs on: need 10, have 12, so that is enough."
  },
  {
   "part": "inside",
   "say": "Put the Cushions and Fairy lights (LED) inside the fort."
  }
 ],
 "fix": "Add one more sheet or blanket at least 97 cm on its short side, to close the last 30 cm.",
 "safety": "Keep the Tealight candle out of the fort, because nothing hot goes under fabric."
}

Under the dining table - rethink-frame

Input:

{
 "task": "notes",
 "fort": "Under the dining table | 2 people, sitting up",
 "parts": "1. Dining table | table | 180 x 90 cm, 76 cm high | x1 | frame\n2. Tablecloth | sheet | 300 x 180 cm | x1 | roof\n3. Fleece throw | sheet | 170 x 130 cm | x1 | walls\n4. Books | weight | x4 | holders\n5. Beanbag | cushion | x1 | inside",
 "facts": "verdict: rethink-frame\nwhy: about 76 cm of headroom where sitting up needs about 90 cm\nframe: table den - the table, with the roof sheet over its top; uses Dining table (part 1)\nsize: 180 x 90 cm under the table, 76 cm high\nheadroom: about 76 cm under the table top; sitting up needs about 90 cm (estimates)\nroom: 2 people sitting up, about 60 x 60 cm a person (estimates): room for 3\nroof: Tablecloth (part 2), 300 x 180 cm, long side across the table\nwalls: the four sides need 480 cm of fabric (the door keeps 60 cm open); a wall sheet needs at least 81 cm on its short side to reach from the top to near the floor; Fleece throw (part 3) closes 170 cm; 310 cm still open\nholders: need 4 (2 for the roof sheet + 2 for each wall sheet); have 4: Books x4 (part 4) - a weight, set on a flat top; enough\ninside: Beanbag (part 5)\nspare: none\nleave out: none\nwhat would help: lounge on cushions instead of sitting up (about 70 cm of headroom)\nsafety: Nothing hot under the fabric: no candles, heaters or hot bulbs - battery or LED lights only. Keep the door end clear, never hang fabric from shelves or lamps that could tip, and never climb on the frame."
}

Reply:

{
 "task": "notes",
 "verdict": "rethink-frame",
 "headline": "The frame can't do it as listed: about 76 cm of headroom where sitting up needs about 90 cm.",
 "notes": [
  {
   "part": "frame",
   "say": "Stand the dining table as the frame, with the roof sheet over its top. It is 180 x 90 cm, 76 cm high."
  },
  {
   "part": "roof",
   "say": "Lay the tablecloth over the table, long side across. It is 300 x 180 cm."
  },
  {
   "part": "walls",
   "say": "Hang the fleece throw to close 170 cm of wall. 310 cm of wall stays open."
  },
  {
   "part": "holders",
   "say": "Set the books on a flat top. You need 4 and have 4 - that is enough."
  },
  {
   "part": "inside",
   "say": "Put the beanbag inside the fort."
  }
 ],
 "fix": "Lounge on cushions instead of sitting up for about 70 cm of headroom.",
 "safety": "Use battery or LED lights only and keep the door end clear."
}

A one-person hideout - more-clips

Input:

{
 "task": "notes",
 "fort": "A one-person hideout | 1 person, lounging on cushions",
 "parts": "1. Sofa | sofa | 200 wide, back 85 cm high | x1 | frame\n2. Armchairs | chair | 45 wide, back 90 cm high | x2 | frame\n3. Patchwork quilt | sheet | 220 x 200 cm | x1 | roof\n4. Single duvet cover | sheet | 200 x 135 cm | x1 | walls\n5. Clothes pegs | clip | x5 | holders\n6. Pillows | cushion | x3 | inside\n7. Torch | light | x1 | inside",
 "facts": "verdict: more-clips\nframe: sofa and chairs - the sofa, with a row of chairs facing away from the sofa and their backs toward the sofa back, and the roof sheet across from the sofa back to the chair backs; uses Sofa (part 1), Armchairs x2 (part 2)\nsize: span 110 cm between the rows, 90 cm deep along them; tops 85 cm and 90 cm high\nheadroom: the sheet dips about 11 cm, leaving about 76 cm at the middle; lounging on cushions needs about 70 cm (estimates)\nroom: 1 person lounging on cushions, about 60 x 110 cm a person (estimates): room for 1\nroof: Patchwork quilt (part 3), 220 x 200 cm, long side across the span, 15 cm tucked over each row\nwalls: the two open ends need 160 cm of fabric (the door keeps 60 cm open); a wall sheet needs at least 93 cm on its short side to reach from the top to near the floor; Single duvet cover (part 4) closes 160 cm; all closed\nholders: need 8 (3 along each row for the roof + 2 for each wall sheet); have 5: Clothes pegs x5 (part 5) - clips on; 3 short\ninside: Pillows x3 (part 6), Torch (part 7)\nspare: none\nleave out: none\nwhat would help: 3 more clips or pegs\nsafety: Nothing hot under the fabric: no candles, heaters or hot bulbs - battery or LED lights only. Keep the door end clear, never hang fabric from shelves or lamps that could tip, and never climb on the frame."
}

Reply:

{
 "task": "notes",
 "verdict": "more-clips",
 "headline": "The frame and fabric both work, but you still need 3 more clips or pegs.",
 "notes": [
  {
   "part": "frame",
   "say": "Stand the Armchairs facing away from the Sofa, their backs toward the Sofa back. Keep a 110 cm span between the rows and 90 cm deep along them."
  },
  {
   "part": "roof",
   "say": "Drape the Patchwork quilt, long side across the span. Tuck 15 cm over each row."
  },
  {
   "part": "walls",
   "say": "Hang the Single duvet cover to close 160 cm. The walls are all closed."
  },
  {
   "part": "holders",
   "say": "Clip the Clothes pegs on; you need 8 but have 5. Add 3 more clips or pegs."
  },
  {
   "part": "inside",
   "say": "Put the Pillows and Torch inside the fort."
  }
 ],
 "fix": "Add 3 more clips or pegs.",
 "safety": "Use battery or LED lights only, and keep the door end clear."
}

Build the input yourself

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

const K = FortKit, R = ReplyKit;
const built = K.buildInput(K.form({ title: "Reading nook", people: "1", pose: "lounge", sith: "", tallh: "",
  text: "4 Dining chairs | 45x50x92
Double duvet cover | 200x200
Clothes pegs | 8" }));  // built.ok is false, with built.note, until there is a frame and a sheet
const wire = K.toWire(built.input);   // POST this to /estimate and /run; built.analysis holds the chosen frame (g.fr), fabric (g.fab), holders (g.hol) and verdict
// then: const reply = R.normalize(R.parseReply(job.output.output)); R.check(reply, built).notes lists any disagreement

Form keys: title (up to 60 characters), people (1 to 4), pose (sit, lounge or lie), sith (seated height in cm, 60 to 120, or empty for 90), tallh (the tallest person in cm, 140 to 210, or empty for 180) and text (one thing a line, cells split by | or tabs, up to 3,000 characters). Furniture sizes are width x depth x height in cm; a thing with no size gets a typical one, marked as an estimate.

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://fort-to-scale.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":"fort-to-scale"}' | 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": "notes", "fort": "Friday film fort | 2 people, lounging on cushions", "parts": "1. Sofa | sofa | 210 wide, back 88 cm high | x1 | frame ...", "facts": "verdict: build-it ..."}
# - 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 fort-to-scale:<task>:<inputHash>:a<attempt> (the one task is notes), 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="fort-to-scale:notes:$(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: fort-to-scale:notes:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json