← Table Truce
API
Tokens

Drive Table Truce from your own code

The one metered step - reading the table - is available over HTTP. A natural use is an event-planning tool, a family group-chat bot or a wedding seating app that already knows who is coming and where they sit: your code sends the guests, the ties and the seats, and gets back the swaps worth making and the lines a host can actually say.

The split first: the page does the mechanics; the model gives the judgement; the page checks it. Parsing the guest list and the ties, laying out the table, the seat solver, the tension score and the hot-spot ids come from tablekit.js, which runs in node as well as in the browser. Its reconcile function then re-scores the table after the suggested swaps (they must lower the tension and never seat a feud side by side), checks that a calm verdict is not given over a side-by-side feud, that every high and medium hot spot has lines, that the group message never names a feud's topic, and that every cited id is on the page. The tension weights are the page's own rules of thumb, not research findings.

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":"table-truce"}), 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 a required field 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 read 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, truce:

taskWhat it returnsVerdict field
truceA title, a summary; swaps [{a, b, why, cite}] (two guest ids whose seats to exchange); hot_spots [{id, read, opener, redirect, exit, cite}] keyed by the page's H#; host_moves [{when, text, cite}] with when before, arrival, dinner or dessert; group_text (one message for everyone); safe_topics; caveats.verdict: calm, watch, reshuffle

reshuffle means a feud sits side by side and swapping unpinned guests can fix it (swaps are then required); watch means nothing high can be fixed but something medium remains, or a feud sits one seat apart; calm means no high or medium hot spots. Cites are the page's ids: guests G1…, ties F1… (keep apart), P1… (keep together), L1… (get on), seats S1…, hot spots H1…, plus N-note and T-topics.

The declared input schema carries scalars only, so object-valued fields travel JSON-encoded as strings (TableKit.toWire does this). task, table, guests, ties, seating and checks are required.

Input fields

FieldTypeNotes
taskstringAlways truce.
tablestring (JSON){shape, shape_name, seats, kitchen_seat, aisle_seats[]}; shape is round (3-14 seats) or long (4-24).
guestsstring (JSON)[{id, name, tags[]}]; tags: host, big talker, quiet, kid, needs an aisle seat, first time here, elder.
tiesstring (JSON)[{id, kind, a, b, topic}]; kind feud, together or likes.
seatingstring (JSON)[{seat, guest, name, beside[], across[], pinned}], one row per seat; an empty seat has guest: "".
checksstring (JSON){tension, counts{high, medium, low}, hot_spots[{id, level, kind, text, guests[], ties[]}]} from TableKit.score.
notestringOptional, the host's own words; over 600 characters keeps the start and the end around a marker.
topicsstringOptional, subjects to steer around (300 characters).
retry_notestringOnly on a reformat retry.

The reply

The job's output.output is a string holding one JSON object. Parse it with TableKit.parseReply, then normalize, then check it with reconcile(reply, {st, seating, pins}). TableKit.swapGuests(seating, a, b) applies one swap; score(seating, st) re-scores.

Worked examples

Replies produced against the app's prompt for the sample tables (made-up 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 seating folded.

task: truce - After the election (watch)

Input:

