← Touch Wood
API

Drive Touch Wood from your own code

The one metered task, luck-test, is available over HTTP. It takes 1 superstition from the person's own life - a lucky habit they do or carry, or a sign that just happens - what counts as things going well, the page's tally (how often things went well or badly with it and without it, the rates, the gap, the verdict and the fair-test plan) and up to 4 short notes, and returns the verdict, the numbers in plain words, 3 likely suspects behind the pattern, a fair test, a headline and a closing line.

The division of labour first: the page does the arithmetic, a fixed rule decides the verdict and the page sets the fair-test plan; the model explains them. After a run the page checks that the verdict follows the rule, that exactly 3 suspects come from the fixed list in its order (with plain chance and memory included when the rule needs them), that every number in the numbers line is in the tally, that the fair test names the page's count, and that no ids leak into the prose.

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":"touch-wood"}), 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, belief, goal, tally, notes) 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: luck-test (the one task is luck-test); a missing or different one is still answered as luck-test. Rates are rounded to whole percents and the gap is taken toward the claim (for an unlucky habit or sign, a lower well rate with it counts toward the claim). The verdict rule, checked in order: fewer than 5 times with it or without it, too-few-tries; otherwise counted from memory, memory-keeping-score; otherwise a gap of 20 points or more, holds-up-so-far; otherwise -20 points or less, runs-backwards; otherwise coin-flip. The plan aims for at least 10 times on each side and at least 10 more in all: coin for a habit, watch for a sign, keep (change nothing, write it down) when the belief mentions a medicine, a treatment or safety gear.

FieldRequiredWhat it holds
taskyesAlways luck-test.
beliefyesThe habit or sign and what it is said to do, up to 140 characters.
goalyesWhat counts as things going well, up to 90 characters.
tallyyesThe page's lines: kind: habit (...), claim: lucky (...), source: notes (...), with it: 10 times | went well 6 | went badly 4 | well rate 60%, without it: ..., gap toward the claim: +40 points, verdict: holds-up-so-far, plan: coin | 10 more times | a coin decides each time whether to do it.
notesyesUp to 4 lines note 1 | text, or none.
retry_notenoOnly on the page's single reformat retry.

The reply is one JSON object:

{"task": "luck-test", "verdict": "coin-flip", "headline": "...", "numbers": "...", "suspects": [{"id": "chance", "why": "..."}, ...], "fair_test": "...", "closing": "..."}
FieldWhat it holds
verdicttoo-few-tries, memory-keeping-score, holds-up-so-far, runs-backwards or coin-flip, copied from the tally.
headlineThe verdict in plain words, at most 16 words.
numbersWhat the tally says, using only its digits, at most 45 words.
suspectsExactly 3, each a different id from chance, memory, other-cause, picking-moments, bounce-back, self-fulfilling, kept in that order; why at most 30 words.
fair_testThe page's plan in plain words with its count, at most 40 words.
closingOne light sentence that fits the verdict, at most 20 words.

The page then checks the reply (LuckKit.check) and lists any disagreement under "The page's check".

Worked examples - every verdict

These are the five free samples on the page: the exact input the page sends and the reply that replays for free.

A doorframe tap, barely tried - too-few-tries

Input:

{
 "task": "luck-test",
 "belief": "tapping the doorframe twice on the way out makes the bus turn up on time",
 "goal": "the bus comes within 5 minutes",
 "tally": "kind: habit (something the person does or carries)\nclaim: lucky (said to make things go well)\nsource: notes (written down at the time)\nwith it: 4 times | went well 3 | went badly 1 | well rate 75%\nwithout it: 4 times | went well 2 | went badly 2 | well rate 50%\ngap toward the claim: +25 points\nverdict: too-few-tries\nplan: coin | 12 more times | a coin decides each time whether to do it",
 "notes": "note 1 | started tapping last month\nnote 2 | the late bus was during roadworks"
}

Reply:

{
 "task": "luck-test",
 "verdict": "too-few-tries",
 "headline": "Too few times either way so far - not enough yet to tell if the tap helps.",
 "numbers": "You tapped the doorframe 4 times and skipped it 4 times; things went well 75% of the times you tapped versus 50% when you did not - a gap of 25 points higher, but only 4 tries each so far.",
 "suspects": [
  {
   "id": "chance",
   "why": "With only 4 tries each way, random swings in bus timing could easily create or hide a gap like this."
  },
  {
   "id": "other-cause",
   "why": "A note says the bad outcome happened during roadworks, so something else that comes along with it could explain that result, not the tap."
  },
  {
   "id": "picking-moments",
   "why": "It's worth checking whether the tap happens every time you leave or only sometimes, since picked moments could skew the result."
  }
 ],
 "fair_test": "Let a coin decide 12 more times whether you tap the doorframe, and write down what happens each time, including the ones you might otherwise forget.",
 "closing": "Early days for the doorframe tap - let's see what the next tries reveal."
}

