← Who Brings What
API
Tokens

Drive Who Brings What from your own code

The two metered steps - the host check and the menu cards - are available over HTTP. A natural use is a group-chat bot or an event tool: your code keeps the sign-up sheet, sends it for a check whenever someone signs up, posts the group message, and prints the cards the day before.

The split first: the page does the arithmetic; the model writes the judgement. The who-eats-what grid, plate checks, serving estimates, flags and reply checks ship as a plain script, potkit.js (PotKit.normSheet, facts, marks, buildInput, toWire, parseReply, normalize, reconcileHost, reconcileCards), which you can load in node. Serving targets inside it are rule-of-thumb estimates, not measurements.

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

StatusCodeMeaning
400validation_errorThe body is not a JSON object.
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: both tasks 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 comes first

Every run sends task. There are two:

taskWhat comes back
hostA verdict - ready, gaps or rework - with a headline; a serving style (buffet, family, stations, mixed) with why and a table flow; one fix per guest without a full plate; 0–8 gap suggestions, each with a course, the diets it suits and the guest to ask; duplicate fixes; the drinks posture and a zero-proof option; kitchen fixes; safety notes; an after-dinner idea; a group-chat message; warnings.
cardsA menu (title, subtitle, sections in meal order with one line per dish, footer), one tent card per dish (line, contains, marks, caution) and a legend of the marks used.

The declared input schema carries scalars only, so object-valued fields travel JSON-encoded as strings (PotKit.toWire does this). task, event, dishes and facts are required.

Input fields - host

FieldTypeNotes
eventstring (JSON){name, date, ovens, burners, outlets, fridge}; fridge is tight, some or roomy.
guestsstring (JSON)Array of {name, party, diets[], host, bringing[]}. Diet ids: vegan, vegetarian, pescatarian, gluten_free, dairy_free, egg_free, tree_nut_allergy, peanut_allergy, shellfish_allergy, sesame_allergy, soy_free, no_pork, no_alcohol. At most 40 guests.
dishesstring (JSON)Array of {name, by, course, contains[], unsure, serves, needs, temp_f?}. Courses: appetizer, main, side, salad, bread, dessert, drinks, other. Contains: meat, poultry, pork, fish, shellfish, dairy, egg, gluten, nuts, peanuts, soy, sesame, alcohol. Needs: none, oven, stove, outlet, fridge, microwave. At most 60 dishes (the page cuts whole rows and says so).
notesstringAt most 1,200 characters (the page keeps the start and end of longer text with a [...] marker).
factsstring (JSON){people, per_guest[], courses[], duplicates[], kitchen, allergens[], unassigned[], flags[]} from PotKit.facts; every warn-level flag must be answered.

Input fields - cards

FieldTypeNotes
eventstring (JSON){name, date}.
dishesstring (JSON)Array of {name, by, course, contains[], unsure, marks[]}; marks from PotKit.marks (VG, V, GF, DF, NF; none when unsure).
stylestringharvest table, candlelit, playful or minimal.
notesstringOptional.
factsstring (JSON){dishes, marks_legend[], allergens[], unsure[]}.

Either task may carry retry_note on a reformat retry.

The reply

The job's output.output is a string holding one JSON object. Parse it with PotKit.parseReply, then normalize, then check it: reconcileHost(reply, sheet) flags guests without a plate who got no fix, short courses with no gap, gaps asked of people not on the sheet, unanswered duplicates and kitchen clashes, allergens missing from the safety notes and a ready verdict over open warnings; reconcileCards(reply, sheet) flags dishes missing from the menu or invented, marks that differ from the sheet, contains lines that leave an allergen out and cautions that name a guest. The page always prints its own marks and contains lines.

Worked examples

Real replies, produced against the app's prompt by a validation run (lists shortened here; the full replies ship in example-result.js). The page replays each of them free from its example buttons; the three host checks land on all three verdicts.

task: host - twelve friends, mixed diets (gaps)

Input (decoded for reading; on the wire event, guests, dishes and facts are JSON strings):