{
 "task": "truce",
 "table": "{\"shape\":\"long\",\"shape_name\":\"Long table\",\"seats\":12,\"kitchen_seat\":\"S1\",\"aisle_seats\":[\"S1\",\"S2\",\"S6\",\"S7\",\"S8\",\"S12\"]}",
 "guests": "[{\"id\":\"G1\",\"name\":\"Grandma June\",\"tags\":[\"needs an aisle seat\",\"elder\"]},{\"id\":\"G2\",\"name\":\"Marisol\",\"tags\":[\"host\"]},{\"id\":\"G3\",\"name\":\"Uncle Ray\",\"tags\":[\"big talker\"]},\"... 9 more\"]",
 "ties": "[{\"id\":\"F1\",\"kind\":\"feud\",\"a\":\"G3\",\"b\":\"G4\",\"topic\":\"the election\"},{\"id\":\"F2\",\"kind\":\"feud\",\"a\":\"G3\",\"b\":\"G9\",\"topic\":\"the stock tips\"},{\"id\":\"P1\",\"kind\":\"together\",\"a\":\"G5\",\"b\":\"G6\",\"topic\":\"\"},{\"id\":\"P2\",\"kind\":\"together\",\"a\":\"G8\",\"b\":\"G6\",\"topic\":\"\"},{\"id\":\"P3\",\"kind\":\"together\",\"a\":\"G9\",\"b\":\"G10\",\"topic\":\"\"},{\"id\":\"L1\",\"kind\":\"likes\",\"a\":\"G11\",\"b\":\"G12\",\"topic\":\"\"},{\"id\":\"L2\",\"kind\":\"likes\",\"a\":\"G4\",\"b\":\"G12\",\"topic\":\"\"}]",
 "seating": "[{\"seat\":\"S1\",\"guest\":\"G1\",\"name\":\"Grandma June\",\"beside\":[\"G3\",\"G4\"],\"across\":[],\"near\":[],\"pinned\":true},{\"seat\":\"S2\",\"guest\":\"G3\",\"name\":\"Uncle Ray\",\"beside\":[\"G1\",\"G8\"],\"across\":[\"G4\"],\"near\":[\"G12\"],\"pinned\":true},\"... 10 more seats\"]",
 "checks": "{\"tension\":8,\"counts\":{\"high\":0,\"medium\":1,\"low\":1},\"hot_spots\":[{\"id\":\"H1\",\"level\":\"medium\",\"kind\":\"feud\",\"text\":\"Uncle Ray and Aunt Dana sit face to face - the election\",\"guests\":[\"G3\",\"G4\"],\"ties\":[\"F1\"]},{\"id\":\"H2\",\"level\":\"low\",\"kind\":\"host\",\"text\":\"Marisol is hosting but sits far from the kitchen\",\"guests\":[\"G2\"],\"ties\":[]}]}",
 "note": "Grandma insists Ray and Dana sit either side of her at the head, like every year, and I am not fighting her on it. Ray brings up the election within ten minutes, every time.",
 "topics": "the election, the stock market"
}

Reply:

{
 "task": "truce",
 "verdict": "watch",
 "title": "Pinned Head Seats, One Spot to Watch",
 "summary": "The table is mostly calm. The one medium hot spot is Ray and Dana sitting face to face at the head of the table, but that can't be fixed with a swap since Grandma, Ray, and Dana are all pinned in those seats - so the fix here is conversational, not a reshuffle. Marisol's seat is also a long walk from the kitchen; moving her closer would mean breaking up guests who get along, so it's better handled with a bit of help serving than a seat change. Nothing else at the table needs attention tonight.",
 "swaps": [],
 "hot_spots": [
  {
   "id": "H1",
   "read": "Ray and Dana sit directly across from each other at the head of the table, and the note says Ray tends to bring up a touchy subject almost as soon as he sits down.",
   "opener": "Ray, Dana - before anything else, I want to hear what's new with each of you this year, so let's catch up first.",
   "redirect": "We could go around on that one all night - can I get everyone's vote on which pie goes out first?",
   "exit": "If it starts to heat up, either of them can slip into the kitchen to grab the next dish - no big deal, just a breather.",
   "cite": [
    "G3",
    "G4",
    "F1"
   ]
  },
  {
   "id": "H2",
   "read": "Marisol is hosting but her seat is one of the farthest from the kitchen, so she'll be doing a lot of extra walking while serving.",
   "opener": "Before everyone sits down, I'll ask one person to be the kitchen runner tonight so Marisol isn't up every few minutes.",
   "redirect": "Let's get plates moving table-to-table so Marisol isn't the only one carrying dishes down the line.",
   "exit": "Marisol can trade seats with whoever's nearest the kitchen once the mains are out, so she's not stuck hiking back and forth all night.",
   "cite": [
    "G2",
    "S1"
   ]
  }
 ],
 "host_moves": [
  {
   "when": "before",
   "text": "Have a quiet word with Ray ahead of time asking him to keep this one light at the table this year.",
   "cite": [
    "G3",
    "F1"
   ]
  },
  {
   "when": "arrival",
   "text": "Give Ray a job right when he walks in - pouring drinks or carving - so he's occupied before conversation gets rolling.",
   "cite": [
    "G3"
   ]
  },
  "..."
 ],
 "group_text": "So excited to have everyone together this year! Bring a favorite side if you're able, and we'll do a quick toast before we dig in. Let's toss phones in a basket by the door so we're all actually in it together - and come hungry, there's plenty of pie. Can't wait to see you all!",
 "safe_topics": [
  "What everyone's bringing to the table",
  "Owen and Lily's latest kid stories",
  "..."
 ],
 "caveats": [
  "This reads only the seating chart and notes given - it doesn't know anything about these guests beyond what's listed here.",
  "Ray and Dana's seats are fixed by Grandma's request, so their face-off across the table can be managed in the moment but not moved."
 ]
}