A ladybird, counted from memory - memory-keeping-score

Input:

{
 "task": "luck-test",
 "belief": "a ladybird landing on me means good news that day",
 "goal": "some good news by the evening",
 "tally": "kind: sign (something that just happens)\nclaim: lucky (said to make things go well)\nsource: memory (counted from memory afterwards)\nwith it: 7 times | went well 6 | went badly 1 | well rate 86%\nwithout it: 22 times | went well 10 | went badly 12 | well rate 45%\ngap toward the claim: +41 points\nverdict: memory-keeping-score\nplan: watch | 10 more times | write down whether it happened before knowing how things went",
 "notes": "note 1 | the day a ladybird landed on my sleeve I got the flat"
}

Reply:

{
 "task": "luck-test",
 "verdict": "memory-keeping-score",
 "headline": "Recalled afterward, these numbers favor the good days, so they can't settle this on their own.",
 "numbers": "You recall good news by evening 86% of the 7 times a ladybird landed on you, versus 45% of the 22 times it did not - a gap of 41 points higher.",
 "suspects": [
  {
   "id": "memory",
   "why": "The sleeve note about getting the flat is a vivid, memorable instance - exactly the kind of hit that memory holds onto while quieter days fade."
  },
  {
   "id": "other-cause",
   "why": "Something else that comes along with noticing a ladybird could be what actually brings the good news, not the sign itself."
  },
  {
   "id": "picking-moments",
   "why": "It's worth checking whether you tend to notice a ladybird mainly in particular situations, which could already line up with good news."
  }
 ],
 "fair_test": "For the next 10 times, write down whether a ladybird lands on you before you know how the day turns out, so every instance counts, not just memorable ones.",
 "closing": "Writing it down as it happens, rather than after, should give a clearer read than memory alone."
}

Touching wood before sending - holds-up-so-far

Input:

{
 "task": "luck-test",
 "belief": "saying touch wood and tapping the desk before I send a job application",
 "goal": "I hear back from them",
 "tally": "kind: habit (something the person does or carries)\nclaim: lucky (said to make things go well)\nsource: notes (written down at the time)\nwith it: 10 times | went well 6 | went badly 4 | well rate 60%\nwithout it: 10 times | went well 2 | went badly 8 | well rate 20%\ngap toward the claim: +40 points\nverdict: holds-up-so-far\nplan: coin | 10 more times | a coin decides each time whether to do it",
 "notes": "note 1 | I keep a spreadsheet of every application\nnote 2 | I forget to tap when I send them late at night"
}

Reply:

{
 "task": "luck-test",
 "verdict": "holds-up-so-far",
 "headline": "So far, applications go better when you say touch wood and tap the desk first.",
 "numbers": "You heard back 60% of the 10 times you said touch wood and tapped the desk, versus 20% of the 10 times you did not - a gap of 40 points higher.",
 "suspects": [
  {
   "id": "other-cause",
   "why": "You said you forget to tap when sending late at night, so something else about those late-night applications - not the ritual - could be behind the worse results."
  },
  {
   "id": "picking-moments",
   "why": "The tap mostly happens except late at night, so the ritual lines up with particular moments to send, not random ones."
  },
  {
   "id": "self-fulfilling",
   "why": "Saying touch wood and tapping before sending could itself make you pause and double-check the application, which could change how it turns out."
  }
 ],
 "fair_test": "Let a coin decide 10 more times whether you say touch wood and tap the desk before sending, and write down what happens each time, including the ones you might forget.",
 "closing": "The ritual is ahead so far - a coin-decided run should show whether that holds without you choosing it."
}

A black cat on the way in - runs-backwards

Input:

{
 "task": "luck-test",
 "belief": "a black cat crossing my path on the walk to work means a rough day",
 "goal": "a calm day at work",
 "tally": "kind: sign (something that just happens)\nclaim: unlucky (said to make things go badly)\nsource: notes (written down at the time)\nwith it: 8 times | went well 6 | went badly 2 | well rate 75%\nwithout it: 18 times | went well 9 | went badly 9 | well rate 50%\ngap toward the claim: -25 points\nverdict: runs-backwards\nplan: watch | 10 more times | write down whether it happened before knowing how things went",
 "notes": "note 1 | the cat lives at number 12\nnote 2 | it is mostly out on sunny mornings"
}

Reply:

{
 "task": "luck-test",
 "verdict": "runs-backwards",
 "headline": "So far, work goes calmer on the days the cat crosses your path, not rougher.",
 "numbers": "Work went well 75% of the 8 times the cat crossed your path, versus 50% of the 18 times it did not - a gap of 25 points lower.",
 "suspects": [
  {
   "id": "other-cause",
   "why": "A note mentions the cat is mostly out on sunny mornings, so good weather - not the cat - could be what makes the day calmer."
  },
  {
   "id": "picking-moments",
   "why": "You mostly notice the cat on sunny mornings, so the sign may line up with specific situations rather than happening at random."
  },
  {
   "id": "self-fulfilling",
   "why": "Expecting a rough day after seeing the cat could make you extra careful on the walk, which might be part of why the day goes calmer instead."
  }
 ],
 "fair_test": "For the next 10 times you walk to work, write down whether the cat crosses your path before you know how the day goes, counting every walk, not just the memorable ones.",
 "closing": "Right now the pattern runs the other way - tracking it as it happens should give a steadier picture."
}

Right foot first - coin-flip

Input:

{
 "task": "luck-test",
 "belief": "stepping out of the front door right foot first makes the day go smoothly",
 "goal": "a day with no hiccups",
 "tally": "kind: habit (something the person does or carries)\nclaim: lucky (said to make things go well)\nsource: notes (written down at the time)\nwith it: 12 times | went well 8 | went badly 4 | well rate 67%\nwithout it: 11 times | went well 7 | went badly 4 | well rate 64%\ngap toward the claim: +3 points\nverdict: coin-flip\nplan: coin | 10 more times | a coin decides each time whether to do it",
 "notes": "none"
}

Reply:

{
 "task": "luck-test",
 "verdict": "coin-flip",
 "headline": "Right foot first or not, your days turned out about the same either way.",
 "numbers": "You had a day with no hiccups 67% of the 12 times you stepped out right foot first, versus 64% of the 11 times you did not - a gap of just 3 points.",
 "suspects": [
  {
   "id": "chance",
   "why": "With days split about 12 and 11 either way, ordinary variation alone could easily explain such a small, close gap."
  },
  {
   "id": "other-cause",
   "why": "Something else that happens around the time you step out - separate from which foot you lead with - could be shaping how the day goes."
  },
  {
   "id": "picking-moments",
   "why": "It's worth checking whether you only lead with your right foot on particular kinds of days, which could line up with how the day goes anyway."
  }
 ],
 "fair_test": "Let a coin decide 10 more times whether you lead with your right foot, and write down what happens each time, including the days you might otherwise forget.",
 "closing": "So far it's a near-even split - letting a coin decide for a while could sharpen the picture."
}

Build the input yourself

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

const K = LuckKit;
const built = K.buildInput(K.form({ belief: "wearing my green scarf on market day means the stall sells out",
  goal: "the stall sells out", kind: "habit", claim: "lucky", source: "notes", wg: 7, wb: 3, og: 4, ob: 6, notes: [] }));
built.input.tally;                      // the page's lines: rates, gap, verdict and plan
const wire = K.toWire(built.input);     // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// built.facts.verdict                  // the verdict the page's rule gives
// K.check(reply, built).notes          // the page's re-check of the reply
// K.md(built, reply, "my test")        // the test 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://touch-wood.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":"touch-wood"}' | 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": "luck-test", "belief": "stepping out of the front door right foot first makes the day go smoothly", "goal": "a day with no hiccups", "tally": "kind: habit (...)\nclaim: lucky (...)\nsource: notes (...)\nwith it: 12 times | went well 8 | went badly 4 | well rate 67%\nwithout it: 11 times | went well 7 | went badly 4 | well rate 64%\ngap toward the claim: +3 points\nverdict: coin-flip\nplan: coin | 10 more times | a coin decides each time whether to do it", "notes": "none"}
# - 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 touch-wood:<task>:<inputHash>:a<attempt> (the one task is luck-test), 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="touch-wood:luck-test:$(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: touch-wood:luck-test:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json