← After the Masters
Recreation API
Tokens

Drive After the Masters from your own code

The one metered task, restage, is available over HTTP. It takes a painting (its figures, roles, key elements, which way it hangs and, when known, roughly where its light comes from), the cast who can pose (people and stand-ins), the props on hand and an optional note about the space, and returns a verdict (hang it, re-hang, or swap the painting), a casting row with a pose cue for every figure, a stand-in for every key element with its status, the light, the phone framing, four to seven shoot stops, a wall label and - for a swap - a painting the cast can fill.

The division of labour first: the page owns the painting notes, the draft casting, the keyword prop guess and the hanging rule; the model owns the plan; the page re-checks the reply. All of it comes from mastkit.js, which you can run yourself (see Build the input yourself).

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":"after-the-masters"}), 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, painting, orientation, figures, roles, elements, light, cast, cast_count, props, catalogue, draft) 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 plan 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, the statuses and the verdict

Every run sends task: restage; a missing or different one is still answered as restage. Each key element gets one status: covered (a listed prop, a stand-in or the shooting place plays it), improvise (make or borrow something simple) or cut (leave it out of frame).

The verdict is checked in order, first match wins: swap if cast_count times 2 is less than figures; otherwise rehang (shown as "re-hang") if cast_count is less than figures or any element is improvise or cut; otherwise hang (shown as "hang it").

The input

Every value is a string (objects are never sent). app.js builds it with MastKit.buildInput(form).input.

FieldRequiredWhat it holds
taskyesAlways restage.
paintingyesThe title, then the artist and year when known.
orientationyesportrait, landscape or square.
figures / rolesyesHow many figures, then one line per figure: <n> | <who>.
elementsyesThe key elements, one per line: <n> | <element> (at most 8).
lightyesfrom the left (approximate) and so on, or not listed.
cast / cast_countyesOne line per cast member, <name> | person or <name> | stand-in (at most 12), and the count.
propsyesOne prop per line, at most 30.
spacenoA note on where the shoot happens, at most 300 characters.
catalogueyesThe other paintings the page knows: <title> | <n> figure(s) | <elements; ...> - the only titles a swap may suggest.
draftyesThe page's keyword guess: <n> | <element> | likely: <prop> or nothing listed. Only a guess.
clip_notenoWhat the page shortened, if anything.
retry_notenoSent only on the page's single reformat retry.

The reply

The reply is one JSON object in job.output.output. The page parses it with MastKit.parseReply and MastKit.normalize (a cut-off stream is closed by MastKit.closeJson and whatever parsed is shown).

{"task": "restage", "verdict": "hang | rehang | swap", "verdict_reason": "...", "title": "...", "medium": "...", "label": "...",
 "cast": [{"n": 1, "figure": "...", "played_by": "... or empty", "pose": "..."}],
 "swaps": [{"n": 1, "element": "...", "use": "...", "status": "covered | improvise | cut"}],
 "light": {"side": "left | right | front | above | behind", "setup": "..."},
 "frame": {"orientation": "portrait | landscape | square", "camera": "...", "crop": "..."},
 "stops": ["..."],
 "alternative": null}
FieldWhat it holds
castOne row per figure: who plays it (copied exactly from cast, or empty when nobody is left) and a pose cue of at most 25 words.
swapsOne row per key element: the stand-in and its status.
light / frameThe side the light comes from and how to get it; the orientation, phone height, distance, lens and crop.
stopsFour to seven shoot steps, each at most 20 words - the page plays them on its audio-guide handset.
title / medium / labelThe wall label: a title of at most 8 words, materials from the props and cast, and two sentences of at most 45 words.
alternative{"painting", "why"} for a swap - a catalogue title the cast can fill - else null.

The page then checks the reply against the brief (MastKit.check): the verdict against the hanging rule, one casting row per figure, every name in the cast and nobody cast twice, every covered stand-in sharing a word with a listed prop, cast member or the place, the light side against the painting's, the frame's orientation, four to seven stops, a swap painting from the catalogue that the cast can fill, no he or she for anyone and no internal field name in the prose.

