← Margin Cues
Margin Cues API
Tokens

Drive Margin Cues from your own code

Both metered runs are available over HTTP. page takes a student's notes, split into numbered parts, and returns a cue-column study page: a question for the margin beside condensed notes for each idea, a short summary, the key terms the notes define, and the gaps to chase - with a ready / patchy / thin verdict. recall takes that page's cues plus the answers the student wrote with the notes covered, and returns a grade per cue (solid / partial / gap / wrong) with a simpler explanation and a follow-up question, plus a ready / review / relearn verdict. A natural use is a note-taking or flashcard app: your code sends the notes, then later the answers, and gets structured pages and report cards back.

The division of labour first: the page splits and scans; the model writes and grades; the page re-checks the reply. Splitting the notes into parts, finding definition-style lines, flagged-unclear spots and housekeeping, drafting a cue per part and the page's own verdict come from cuekit.js; the run inputs and the reply checks come from cuereply.js. Both run in node as well as in the browser. checkPage holds a page reply to the notes (every part has a cue or a reason it was skipped, no unknown part ids, no internal labels in prose, gaps listed when the verdict is patchy or thin); checkRecall holds a recall reply to the cue set (one grade per cue, a blank answer never above gap, the verdict matching the grade counts). Replies are built only from the notes sent - the prompt forbids filling a gap with outside facts.

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":"margin-cues"}), 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: both runs are metered and need 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 first: page or recall. The declared input schema carries scalars only, so object-valued fields travel JSON-encoded as strings (CueKit.toWire does this). The schema requires task and today.

taskSendsReturnsVerdicts
pagethe notes as numbered parts plus the page's own scancue rows, summary, terms, gapsready (clear enough to revise from), patchy (usable, two or more gaps), thin (go back to the source first)
recallthe cue set plus answers written from memoryone grade per cue, summary, next stepsready (all solid, at most one partial), relearn (more than half gap or wrong), review (anything else)

Input fields

FieldTaskTypeNotes
task, todaybothstringpage or recall; YYYY-MM-DD.
topicbothstringOptional, up to 140 characters.
sourcepagestringlecture, reading, video or podcast or meeting or seminar.
partspagestring (JSON)[{id, heading, text}] with ids b1, b2 … - blank lines and headings split the notes; at most 30 parts and 9,000 characters are sent (the start and end are kept, the middle dropped).
prescanpagestring (JSON){words, parts_total, parts_sent, parts_left_out, definitions:[{part, terms}], flagged_unclear:[{part, text}], housekeeping:[ids], page_verdict}.
clip_notepagestringNon-empty when parts were left out.
cuesrecallstring (JSON)[{id, cue, notes:[...]}] with ids c1, c2 … - the page reply's rows in order, or the page's draft cues.
answersrecallstring (JSON)[{id, answer}]; an empty answer means blank (graded gap). Up to 600 characters each.
blank_ids, cue_sourcerecallstring (JSON), stringThe ids left blank; where the cues came from.
retry_notebothstringOnly on a reformat retry.

The replies

The job's output.output is a string holding one JSON object.

Worked examples

Replies produced against the app's prompt for the sample notes (made up; lists shortened; the full replies ship in example-result.js). Together they cover every verdict of both tasks and all four grades. The page replays each of them free from its sample buttons.

page - biology lecture, complete notes - ready

Input:

{
 "task": "page",
 "today": "2026-10-20",
 "topic": "Cellular respiration",
 "source": "lecture",
 "parts": "[{\"id\":\"b1\",\"heading\":\"Overview\",\"text\":\"Cellular respiration = cells break down glucose to make ATP\\n- needs oxygen (aerobic)\\n- overall: glucose + oxygen -> carbon dioxide + water + ATP\"},{\"id\":\"b2\",\"heading\":\"Glycolysis\",\"text\":\"- happens in the cytoplasm\\n- splits 1 glucose into 2 pyruvate\\n- net gain 2 ATP, also makes 2 NADH\\n- does not need oxygen\"},\"... 3 more\"]",
 "prescan": "{\"words\":118,\"parts_total\":5,\"parts_sent\":5,\"parts_left_out\":0,\"definitions\":[{\"part\":\"b1\",\"terms\":[\"Cellular respiration\"]},{\"part\":\"b3\",\"terms\":[\"pyruvate\"]},{\"part\":\"b4\",\"terms\":[\"oxygen\"]},{\"part\":\"b5\",\"terms\":[\"fermentation\"]}],\"flagged_unclear\":[],\"housekeeping\":[],\"page_verdict\":\"ready\"}",
 "clip_note": ""
}

