← Eight by Eight
API
Tokens

Drive Eight by Eight from your own code

The one metered step - the race-day strategy call - is available over HTTP. A natural use is a coach preparing a squad: a script builds two plans per athlete, sends each pair, and prints the calls as a pace band per athlete.

Be clear about the split first: the page does the arithmetic; the model makes the calls. The split model, the comparison and the pacing flags ship as a plain script, racekit.js (RaceKit.plan, compare, facts, buildInput, toWire, normalize, reconcile), which you can load in node. Reference splits inside it are a rough typical shape per division, not official results data.

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":"eight-by-eight"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare: never wrap it as {"input": {…}}.

StatusCodeMeaning
400validation_errorThe body is not a JSON object.
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 strategy call 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 comes first

The app has one task. Always send "task": "strategy":

taskWhat comes back
strategyA verdict (race Plan A, Plan B or a blend, expected finish, confidence, why), both plans weighed (upside, downside, risk, where each breaks), exactly 16 segment calls (hold / steady / push / recover, a target split, a cue, a reason), a transitions target with tips, 3–5 contingencies, 2–4 what-ifs with their effect in seconds, and warnings.

Input fields

The declared input schema carries scalars only, so the object-valued fields travel JSON-encoded as strings (RaceKit.toWire does this).

FieldTypeNotes
taskstring, requiredAlways strategy.
divisionstring, requiredOpen men, Open women, Pro men or Pro women.
plan_a, plan_bstring (JSON), required{label, mode, finish, runs_total, stations_total, transitions_total, knobs, splits}; splits is 16 [segment_id, "m:ss"] pairs in race order: run1, ski, run2, push, run3, pull, run4, bbj, run5, row, run6, farmers, run7, lunges, run8, wallballs.
factsstring (JSON){a_finish, b_finish, b_minus_a, biggest_differences[], flags[], last_race?} - the page's comparison and pacing flags; every flag must be answered.
athletestringOptional notes, at most 1,200 characters (the page keeps the start and end of longer notes with a [...] marker).
eventstringOptional event name.
retry_notestringOnly on a reformat retry.

The reply

The job's output.output is a string holding one JSON object: {task, verdict, scenarios, calls, transitions, contingencies, what_ifs, warnings}. Parse it with RaceKit.parseReply, normalise it, then check it with RaceKit.reconcile — the page flags missing segment calls, targets more than 15% from the picked plan (for a blend, from the nearer plan), targets that do not add up to the stated finish, too many pushes and pacing flags left unanswered.

Worked examples

Real replies, produced against the app's prompt (calls shortened here; the full replies ship in example-result.js). The page replays each of them free from its example buttons: the first-race athlete gets Plan B, the sub-1:15 attempt gets Plan A, and the Pro front-pack plan gets a blend because its B plan carries two pacing flags.

Open men, sub-1:15, last race pasted

Input (decoded for reading; on the wire plan_a, plan_b and facts are JSON strings):

{
  "task": "strategy",
  "division": "Open men",
  "plan_a": {
    "label": "Even sub-75",
    "mode": "goal",
    "finish": "1:14:58",
    "runs_total": "36:02",
    "stations_total": "32:26",
    "transitions_total": "6:30",
    "knobs": {
      "goal": "1:14:59",
      "fade_pct": 4,
      "per_transition_s": 26,
      "strengths": {
        "ski": "avg",
        "push": "avg",
        "pull": "avg",
        "bbj": "avg",
        "row": "avg",
        "farmers": "strong",
        "lunges": "weak",
        "wallballs": "weak"
      }
    },
    "splits": [
      [
        "run1",
        "4:15"
      ],
      [
        "ski",
        "4:09"
      ],
      [
        "run2",
        "4:22"
      ],
      [
        "...",
        "13 more"
      ]
    ]
  },
  "plan_b": {
    "label": "Bank the runs",
    "mode": "goal",
    "finish": "1:15:02",
    "runs_total": "35:41",
    "stations_total": "32:51",
    "transitions_total": "6:30",
    "knobs": {
      "goal": "1:14:59",
      "fade_pct": 9,
      "per_transition_s": 26,
      "strengths": {
        "ski": "avg",
        "push": "avg",
        "pull": "avg",
        "bbj": "avg",
        "row": "avg",
        "farmers": "strong",
        "lunges": "weak",
        "wallballs": "weak"
      }
    },
    "splits": [
      [
        "run1",
        "3:57"
      ],
      [
        "ski",
        "4:09"
      ],
      [
        "run2",
        "4:07"
      ],
      [
        "...",
        "13 more"
      ]
    ]
  },
  "facts": {
    "a_finish": "1:14:58",
    "b_finish": "1:15:02",
    "b_minus_a": "+0:04",
    "biggest_differences": [
      "Wall balls +0:25",
      "Run 1 -0:18",
      "Run 2 -0:15",
      "Run 8 +0:07"
    ],
    "flags": [
      "Plan B: Run 1 at 3:57 is more than 8% quicker than the average run (4:28) - the classic blow-up start."
    ],
    "last_race": {
      "covered": "17 of 17",
      "net_vs_plan_a": "+7:43",
      "leaks": [
        "Wall balls +1:40",
        "Sandbag lunges +1:04",
        "Transitions (15) +1:01",
        "Run 8 +0:39"
      ]
    }
  },
  "event": "Chicago",
  "athlete": "Raced 1:19:40 in Boston two weeks ago. Strong runner (5 km 18:30). Lunges and wall balls fell apart - broke wall balls into sets of 5 after 50. Training compromised runs twice a week now."
}