{
  "task": "host",
  "event": {
    "name": "Friends' Thanksgiving potluck",
    "date": "2026-11-21",
    "ovens": 1,
    "burners": 4,
    "outlets": 2,
    "fridge": "some"
  },
  "guests": [
    {
      "name": "Maya",
      "party": 1,
      "diets": [],
      "host": true,
      "bringing": [
        "Roast turkey"
      ]
    },
    {
      "name": "Priya",
      "party": 1,
      "diets": [
        "vegan"
      ],
      "host": false,
      "bringing": [
        "Roasted squash and pecan salad"
      ]
    },
    {
      "name": "Theo",
      "party": 1,
      "diets": [
        "gluten_free"
      ],
      "host": false,
      "bringing": [
        "Green bean casserole"
      ]
    },
    {
      "...": "6 more guests"
    }
  ],
  "dishes": [
    {
      "name": "Roast turkey",
      "by": "Maya",
      "course": "main",
      "contains": [
        "poultry"
      ],
      "unsure": false,
      "serves": 12,
      "needs": "oven",
      "temp_f": 325
    },
    {
      "name": "Mac and cheese",
      "by": "Jonah",
      "course": "side",
      "contains": [
        "dairy",
        "gluten",
        "egg"
      ],
      "unsure": false,
      "serves": 10,
      "needs": "oven",
      "temp_f": 350
    },
    {
      "name": "Baked mac & cheese",
      "by": "Lena",
      "course": "side",
      "contains": [
        "dairy",
        "gluten"
      ],
      "unsure": false,
      "serves": 8,
      "needs": "oven",
      "temp_f": 375
    },
    {
      "...": "6 more dishes"
    }
  ],
  "notes": "Small apartment, one oven. Maya is roasting the turkey from 11. People arrive at 4, dinner at 5. Two kids coming with Noor. Sam's peanut allergy is the serious kind.",
  "facts": {
    "people": 11,
    "per_guest": [
      {
        "guest": "Maya",
        "party": 1,
        "diets": [],
        "can_eat": 9,
        "cannot": 0,
        "unsure": 0,
        "mains": 1,
        "savory": 5,
        "status": "ok"
      },
      {
        "guest": "Priya",
        "party": 1,
        "diets": [
          "vegan"
        ],
        "can_eat": 2,
        "cannot": 7,
        "unsure": 0,
        "mains": 0,
        "savory": 1,
        "status": "none"
      },
      {
        "...": "one row per guest"
      }
    ],
    "courses": [
      {
        "course": "appetizer",
        "dishes": 0,
        "servings": 0,
        "target": 11,
        "state": "short"
      },
      {
        "course": "main",
        "dishes": 1,
        "servings": 12,
        "target": 11,
        "state": "ok"
      },
      {
        "...": "one row per course"
      }
    ],
    "duplicates": [
      [
        "Mac and cheese",
        "Baked mac & cheese"
      ]
    ],
    "kitchen": {
      "ovens": 1,
      "oven_dishes": 4,
      "oven_temps": [
        "325-350F",
        "375F"
      ],
      "burners": 4,
      "stove_dishes": 0,
      "outlets": 2,
      "outlet_dishes": 0,
      "fridge": "some",
      "fridge_dishes": 2
    },
    "allergens": [
      {
        "allergen": "peanuts",
        "allergy": "peanut allergy",
        "guest": "Sam",
        "dishes": [
          "Peanut butter blossom cookies"
        ]
      }
    ],
    "unassigned": [
      "Noor"
    ],
    "flags": [
      "guest_none: Priya (vegan) can eat 2 dishes - not a plate.",
      "guest_thin: Lena (vegetarian) has no main they can eat.",
      "course_short: Starters: about 0 servings against roughly 11 needed (estimate).",
      "course_over: Desserts: about 32 servings against roughly 11 - plenty, maybe too many.",
      "course_short: Drinks: about 12 servings against roughly 22 needed (estimate).",
      "duplicate: Mac and cheese / Baked mac & cheese look like the same dish.",
      "allergen_on_table: Sam has a peanut allergy and Peanut butter blossom cookies contains peanuts.",
      "oven_conflict: 2 different oven temperatures (325-350F, 375F) for 1 oven - someone reheats in turns or arrives hot.",
      "unassigned: Not bringing anything yet: Noor."
    ]
  }
}

Reply:

