← First Badges
First Badges API
Tokens

Drive First Badges from your own code

The one metered run is available over HTTP: send a kid's age, what they can already do, what they are into, what you can teach and your minutes a week, and it returns a sash of 3, 4 or 6 life-skill badges - each with an area, a ready/stretch step, a three-step first lesson (show it, do it together, let them try alone), a look-for, practice and a safety line where heat, blades or tools are involved.

The division of labour first: the page sizes and schedules; the model picks and writes; the page re-checks the writing. The sash size, the starter list's ideas for the age (rough age ranges - estimates), what the can-do list already covers and the one-new-badge-a-week schedule are arithmetic in badgekit.js, free and in the browser. Build the input with the same script the page uses (see Build the input yourself) so your page_sash and page_ideas match the page's.

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":"first-badges"}), 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 such as task or age 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 sash 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

Every run sends task: sash - there is one lane. The sash size comes from three ordered, non-overlapping rules on the page's counts; the first that matches wins:

OrderSizeWhenBadges
1starterUnder 45 minutes a week, or fewer than 2 lines in can_do and into together.3, over at least 2 areas, and asks lists 1 to 3 things to tell the page next time.
2halfUnder 90 minutes a week.4, over at least 3 areas.
3fullEverything else.6, over at least 4 areas.

Each badge's step follows two ordered rules too: ready when it leans on one can_do line (copied word for word into builds_on), otherwise stretch with a first_step. The declared input schema carries scalars only, so every value is a string (BadgeKit.toWire does this).

Input fields

FieldTypeNotes
taskstringsash.
agestringWhole years, 3 to 17. Required.
nicknamestringWhat to call the kid, up to 24 characters, or empty (the reply then says "your kid").
can_dostringWhat they can already do, one per line. Lists past 1,200 characters keep their first lines and say how many were left out.
intostringWhat they are into, one per line.
grownupstringWhat you know or could teach, one per line.
minutesstringMinutes a week you can give (20 to 180 on the page).
focusstringany or one area: hands, kitchen, money, around, people, body, mind, home.
page_sashstringThe size the rules above give.
page_countsstringminutes a week 120; can-do lines 5; into lines 3; badges 6.
page_ideasstringUp to 12 starter-list skills for the age, not already covered, as area: name lines - a menu the model may use.
retry_notestringOnly on a reformat retry.

The reply

The job's output.output is a string holding one JSON object: task, sash, title, badges ([{name, area, step, builds_on, first_step, why, lesson: {minutes, you_need, steps, say}, look_for, practice, safety}], where lesson.minutes is 10, 15 or 20 and lesson.steps holds three steps: show, together, alone), note and asks (only for starter). Parse it with BadgeKit.normalize(BadgeKit.parseReply(text)) and re-check it with BadgeKit.check(reply, form): that flags a size or badge count that differs from the rules, a ready badge whose builds_on is not one of your lines, a stretch badge that leans on a can-do line, a badge that may repeat something already on the list, too few areas, a missed focus, a kitchen or tool badge without a safety line, a gendered pronoun, a month or weekday, and badges that will not fit the weekly minutes.

Worked examples

Replies produced against the app's prompt for the four samples (made-up kids; long fields shortened; the full replies ship in example-result.js). Together they cover every sash size and both steps.

Robin, 9, 120 minutes a week - full sash

Input:

{
 "task": "sash",
 "age": "9",
 "nickname": "Robin",
 "can_do": "rides a bike without stabilisers\nmakes toast\nties shoes\ncracks eggs into a bowl\nwalks to the corner shop with me",
 "into": "dinosaurs\nbuilding forts out of cushions\ncomic books",
 "grownup": "fixing bikes\nbaking bread",
 "minutes": "120",
 "focus": "any",
 "page_sash": "full",
 "page_counts": "minutes a week 120; can-do lines 5; into lines 3; badges 6",
 "page_ideas": "hands: Button Mender\naround: Map Reader\npeople: Phone Caller\nbody: Scrape Fixer\n ... (243 characters)"
}

