← Sink or Bob
API

Drive Sink or Bob from your own code

The one metered task, dunk, is available over HTTP. It takes 2 to 8 everyday things and the water (plain or salty), plus the page's own density ranges for any it knows. For each item it returns the everyday hunch it tests, two basic facts about what it is made of, a density range in g/cm3 and a verdict (bobs, rides-low, sinks or depends), then one safe way to check at home and a tally - or a polite decline when the line-up holds a living creature or a hazardous item.

The division of labour first: the page knows rough densities for about 50 common things and applies a fixed rule; the model reasons each item from first principles and estimates the ones the page does not know; the page then re-checks every verdict against the model's own numbers, the table ranges, the item count, the tally and the word limits. Build items and known the same way (see "Build the input yourself") so the lines match.

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":"sink-or-bob"}), 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, water, count, items, known) 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: dunking a line-up 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 status and the verdicts

Every run sends task: dunk (the one task is dunk); a missing or different one is still answered as dunk. status is declined when any item is a living creature or a person, a weapon, an explosive or firework, a fuel or household chemical, or a drug. Otherwise the water's density D is 1.00 for plain and 1.10 for salty, and the low-riding line L is 0.90 or 0.99. Each verdict is checked in this order against the item's own low and high, stopping at the first match: depends (low below D and high above D), sinks (low is D or more), rides-low (high is L or more), bobs (every other case).

Input (every field is a string)

FieldRequiredWhat it holds
taskyesAlways dunk.
wateryesplain or salty.
countyesHow many items, 2 to 8, as digits.
itemsyesNumbered lines, one per item, each name 2 to 40 characters - for example 1. apple then 2. orange, peeled.
knownyesNumbered lines n. low-high with two decimals for the items the page's table knows (for example 1. 0.73-0.85), or none. The model copies these ranges exactly.
retry_notenoOnly on the page's one reformat retry, after an unusable reply.

Reply (one JSON object)

{"task": "dunk", "status": "ok", "reason": "",
 "items": [{"n": 1, "name": "apple", "verdict": "bobs", "low": 0.73, "high": 0.85, "hunch": "...", "axioms": ["...", "..."], "why": "...", "depends_on": ""}, ...],
 "try_it": "...", "closing": "..."}
FieldWhat it holds
status, reasonok or declined; for declined, one sentence of at most 25 words, items empty and try_it and closing empty.
itemsExactly count entries in input order. Each: n, name (copied), verdict, low and high (numbers, g/cm3, two decimals), hunch (at most 12 words), axioms (exactly 2 facts, each at most 14 words), why (at most 30 words) and depends_on (at most 12 words, only for depends).
try_itOne safe home check with a bowl of water, at most 30 words, naming any item to keep dry.
closingOne sentence of at most 20 words giving the count of each verdict, as digits.

The page then checks the reply (TubKit.check) and lists any disagreement under "The page's check".

Worked examples

These are the five free samples on the page: the exact input and a stand-in model's reply, unedited. Together they cover every outcome - the fruit bowl (all four verdicts), the desk drawer (an item to keep dry), strong salt water (salty), a pocketful (nothing in the page's table) and a bath-time line-up (declined).

The fruit bowl - all four verdicts

Input:

{
 "task": "dunk",
 "water": "plain",
 "count": "6",
 "items": "1. apple\n2. orange\n3. orange, peeled\n4. lime\n5. grapes\n6. banana",
 "known": "1. 0.73-0.85\n2. 0.78-0.88\n3. 1.03-1.10\n4. 0.95-1.05\n5. 1.05-1.15\n6. 0.90-0.98"
}

Reply:

{
 "task": "dunk",
 "status": "ok",
 "reason": "",
 "items": [
  {
   "n": 1,
   "name": "apple",
   "verdict": "bobs",
   "low": 0.73,
   "high": 0.85,
   "hunch": "You might assume a whole apple always floats.",
   "axioms": [
    "Apple flesh holds many tiny trapped air pockets throughout.",
    "Its skin and flesh contain no added water or metal."
   ],
   "why": "The trapped air keeps the apple lighter than plain water, so it floats well; the hunch that apples float holds true.",
   "depends_on": ""
  },
  {
   "n": 2,
   "name": "orange",
   "verdict": "bobs",
   "low": 0.78,
   "high": 0.88,
   "hunch": "An orange is assumed to float just like an apple.",
   "axioms": [
    "Orange peel is thick and filled with tiny air pockets.",
    "The peel's trapped air outweighs the denser juicy pulp inside."
   ],
   "why": "The peel's air pockets keep the whole orange lighter than plain water, so it floats well; the floating hunch holds.",
   "depends_on": ""
  },
  {
   "n": 3,
   "name": "orange, peeled",
   "verdict": "sinks",
   "low": 1.03,
   "high": 1.1,
   "hunch": "You might assume peeling an orange barely changes whether it floats.",
   "axioms": [
    "Peeled orange has no air-trapping rind left around it.",
    "Its pulp is mostly dense juice and fiber."
   ],
   "why": "Without the peel's trapped air, the pulp is denser than plain water, so it sinks; the hunch that peeling barely matters fails.",
   "depends_on": ""
  },
  {
   "n": 4,
   "name": "lime",
   "verdict": "depends",
   "low": 0.95,
   "high": 1.05,
   "hunch": "You might assume limes always float like other citrus.",
   "axioms": [
    "Lime peel holds some trapped air like other citrus fruits.",
    "Lime pulp and juice are nearly as dense as water."
   ],
   "why": "The peel's air and the dense pulp roughly balance plain water's density, so it depends; the floating hunch could go either way.",
   "depends_on": "More juice sinks it; more peel air floats it."
  },
  {
   "n": 5,
   "name": "grapes",
   "verdict": "sinks",
   "low": 1.05,
   "high": 1.15,
   "hunch": "You might assume grapes float like other round fruits.",
   "axioms": [
    "Grape skin is thin and traps very little air.",
    "Grape flesh is mostly dense sugary juice."
   ],
   "why": "With almost no trapped air, the sugary juice makes grapes denser than plain water, so they sink; the floating hunch fails.",
   "depends_on": ""
  },
  {
   "n": 6,
   "name": "banana",
   "verdict": "rides-low",
   "low": 0.9,
   "high": 0.98,
   "hunch": "You might assume a banana floats high and dry.",
   "axioms": [
    "Banana flesh is dense and starchy with little air.",
    "Its peel traps only a thin layer of air."
   ],
   "why": "The starchy flesh nearly matches plain water's density, so the banana floats but rides low; the hunch of floating high fails.",
   "depends_on": ""
  }
 ],
 "try_it": "Fill a bowl with plain water and gently drop in the apple, orange, lime and banana to compare how each floats.",
 "closing": "2 bob, 1 ride low, 2 sink, 1 depend."
}

The desk drawer - an item to keep dry

Input:

{
 "task": "dunk",
 "water": "plain",
 "count": "7",
 "items": "1. wine cork\n2. pencil\n3. house key\n4. birthday candle\n5. crayon\n6. rubber band\n7. AA battery",
 "known": "1. 0.12-0.24\n2. 0.50-0.70\n3. 8.40-8.70\n4. 0.88-0.93\n5. 1.02-1.10\n7. 2.70-3.10"
}

Reply:

{
 "task": "dunk",
 "status": "ok",
 "reason": "",
 "items": [
  {
   "n": 1,
   "name": "wine cork",
   "verdict": "bobs",
   "low": 0.12,
   "high": 0.24,
   "hunch": "You might assume a cork always floats easily in water.",
   "axioms": [
    "Cork is a lightweight natural material full of air cells.",
    "It holds no metal or dense filling inside it."
   ],
   "why": "The many trapped air cells make cork far lighter than plain water, so it floats well; the floating hunch holds true.",
   "depends_on": ""
  },
  {
   "n": 2,
   "name": "pencil",
   "verdict": "bobs",
   "low": 0.5,
   "high": 0.7,
   "hunch": "You might assume a wooden pencil floats like a stick.",
   "axioms": [
    "Pencil wood is light and holds small air pockets.",
    "Its thin graphite core adds little extra weight."
   ],
   "why": "The wood's air pockets keep the pencil lighter than plain water despite the core, so it floats; the stick hunch holds.",
   "depends_on": ""
  },
  {
   "n": 3,
   "name": "house key",
   "verdict": "sinks",
   "low": 8.4,
   "high": 8.7,
   "hunch": "You might assume a small key is too light to sink.",
   "axioms": [
    "House keys are solid metal with no hollow space.",
    "Metal is far denser than water with no trapped air."
   ],
   "why": "As solid dense metal with no trapped air, the key is far denser than plain water, so it sinks; the small-and-light hunch fails.",
   "depends_on": ""
  },
  {
   "n": 4,
   "name": "birthday candle",
   "verdict": "rides-low",
   "low": 0.88,
   "high": 0.93,
   "hunch": "You might assume a wax candle floats high like a cork.",
   "axioms": [
    "Candle wax is light but holds very little trapped air.",
    "Its thin wick adds only a tiny bit of weight."
   ],
   "why": "The wax is only slightly lighter than plain water, so the candle floats but rides low; the cork-like floating hunch fails.",
   "depends_on": ""
  },
  {
   "n": 5,
   "name": "crayon",
   "verdict": "sinks",
   "low": 1.02,
   "high": 1.1,
   "hunch": "You might assume a wax crayon floats like a candle.",
   "axioms": [
    "Crayon wax is mixed with dense pigment and filler.",
    "It holds no air pockets inside its solid stick."
   ],
   "why": "The pigment-filled wax makes the crayon slightly denser than plain water with no air to help, so it sinks; the candle comparison fails.",
   "depends_on": ""
  },
  {
   "n": 6,
   "name": "rubber band",
   "verdict": "sinks",
   "low": 1.1,
   "high": 1.2,
   "hunch": "You might assume a stretchy rubber band floats easily.",
   "axioms": [
    "Rubber bands are solid rubber with no trapped air.",
    "Vulcanized rubber is denser than plain water."
   ],
   "why": "As solid rubber with no air pocket, the band is denser than plain water, so it sinks; the stretchy-and-light hunch fails.",
   "depends_on": ""
  },
  {
   "n": 7,
   "name": "AA battery",
   "verdict": "sinks",
   "low": 2.7,
   "high": 3.1,
   "hunch": "You might assume a small battery is too light to sink.",
   "axioms": [
    "An AA battery is a sealed metal and chemical cylinder.",
    "It holds no trapped air inside its solid casing."
   ],
   "why": "As a dense sealed metal cylinder with no trapped air, the battery is far denser than plain water, so it sinks; the small-and-light hunch fails.",
   "depends_on": ""
  }
 ],
 "try_it": "Fill a bowl with plain water and drop in the cork, pencil and candle to watch them float, but keep the battery dry.",
 "closing": "2 bob, 1 ride low, 4 sink."
}

Strong salt water - salty

Input:

{
 "task": "dunk",
 "water": "salty",
 "count": "6",
 "items": "1. fresh egg\n2. potato\n3. golf ball\n4. grapes\n5. ice cube\n6. old egg",
 "known": "1. 1.06-1.10\n2. 1.07-1.10\n3. 1.13-1.15\n4. 1.05-1.15\n5. 0.91-0.92\n6. 0.92-0.99"
}

Reply:

{
 "task": "dunk",
 "status": "ok",
 "reason": "",
 "items": [
  {
   "n": 1,
   "name": "fresh egg",
   "verdict": "rides-low",
   "low": 1.06,
   "high": 1.1,
   "hunch": "You might assume a fresh egg always sinks in water.",
   "axioms": [
    "A fresh egg has only a tiny air pocket inside.",
    "Its shell and contents are mostly dense liquid and mineral."
   ],
   "why": "With only a small air pocket, the egg is just a bit lighter than salty water, so it floats but rides low; the always-sinks hunch fails.",
   "depends_on": ""
  },
  {
   "n": 2,
   "name": "potato",
   "verdict": "rides-low",
   "low": 1.07,
   "high": 1.1,
   "hunch": "You might assume a solid potato always sinks right away.",
   "axioms": [
    "A potato is dense starchy flesh with very little air.",
    "It holds no hollow space or trapped air pockets."
   ],
   "why": "The starchy flesh is only slightly lighter than salty water, so the potato floats but rides low; the always-sinks hunch mostly fails.",
   "depends_on": ""
  },
  {
   "n": 3,
   "name": "golf ball",
   "verdict": "sinks",
   "low": 1.13,
   "high": 1.15,
   "hunch": "You might assume a golf ball is light enough to float.",
   "axioms": [
    "A golf ball has a dense rubber or resin core.",
    "Its core holds no significant trapped air pocket."
   ],
   "why": "The dense solid core makes the golf ball heavier than salty water, so it sinks; the light-and-floaty hunch fails.",
   "depends_on": ""
  },
  {
   "n": 4,
   "name": "grapes",
   "verdict": "depends",
   "low": 1.05,
   "high": 1.15,
   "hunch": "You might assume grapes always behave the same in water.",
   "axioms": [
    "Grape skin is thin and traps very little air.",
    "Grape flesh is dense sugary juice close to water."
   ],
   "why": "The sugary juice sits so close to salty water's density that grapes could float or sink; the always-the-same hunch is unresolved.",
   "depends_on": "More juice sinks it; more trapped air floats it."
  },
  {
   "n": 5,
   "name": "ice cube",
   "verdict": "bobs",
   "low": 0.91,
   "high": 0.92,
   "hunch": "You might assume ice always sinks like solid water would.",
   "axioms": [
    "Ice is frozen water with a slightly expanded crystal structure.",
    "That expansion makes it lighter than the same liquid water."
   ],
   "why": "Because ice is less dense than liquid, it floats well above salty water's density; the sinks-like-solid hunch fails.",
   "depends_on": ""
  },
  {
   "n": 6,
   "name": "old egg",
   "verdict": "rides-low",
   "low": 0.92,
   "high": 0.99,
   "hunch": "You might assume an egg's age does not change whether it sinks.",
   "axioms": [
    "An aged egg holds a bigger air pocket than when laid.",
    "Its shell and insides are otherwise dense liquid and mineral."
   ],
   "why": "The larger air pocket now makes the egg only slightly lighter than salty water, so it floats but rides low; the age-does-not-matter hunch fails.",
   "depends_on": ""
  }
 ],
 "try_it": "Mix salt into a bowl of water and lower in the fresh egg, potato, grapes, ice cube and old egg to compare.",
 "closing": "1 bob, 3 ride low, 1 sink, 1 depend."
}

A pocketful - nothing in the page's table

Input:

{
 "task": "dunk",
 "water": "plain",
 "count": "5",
 "items": "1. plastic bottle cap\n2. paper clip\n3. spoon\n4. lip balm tube\n5. hair tie",
 "known": "none"
}

Reply:

{
 "task": "dunk",
 "status": "ok",
 "reason": "",
 "items": [
  {
   "n": 1,
   "name": "plastic bottle cap",
   "verdict": "rides-low",
   "low": 0.9,
   "high": 0.95,
   "hunch": "You might assume a small plastic cap floats high and dry.",
   "axioms": [
    "Plastic bottle caps are solid with no trapped air inside.",
    "Common cap plastics sit just under water's own density."
   ],
   "why": "With no trapped air, the solid plastic is only slightly lighter than plain water, so it floats but rides low; the floats-high hunch fails.",
   "depends_on": ""
  },
  {
   "n": 2,
   "name": "paper clip",
   "verdict": "sinks",
   "low": 7.8,
   "high": 8.0,
   "hunch": "You might assume a tiny paper clip is too light to sink.",
   "axioms": [
    "A paper clip is bent from solid steel wire.",
    "Steel wire holds no trapped air inside it."
   ],
   "why": "As solid steel with no trapped air, the clip is far denser than plain water, so it sinks; the tiny-and-light hunch fails.",
   "depends_on": ""
  },
  {
   "n": 3,
   "name": "spoon",
   "verdict": "depends",
   "low": 0.9,
   "high": 7.9,
   "hunch": "You might assume any spoon behaves the same in water.",
   "axioms": [
    "A metal spoon is solid metal with no trapped air.",
    "A plastic spoon is solid plastic lighter than water."
   ],
   "why": "Whether it sinks or floats turns on its material, since metal is far denser than plain water and plastic is lighter; the one-spoon hunch is unresolved.",
   "depends_on": "Metal spoon sinks it; plastic spoon floats it."
  },
  {
   "n": 4,
   "name": "lip balm tube",
   "verdict": "rides-low",
   "low": 0.85,
   "high": 0.95,
   "hunch": "You might assume a small tube always floats out of the way.",
   "axioms": [
    "A lip balm tube holds mostly wax and light plastic.",
    "Its casing traps little to no extra air."
   ],
   "why": "The wax and plastic together sit just below plain water's density, so the tube floats but rides low; the floats-easily hunch mostly fails.",
   "depends_on": ""
  },
  {
   "n": 5,
   "name": "hair tie",
   "verdict": "sinks",
   "low": 1.05,
   "high": 1.15,
   "hunch": "You might assume a stretchy hair tie is light enough to float.",
   "axioms": [
    "A hair tie is a solid loop of elastic material.",
    "Its stretchy core traps no meaningful air pocket."
   ],
   "why": "As solid elastic with no trapped air, the hair tie is a bit denser than plain water, so it sinks; the stretchy-and-light hunch fails.",
   "depends_on": ""
  }
 ],
 "try_it": "Fill a bowl with plain water and drop in the bottle cap, spoon and lip balm tube to compare how each floats.",
 "closing": "2 ride low, 2 sink, 1 depend."
}

Bath-time line-up - declined

Input:

{
 "task": "dunk",
 "water": "plain",
 "count": "3",
 "items": "1. rubber duck\n2. toy sailboat\n3. goldfish",
 "known": "1. 0.10-0.30"
}

Reply:

{
 "task": "dunk",
 "status": "declined",
 "reason": "This page only dunks everyday objects; living things and hazardous items never go in the tub.",
 "items": [],
 "try_it": "",
 "closing": ""
}

Build the input yourself

Load the engine in node (global.window = global; require("./tub.js"); require("./tub-svg.js")), then:

const K = TubKit, D = TubDraw;
const built = K.buildInput({ water: "plain", items: [{ name: "apple", guess: "bob" }, { name: "orange, peeled", guess: "bob" }, { name: "hair tie", guess: "" }] });
built.input.known;                       // "1. 0.73-0.85\n2. 1.03-1.10" - the page's table
built.parsed.items.map(x => x.page);     // ["bobs", "sinks", ""] - the page's own rule
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 reply
// K.score(built.parsed, reply)          // your calls: { called, right, open }
// D.svg({ water: "plain", items: reply.items })   // the tub drawing
// K.md(built, reply)                    // the dunked line-up 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://sink-or-bob.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":"sink-or-bob"}' | 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": "dunk", "water": "plain", "count": "6", "items": "1. apple
2. ..."} - see the field table
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 sink-or-bob:<task>:<inputHash>:a<attempt> (the one task is dunk), 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="sink-or-bob:dunk:$(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: sink-or-bob:dunk:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json