← Two Deep
API
Tokens

Drive Two Deep from your own code

The one metered step - the coordinator's check of one event day's volunteer plan - is available over HTTP. A natural use is a volunteer sign-up sheet or a scheduling spreadsheet: your code sends the plan with the page's coverage grid, and gets back a status, quote, reason and fix for every post, the posts to add, a briefing card for the start of each shift, a message asking for the open shifts, next steps and a verdict.

The split first: the page counts; the model judges; the page holds the model to the count. Reading the plan, the shift-slot sizing, the half-hour grid, double bookings, busy backups, lead gaps and the briefing's three parts come from rosterkit.js, which runs in node as well as in the browser. Its check function then compares the reply with the grid: a post is never shown better than the grid counted, every quote must be in the plan as written, every missing post the page found must be listed, the briefing card must carry the hand-off rule, and a plan with open shifts must get a recruiting message.

Get a token on the token page (step 1 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":"two-deep"}), 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, or task 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 coordinator's check 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

Every run sends task. There is one task, check. It returns title, verdict, summary; posts [{post, status, says, why, fix}], one per post in the page's order; missing_posts [{post, why}]; briefing_card (three to six lines, always with the hand-off rule); recruit_ask (a 2-4 sentence message, or empty); next_steps (three to five); caveats.

VerdictMeaning
readyTwo deep: every post has enough named people plus a named backup in every half hour, every shift has one lead, and the briefing has its three parts.
patchThe shape is right (75% or more of post-hours named, a lead named) but there are holes: missing backups, a short half hour, a busy backup, a lead gap, a missing briefing part or post.
rebuildUnder 75% of post-hours have a named person, no shift lead is named, or the posts are not listed.
Post statusMeaning
two_deepEnough named people plus a named backup in every half hour. fix is empty.
thinEnough people, but some half hours with no named backup.
gapSome half hours short of people (a TBD slot counts as nobody).

The declared input schema carries scalars only, so object-valued fields travel JSON-encoded as strings (RosterKit.toWire does this). Only task is required by the schema.

Input fields

FieldTypeNotes
taskstringAlways check.
todaystringYYYY-MM-DD.
event_datestringThe event day, YYYY-MM-DD, or (not given).
rosterstringThe plan as pasted. Past 9,000 characters the middle is cut on whole lines around a marker.
factsstring (JSON){event, window, days_to_event, shift_hours, shift_assumed, posts_n, volunteers_n, first_timers, need_hours, filled_hours, coverage_pct, backup_pct, slots_total, open_person_hours, open_slots, lead_gaps[], lead_overlaps[], double_bookings[], busy_backups[], briefing{}, needs{}}.
postsstring (JSON)[{post, need, window, hours, shift_slots, page_status, gap_times[], thin_times[], primaries[], backups[], tbd, sensitive, unlisted}].
leadsstring (JSON)[{name, window}].
page_readsstring (JSON)[{level, area, code, text}]; level is missing, fix or info.
page_statusstring (JSON)The page's status per area (posts, coverage, backups, leads, briefing): ok, fix or missing.
page_verdictstringThe page's own reading: ready, patch or rebuild.
notesstringOptional, the coordinator's own comments; past 600 characters the middle is cut.
retry_notestringOnly on a reformat retry.

The reply

The job's output.output is a string holding one JSON object. Parse it with RosterKit.parseReply, then normalize, then check it with check(reply, state), which returns {rows, notes}: one row per post (the reply's status, or the grid's when the reply skipped the post or called it better than the grid counted) and the notes (ok, info, warn, bad).

Worked examples

Replies produced against the app's prompt for the sample plans (made-up events and names; lists shortened; the full replies ship in example-result.js). The page replays each of them free from its sample buttons. Inputs are shown with the long fields folded.

task: check - Saturday pantry distribution - ready

Input:

{
 "task": "check",
 "today": "2026-10-19",
 "event_date": "2026-11-21",
 "roster": "Event: Saturday holiday food distribution\nDate: 2026-11-21\nHours: 7:30-12:30\nLead: Rosa Delvarre\nShift length: 2.5 h\n\nPosts:\n- Setup | 3 | 7:30-8:30 | tables, pallets, signs\n- Check-in table | 2 | 8:30-12:00 | client reco ... (1586 characters)",
 "facts": "{\"event\":\"Saturday holiday food distribution\",\"window\":\"7:30-12:30\",\"days_to_event\":33,\"shift_hours\":2.5,\"shift_assumed\":false,\"posts_n\":7,\"volunteers_n\":16,\"first_timers\":1,\"need_hours\":36,\"filled_hours\":36,\"coverage_pct\":100,\"backup_pct\":100,\"slots_total\":18,\"open_person_hours\":0,\"open_slots\":0,\"lead_gaps\":[],\"lead_overlaps\":[],\"double_bookings\":[],\"busy_backups\":[],\"briefing\":{\"choreography\":true,\"contacts\":true,\"conduct\":true},\"needs\":{\"setup\":true,\"teardown\":true,\"floater\":true,\"checkin\":true,\"safety\":true}}",
 "posts": "[{\"post\":\"Setup\",\"need\":3,\"window\":\"7:30-8:30\",\"hours\":1,\"shift_slots\":2,\"page_status\":\"two_deep\",\"gap_times\":[],\"thin_times\":[],\"primaries\":[\"Ken Ivanto\",\"Lena Parrow\",\"Omar Azrine\"],\"backups\":[\"Hal Morrask\"],\"tbd\":0,\"sensitive\":false,\"unlisted\":false},{\"post\":\"Check-in table\",\"need\":2,\"window\":\"8:30-12:00\",\"hours\":3.5,\"shift_slots\":3,\"page_status\":\"two_deep\",\"gap_times\":[],\"thin_times\":[],\"primaries\":[\"Ann Leesworth\",\"Tom Parlane\"],\"backups\":[\"Joe Kimrow\"],\"tbd\":0,\"sensitive\":true,\"unlisted\":false},\"... 5 more\"]",
 "leads": "[{\"name\":\"Rosa Delvarre\",\"window\":\"7:30-10:00\"},{\"name\":\"Marcus Hilvane\",\"window\":\"10:00-12:30\"}]",
 "page_reads": "[{\"level\":\"info\",\"area\":\"coverage\",\"code\":\"first_timers\",\"text\":\"1 first-timer - ask them to arrive 30 minutes early instead of 15.\"}]",
 "page_status": "{\"posts\":\"ok\",\"coverage\":\"ok\",\"backups\":\"ok\",\"leads\":\"ok\",\"briefing\":\"ok\"}",
 "page_verdict": "ready",
 "notes": ""
}

Reply:

{
 "task": "check",
 "title": "Saturday Holiday Food Distribution Plan",
 "verdict": "ready",
 "summary": "This plan is ready to run: every post has its full count of named primaries plus a named backup, covering 100% of the 36 post-hours needed with a backup in 100% of staffed half hours and no open shifts. The two shift leads, Rosa Delvarre and Marcus Hilvane, hand off cleanly at 10:00 with no gap or overlap, and the briefing already has all three parts. The one thing worth a second look before the day: Rosa Delvarre is both the 7:30-10:00 shift lead and the only named backup for First aid and safety during that window.",
 "posts": [
  {
   "post": "Setup",
   "status": "two_deep",
   "says": "Ken Ivanto 7:30-8:30; Lena Parrow 7:30-8:30; Omar Azrine 7:30-8:30; Hal Morrask (backup) 7:30-8:30",
   "why": "Ken Ivanto, Lena Parrow and Omar Azrine meet the need of 3 for the whole 7:30-8:30 hour, with Hal Morrask named as backup throughout.",
   "fix": ""
  },
  {
   "post": "Check-in table",
   "status": "two_deep",
   "says": "Ann Leesworth 8:30-12:00; Tom Parlane 8:30-12:00; Joe Kimrow (backup, on call) 8:30-12:00",
   "why": "Ann Leesworth and Tom Parlane cover the need of 2 the entire 8:30-12:00 window, and Joe Kimrow is a named, on-call backup - no TBD on a post that handles client records.",
   "fix": ""
  },
  {
   "post": "Car line",
   "status": "two_deep",
   "says": "Dan Ruvallo 8:30-10:00; Priya Shalvane 8:30-12:00; Mia Chenwick 10:00-12:00; Sam Rellick (backup) 8:30-12:00",
   "why": "Dan Ruvallo and Priya Shalvane hold the need of 2 until 10:00, then Priya Shalvane and Mia Chenwick hand off and hold it to 12:00, with Sam Rellick named as backup the whole time.",
   "fix": ""
  },
  "..."
 ],
 "missing_posts": [],
 "briefing_card": [
  "Seven posts today: setup, check-in (client records), car line, box loading, floater, first aid and safety, and teardown - know where yours is before the 8:15 walk-through.",
  "Problems or questions: text the shift lead on duty (Rosa Delvarre until 10:00, then Marcus Hilvane) or use radio channel 2.",
  "First aid: Carmen Ortavel. In a real emergency, call 911.",
  "If a client is upset or anything feels unsafe: do not argue, step back, get the shift lead, and report it before you leave.",
  "..."
 ],
 "recruit_ask": "",
 "next_steps": [
  "Remind Nia Scotwell, the first-timer, to arrive by 8:00 (30 minutes early); remind everyone else for the 8:15 briefing.",
  "Confirm Joe Kimrow and Hal Morrask, the on-call backups, know they may be called and how to reach the shift lead on duty.",
  "Walk Rosa Delvarre and Carmen Ortavel through First aid and safety coverage for 8:30-10:00, since Rosa is also leading the shift then.",
  "..."
 ],
 "caveats": [
  "This reading only covers what was pasted; if anyone named here cancels, the coverage above no longer holds.",
  "Rosa Delvarre is both the 7:30-10:00 shift lead and the only backup for First aid and safety in that window - fine on paper, but worth confirming in person."
 ]
}

task: check - Community holiday dinner - patch

Input:

{
 "task": "check",
 "today": "2026-10-19",
 "event_date": "2026-11-26",
 "roster": "Event: Community Thanksgiving dinner\nDate: 2026-11-26\nHours: 10:00-15:00\nLead: Joan Pruvell\nShift length: 2.5 h\n\nPosts:\n- Setup | 4 | 10:00-11:30\n- Greeters and check-in | 2 | 11:30-14:00\n- Serving line | 4 | 11:30-14:00\n ... (1199 characters)",
 "facts": "{\"event\":\"Community Thanksgiving dinner\",\"window\":\"10:00-15:00\",\"days_to_event\":38,\"shift_hours\":2.5,\"shift_assumed\":false,\"posts_n\":6,\"volunteers_n\":15,\"first_timers\":1,\"need_hours\":33,\"filled_hours\":32,\"coverage_pct\":97,\"backup_pct\":36,\"slots_total\":15,\"open_person_hours\":1,\"open_slots\":1,\"lead_gaps\":[\"12:30-13:00\"],\"lead_overlaps\":[],\"double_bookings\":[],\"busy_backups\":[{\"name\":\"Cora Lindquell\",\"backup_for\":\"Dish room\",\"busy_on\":\"Takeout plates\",\"at\":\"12:00-14:00\"}],\"briefing\":{\"choreography\":true,\"contacts\":true,\"conduct\":false},\"needs\":{\"setup\":true,\"teardown\":true,\"floater\":false,\"checkin\":true,\"safety\":false}}",
 "posts": "[{\"post\":\"Setup\",\"need\":4,\"window\":\"10:00-11:30\",\"hours\":1.5,\"shift_slots\":3,\"page_status\":\"thin\",\"gap_times\":[],\"thin_times\":[\"10:00-11:30\"],\"primaries\":[\"Bill Amberline\",\"Cora Lindquell\",\"Pete Shandry\",\"Dee Wilbrant\"],\"backups\":[],\"tbd\":0,\"sensitive\":false,\"unlisted\":false},{\"post\":\"Greeters and check-in\",\"need\":2,\"window\":\"11:30-14:00\",\"hours\":2.5,\"shift_slots\":2,\"page_status\":\"thin\",\"gap_times\":[],\"thin_times\":[\"11:30-14:00\"],\"primaries\":[\"Fran Hallowby\",\"Gus Itley\",\"Hana Chovane\"],\"backups\":[],\"tbd\":0,\"sensitive\":true,\"unlisted\":false},\"... 4 more\"]",
 "leads": "[{\"name\":\"Joan Pruvell\",\"window\":\"10:00-12:30\"},{\"name\":\"Walt Simmerby\",\"window\":\"13:00-15:00\"}]",
 "page_reads": "[{\"level\":\"fix\",\"area\":\"posts\",\"code\":\"need_floater\",\"text\":\"Floater: no post or named contact for it. A floater covers breaks and no-shows without anyone leaving a post.\"},{\"level\":\"fix\",\"area\":\"posts\",\"code\":\"need_safety\",\"text\":\"Safety or conduct contact: no post or named contact for it. Name who handles a safety or conduct problem, and how volunteers reach them.\"},\"... 10 more\"]",
 "page_status": "{\"posts\":\"fix\",\"coverage\":\"fix\",\"backups\":\"fix\",\"leads\":\"fix\",\"briefing\":\"fix\"}",
 "page_verdict": "patch",
 "notes": "Same church hall as last year. Serving line is the busiest from noon to one."
}

Reply:

{
 "task": "check",
 "title": "Community Thanksgiving Dinner, Nov 26",
 "verdict": "patch",
 "summary": "This is a patch, not a rebuild: coverage is strong at 97% of post-hours filled and two shift leads are named, but only 36% of staffed half-hours have a named backup and 1 post-hour is still open. The most important fix is Dish room 13:30-14:30, where Ray Foxleigh's shift ends early and leaves Olga Ruvado alone against a need of two, while the post's named backup, Cora Lindquell, is committed to Takeout plates at the same time. A shift-lead gap (12:30-13:00) and a missing conduct-and-safety script also need patching before the day.",
 "posts": [
  {
   "post": "Setup",
   "status": "thin",
   "says": "Bill Amberline 10:00-11:30",
   "why": "Four named people cover Setup 10:00-11:30, but no one is named as backup, and nobody else on the roster is free during that window to stand in.",
   "fix": "Ask a new volunteer to arrive early and back up Setup 10:00-11:30, since everyone else on the roster starts at 11:30 or later."
  },
  {
   "post": "Greeters and check-in",
   "status": "thin",
   "says": "Fran Hallowby 11:30-14:00...Hana Chovane (first time) 13:00-14:00",
   "why": "Fran Hallowby plus Gus Itley or Hana Chovane keep two people on this post all window, but no one is named as backup, and Hana Chovane is a first-timer.",
   "fix": "Ask Dee Wilbrant, free between Setup and Cleanup, to back up Greeters and check-in 11:30-14:00."
  },
  {
   "post": "Serving line",
   "status": "two_deep",
   "says": "Pete Shandry (backup) 11:30-14:00",
   "why": "Six primaries cover the window in overlapping shifts and Pete Shandry is named as backup the entire 11:30-14:00, so every half hour has a primary and a backup.",
   "fix": ""
  },
  "..."
 ],
 "missing_posts": [
  {
   "post": "Floater",
   "why": "No post or named contact covers breaks and no-shows, so right now nobody can step in without someone leaving their own post."
  },
  {
   "post": "Safety or conduct contact",
   "why": "No post or named contact handles a safety or conduct problem, and the briefing has no conduct-and-safety script yet."
  }
 ],
 "briefing_card": [
  "Know your post and station: Setup, Greeters and check-in, Serving line, Dish room, Takeout plates, or Cleanup.",
  "First-timers arrive 30 minutes early; everyone else 15.",
  "Questions or a problem: find Joan Pruvell or Walt Simmerby, or text the volunteer group chat.",
  "If someone is upset, unsafe, or harassed: do not argue, step back, get the shift lead, and report it.",
  "..."
 ],
 "recruit_ask": "We're one post-hour short for the Community Thanksgiving dinner on 2026-11-26: Dish room needs one more person (or a longer shift) for 13:30-14:30. We'd also love named backups for Setup (10:00-11:30), Greeters and check-in (11:30-14:00), Takeout plates (12:00-14:00), and Cleanup (14:00-15:00). Any one of these shifts would help - thank you for thinking about it.",
 "next_steps": [
  "Fix Dish room 13:30-14:30: ask Ray Foxleigh to stay until 14:30, or recruit one more person, and line up a different backup there since Cora Lindquell is already committed to Takeout plates 12:00-14:00.",
  "Name a backup for Setup (10:00-11:30), Greeters and check-in (11:30-14:00), Takeout plates (12:00-14:00), and Cleanup (14:00-15:00).",
  "Close the shift-lead gap 12:30-13:00 by asking Joan Pruvell or Walt Simmerby to extend, or naming a third lead.",
  "..."
 ],
 "caveats": [
  "This only reflects what was pasted; people may cancel or new signups may close some of these gaps before the day.",
  "Cora Lindquell is scheduled as Dish room's backup and as Takeout plates' only primary at the same time, so if either post needs her the other goes uncovered."
 ]
}

task: check - Two-entrance food drive - rebuild

Input:

{
 "task": "check",
 "today": "2026-10-19",
 "event_date": "2026-11-14",
 "roster": "Event: Holiday food drive at two grocery entrances\nDate: Nov 14, 2026\nHours: 9am-5pm\n\nPosts:\n- North entrance table | 2 | 9am-5pm\n- South entrance table | 2 | 9am-5pm\n- Cash and gift-card box | 1 | 9am-5pm\n- Truck runs | ... (436 characters)",
 "facts": "{\"event\":\"Holiday food drive at two grocery entrances\",\"window\":\"9:00-17:00\",\"days_to_event\":26,\"shift_hours\":3,\"shift_assumed\":true,\"posts_n\":4,\"volunteers_n\":5,\"first_timers\":0,\"need_hours\":50,\"filled_hours\":14,\"coverage_pct\":28,\"backup_pct\":0,\"slots_total\":19,\"open_person_hours\":36,\"open_slots\":12,\"lead_gaps\":[\"9:00-17:00\"],\"lead_overlaps\":[],\"double_bookings\":[],\"busy_backups\":[],\"briefing\":{\"choreography\":false,\"contacts\":false,\"conduct\":false},\"needs\":{\"setup\":false,\"teardown\":false,\"floater\":false,\"checkin\":false,\"safety\":false}}",
 "posts": "[{\"post\":\"North entrance table\",\"need\":2,\"window\":\"9:00-17:00\",\"hours\":8,\"shift_slots\":6,\"page_status\":\"gap\",\"gap_times\":[\"12:00-17:00\"],\"thin_times\":[\"9:00-12:00\"],\"primaries\":[\"Amy Foxcote\",\"Ben Ortweil\"],\"backups\":[],\"tbd\":1,\"sensitive\":false,\"unlisted\":false},{\"post\":\"South entrance table\",\"need\":2,\"window\":\"9:00-17:00\",\"hours\":8,\"shift_slots\":6,\"page_status\":\"gap\",\"gap_times\":[\"9:00-17:00\"],\"thin_times\":[],\"primaries\":[\"Cal Ngarro\"],\"backups\":[],\"tbd\":0,\"sensitive\":false,\"unlisted\":false},\"... 2 more\"]",
 "leads": "[]",
 "page_reads": "[{\"level\":\"fix\",\"area\":\"posts\",\"code\":\"need_setup\",\"text\":\"Setup: no post or named contact for it. Most event days need one.\"},{\"level\":\"fix\",\"area\":\"posts\",\"code\":\"need_teardown\",\"text\":\"Teardown: no post or named contact for it. Most event days need one.\"},\"... 14 more\"]",
 "page_status": "{\"posts\":\"fix\",\"coverage\":\"fix\",\"backups\":\"fix\",\"leads\":\"missing\",\"briefing\":\"missing\"}",
 "page_verdict": "rebuild",
 "notes": "Our club runs this every year; last year two people did not show."
}

Reply:

{
 "task": "check",
 "title": "Holiday Food Drive at Two Entrances",
 "verdict": "rebuild",
 "summary": "This plan needs a rebuild: only 28% of post-hours have a named person and 0% of staffed half hours have a backup, with about 12 shifts still open. No shift lead is named for the 9:00-17:00 day, and the briefing has none of its three parts. The single biggest fix is naming someone for the cash and gift-card box, since it handles money and is still only a TBD for the whole day.",
 "posts": [
  {
   "post": "North entrance table",
   "status": "gap",
   "says": "Amy Foxcote 9am-12pm; Ben Ortweil 9am-12pm; TBD 12pm-5pm",
   "why": "Amy Foxcote and Ben Ortweil cover the 2 needed 9:00-12:00 but have no named backup in that block, and 12:00-17:00 is only a TBD slot, which counts as nobody - 5 hours short of the 2 needed at once.",
   "fix": "Ask Amy Foxcote or Ben Ortweil to extend into 12:00-17:00, or recruit a new volunteer for that block, and ask a new volunteer to back up the 9:00-12:00 shift."
  },
  {
   "post": "South entrance table",
   "status": "gap",
   "says": "Cal Ngarro 9am-1pm",
   "why": "Cal Ngarro alone covers the morning to 1:00, but this post needs 2 at once, so the whole 9:00-17:00 window is short, and nobody at all is named from 1:00 onward.",
   "fix": "Recruit a new volunteer to pair with Cal Ngarro 9:00-13:00, and two new volunteers to staff 13:00-17:00 since nobody is named then."
  },
  {
   "post": "Cash and gift-card box",
   "status": "gap",
   "says": "Cash and gift-card box: TBD",
   "why": "Nobody is named for this post for any part of 9:00-17:00, and it handles money - a TBD here is a gap, not a placeholder.",
   "fix": "Recruit a new volunteer to be named for the cash and gift-card box 9:00-17:00, plus a second new volunteer as a named backup, since a money post can't run on TBD."
  },
  "..."
 ],
 "missing_posts": [
  {
   "post": "Setup crew",
   "why": "The plan has no setup post or contact, so nobody is named to get both entrance tables and the cash and gift-card box ready before 9:00."
  },
  {
   "post": "Teardown crew",
   "why": "The plan has no teardown post, so nobody is named to pack up both tables after 17:00."
  },
  {
   "post": "Floater",
   "why": "No floater is named to cover a break or a no-show at any of the four posts without leaving one of them empty."
  },
  {
   "post": "Check-in or welcome table",
   "why": "The plan has no check-in or welcome post, so nobody is named to sign volunteers in as they arrive."
  },
  {
   "post": "Safety and conduct contact",
   "why": "The plan names no safety or conduct contact, so volunteers at either entrance have nobody to go to if someone is upset or unsafe."
  }
 ],
 "briefing_card": [
  "North and South entrance tables are staffed by two at a time; the cash and gift-card box handles money; truck runs start at noon.",
  "Questions or a problem: find [the shift lead] - reach them by [phone or channel] (none is named in the plan yet).",
  "If a shopper or donor is upset, unsafe, or harassing anyone: do not argue, step back, get the shift lead, and report it.",
  "The cash and gift-card box always has a named person on it, never left on TBD.",
  "..."
 ],
 "recruit_ask": "We're still short of volunteers for the holiday food drive at the two grocery entrances on Nov 14, 2026. We need people for the North entrance table 12:00-17:00, the South entrance table 9:00-17:00, the cash and gift-card box 9:00-17:00, and truck runs 12:00-15:00. If you've helped before, could you take one of these shifts, or pass this along to someone who can? Any time you can give gets us closer to fully covered on the day.",
 "next_steps": [
  "Name a shift lead for Nov 14 - right now no one is named for the 9:00-17:00 day.",
  "Fill the open posts and times above, especially the cash and gift-card box, which can't stay TBD.",
  "Name backups for North entrance 9:00-12:00 and truck runs 15:00-17:00 once the open slots are filled.",
  "..."
 ],
 "caveats": [
  "This reading only covers what was pasted - any names, times or contacts not in the plan aren't counted.",
  "People cancel; confirm names close to Nov 14, especially since two people didn't show last year."
 ]
}

Build the input yourself

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

const K = RosterKit;
const st = K.state({
  today: "2026-10-19", notes: "",
  roster: "Event: Saturday food distribution\nDate: 2026-11-21\nHours: 8:00-12:00\nLead: [name]\n" +
          "Posts:\n- Check-in | 2 | 8:00-12:00\n- Floater | 1 | 8:00-12:00\n" +
          "Roster:\n- Check-in: [name] 8:00-12:00; [name] 8:00-10:00; [name] (backup)\n- Floater: [name] 8:00-12:00"
});
console.log(st.facts.coverage_pct, st.rows.map(r => r.post.name + " " + r.status));
const { input } = K.buildInput(st, { today: "2026-10-19" });
const wire = K.toWire(input);           // POST this to /estimate and /run
// later: const reply = K.normalize(K.parseReply(job.output.output));
// const { rows, notes } = K.check(reply, st);

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://two-deep.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":"two-deep"}' | 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": "check", "today": "2026-10-19", "event_date": "2026-11-26", "roster": "Event: ...", "facts": "{...}", "posts": "[...]", "leads": "[...]", "page_reads": "[...]", "page_status": "{...}", "page_verdict": "patch"}
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 two-deep:check:<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="two-deep:check:$(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: two-deep:check:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json