Reply:

{
 "task": "sash",
 "sash": "full",
 "title": "Robin's First Badges Sash",
 "badges": [
  {
   "name": "Street Crosser",
   "area": "around",
   "step": "ready",
   "builds_on": "walks to the corner shop with me",
   "first_step": "",
   "why": "Robin already walks to the corner shop with you, so learning to check and cross a street safely is a natural next step.",
   "lesson": {
    "minutes": 15,
    "you_need": [
     "comfortable shoes"
    ],
    "steps": [
     "You stop at the curb, look both ways out loud, and say when it is clear to cross.",
     "Cross together, with them checking both ways out loud and you confirming before you step off the curb.",
     "They check both ways and decide when to cross while you watch closely from beside them."
    ],
    "say": "Let's practice checking both ways before we cross, just like we do together."
   },
   "look_for": "Robin stops at the curb, looks both ways out loud, and waits for a clear gap before crossing.",
   "practice": "Have them lead the looking and timing every time you cross a street together.",
   "safety": "Stay right beside Robin at the kerb, and Robin does not cross a road without you yet."
  },
  {
   "name": "Bike Checker",
   "area": "hands",
   "step": "ready",
   "builds_on": "rides a bike without stabilisers",
   "first_step": "",
   "why": "Robin already rides a bike without stabilisers, and you know how to fix bikes, so checking it together is a natural next step.",
   "lesson": {
    "minutes": 15,
    "you_need": [
     "the bike"
    ],
    "steps": [
     "You squeeze each tyre and press the brake levers, saying what firm tyres and good brakes should feel like.",
     "Check the bike together, you holding it steady while they squeeze the tyres and test the brakes.",
     "They check the tyres and brakes alone while you watch, then tell you if anything feels off."
    ],
    "say": "Let's check your bike together before you ride, so you know it's ready to go."
   },
   "look_for": "Robin checks their own tyres and brakes before a ride without being asked and tells you what they find.",
   "practice": "Have them check the bike together before every ride until it becomes automatic.",
   "safety": ""
  },
  "... (6 badges in all)"
 ],
 "note": "Start with one badge a week in the order given, and keep practicing the earlier ones so each new skill sticks.",
 "asks": []
}

a 13-year-old, money focus, 60 minutes - half sash

Input:

{
 "task": "sash",
 "age": "13",
 "nickname": "",
 "can_do": "makes pasta on the stove\ntakes the bus to school\nhas a bank card\ndoes homework without reminders",
 "into": "skateboarding\nediting videos",
 "grownup": "spreadsheets\nsewing",
 "minutes": "60",
 "focus": "money",
 "page_sash": "half",
 "page_counts": "minutes a week 60; can-do lines 4; into lines 2; badges 4",
 "page_ideas": "money: Budget Keeper\nmind: Week Planner\nhands: Flat Fixer\nhome: Laundry Runner\na ... (249 characters)"
}

Reply:

{
 "task": "sash",
 "sash": "half",
 "title": "Money and Time Skills Sash",
 "badges": [
  {
   "name": "Budget Keeper",
   "area": "money",
   "step": "ready",
   "builds_on": "has a bank card",
   "first_step": "",
   "why": "Your kid already has a bank card, so keeping track of what goes in and out of it is a good next skill.",
   "lesson": {
    "minutes": 15,
    "you_need": [
     "the bank card",
     "a notebook",
     "a pen"
    ],
    "steps": [
     "You look at a recent bank statement together, pointing out what each charge was for.",
     "You write down a few recent purchases together in a simple spending notebook.",
     "They log their own spending for a few days alone while you check in."
    ],
    "say": "Let's look at your bank card spending and start tracking it together."
   },
   "look_for": "Your kid writes down what they spent without being asked or reminded.",
   "practice": "Check the spending notebook together each week and add any new purchases.",
   "safety": ""
  },
  {
   "name": "Week Planner",
   "area": "mind",
   "step": "ready",
   "builds_on": "does homework without reminders",
   "first_step": "",
   "why": "Your kid already does homework without reminders, so planning a weekly schedule around skating and video projects is a good next step.",
   "lesson": {
    "minutes": 15,
    "you_need": [
     "a sheet of paper",
     "a pen"
    ],
    "steps": [
     "You map out this week on paper, saying out loud why each block goes where.",
     "You fill in time for skating and video editing together on the plan.",
     "They plan out their own week alone while you look over the result after."
    ],
    "say": "Let's plan out your week together, including time for skating and editing."
   },
   "look_for": "Your kid fills in a weekly plan alone, including school, chores and free time.",
   "practice": "Plan the coming week together each week until they start doing it alone.",
   "safety": ""
  },
  "... (4 badges in all)"
 ],
 "note": "Start with one badge a week, keep the earlier ones going, and move to the next when your kid is ready.",
 "asks": []
}