Reply:

{
  "task": "strategy",
  "verdict": {
    "pick": "A",
    "headline": "Race Plan A: it is faster on paper, steadier, and protects your weak stations late.",
    "expected_finish": "1:14:58",
    "confidence": "medium",
    "why": "Plan A finishes 4 seconds faster than Plan B (1:14:58 vs 1:15:02) while carrying only a 4% fade against Plan B's 9%, so it does not ask you to bank speed you have to pay back later. Plan B's 3:57 opening run is flagged as more than 8% quicker than the average run, the classic blow-up start, and that risk is not needed since your running speed is already strong. Your last race leaked most of its 7:43 overage at wall balls and sandbag lunges, not in the runs, so protecting those two stations matters more than banking seconds on Run 1 or Run 2."
  },
  "scenarios": [
    {
      "id": "A",
      "label": "Even sub-75",
      "upside": "Lower 4% fade and even splits mean the plan is already the faster of the two on paper, and it does not spend extra energy early that has to be repaid late.",
      "downside": "The 4% fade cushion is thin if wall balls or sandbag lunges fatigue harder than modeled, which is exactly what happened in your last race.",
      "risk": "medium",
      "blowup_point": "Wall balls: if the pace fades beyond the plan's 4% assumption the way it did last race, this is where the finish time slips."
    },
    {
      "id": "B",
      "label": "Bank the runs",
      "upside": "Uses your strong running background to bank about 33 seconds across Run 1 and Run 2 while the legs are freshest.",
      "downside": "The 9% fade it bakes in is a bigger bet on recovery, and it still finishes 4 seconds slower than Plan A overall.",
      "risk": "high",
      "blowup_point": "Run 1: flagged at 3:57, more than 8% quicker than the average run split, the classic blow-up start that can cascade fatigue into the lunges and wall balls."
    }
  ],
  "calls": [
    {
      "segment": "run1",
      "call": "hold",
      "target": "4:15",
      "cue": "Easy pace, save the legs",
      "why": "Plan B's 3:57 opener was flagged as a blow-up start; holding at 4:15 avoids that pattern and saves legs for lunges and wall balls."
    },
    {
      "segment": "ski",
      "call": "steady",
      "target": "4:09",
      "cue": "Smooth strokes, long pull",
      "why": "Average ski strength, so race the plan rather than chase the field."
    },
    {
      "segment": "run2",
      "call": "steady",
      "target": "4:22",
      "cue": "Settle into rhythm",
      "why": "Legs are still fresh, so this run holds goal pace without needing to bank time."
    },
    {
      "...": "13 more calls, one per segment"
    }
  ],
  "transitions": {
    "target_total": "6:30",
    "tips": [
      "Jog, don't walk, between the mat and the station line - transitions leaked 1:01 last race.",
      "Pre-stage chalk at the sled pull and farmers carry stations so hands are ready on arrival.",
      "Sip water only at the transitions after ski and after row, from a bottle staged in your lane.",
      "Double-knot laces before the start so no transition is spent retying shoes."
    ]
  },
  "contingencies": [
    {
      "if": "Wall balls reps start failing or you get a no-rep",
      "then": "Drop immediately to broken sets of 5, the same adjustment that worked in Boston, instead of waiting for a second miss to break the set."
    },
    {
      "if": "Sandbag lunges form breaks down or pace drops more than 10 seconds under target",
      "then": "Shorten the stride and slow the cadence rather than stopping; a controlled slower lunge beats the stop-and-start pattern that cost 1:04 last race."
    },
    {
      "if": "Run 5 or Run 6 comes in 20 seconds or more over target",
      "then": "Do not chase it back on the next run; hold the remaining run splits and let the time loss stand rather than risking wall balls or lunges."
    },
    {
      "if": "The sled will not move after the first push or pull attempt",
      "then": "Reset with shorter, faster steps and a lower hip position rather than straining upright; do not spend more than 10 seconds resetting."
    },
    {
      "if": "Legs feel heavy entering Run 8, as in the 0:39 leak last race",
      "then": "Shorten the stride and raise cadence instead of slowing further, to protect the ability to push wall balls at the finish."
    }
  ],
  "what_ifs": [
    {
      "change": "Open with Plan B's faster Run 1 and Run 2 (3:57, 4:07) but keep Plan A's pacing everywhere else.",
      "effect_seconds": -33,
      "worth_it": false,
      "note": "Saves 33 seconds on paper, but the flagged 8%-faster opening risks compounding fatigue into wall balls and lunges, the stations already leaking the most time; this is the tempting change that is not worth it."
    },
    {
      "change": "Push wall balls unbroken instead of planning broken sets of 5.",
      "effect_seconds": -20,
      "worth_it": false,
      "note": "Could save time if it holds, but the last race leaked 1:40 trying to grind reps before breaking; the planned broken sets are what actually recovered pace."
    },
    {
      "change": "Tighten transitions by jogging the mat-to-station gap instead of walking it.",
      "effect_seconds": -60,
      "worth_it": true,
      "note": "Pure technique fix with no fatigue cost; last race's transitions leaked 1:01, and this change targets exactly that leak."
    },
    {
      "change": "Push harder on Run 4 to bank time instead of saving that effort for farmers carry.",
      "effect_seconds": -10,
      "worth_it": false,
      "note": "A harder run between two stations taxes the legs ahead of sled pull, burpees, and lunges for a small gain; better to spend aggression on farmers carry, your strongest station."
    }
  ],
  "warnings": [
    "The athlete note mentions running training has been cut to twice a week recently; if that reflects pain or injury rather than scheduling, do not push through sharp or worsening pain on the runs today, and ease off rather than racing through it. This is not medical advice."
  ]
}