Worked examples, one per verdict

These are the three free samples on the page: the exact input and a stand-in model's reply, unedited.

The farmhouse pair - hang it

Input:

{
 "task": "restage",
 "painting": "American Gothic - Grant Wood, 1930",
 "orientation": "portrait",
 "figures": "2",
 "roles": "1 | the farmer holding the pitchfork\n2 | the companion at the farmer's shoulder",
 "elements": "1 | a pitchfork held upright\n2 | round glasses\n3 | a dark jacket\n4 | a patterned apron\n5 | a pointed-arch window behind",
 "light": "not listed",
 "cast": "Marta | person\nMr Buttons, the tailor's dummy | stand-in",
 "cast_count": "2",
 "props": "garden fork\nwire-rimmed reading glasses\nblack work jacket\nflowered kitchen apron\nfront door with an arched glass panel\nwhite shirt with a round collar",
 "space": "The hallway by the front door. Daylight comes in through the door's glass panel.",
 "catalogue": "Mona Lisa | 1 figure | a dark dress; a thin veil over the hair; a chair arm to rest the hands on; a hazy landscape behind\nGirl with a Pearl Earring | 1 figure | a blue and yellow head wrap; a large pearl earring; a brownish jacket with a white collar; a plain dark background\nThe Milkmaid | 1 figure | a jug pouring into a bowl; bread on the table; a white cap; a yellow bodice and a blue apron; a window on the left\nArrangement in Grey and Black No. 1 | 1 figure | a straight-backed chair; a long black dress; a white lace cap; a dark curtain at one side; a framed picture on the wall\nThe Scream | 1 figure | a swirling orange sky; a long railing running away; two small figures far behind; dark water below\nWanderer above the Sea of Fog | 1 figure | a dark coat; a walking stick; a rocky high point to stand on; fog below\nThe Arnolfini Portrait | 2 figures | a green dress; a hanging chandelier; a round mirror on the back wall; a small dog at the front; a window on the left\nThe Gleaners | 3 figures | bundles of gathered stalks; head scarves; a wide flat field; haystacks far behind",
 "draft": "1 | a pitchfork held upright | likely: garden fork\n2 | round glasses | likely: wire-rimmed reading glasses\n3 | a dark jacket | likely: black work jacket\n4 | a patterned apron | likely: flowered kitchen apron\n5 | a pointed-arch window behind | likely: front door with an arched glass panel"
}

Reply:

{
 "task": "restage",
 "verdict": "hang",
 "verdict_reason": "One person and one stand-in fill both figures, and all five elements come from the props list.",
 "title": "American Gothic, Front Door Edition",
 "medium": "Garden fork, reading glasses, work jacket, kitchen apron and tailor's dummy.",
 "label": "In the hallway, Marta and Mr Buttons the tailor's dummy recreate American Gothic, garden fork held upright before the arched glass of the front door. Reading glasses, a work jacket and a kitchen apron complete the pair, caught in daylight from the doorway.",
 "cast": [
  {
   "n": 1,
   "figure": "the farmer holding the pitchfork",
   "played_by": "Marta",
   "pose": "Marta stands upright, holds the garden fork in the right hand, point up, and looks straight ahead at the camera."
  },
  {
   "n": 2,
   "figure": "the companion at the farmer's shoulder",
   "played_by": "Mr Buttons, the tailor's dummy",
   "pose": "Mr Buttons stands beside Marta at shoulder height, upright, facing forward toward the camera."
  }
 ],
 "swaps": [
  {
   "n": 1,
   "element": "a pitchfork held upright",
   "use": "garden fork, held upright in the right hand",
   "status": "covered"
  },
  {
   "n": 2,
   "element": "round glasses",
   "use": "wire-rimmed reading glasses, worn on the face",
   "status": "covered"
  },
  {
   "n": 3,
   "element": "a dark jacket",
   "use": "black work jacket, worn over the shoulders",
   "status": "covered"
  },
  {
   "n": 4,
   "element": "a patterned apron",
   "use": "flowered kitchen apron, tied at the waist",
   "status": "covered"
  },
  {
   "n": 5,
   "element": "a pointed-arch window behind",
   "use": "front door with an arched glass panel, placed behind the figures",
   "status": "covered"
  }
 ],
 "light": {
  "side": "behind",
  "setup": "Stand with backs to the arched glass panel in the front door so daylight lights them from behind; add a lamp or window on the front side to fill faces."
 },
 "frame": {
  "orientation": "portrait",
  "camera": "Hold the phone upright at chest height, step back about six feet, and use the main lens, not the wide one.",
  "crop": "Frame from the top of the heads to the waist, keeping both figures centered and the glass door panel visible behind them."
 },
 "stops": [
  "Set up in the hallway with the front door's arched glass panel as the backdrop behind the pair.",
  "Dress Marta in the black work jacket and flowered kitchen apron, with the wire-rimmed reading glasses ready.",
  "Position Mr Buttons at Marta's shoulder, upright, standing in for the companion figure.",
  "Hand Marta the garden fork, held upright, point up and away from faces.",
  "Let the glass panel light them from behind; add a lamp or window on the front side.",
  "Shoot on the phone's main lens, holding it at chest height, framed head to waist.",
  "Compare the photo with American Gothic, checking pose, props and light before finishing."
 ],
 "alternative": null
}