{
  "task": "host",
  "verdict": "gaps",
  "headline": "The table's mostly solid - turkey and sides cover the room - but Priya (vegan) doesn't have a real plate yet, and the peanut cookies need to stay well clear of Sam's allergy.",
  "service_style": {
    "style": "mixed",
    "why": "The small apartment and one-oven kitchen argue for a simple buffet line, but Sam's serious peanut allergy means dessert needs its own clearly separate spot - so it's buffet for the meal and a small station for dessert.",
    "flow": "Put the turkey and hot sides out first near the kitchen so plates start there, group the two mac and cheeses together so people can see they're the same dish, label Priya's vegan salad and Theo's gluten-free casserole clearly, and set desserts on a separate table or counter with the peanut cookies at the far end, apart from the pecan pie and out of Sam's path."
  },
  "guest_fixes": [
    {
      "guest": "Priya",
      "status": "none",
      "fix": "Add a vegan side or starter rather than asking her to bring more - the starters course is empty anyway, so one hearty vegan dish gives her an actual plate instead of just her own salad."
    },
    {
      "guest": "Lena",
      "status": "thin",
      "fix": "Call her own baked mac and cheese out as a vegetarian main on the table instead of just a side, so she has a hearty plate without anyone needing to cook something new."
    }
  ],
  "gaps": [
    {
      "course": "appetizer",
      "suggestion": "a vegan veggie platter with hummus or a dip",
      "suits": [
        "vegan",
        "vegetarian",
        "gluten_free",
        "dairy_free"
      ],
      "ask": "Noor",
      "why": "Starters are sitting at zero servings and Noor isn't bringing anything yet, so one simple no-cook platter fills the course and gives Priya real options before the mains come out."
    },
    {
      "course": "drinks",
      "suggestion": "a warm spiced cider or sparkling juice",
      "suits": [
        "no_alcohol"
      ],
      "ask": "Maya",
      "why": "Drinks are short against the estimate and the only drink on the sheet has alcohol, so a real zero-proof pitcher covers Marcus and anyone else not drinking."
    }
  ],
  "duplicates": [
    {
      "dishes": [
        "Mac and cheese",
        "Baked mac & cheese"
      ],
      "fix": "Jonah and Lena are both bringing a baked mac and cheese - ask one of them to swap to a different side, like a green vegetable or potato dish, so the side table has real variety instead of two versions of the same thing."
    }
  ],
  "drinks": {
    "posture": "Keep it casual with the sangria out for whoever wants it, but don't make alcohol the only option on the table.",
    "zero_proof": "Ask Maya to set out a warm cider or sparkling juice so Marcus and any non-drinkers have a real alcohol-free drink, not just a water pitcher."
  },
  "kitchen": [
    {
      "issue": "One oven has to fit four dishes at two different temperature ranges - the turkey at 325F, Jonah's and Theo's dishes at 350F, Lena's at 375F.",
      "fix": "Let the turkey finish and rest first, then run the three sides through the still-warm oven one or two at a time in the 15-20 minutes before serving - a sheet of foil keeps the first ones hot while the next bakes."
    }
  ],
  "safety": [
    "Sam's peanut allergy is the serious kind - keep the peanut butter blossom cookies on their own plate at the far end of the dessert table, away from the pecan pie, with their own serving tongs.",
    "Label every dish, especially the nut and peanut sweets, so Theo (gluten-free) and Ana (dairy-free) can scan quickly too."
  ],
  "after_dinner": "A relaxed dessert-and-coffee round or a simple board game suits a crowd with two kids in it - nothing that needs more kitchen time.",
  "group_message": "Hi everyone! Thanksgiving potluck is set for 2026-11-21, dinner at 5 (doors at 4). Lineup so far: Maya's roast turkey, Jonah's and Lena's mac and cheese, Theo's green bean casserole, Ana's skillet cornbread and peanut butter blossom cookies, Priya's roasted squash and pecan salad, Marcus's pecan pie, and Sam's cider sangria.\n\nTwo small asks: Noor, could you bring a starter or veggie platter - we're light on apps? And Maya, could you add a non-alcoholic cider or sparkling juice so there's a real alcohol-free option next to the sangria?\n\nHeads up: Sam has a serious peanut allergy, so the peanut butter blossom cookies will be kept well apart from everything else - please don't swap serving spoons between dishes. Thanks all, can't wait!",
  "warnings": [
    "Desserts are stacked - pecan pie plus two dozen cookies is plenty for 11 people, so there's room to send some home rather than add more.",
    "Noor's bringing two kids - worth double-checking something plain and familiar, like the cornbread or a mac and cheese, is easy for them to eat."
  ]
}