Juno, 5, 30 minutes and a one-line brief - starter sash

Input:

{
 "task": "sash",
 "age": "5",
 "nickname": "Juno",
 "can_do": "brushes teeth",
 "into": "",
 "grownup": "",
 "minutes": "30",
 "focus": "any",
 "page_sash": "starter",
 "page_counts": "minutes a week 30; can-do lines 1; into lines 0; badges 3",
 "page_ideas": "hands: Lace Tier\nmoney: Coin Namer\naround: Address Keeper\npeople: Hello Sayer\nho ... (247 characters)"
}

Reply:

{
 "task": "sash",
 "sash": "starter",
 "title": "Juno's First Badges Sash",
 "badges": [
  {
   "name": "Shoe Lacer",
   "area": "hands",
   "step": "stretch",
   "builds_on": "",
   "first_step": "Threading a lace through each shoe hole in order, pulling it snug after every pass.",
   "why": "Many kids start managing their own shoelaces around this age.",
   "lesson": {
    "minutes": 15,
    "you_need": [
     "a lace-up shoe"
    ],
    "steps": [
     "You thread a lace through one hole while saying what you are doing out loud.",
     "You and your kid thread a few holes together, taking turns pulling each lace snug.",
     "They lace the shoe alone while you watch and step in only if asked."
    ],
    "say": "Let's see how far you can lace this shoe today."
   },
   "look_for": "Juno threads each hole in order and pulls the lace snug without any help.",
   "practice": "Let Juno lace their own shoes most days before putting them on.",
   "safety": ""
  },
  {
   "name": "Steady Pourer",
   "area": "kitchen",
   "step": "stretch",
   "builds_on": "",
   "first_step": "Pouring a little water from one cup into another without spilling any of it.",
   "why": "Many kids start pouring their own drinks around this age.",
   "lesson": {
    "minutes": 10,
    "you_need": [
     "a small jug",
     "two cups",
     "a towel"
    ],
    "steps": [
     "You pour slowly from the jug into a cup while saying what you are doing.",
     "You hold the jug together and pour into a cup, taking turns tipping it slowly.",
     "They pour from the jug into a cup alone while you watch for any spills."
    ],
    "say": "Want to practice pouring your own drink today?"
   },
   "look_for": "Juno tips the jug and stops pouring right at the top of the cup without spilling.",
   "practice": "Let Juno pour their own drink at the table most days.",
   "safety": "Stay next to Juno while pouring, and keep anything hot off the table for now."
  },
  "... (3 badges in all)"
 ],
 "note": "Start one badge this week, and keep practicing the earlier ones as you add new ones.",
 "asks": [
  "List a few more things your kid can already do without help.",
  "Name one thing your kid is into right now, even something small.",
  "Say what you enjoy teaching so this next sash can use it."
 ]
}

Ash, 15, leaning into getting around - full sash

Input:

{
 "task": "sash",
 "age": "15",
 "nickname": "Ash",
 "can_do": "cooks a simple dinner\ndoes own laundry\nrides the bus across town\nkeeps a savings jar",
 "into": "climbing\nsci-fi novels\nfixing old phones",
 "grownup": "car maintenance\nbudgeting",
 "minutes": "180",
 "focus": "around",
 "page_sash": "full",
 "page_counts": "minutes a week 180; can-do lines 4; into lines 3; badges 6",
 "page_ideas": "around: Trip Planner\nmoney: Budget Keeper\nkitchen: Family Cook\npeople: Appointme ... (255 characters)"
}

Reply:

{
 "task": "sash",
 "sash": "full",
 "title": "Ash's Around and Money Sash",
 "badges": [
  {
   "name": "Trip Planner",
   "area": "around",
   "step": "ready",
   "builds_on": "rides the bus across town",
   "first_step": "",
   "why": "Ash already rides the bus across town, so planning a new route with a transfer is a good next step.",
   "lesson": {
    "minutes": 20,
    "you_need": [
     "a phone or printed map",
     "a notebook"
    ],
    "steps": [
     "You look up a new bus route with a transfer, saying each step out loud as you plan.",
     "You ride the new route together once, pointing out each stop and transfer along the way.",
     "They plan and ride the same new route alone while you wait at the end stop."
    ],
    "say": "Let's plan a new bus route with a transfer and try it together."
   },
   "look_for": "Ash plans a new route with a transfer and rides it alone without help.",
   "practice": "Plan and ride a different new bus route together every so often.",
   "safety": "You stay reachable and nearby while Ash tries the route, and Ash checks in with you if plans change."
  },
  {
   "name": "Recipe Reader",
   "area": "kitchen",
   "step": "ready",
   "builds_on": "cooks a simple dinner",
   "first_step": "",
   "why": "Ash already cooks a simple dinner, so learning to read and follow a new recipe is a good next step.",
   "lesson": {
    "minutes": 20,
    "you_need": [
     "a written recipe",
     "a few ingredients",
     "a pan"
    ],
    "steps": [
     "You read a recipe out loud together, explaining what each step and measurement means.",
     "You cook the recipe together, taking turns on each step and checking it twice.",
     "They read and cook a new recipe alone while you watch nearby at the stove."
    ],
    "say": "Let's pick a new recipe and read through it together before cooking."
   },
   "look_for": "Ash reads a new recipe and cooks it alone without checking each step with you.",
   "practice": "Pick a new recipe together every so often and let Ash lead the cooking.",
   "safety": "You stay in the kitchen near the stove while Ash cooks, and Ash does not handle hot pans alone yet."
  },
  "... (6 badges in all)"
 ],
 "note": "Start with one badge a week, keep the earlier ones going, and let Ash take the lead once each skill is comfortable.",
 "asks": []
}

Build the input yourself

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

const K = BadgeKit;
const form = { age: "9", nickname: "Robin", can_do: "rides a bike without stabilisers\nmakes toast\ncracks eggs into a bowl",
  into: "dinosaurs", grownup: "fixing bikes", minutes: "120", focus: "any" };
console.log(K.lean(form));                                // the sash size and why
console.log(K.ideas(form).map(e => e.name));              // the starter list's ideas for this age (rough guide)
const wire = K.toWire(K.buildInput(form).input);          // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// const { notes, weeks } = K.check(reply, form);         // the page's re-check and the one-a-week schedule
// K.plainText(reply, form)                               // the lesson cards as plain text
// K.draft(form)                                          // the free draft sash, from the starter list alone

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://first-badges.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":"first-badges"}' | 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": "sash", "age": "9", "nickname": "Robin", "can_do": "rides a bike without stabilisers\nmakes toast\n...", "into": "dinosaurs\n...", "grownup": "fixing bikes\nbaking bread", "minutes": "120", "focus": "any", "page_sash": "full", "page_counts": "minutes a week 120; ...", "page_ideas": "hands: Button Mender\n..."}
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 first-badges:<task>:<inputHash>:a<attempt> (there is one lane, sash), 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="first-badges:sash:$(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: first-badges:sash:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json