The pearl, minus the pearl - re-hang

Input:

{
 "task": "restage",
 "painting": "Girl with a Pearl Earring - Johannes Vermeer, c. 1665",
 "orientation": "portrait",
 "figures": "1",
 "roles": "1 | the sitter glancing back over one shoulder",
 "elements": "1 | a blue and yellow head wrap\n2 | a large pearl earring\n3 | a brownish jacket with a white collar\n4 | a plain dark background",
 "light": "from the left (approximate)",
 "cast": "Priya | person",
 "cast_count": "1",
 "props": "blue silk scarf\nyellow pillowcase\nbrown corduroy jacket\nwhite collared shirt\ndesk lamp",
 "space": "A small bedroom with one window. Free to shoot in the evenings.",
 "catalogue": "American Gothic | 2 figures | a pitchfork held upright; round glasses; a dark jacket; a patterned apron; a pointed-arch window behind\nMona Lisa | 1 figure | a dark dress; a thin veil over the hair; a chair arm to rest the hands on; a hazy landscape behind\nThe Milkmaid | 1 figure | a jug pouring into a bowl; bread on the table; a white cap; a yellow bodice and a blue apron; a window on the left\nArrangement in Grey and Black No. 1 | 1 figure | a straight-backed chair; a long black dress; a white lace cap; a dark curtain at one side; a framed picture on the wall\nThe Scream | 1 figure | a swirling orange sky; a long railing running away; two small figures far behind; dark water below\nWanderer above the Sea of Fog | 1 figure | a dark coat; a walking stick; a rocky high point to stand on; fog below\nThe Arnolfini Portrait | 2 figures | a green dress; a hanging chandelier; a round mirror on the back wall; a small dog at the front; a window on the left\nThe Gleaners | 3 figures | bundles of gathered stalks; head scarves; a wide flat field; haystacks far behind",
 "draft": "1 | a blue and yellow head wrap | likely: blue silk scarf\n2 | a large pearl earring | nothing listed\n3 | a brownish jacket with a white collar | likely: brown corduroy jacket\n4 | a plain dark background | nothing listed"
}

Reply:

{
 "task": "restage",
 "verdict": "rehang",
 "verdict_reason": "One person stands in for the one figure, and two of the four elements have no exact match on the list.",
 "title": "Girl with a Paper Pearl, Evening Edition",
 "medium": "Blue silk scarf, yellow pillowcase, brown corduroy jacket and white collared shirt",
 "label": "A scarf and a pillowcase trade places as a painter's head wrap, and a borrowed paper circle stands in for a pearl. Turned toward the lamp, the sitter looks back the way the painting asks, caught for one evening in corduroy and cotton.",
 "cast": [
  {
   "n": 1,
   "figure": "the sitter glancing back over one shoulder",
   "played_by": "Priya",
   "pose": "Priya turns the shoulders away from the camera, looks back over one shoulder toward the lens, chin tucked slightly, hands resting below the frame."
  }
 ],
 "swaps": [
  {
   "n": 1,
   "element": "a blue and yellow head wrap",
   "use": "blue silk scarf and yellow pillowcase, wrapped together as a head wrap",
   "status": "covered"
  },
  {
   "n": 2,
   "element": "a large pearl earring",
   "use": "borrow or fold a white paper circle as the earring",
   "status": "improvise"
  },
  {
   "n": 3,
   "element": "a brownish jacket with a white collar",
   "use": "brown corduroy jacket and white collared shirt, collar worn open over the jacket",
   "status": "covered"
  },
  {
   "n": 4,
   "element": "a plain dark background",
   "use": "crop in close so the background does not show",
   "status": "cut"
  }
 ],
 "light": {
  "side": "left",
  "setup": "Place Priya side-on to the window in daylight, or use the desk lamp at head height on the left in the evening."
 },
 "frame": {
  "orientation": "portrait",
  "camera": "Hold the phone upright at face height, about an arm's length away, using the main lens, not the wide one.",
  "crop": "Frame head and shoulders only, cropping tight so the background stays out of the shot."
 },
 "stops": [
  "Pick a corner of the bedroom and stand close so the frame crops the background out.",
  "Dress Priya in the brown corduroy jacket over the white collared shirt, collar turned out.",
  "Wrap the blue silk scarf and yellow pillowcase as the head wrap, then fold a paper circle for the earring.",
  "Have Priya turn and look back over one shoulder, chin tucked slightly.",
  "Turn Priya toward the window or desk lamp so the light comes from the left.",
  "Hold the phone at face height with the main lens and take the photo.",
  "Compare the photo with the painting and adjust the head wrap, collar or light."
 ],
 "alternative": null
}

Three gleaners, one Lou - swap the painting

Input:

{
 "task": "restage",
 "painting": "The Gleaners - Jean-Francois Millet, 1857",
 "orientation": "landscape",
 "figures": "3",
 "roles": "1 | the gleaner bent lowest\n2 | the second bent gleaner\n3 | the gleaner half standing",
 "elements": "1 | bundles of gathered stalks\n2 | head scarves\n3 | a wide flat field\n4 | haystacks far behind",
 "light": "not listed",
 "cast": "Lou | person",
 "cast_count": "1",
 "props": "straw sun hat\ngreen headscarf\narmful of dried grasses\nlong brown skirt\nphone tripod",
 "space": "A mown field at the edge of the village.",
 "catalogue": "American Gothic | 2 figures | a pitchfork held upright; round glasses; a dark jacket; a patterned apron; a pointed-arch window behind\nMona Lisa | 1 figure | a dark dress; a thin veil over the hair; a chair arm to rest the hands on; a hazy landscape behind\nGirl with a Pearl Earring | 1 figure | a blue and yellow head wrap; a large pearl earring; a brownish jacket with a white collar; a plain dark background\nThe Milkmaid | 1 figure | a jug pouring into a bowl; bread on the table; a white cap; a yellow bodice and a blue apron; a window on the left\nArrangement in Grey and Black No. 1 | 1 figure | a straight-backed chair; a long black dress; a white lace cap; a dark curtain at one side; a framed picture on the wall\nThe Scream | 1 figure | a swirling orange sky; a long railing running away; two small figures far behind; dark water below\nWanderer above the Sea of Fog | 1 figure | a dark coat; a walking stick; a rocky high point to stand on; fog below\nThe Arnolfini Portrait | 2 figures | a green dress; a hanging chandelier; a round mirror on the back wall; a small dog at the front; a window on the left",
 "draft": "1 | bundles of gathered stalks | nothing listed\n2 | head scarves | nothing listed\n3 | a wide flat field | nothing listed\n4 | haystacks far behind | nothing listed"
}

Reply:

{
 "task": "restage",
 "verdict": "swap",
 "verdict_reason": "One person cast for three figures, fewer than half the figures needed, so the painting is swapped.",
 "title": "The Lone Gleaner, Field Edition",
 "medium": "Long brown skirt, green headscarf, and an armful of dried grasses.",
 "label": "After Millet's The Gleaners, one gleaner now bends low over a mown field, an armful of dried grasses gathered beneath a green headscarf. The wide harvest becomes a single gesture, held at the village's edge.",
 "cast": [
  {
   "n": 1,
   "figure": "the gleaner bent lowest",
   "played_by": "Lou",
   "pose": "Lou bends low at the waist, knees bent, eyes down toward the field, arms reaching to gather the armful of dried grasses."
  },
  {
   "n": 2,
   "figure": "the second bent gleaner",
   "played_by": "",
   "pose": "Left out of frame; only Lou appears, so the second bent gleaner is not shown in this shoot."
  },
  {
   "n": 3,
   "figure": "the gleaner half standing",
   "played_by": "",
   "pose": "Also left out of frame; the gleaner half standing is not part of this shoot with only Lou cast."
  }
 ],
 "swaps": [
  {
   "n": 1,
   "element": "bundles of gathered stalks",
   "use": "Armful of dried grasses, bundled and held in both arms",
   "status": "covered"
  },
  {
   "n": 2,
   "element": "head scarves",
   "use": "Green headscarf, tied snugly over the hair",
   "status": "covered"
  },
  {
   "n": 3,
   "element": "a wide flat field",
   "use": "The mown field at the edge of the village, filling the background",
   "status": "covered"
  },
  {
   "n": 4,
   "element": "haystacks far behind",
   "use": "Crop the frame before any haystacks would show in the distance",
   "status": "cut"
  }
 ],
 "light": {
  "side": "left",
  "setup": "Outdoors in the field, turn so daylight comes from the left, giving sidelight across the bent pose."
 },
 "frame": {
  "orientation": "landscape",
  "camera": "Hold the phone horizontal at chest height, step back enough to fit Lou and the open field, using the main lens.",
  "crop": "The frame holds Lou bent low over the dried grasses in the mown field; crop out any sky above."
 },
 "stops": [
  "Walk to the mown field at the edge of the village and pick the open stretch as the backdrop.",
  "Dress Lou in the long brown skirt and tie the green headscarf snugly over the hair.",
  "Gather the armful of dried grasses and set the phone tripod low in the field.",
  "Have Lou bend low at the waist, eyes down, arms reaching for the gathered grasses.",
  "Turn so daylight falls from the left, giving sidelight across the bent pose.",
  "Shoot landscape on the main lens, framing Lou and the field, leaving sky out.",
  "Compare the shot with The Gleaners, checking the bend, the scarf and the field."
 ],
 "alternative": {
  "painting": "Mona Lisa",
  "why": "A green headscarf could stand in for the thin veil over the hair, and the long brown skirt could suggest the dark dress."
 }
}

Build the input yourself

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

const K = MastKit;
const form = K.form({ painting: "milkmaid", cast: "Ana", standins: "",
  props: "white jug\nmixing bowl\nloaf of bread\nyellow cardigan\nblue apron\nwhite cotton cap", space: "Kitchen table by a window" });
K.painting(form);                                // {title: "The Milkmaid", figures: 1, roles: [...], elements: [...], light: "left", ...}
const built = K.buildInput(form);
built.page;                                      // the page's hanging-rule guess: "hang", "rehang" or "swap"
const wire = K.toWire(built.input);              // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// K.check(reply, built).notes                   // the page's re-check of the plan
// K.md(built, reply)                            // the casting, props, light, framing and tour 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://after-the-masters.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":"after-the-masters"}' | 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": "restage", "painting": "Girl with a Pearl Earring - Johannes Vermeer, c. 1665", "figures": "1", "roles": "1 | ...", "elements": "1 | ...", "cast": "Priya | person", "cast_count": "1", "props": "blue silk scarf\n...", "draft": "1 | ... | likely: ..."}
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 after-the-masters:<task>:<inputHash>:a<attempt> (the one task is restage), 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="after-the-masters:restage:$(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: after-the-masters:restage:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json