task: host - six at the table (ready)

Verdict, style and headline only:

{
  "verdict": "ready",
  "headline": "This sheet is ready to go: six guests, nine dishes, and every course covered with no gaps.",
  "service_style": {
    "style": "family",
    "why": "Six people at one long table with sit-down service is a classic family-style group, and with only a vegetarian and a gluten-free guest, passing dishes down the table is easy for everyone to manage themselves.",
    "flow": "Set the chicken and the mushroom wellington as the two anchor mains in the middle of the table, pass the root vegetables, mashed potatoes and salad down each side, and put the cheese board and sourdough together at one end since both carry gluten. Give them their own serving knife so the gluten-free side stays clean. Keep the cider jug near an outlet at the far end."
  }
}

task: host - office potluck (rework)

{
  "verdict": "rework",
  "headline": "This sheet needs real rework: Leo has no plate at all, mains, sides, bread and drinks are all short, and two tree-nut desserts/salad are sitting right where Hana's allergy meets them.",
  "guest_fixes": [
    {
      "guest": "Raj",
      "status": "thin",
      "fix": "Raj has no main he can trust yet - a confirmed vegetarian main would give him one and also cover Omar, so ask an unassigned guest rather than Raj himself."
    },
    {
      "guest": "Hana",
      "status": "thin",
      "fix": "Hana can't count Grandma's chili since it's unconfirmed, so she has no main - a nut-free, confirmed dish would finally give her one, on top of keeping the nut desserts away from her."
    },
    {
      "guest": "Leo",
      "status": "none",
      "fix": "Leo can only eat one thing on the whole table right now - he needs an actual vegan main or hearty side, not a garnish, before he has a real plate."
    },
    {
      "guest": "Grace",
      "status": "thin",
      "fix": "Grace has no gluten-free main or bread - a gluten-free side or bread basket would give her more than just the one salad."
    },
    {
      "guest": "Omar",
      "status": "thin",
      "fix": "Omar's only main is Grandma's chili and it's unsure, so he can't rely on it - get Omar to confirm what's in it, or line up a second, confirmed no-pork main."
    }
  ]
}

task: cards - the friends' menu (harvest table)

Input (decoded):

{
  "task": "cards",
  "event": {
    "name": "Friends' Thanksgiving potluck",
    "date": "2026-11-21"
  },
  "dishes": [
    {
      "name": "Roast turkey",
      "by": "Maya",
      "course": "main",
      "contains": [
        "poultry"
      ],
      "unsure": false,
      "marks": [
        "GF",
        "DF",
        "NF"
      ]
    },
    {
      "name": "Mac and cheese",
      "by": "Jonah",
      "course": "side",
      "contains": [
        "dairy",
        "gluten",
        "egg"
      ],
      "unsure": false,
      "marks": [
        "V",
        "NF"
      ]
    },
    {
      "name": "Baked mac & cheese",
      "by": "Lena",
      "course": "side",
      "contains": [
        "dairy",
        "gluten"
      ],
      "unsure": false,
      "marks": [
        "V",
        "NF"
      ]
    },
    {
      "...": "6 more dishes"
    }
  ],
  "style": "harvest table",
  "notes": "Small apartment, one oven. Maya is roasting the turkey from 11. People arrive at 4, dinner at 5. Two kids coming with Noor. Sam's peanut allergy is the serious kind.",
  "facts": {
    "dishes": 9,
    "marks_legend": [
      {
        "mark": "VG",
        "means": "vegan"
      },
      {
        "mark": "V",
        "means": "vegetarian"
      },
      {
        "mark": "GF",
        "means": "no gluten ingredients"
      },
      {
        "mark": "DF",
        "means": "no dairy"
      },
      {
        "mark": "NF",
        "means": "no nuts or peanuts"
      }
    ],
    "allergens": [
      {
        "allergen": "peanuts",
        "allergy": "peanut allergy",
        "guest": "Sam",
        "dishes": [
          "Peanut butter blossom cookies"
        ]
      }
    ],
    "unsure": []
  }
}

