← Ten Arms
API

Drive Ten Arms from your own code

The one metered task, search-plan, is available over HTTP. It takes 1 lost everyday item, the last moment it was surely in hand, what happened since and up to 9 spots it could be, and returns a reading per spot: the person's own words that bear on it, a score, how easily a careful look would show the item there, why it could be there and how to search it - plus the steps of the story that no spot covers and a retrace line.

The division of labour first: the model reads the words; the page does every number. It turns the scores into chances, adds a share for somewhere not listed, applies the person's search marks with Bayes' rule and picks the verdict. Search marks are never sent, so marking spots costs nothing.

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":"ten-arms"}), 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, item, last_seen, since, spots, spot_count, 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: a reading is 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 verdicts

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

FieldRequiredWhat it holds
taskyesAlways search-plan.
itemyesThe lost thing in the person's words, up to 60 characters.
last_seenyesThe last moment they surely had it, up to 200 characters.
sinceyesWhat they did after that, up to 280 characters, or not given.
spotsyesNumbered lines, N. name with an optional - note: ...; 2 to 9 spots.
spot_countyesHow many spots are listed.
factsyesThe page's lines (spot count, the not-listed share, judge before any searching).
retry_notenoOnly on the page's one reformat retry.

The page's arithmetic: "somewhere not listed" = 10 + 10 per uncovered entry (at most 50). The rest is shared over the spots in parts of score + 1 (largest remainder). Look-ease: easy 0.9, fair 0.6, hard 0.3. A spot marked searched keeps chance × (1 − look-ease × f), f = 1 for searched well and 0.5 for a quick look, and everything is renormalised. The order is chance now × look-ease, then the easier spot, then the lower number. Verdict, first match wins: widen-the-search when not listed holds 40% or more; go-straight-there when the best find chance is at least twice the runner-up's; otherwise sweep-top-three.

The reply