task: truce - Friendsgiving with an ex (calm)

Input:

{
 "task": "truce",
 "table": "{\"shape\":\"round\",\"shape_name\":\"Round table\",\"seats\":8,\"kitchen_seat\":\"S1\",\"aisle_seats\":[\"S5\",\"S6\"]}",
 "guests": "[{\"id\":\"G1\",\"name\":\"Ana\",\"tags\":[\"host\"]},{\"id\":\"G2\",\"name\":\"Jules\",\"tags\":[]},{\"id\":\"G3\",\"name\":\"Theo\",\"tags\":[]},\"... 5 more\"]",
 "ties": "[{\"id\":\"F1\",\"kind\":\"feud\",\"a\":\"G2\",\"b\":\"G3\",\"topic\":\"the breakup\"},{\"id\":\"P1\",\"kind\":\"together\",\"a\":\"G4\",\"b\":\"G5\",\"topic\":\"\"},{\"id\":\"L1\",\"kind\":\"likes\",\"a\":\"G6\",\"b\":\"G7\",\"topic\":\"\"},{\"id\":\"L2\",\"kind\":\"likes\",\"a\":\"G8\",\"b\":\"G2\",\"topic\":\"\"},{\"id\":\"L3\",\"kind\":\"likes\",\"a\":\"G1\",\"b\":\"G3\",\"topic\":\"\"}]",
 "seating": "[{\"seat\":\"S1\",\"guest\":\"G4\",\"name\":\"Sam\",\"beside\":[\"G1\",\"G5\"],\"across\":[],\"near\":[\"G3\",\"G2\"],\"pinned\":false},{\"seat\":\"S2\",\"guest\":\"G1\",\"name\":\"Ana\",\"beside\":[\"G4\",\"G3\"],\"across\":[],\"near\":[\"G7\",\"G5\"],\"pinned\":false},\"... 6 more seats\"]",
 "checks": "{\"tension\":0,\"counts\":{\"high\":0,\"medium\":0,\"low\":0},\"hot_spots\":[]}",
 "note": "Jules and Theo broke up in the spring and both said they are fine to come. Mina is Sam's new partner and knows nobody else."
}

Reply:

{
 "task": "truce",
 "verdict": "calm",
 "title": "A Calm Table, Already Sorted",
 "summary": "This table already landed in a good spot: the seating keeps Jules and Theo on opposite sides of the circle instead of side by side, keeps Sam and Mina together, and puts every pair who gets along elbow to elbow. There is nothing here that needs fixing before people sit down. The one thing worth extra attention is Mina - it is her first time at this table, so a warm welcome from more than just Sam will go a long way.",
 "swaps": [],
 "hot_spots": [],
 "host_moves": [
  {
   "when": "before",
   "text": "Give Jules and Theo a quick call each before the day, just so they know you've got them on opposite sides of the table and don't need to think twice about it.",
   "cite": [
    "F1"
   ]
  },
  {
   "when": "arrival",
   "text": "Meet Mina at the door and walk her over yourself, introducing her to Kofi and Lu before she even sits down.",
   "cite": [
    "G5",
    "P1"
   ]
  },
  "..."
 ],
 "group_text": "So excited to have you all for Friendsgiving this year! Bring a side if you're able, otherwise just bring yourselves and your appetite. We'll do a game after the meal, so come ready to laugh a little. Can't wait to see everyone around the table.",
 "safe_topics": [
  "What everyone's bringing to the table",
  "Getting to know Mina a bit",
  "..."
 ],
 "caveats": [
  "This read only knows what the guest list, ties and note say - if anything about Jules and Theo or anyone else has changed since, trust your own read over this.",
  "It assumes this exact seating holds; if seats move for any other reason, it's worth a quick recheck."
 ]
}