Reply:

{
 "task": "page",
 "title": "How Cells Make ATP From Glucose",
 "verdict": "ready",
 "verdict_reason": "Every stage from glycolysis to the electron transport chain is covered clearly, with only pyruvate's chemical nature left unexplained.",
 "rows": [
  {
   "cue": "What does cellular respiration do, and why does it need oxygen?",
   "parts": [
    "b1"
   ],
   "notes": [
    "Cellular respiration breaks down glucose to make ATP.",
    "It is aerobic, meaning it needs oxygen.",
    "Overall reaction: glucose + oxygen -> carbon dioxide + water + ATP."
   ],
   "kind": "definition"
  },
  {
   "cue": "How does glycolysis break down glucose without oxygen?",
   "parts": [
    "b2"
   ],
   "notes": [
    "Glycolysis happens in the cytoplasm.",
    "It splits 1 glucose into 2 pyruvate.",
    "Net gain is 2 ATP and 2 NADH.",
    "It does not need oxygen."
   ],
   "kind": "process"
  },
  "... 3 more"
 ],
 "summary": "Cellular respiration breaks glucose down into ATP, mostly using oxygen. Glycolysis starts the process in the cytoplasm, then the Krebs cycle and electron transport chain in the mitochondria release most of the ATP. Oxygen accepts the final electrons there and forms water. Without oxygen, fermentation keeps only the small ATP gain from glycolysis.",
 "terms": [
  {
   "term": "Cellular respiration",
   "meaning": "Cells break down glucose to make ATP; it is aerobic, meaning it needs oxygen."
  },
  {
   "term": "Oxygen",
   "meaning": "The final electron acceptor in the electron transport chain; accepting electrons there forms water."
  },
  "... 1 more"
 ],
 "gaps": [
  {
   "parts": [
    "b3"
   ],
   "issue": "Pyruvate is named as what glycolysis makes and what the Krebs cycle converts, but the notes never say what pyruvate actually is.",
   "ask": "Check the textbook chapter or lecture slides on glycolysis for what pyruvate is."
  }
 ],
 "skipped": []
}

page - history reading with flagged spots - patchy

Input:

{
 "task": "page",
 "today": "2026-10-20",
 "topic": "Causes of the 1929 stock market crash",
 "source": "reading",
 "parts": "[{\"id\":\"b1\",\"heading\":\"Ch 4 - the crash of 1929\",\"text\":\"Buying on margin: people borrowed to buy stocks, only paid 10% down\\n- when prices fell, brokers made margin calls -> forced selling\"},{\"id\":\"b2\",\"heading\":\"\",\"text\":\"Overproduction in farming + factories, goods piling up\\n- farm prices already falling in the 1920s\"},\"... 5 more\"]",
 "prescan": "{\"words\":122,\"parts_total\":7,\"parts_sent\":7,\"parts_left_out\":0,\"definitions\":[{\"part\":\"b1\",\"terms\":[\"Buying on margin\"]},{\"part\":\"b4\",\"terms\":[\"Banks failing\"]},{\"part\":\"b5\",\"terms\":[\"Fed policy\"]},{\"part\":\"b6\",\"terms\":[\"Uneven wealth\"]}],\"flagged_unclear\":[{\"part\":\"b3\",\"text\":\"bankers tried to prop up prices on Thursday ?? did it work\"},{\"part\":\"b4\",\"text\":\"lecture slide said 900 banks?? check\"},{\"part\":\"b5\",\"text\":\"Fed policy - raised interest rates in 1928? ask prof why that mattered\"},{\"part\":\"b7\",\"text\":\"missed the part about international trade / tariffs...\"}],\"housekeeping\":[],\"page_verdict\":\"patchy\"}",
 "clip_note": ""
}

Reply:

{
 "task": "page",
 "title": "Causes of the 1929 Stock Market Crash",
 "verdict": "patchy",
 "verdict_reason": "Several facts are flagged as unclear or inconsistent, and a whole cause is missing from the notes, so check those before relying on this page.",
 "rows": [
  {
   "cue": "How did buying on margin lead to forced selling?",
   "parts": [
    "b1"
   ],
   "notes": [
    "People borrowed to buy stocks, paying only 10% down.",
    "When prices fell, brokers made margin calls, forcing selling."
   ],
   "kind": "process"
  },
  {
   "cue": "Why were goods piling up in farms and factories?",
   "parts": [
    "b2"
   ],
   "notes": [
    "Overproduction in farming and factories left goods piling up.",
    "Farm prices were already falling in the 1920s."
   ],
   "kind": "cause"
  },
  "... 5 more"
 ],
 "summary": "These notes cover causes of the 1929 crash: margin buying that led to forced selling, overproduction that piled up goods, uneven wealth that weakened demand, and a 1928 Fed rate hike. They also note bank failures and the Black Thursday and Black Tuesday dates. A few figures and the section on international trade are unclear or missing, so check them before relying on this page.",
 "terms": [
  {
   "term": "Buying on margin",
   "meaning": "Borrowing to buy stocks while paying only 10% down."
  },
  {
   "term": "Banks failing",
   "meaning": "Banks failing, so people lost their savings since there was no deposit insurance yet."
  },
  "... 2 more"
 ],
 "gaps": [
  {
   "parts": [
    "b3"
   ],
   "issue": "It's unclear whether the bankers' attempt to prop up prices on Black Thursday actually worked.",
   "ask": "Check your professor's lecture or the textbook chapter to find out if that effort succeeded."
  },
  {
   "parts": [
    "b4"
   ],
   "issue": "The bank failure number looks inconsistent: the textbook says about 9,000 failed, but a lecture slide said 900.",
   "ask": "Check the lecture slide and the textbook chapter to see which bank failure count is correct."
  },
  "... 2 more"
 ],
 "skipped": []
}

page - half-asleep economics fragments - thin

Input:

{
 "task": "page",
 "today": "2026-10-20",
 "topic": "Price elasticity of demand",
 "source": "lecture",
 "parts": "[{\"id\":\"b1\",\"heading\":\"\",\"text\":\"elasticity\\n% change qty / % change price\"},{\"id\":\"b2\",\"heading\":\"\",\"text\":\"elastic >1\\ninelastic\"},\"... 3 more\"]",
 "prescan": "{\"words\":17,\"parts_total\":5,\"parts_sent\":5,\"parts_left_out\":0,\"definitions\":[],\"flagged_unclear\":[{\"part\":\"b5\",\"text\":\"total revenue thing...\"}],\"housekeeping\":[],\"page_verdict\":\"thin\"}",
 "clip_note": ""
}

Reply:

{
 "task": "page",
 "title": "Price elasticity of demand",
 "verdict": "thin",
 "verdict_reason": "The notes are a handful of short fragments and key pieces - the inelastic cutoff, the insulin example, and the revenue link - are left incomplete, so there isn't enough here to study from yet.",
 "rows": [
  {
   "cue": "How do you calculate price elasticity?",
   "parts": [
    "b1"
   ],
   "notes": [
    "Elasticity = % change in quantity / % change in price"
   ],
   "kind": "definition"
  },
  {
   "cue": "What counts as elastic demand?",
   "parts": [
    "b2"
   ],
   "notes": [
    "Elastic demand: ratio over 1",
    "Inelastic demand is also mentioned"
   ],
   "kind": "compare"
  },
  "... 3 more"
 ],
 "summary": "Price elasticity of demand compares the percent change in quantity to the percent change in price. A ratio above 1 counts as elastic, with an inelastic case also named. Having substitutes pushes demand toward being more elastic, and gas and insulin are given as examples. A link to total revenue is raised but not finished.",
 "terms": [
  {
   "term": "elasticity",
   "meaning": "% change in quantity divided by % change in price"
  },
  {
   "term": "elastic",
   "meaning": "when that ratio is greater than 1"
  }
 ],
 "gaps": [
  {
   "parts": [
    "b2"
   ],
   "issue": "the notes name an inelastic case but never give the cutoff for it",
   "ask": "check the textbook chapter or slides for the inelastic elasticity range"
  },
  {
   "parts": [
    "b3"
   ],
   "issue": "the insulin example is marked with a question mark, so it's unclear if it's confirmed",
   "ask": "ask the lecturer or check the slides whether insulin belongs here"
  },
  "... 1 more"
 ],
 "skipped": []
}

recall - biology answers - ready

Input:

{
 "task": "recall",
 "today": "2026-10-20",
 "topic": "Cellular respiration",
 "cues": "[{\"id\":\"c1\",\"cue\":\"What does cellular respiration do, and why does it need oxygen?\",\"notes\":[\"Cellular respiration breaks down glucose to make ATP.\",\"It is aerobic, meaning it needs oxygen.\",\"Overall reaction: glucose + oxygen -> carbon dioxide + water + ATP.\"]},{\"id\":\"c2\",\"cue\":\"How does glycolysis break down glucose without oxygen?\",\"notes\":[\"Glycolysis happens in the cytoplasm.\",\"It splits 1 glucose into 2 pyruvate.\",\"Net gain is 2 ATP and 2 NADH.\",\"It does not need oxygen.\"]},\"... 3 more\"]",
 "answers": "[{\"id\":\"c1\",\"answer\":\"It breaks glucose down to make ATP for the cell. It is aerobic so it needs oxygen, and the overall equation is glucose + oxygen -> CO2 + water + ATP.\"},{\"id\":\"c2\",\"answer\":\"In the cytoplasm one glucose is split into 2 pyruvate. You get a net 2 ATP and 2 NADH, and no oxygen is needed.\"},\"... 3 more\"]",
 "blank_ids": "[]",
 "cue_source": "the cue-column page"
}

Reply:

{
 "task": "recall",
 "verdict": "ready",
 "summary": "Your answers were solid across the aerobic stages, nailing the locations, molecules and ATP counts in your own words. The only soft spot was the no-oxygen cue, where you described the lactic acid outcome but left out the ATP detail the question was really asking about.",
 "grades": [
  {
   "id": "c1",
   "grade": "solid",
   "got": "You covered that respiration breaks down glucose for ATP, is aerobic, and gave the full reaction with oxygen, carbon dioxide, water and ATP.",
   "missing": "",
   "plain": "",
   "nudge": "What are the inputs and outputs of the overall reaction?"
  },
  {
   "id": "c2",
   "grade": "solid",
   "got": "You got the cytoplasm location, the one-glucose-to-two-pyruvate split, the net 2 ATP and 2 NADH, and that no oxygen is needed.",
   "missing": "",
   "plain": "",
   "nudge": "What is the net energy yield from this step, and where does it happen?"
  },
  "... 3 more"
 ],
 "next": [
  "Re-explain out loud that without oxygen, ATP production stays at the 2 ATP made in glycolysis, with no extra added.",
  "Name the fermentation step itself next time, not just its lactic acid result.",
  "... 1 more"
 ]
}

recall - history answers with a swapped date and blanks - review

Input:

{
 "task": "recall",
 "today": "2026-10-20",
 "topic": "Causes of the 1929 stock market crash",
 "cues": "[{\"id\":\"c1\",\"cue\":\"How did buying on margin lead to forced selling?\",\"notes\":[\"People borrowed to buy stocks, paying only 10% down.\",\"When prices fell, brokers made margin calls, forcing selling.\"]},{\"id\":\"c2\",\"cue\":\"Why were goods piling up in farms and factories?\",\"notes\":[\"Overproduction in farming and factories left goods piling up.\",\"Farm prices were already falling in the 1920s.\"]},\"... 5 more\"]",
 "answers": "[{\"id\":\"c1\",\"answer\":\"People bought stocks with borrowed money and only put 10% down. When prices dropped, brokers made margin calls, so people had to sell.\"},{\"id\":\"c2\",\"answer\":\"There was overproduction so stuff piled up.\"},\"... 5 more\"]",
 "blank_ids": "[\"c5\"]",
 "cue_source": "the cue-column page"
}

Reply:

{
 "task": "recall",
 "verdict": "review",
 "summary": "Margin buying and bank failures are solid, but you swapped the names of the two crash days and left the Fed's 1928 rate hike and the trade and tariffs cue unanswered. The overproduction answer also missed the falling farm prices detail.",
 "grades": [
  {
   "id": "c1",
   "grade": "solid",
   "got": "You correctly said people borrowed money to buy stocks with only 10% down, and that falling prices triggered margin calls that forced selling.",
   "missing": "",
   "plain": "",
   "nudge": "What percentage did buyers typically put down when buying stocks on margin?"
  },
  {
   "id": "c2",
   "grade": "partial",
   "got": "You correctly said overproduction left goods piling up in farming and factories.",
   "missing": "You left out that farm prices were already falling in the 1920s.",
   "plain": "Factories and farms made more than people bought, so goods piled up, while farm prices had already been sliding through the 1920s.",
   "nudge": "What was already happening to farm prices before the crash hit?"
  },
  "... 5 more"
 ],
 "next": [
  "Relearn which day was Black Thursday and which was Black Tuesday, and what the bankers did on the first day.",
  "Look up what the Fed did to interest rates in 1928, and add notes on international trade and tariffs before retesting those two cues.",
  "... 1 more"
 ]
}