Pro men, a flagged plan becomes a blend

Verdict only:

{
  "pick": "blend",
  "headline": "Blend Plan B's real pace everywhere, but correct Run 1 and transitions to realistic numbers.",
  "expected_finish": "0:57:47",
  "confidence": "medium",
  "why": "Plan B is about 8 minutes faster on paper because it is built from your own fresh 1 km pace, but the page flagged two spots that cannot really happen: Run 1 at 3:03 beats that same fresh pace, and 14 seconds per transition is faster than most elite athletes manage. Racing Plan B's numbers everywhere else, holding Run 1 to 3:06, and budgeting transitions near 18 seconds each keeps the front-pack ambition you asked for without the two parts of the plan that are not realistic. Burpee broad jumps also keep Plan B's own 4:10, not Plan A's tighter 3:55, because that station is your known weak point."
}

Build the input yourself

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

const R = RaceKit;
const pa = R.plan({ label: "Even", division: "open_m", mode: "goal", goal: R.parseTime("1:14:59"), fade: 4, rox: 26 });
const pb = R.plan({ label: "My numbers", division: "open_m", mode: "numbers", pace: R.parseTime("3:45"), compromise: 12, fade: 6, rox: 26 });
const { input } = R.buildInput({ planA: pa, planB: pb, notes: "Strong runner, weak wall balls." });
const wire = R.toWire(input);               // POST this to /estimate and /run
// later: const res = R.normalize(R.parseReply(job.output.output));
// R.reconcile(res, { planA: pa, planB: pb, facts: R.facts(pa, pb) }).notes

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://eight-by-eight.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":"eight-by-eight"}' | 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": "strategy", "division": "Open women", "plan_a": "{...}", "plan_b": "{...}", "facts": "{...}"}
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 eight-by-eight:<task>:<inputHash>:a<attempt>, 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="eight-by-eight:strategy:$(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: eight-by-eight:strategy:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json