task: truce - Place cards already written (reshuffle)

Input:

{
 "task": "truce",
 "table": "{\"shape\":\"long\",\"shape_name\":\"Long table\",\"seats\":10,\"kitchen_seat\":\"S1\",\"aisle_seats\":[\"S1\",\"S2\",\"S5\",\"S6\",\"S7\",\"S10\"]}",
 "guests": "[{\"id\":\"G1\",\"name\":\"Pat\",\"tags\":[\"host\"]},{\"id\":\"G2\",\"name\":\"Walt\",\"tags\":[\"big talker\"]},{\"id\":\"G3\",\"name\":\"Cheryl\",\"tags\":[]},\"... 7 more\"]",
 "ties": "[{\"id\":\"F1\",\"kind\":\"feud\",\"a\":\"G2\",\"b\":\"G4\",\"topic\":\"the vaccine argument\"},{\"id\":\"F2\",\"kind\":\"feud\",\"a\":\"G9\",\"b\":\"G5\",\"topic\":\"the inheritance\"},{\"id\":\"P1\",\"kind\":\"together\",\"a\":\"G8\",\"b\":\"G7\",\"topic\":\"\"},{\"id\":\"P2\",\"kind\":\"together\",\"a\":\"G3\",\"b\":\"G2\",\"topic\":\"\"},{\"id\":\"L1\",\"kind\":\"likes\",\"a\":\"G10\",\"b\":\"G6\",\"topic\":\"\"}]",
 "seating": "[{\"seat\":\"S1\",\"guest\":\"G1\",\"name\":\"Pat\",\"beside\":[\"G2\",\"G3\"],\"across\":[],\"near\":[],\"pinned\":true},{\"seat\":\"S2\",\"guest\":\"G2\",\"name\":\"Walt\",\"beside\":[\"G1\",\"G4\"],\"across\":[\"G3\"],\"near\":[\"G7\"],\"pinned\":false},\"... 8 more seats\"]",
 "checks": "{\"tension\":29,\"counts\":{\"high\":2,\"medium\":2,\"low\":1},\"hot_spots\":[{\"id\":\"H1\",\"level\":\"high\",\"kind\":\"feud\",\"text\":\"Walt and Dev sit side by side - the vaccine argument\",\"guests\":[\"G2\",\"G4\"],\"ties\":[\"F1\"]},{\"id\":\"H2\",\"level\":\"high\",\"kind\":\"feud\",\"text\":\"Frank and Imani sit side by side - the inheritance\",\"guests\":[\"G9\",\"G5\"],\"ties\":[\"F2\"]},{\"id\":\"H3\",\"level\":\"medium\",\"kind\":\"aisle\",\"text\":\"Rosa needs an aisle seat and is boxed in\",\"guests\":[\"G6\"],\"ties\":[]},{\"id\":\"H4\",\"level\":\"medium\",\"kind\":\"split\",\"text\":\"Kim and Leo are split up\",\"guests\":[\"G8\",\"G7\"],\"ties\":[\"P1\"]},{\"id\":\"H5\",\"level\":\"low\",\"kind\":\"split\",\"text\":\"Cheryl and Walt face each other instead of sitting together\",\"guests\":[\"G3\",\"G2\"],\"ties\":[\"P2\"]}]}",
 "note": "I already wrote the place cards in this order. Walt and Dev had a shouting match last year.",
 "topics": "money, the will"
}

Reply:

{
 "task": "truce",
 "verdict": "reshuffle",
 "title": "Two Feuds, Three Swaps, One Calm Table",
 "summary": "This table has two real feuds seated elbow to elbow, plus Rosa boxed away from the aisle and Kim split from Leo. The swap that matters most is pulling Dev away from Walt, since the note says they had a shouting match last year and the place cards sit them side by side again. Three unpinned swaps - Dev and Leo, Rosa and Kim, Frank and Sue - separate both feuds, get Rosa onto the aisle, and reunite Kim with Leo, without breaking any pair or creating a new clash. One small gap is left on purpose: Cheryl and Walt still aren't side by side.",
 "swaps": [
  {
   "a": "G4",
   "b": "G7",
   "why": "Moves Dev away from Walt and puts Leo in next to where Kim is about to land.",
   "cite": [
    "F1",
    "P1",
    "H1",
    "H4"
   ]
  },
  {
   "a": "G6",
   "b": "G8",
   "why": "Gets Rosa onto an aisle seat and lands Kim right beside Leo.",
   "cite": [
    "P1",
    "H3",
    "H4"
   ]
  },
  {
   "a": "G9",
   "b": "G10",
   "why": "Pulls Frank away from Imani so the inheritance tension isn't sitting elbow to elbow.",
   "cite": [
    "F2",
    "H2"
   ]
  }
 ],
 "hot_spots": [
  {
   "id": "H1",
   "read": "Walt and Dev are seated right next to each other, and the note says they had a shouting match last year over this - the seating itself is the first thing to fix.",
   "opener": "Walt, you're always good for a story - kick us off with something good before we all dig in.",
   "redirect": "Let's leave that one at the door tonight - who's got a vote for what goes on after dinner?",
   "exit": "If it starts to climb, 'can you grab the extra chairs' gives either of them an easy, normal reason to step out for a minute.",
   "cite": [
    "G2",
    "G4",
    "F1",
    "N-note"
   ]
  },
  {
   "id": "H2",
   "read": "Frank and Imani are side by side over something with real weight behind it, so a passing comment there is riskier than anywhere else at the table.",
   "opener": "Frank, since you love a good toast, want to say a few words before we eat?",
   "redirect": "That's a conversation for another day - let's go around and say one thing we're each looking forward to tonight.",
   "exit": "A quiet 'can you help me in the kitchen for a sec' works for either of them without anyone else noticing.",
   "cite": [
    "G9",
    "G5",
    "F2"
   ]
  },
  "..."
 ],
 "host_moves": [
  {
   "when": "before",
   "text": "Re-cut the place cards with Dev and Leo swapped, Rosa and Kim swapped, and Frank and Sue swapped - it separates both tense pairs and gets Rosa onto the aisle before anyone sits down.",
   "cite": [
    "F1",
    "F2",
    "H1",
    "H2",
    "H3"
   ]
  },
  {
   "when": "arrival",
   "text": "Greet Walt and Frank yourself as they come in and get each one talking with whoever's closest right away, so neither has a quiet moment to drift toward last time.",
   "cite": [
    "G2",
    "G9",
    "N-note"
   ]
  },
  "..."
 ],
 "group_text": "Can't wait to have everyone around the table this year! If you're able, bring your favorite dish to share, and let's do a game after dinner - any requests? Come hungry and ready for good food and better company. See you all soon!",
 "safe_topics": [
  "the food everyone's bringing",
  "a holiday game after dinner",
  "..."
 ],
 "caveats": [
  "This reads the table only from the seating and tie data given - it doesn't know anything about Walt, Dev, Frank, or Imani beyond what's listed, so the lines are kept general on purpose.",
  "The note mentions a shouting match between Walt and Dev last year; that's treated as a strong, real feud (reflected in the seating fix) but not as violence, a threat, or anyone being afraid, so this stays a seating-and-conversation plan rather than a safety call.",
  "..."
 ]
}

Build the input yourself

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

const K = TableKit;
const st = K.state({ shape: "long", empty: 0,
  guests: "Marisol: host\nRay: big talker\nDana\nGreg\nTess\nHal: quiet",
  ties: "Ray x Dana: the election\nGreg + Tess" });
const seating = K.solve(st, {}, 1);                 // or your own: one guest id (or null) per seat
const { input, score } = K.buildInput(st, seating, {}, { note: "", topics: "" });
const wire = K.toWire(input);                       // POST this to /estimate and /run
// later: const reply = K.normalize(K.parseReply(job.output.output));
// K.reconcile(reply, { st, seating, pins: {} }) -> [{level, code, text}]

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://table-truce.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":"table-truce"}' | 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": "truce", "table": "{...}", "guests": "[...]", "ties": "[...]", "seating": "[...]", "checks": "{...}"}
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 table-truce:<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="table-truce:truce:$(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: table-truce:truce:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json