recall - economics answers, mostly blank - relearn

Input:

{
 "task": "recall",
 "today": "2026-10-20",
 "topic": "Price elasticity of demand",
 "cues": "[{\"id\":\"c1\",\"cue\":\"How do you calculate price elasticity?\",\"notes\":[\"Elasticity = % change in quantity / % change in price\"]},{\"id\":\"c2\",\"cue\":\"What counts as elastic demand?\",\"notes\":[\"Elastic demand: ratio over 1\",\"Inelastic demand is also mentioned\"]},\"... 3 more\"]",
 "answers": "[{\"id\":\"c1\",\"answer\":\"% change in price divided by % change in quantity\"},{\"id\":\"c2\",\"answer\":\"\"},\"... 3 more\"]",
 "blank_ids": "[\"c2\",\"c5\"]",
 "cue_source": "the cue-column page"
}

Reply:

{
 "task": "recall",
 "verdict": "relearn",
 "summary": "You mixed up the two halves of the elasticity ratio, missed one of the two examples, and left the substitutes and total revenue cues blank or unsure. The ratio itself and the half-finished ideas need the most work.",
 "grades": [
  {
   "id": "c1",
   "grade": "wrong",
   "got": "You named both percentage changes involved in the ratio.",
   "missing": "The notes put the percent change in quantity on top and the percent change in price on the bottom, not the other way round.",
   "plain": "Elasticity is the percent change in quantity divided by the percent change in price.",
   "nudge": "In the elasticity ratio, which percent change sits on top: quantity or price?"
  },
  {
   "id": "c2",
   "grade": "gap",
   "got": "",
   "missing": "The notes say demand counts as elastic when that ratio comes out over 1.",
   "plain": "Demand is elastic when the elasticity ratio works out to more than 1.",
   "nudge": "What does it mean for the ratio to come out above 1?"
  },
  "... 3 more"
 ],
 "next": [
  "Re-learn the elasticity ratio with quantity change over price change, then say it out loud from memory.",
  "Go back to your original notes to find what the total revenue idea and the substitutes point actually say, since one cuts off and the other was skipped.",
  "... 1 more"
 ]
}

Build the input yourself

Load both scripts in node (global.window = {}; require("./cuekit.js"); require("./cuereply.js")), then:

const K = CueKit;
const st = K.state({ topic: "Cellular respiration", source: "lecture", notes: "# Glycolysis\n- in the cytoplasm\n- glucose -> 2 pyruvate, net 2 ATP\n\nKrebs cycle: in the mitochondria\n- ?? CO2 per turn" });
console.log(st.parts.length, st.drafts.map(d => d.cue), st.unclear, st.verdict);
const b = K.buildPage(st, { today: "2026-10-20" });
const pageWire = K.toWire(b.input);                       // POST this to /estimate and /run
// later: const page = K.normalizePage(K.parseReply(job.output.output)); K.checkPage(page, st, b.sentIds).notes
const answers = { "What happens in glycolysis?": "glucose splits into 2 pyruvate" };  // keyed by cue text
const r = K.buildRecall(st, page, answers, { today: "2026-10-20" });
const recallWire = K.toWire(r.input);
// later: const rc = K.normalizeRecall(K.parseReply(job2.output.output)); K.checkRecall(rc, r.cues, answers)

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://margin-cues.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":"margin-cues"}' | 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": "page", "today": "2026-10-20", "topic": "Cellular respiration", "source": "lecture", "parts": "[{\"id\":\"b1\",\"heading\":\"Overview\",\"text\":\"...\"}, ...]", "prescan": "{...}", "clip_note": ""}
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 task and the input: a retry with the same key never bills twice. The page uses margin-cues:<task>:<inputHash>:a<attempt> (task is page or recall), 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="margin-cues:page:$(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: margin-cues:page:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json