Reply:

{
  "task": "cards",
  "menu": {
    "title": "Friends' Thanksgiving Potluck",
    "subtitle": "November 21, 2026 - a warm harvest table with friends.",
    "sections": [
      {
        "course": "main",
        "heading": "The Main Course",
        "items": [
          {
            "dish": "Roast turkey",
            "by": "Maya",
            "line": "The classic roast turkey, this feast's main dish, containing poultry.",
            "marks": [
              "GF",
              "DF",
              "NF"
            ]
          }
        ]
      },
      {
        "course": "side",
        "heading": "Harvest Sides",
        "items": [
          {
            "dish": "Mac and cheese",
            "by": "Jonah",
            "line": "Creamy mac and cheese made with dairy, gluten and egg.",
            "marks": [
              "V",
              "NF"
            ]
          },
          {
            "dish": "Baked mac & cheese",
            "by": "Lena",
            "line": "Baked mac and cheese, Lena's take with dairy and gluten.",
            "marks": [
              "V",
              "NF"
            ]
          },
          {
            "dish": "Green bean casserole",
            "by": "Theo",
            "line": "Green bean casserole, a comforting dairy-based side for the table.",
            "marks": [
              "V",
              "GF",
              "NF"
            ]
          }
        ]
      },
      {
        "...": "one section per course on the sheet"
      }
    ],
    "footer": "VG vegan, V vegetarian, GF gluten-free, DF dairy-free, NF nut-free - ask the cook about anything unlabeled."
  },
  "tents": [
    {
      "dish": "Roast turkey",
      "line": "Golden roast turkey, the table's main dish.",
      "contains": [
        "poultry"
      ],
      "marks": [
        "GF",
        "DF",
        "NF"
      ],
      "caution": ""
    },
    {
      "dish": "Mac and cheese",
      "line": "Creamy mac and cheese with egg and dairy.",
      "contains": [
        "dairy",
        "gluten",
        "egg"
      ],
      "marks": [
        "V",
        "NF"
      ],
      "caution": ""
    },
    {
      "dish": "Baked mac & cheese",
      "line": "Baked mac and cheese with dairy and gluten.",
      "contains": [
        "dairy",
        "gluten"
      ],
      "marks": [
        "V",
        "NF"
      ],
      "caution": ""
    },
    {
      "...": "one tent card per dish"
    }
  ],
  "legend": [
    {
      "mark": "VG",
      "means": "vegan"
    },
    {
      "mark": "V",
      "means": "vegetarian"
    },
    {
      "mark": "GF",
      "means": "no gluten ingredients"
    },
    {
      "mark": "DF",
      "means": "no dairy"
    },
    {
      "mark": "NF",
      "means": "no nuts or peanuts"
    }
  ]
}

Build the input yourself

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

const P = PotKit;
const sheet = P.normSheet({
  event: { name: "Friends' potluck", date: "2026-11-21", ovens: 1, burners: 4, outlets: 2, fridge: "some" },
  guests: [{ id: "g1", name: "Maya", host: true }, { id: "g2", name: "Priya", diets: ["vegan"] }],
  dishes: [{ name: "Roast turkey", by: "g1", course: "main", contains: ["poultry"], serves: 12, needs: "oven", temp: 325 }]
});
const { input } = P.buildInput({ task: "host", sheet });
const wire = P.toWire(input);                         // POST this to /estimate and /run
// later: const check = P.normalize(P.parseReply(job.output.output));
// P.reconcileHost(check, sheet)  -> notes
// then:  P.buildInput({ task: "cards", sheet, style: "candlelit" })

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://who-brings-what.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":"who-brings-what"}' | 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": "host", "event": "{...}", "guests": "[...]", "dishes": "[...]", "facts": "{...}"}
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}' 

Step 5 — POST /run, then poll the job

Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses who-brings-what:<task>:<inputHash>:a<attempt>, and its one reformat retry (with retry_note added) is attempt 2, a new key and a second run. The job's output.output is the reply text; truncated: true means the balance cut it short.

KEY="who-brings-what:host:$(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: who-brings-what:host:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json