{"task": "search-plan", "summary": "...", "spots": [{"n": 1, "score": 0, "spot_it": "fair", "evidence": "...", "why": "...", "how_to_look": "..."}], "elsewhere": {"uncovered": ["..."], "why": "...", "retrace": "..."}, "closing": "..."}
FieldWhat it holds
spots[]One per listed spot, in order: n, score 0 to 3 (3 the story puts the item there, 2 the note says it usually lives there, 1 the story names the spot, 0 none), spot_it (easy, fair, hard), evidence (2 to 10 words copied from the input, or empty), why (at most 20 words), how_to_look (at most 25).
elsewhereuncovered (up to 4 steps of the story no spot covers, copied), why (at most 25 words), retrace (the person's steps in order, at most 30).
summaryWhere the trail goes cold, at most 30 words.
closing1 calm sentence, at most 20 words.

The page then checks the reply (SearchKit.check) - spot order, quotes that are really the person's words, word limits, invented numbers or times, ranking words, safety - and lists any disagreement under "The page's check".

Worked examples - every verdict

These are the 3 free samples on the page: the exact input the page sends and the reply that replays for free. The verdicts come from the sample's search marks (shown in each heading), which are not part of the input.

House keys after the shopping - no spot searched yet - verdict: go straight there

Input:

{
 "task": "search-plan",
 "item": "house keys on a red fob",
 "last_seen": "I unlocked the front door with them when I got back from the shop",
 "since": "I put the shopping away in the kitchen, hung my coat on the hook and sat on the sofa with a cup of tea",
 "spots": "1. front door - note: I sometimes leave them in the lock\n2. key bowl by the door - note: where they are meant to live\n3. kitchen counter - note: where the shopping bags went\n4. coat pocket - note: the coat I wore to the shop\n5. sofa",
 "spot_count": "5",
 "facts": "spots listed: 5 (numbered 1 to 5); reply with exactly 5 spot entries in that order\nthe page adds its own share for somewhere not listed, from the uncovered steps\nchances are judged before any searching: ignore any searches the story says were already done - the page applies the person's search marks itself"
}

Reply:

{
 "task": "search-plan",
 "summary": "Between unlocking the front door and sitting on the sofa, the story never says what became of the keys.",
 "spots": [
  {
   "n": 1,
   "score": 3,
   "spot_it": "fair",
   "evidence": "unlocked the front door with them",
   "why": "You used them to unlock this door, so they could be right here.",
   "how_to_look": "Check the lock and handle closely, and look along the bottom edge of the door."
  },
  {
   "n": 2,
   "score": 2,
   "spot_it": "easy",
   "evidence": "where they are meant to live",
   "why": "The keys are meant to live here, so they might simply be in this bowl.",
   "how_to_look": "Glance into the bowl and move anything inside it aside to see the bottom."
  },
  {
   "n": 3,
   "score": 1,
   "spot_it": "easy",
   "evidence": "put the shopping away in the kitchen",
   "why": "You put the shopping away in the kitchen, so the keys could have been set down here too.",
   "how_to_look": "Scan the whole counter surface, checking under anything sitting on it."
  },
  {
   "n": 4,
   "score": 1,
   "spot_it": "fair",
   "evidence": "hung my coat on the hook",
   "why": "You hung this coat on the hook, so the keys could still be in its pocket.",
   "how_to_look": "Reach all the way into the pocket and feel along the bottom seam."
  },
  {
   "n": 5,
   "score": 1,
   "spot_it": "hard",
   "evidence": "sat on the sofa",
   "why": "You sat on the sofa, so the keys could have been set down there too.",
   "how_to_look": "Check between and under the cushions and run a hand down the sides."
  }
 ],
 "elsewhere": {
  "uncovered": [],
  "why": "The steps after unlocking the door are all covered by the spots already listed, with nothing left outside them.",
  "retrace": "unlocked the front door then put the shopping away in the kitchen then hung your coat on the hook then sat on the sofa with a cup of tea"
 },
 "closing": "Take a calm look through each spot without rushing, and trust what turns up."
}

Reading glasses after a night in bed - bedside table and bathroom shelf searched well - verdict: sweep the top three

Input:

{
 "task": "search-plan",
 "item": "reading glasses in a brown case",
 "last_seen": "I had them on while reading in bed last night",
 "since": "this morning I made breakfast, had a shower and got dressed in the bedroom",
 "spots": "1. bed - note: the duvet is in a heap\n2. bedside table - note: where I usually put them\n3. bathroom shelf\n4. kitchen table - note: where I ate breakfast\n5. work bag - note: I packed it yesterday",
 "spot_count": "5",
 "facts": "spots listed: 5 (numbered 1 to 5); reply with exactly 5 spot entries in that order\nthe page adds its own share for somewhere not listed, from the uncovered steps\nchances are judged before any searching: ignore any searches the story says were already done - the page applies the person's search marks itself"
}

Reply:

{
 "task": "search-plan",
 "summary": "Between reading in bed last night and starting your morning, your story does not say what happened to the glasses.",
 "spots": [
  {
   "n": 1,
   "score": 3,
   "spot_it": "hard",
   "evidence": "in bed last night",
   "why": "You had them on in bed last night, so they could still be there.",
   "how_to_look": "Shake out the duvet and covers, then check along the mattress and underneath it."
  },
  {
   "n": 2,
   "score": 2,
   "spot_it": "easy",
   "evidence": "where I usually put them",
   "why": "This is where you usually put them, so they may be there.",
   "how_to_look": "Look across the top of the bedside table, checking around anything placed on it."
  },
  {
   "n": 3,
   "score": 0,
   "spot_it": "easy",
   "evidence": "",
   "why": "Nothing you wrote points to this spot.",
   "how_to_look": "Scan along the bathroom shelf, checking each item sitting on it."
  },
  {
   "n": 4,
   "score": 1,
   "spot_it": "easy",
   "evidence": "where I ate breakfast",
   "why": "You ate breakfast at the kitchen table, so they could be nearby.",
   "how_to_look": "Check all over the kitchen table and look underneath anything placed on it."
  },
  {
   "n": 5,
   "score": 0,
   "spot_it": "fair",
   "evidence": "I packed it yesterday",
   "why": "You packed the work bag yesterday, so they could be inside.",
   "how_to_look": "Open the work bag and check inside, including any pockets or compartments."
  }
 ],
 "elsewhere": {
  "uncovered": [
   "had a shower",
   "got dressed in the bedroom"
  ],
  "why": "The shower and getting dressed are not covered by the spots above, so something could still be in those steps.",
  "retrace": "reading in bed last night then made breakfast then had a shower then got dressed in the bedroom"
 },
 "closing": "Take your time with the search, and know there is no rush to it."
}

Umbrella after a café and a bus - every spot but the bus searched well - verdict: widen the search

Input:

{
 "task": "search-plan",
 "item": "black folding umbrella",
 "last_seen": "I had it at the café when it started raining",
 "since": "I walked to the bus stop, took the bus home and popped into the corner shop on my street",
 "spots": "1. the café\n2. the bus\n3. the corner shop\n4. umbrella stand in the hall\n5. my backpack - note: it fits in the side pocket",
 "spot_count": "5",
 "facts": "spots listed: 5 (numbered 1 to 5); reply with exactly 5 spot entries in that order\nthe page adds its own share for somewhere not listed, from the uncovered steps\nchances are judged before any searching: ignore any searches the story says were already done - the page applies the person's search marks itself"
}

Reply:

{
 "task": "search-plan",
 "summary": "After the café, nothing you describe says what happened to the umbrella until you reached home.",
 "spots": [
  {
   "n": 1,
   "score": 3,
   "spot_it": "fair",
   "evidence": "I had it at the café",
   "why": "You had the umbrella at the café, so it could still be there.",
   "how_to_look": "Ask the café staff if they have seen or put aside the umbrella."
  },
  {
   "n": 2,
   "score": 1,
   "spot_it": "fair",
   "evidence": "took the bus home",
   "why": "You took the bus home, so the umbrella could have been left on the bus.",
   "how_to_look": "Ask the bus staff whether the umbrella was handed in or left behind."
  },
  {
   "n": 3,
   "score": 1,
   "spot_it": "fair",
   "evidence": "popped into the corner shop",
   "why": "You popped into the corner shop, so the umbrella could have been set down there.",
   "how_to_look": "Ask the corner shop staff if the umbrella was left or handed in."
  },
  {
   "n": 4,
   "score": 0,
   "spot_it": "easy",
   "evidence": "",
   "why": "Nothing in your words points to the umbrella stand in the hall.",
   "how_to_look": "Look at the umbrella stand in the hall; a quick glance will show whether the umbrella is there."
  },
  {
   "n": 5,
   "score": 0,
   "spot_it": "fair",
   "evidence": "it fits in the side pocket",
   "why": "The backpack's side pocket fits the umbrella, so it could be tucked inside.",
   "how_to_look": "Open the backpack and check inside, including the side pocket, by feel and sight."
  }
 ],
 "elsewhere": {
  "uncovered": [
   "the bus stop",
   "on my street"
  ],
  "why": "The bus stop and your street are not among the listed spots, so the umbrella could also be along either stretch.",
  "retrace": "You had it at the café when it started raining, then walked to the bus stop, then took the bus home, then popped into the corner shop on your street."
 },
 "closing": "Take your time checking each spot calmly, without rushing or assuming where the umbrella must be."
}

Build the input yourself

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

const K = SearchKit;
const form = K.form({ item: "house keys on a red fob", last_seen: "I unlocked the front door with them when I got back from the shop",
  since: "I hung my coat on the hook", spots: [{ name: "front door" }, { name: "coat pocket", mark: "quick" }] });
const built = K.buildInput(form);
const wire = K.toWire(built.input);     // POST this to /estimate and /run (search marks are not sent)
// then: const reply = K.normalize(K.parseReply(job.output.output));
// K.plan(form, reply)                  // chances before and now, find chances, order, verdict
// K.check(reply, built).notes          // the page's re-check of the reply
// K.md(built, reply, "my keys")        // the plan 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://ten-arms.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":"ten-arms"}' | 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": "search-plan", "item": "house keys on a red fob", "last_seen": "I unlocked the front door with them when I got back from the shop", "since": "...", "spots": "1. front door - note: ...\n2. key bowl by the door ...", "spot_count": "5", "facts": "spots listed: 5 ..."}
# - 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 ten-arms:<task>:<inputHash>:a<attempt> (the one task is search-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="ten-arms:search-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: ten-arms:search-plan:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json