← Two Tellings
API

Drive Two Tellings from your own code

The one metered task, compare-tellings, is available over HTTP. It takes the name of one Ramayana episode and two tellings of it, two to eight short lines each, and returns a note on every moment: told alike, told differently, or found in one telling only, plus a headline, the shared core and a closing line.

The division of labour first: the page decides; the model describes. The page matches the lines into moments (your own picks first, then shared words), counts the shared moments and picks the verdict, and sends those as facts. The model never changes a match or the verdict, and never ranks a telling.

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": "..." } }

The token is minted for this app (the guest call takes {"slug":"two-tellings"}), 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, moments, facts) 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 reasons are metered and need a personal sign-in.
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 input and the verdicts

Every run sends task: compare-tellings (the one task is compare-tellings); a missing or different one is still answered as compare-tellings. All fields are strings.

FieldRequiredWhat it holds
taskyesAlways compare-tellings.
episodeyesThe episode both tellings tell (up to 60 characters).
telling_ayesFirst line: the first telling's name (where it came from). Then one numbered line per moment: A1. ..., A2. ... (2 to 8 lines, up to 140 characters each).
telling_byesThe same for the second telling, with lines B1. ..., B2. ....
factsyesThe page's moments in order (M1: A1 + B1, M2: A2 only, M3: B2 only ...), the shared count, the verdict id and the names each telling uses.
retry_notenoOnly on a reformat retry: why the previous reply was unusable.

The verdict, first match wins: no shared moment, or under a quarter of the moments shared, is distant-kin (distant kin); 60% or more shared is close-kin (close kin); anything else is cousins.

The reply

One JSON object, no prose around it:

{"task": "compare-tellings", "headline": "...", "moments": [{"id": "M1", "how": "alike", "note": "..."}], "core": "...", "closing": "..."}
FieldWhat it holds
headlineAt most 24 words on how the two tellings sit side by side, agreeing with the verdict.
moments[]One per moment in facts, same order and ids: how is alike or differs for a shared moment, only-a or only-b otherwise; note is at most 40 words from the tellings' own words.
coreOne sentence on what the shared moments have in common.
closingOne sentence inviting you to notice one difference the next time the episode is told.

The page then checks the reply (TellKit.check) - every moment present and filed the way the page matched it, names neither telling uses, ranking words, outside sources, he/she for a figure, time or weather words, and contact details - and lists anything it finds under "The page's check".

Worked examples - all three verdicts

These are the 3 free samples on the page: the exact input the page sends and the reply that replays for free. Together they cover all three verdicts.

The golden deer - verdict: close kin

Input:

{
 "task": "compare-tellings",
 "episode": "The golden deer",
 "telling_a": "How my grandmother told it\nA1. Sita sees a golden deer near the hut and asks Rama to catch it.\nA2. Rama chases the golden deer deep into the forest.\nA3. The deer cries out in Rama's voice, calling for Lakshmana.\nA4. Lakshmana draws a line around the hut and asks Sita not to cross it.\nA5. Ravana comes to the hut as a holy man begging for food, and Sita steps over the line.",
 "telling_b": "A picture book we had\nB1. A deer with golden skin and silver spots plays near the hut, and Sita wants it.\nB2. Rama follows the deer into the forest and finally shoots an arrow.\nB3. The dying deer shouts for help in Rama's voice.\nB4. Lakshmana leaves the hut to find Rama after Sita insists.\nB5. Ravana arrives at the hut dressed as a wandering monk and carries Sita away.",
 "facts": "M1: A1 + B1\nM2: A2 + B2\nM3: A3 + B3\nM4: A4 + B4\nM5: A5 + B5\nshared moments: 5 of 5 (only in the first telling 0, only in the second telling 0)\nverdict: close-kin\nnames in both tellings: Sita, Rama, Lakshmana, Ravana; only in the first telling: none; only in the second telling: none"
}

Reply:

{
 "task": "compare-tellings",
 "headline": "The two tellings share all five moments of the golden deer, making them close kin.",
 "moments": [
  {
   "id": "M1",
   "how": "differs",
   "note": "In the first telling, Sita sees the golden deer and asks Rama to catch it; in a picture book we had, a deer with golden skin and silver spots plays near the hut and Sita wants it."
  },
  {
   "id": "M2",
   "how": "differs",
   "note": "In the first telling, Rama chases the golden deer deep into the forest; in a picture book we had, Rama follows the deer into the forest and finally shoots an arrow."
  },
  {
   "id": "M3",
   "how": "differs",
   "note": "In the first telling, the deer cries out in Rama's voice, calling for Lakshmana; in a picture book we had, the dying deer shouts for help in Rama's voice."
  },
  {
   "id": "M4",
   "how": "differs",
   "note": "In the first telling, Lakshmana draws a line around the hut and asks Sita not to cross it; in a picture book we had, Lakshmana leaves the hut to find Rama after Sita insists."
  },
  {
   "id": "M5",
   "how": "differs",
   "note": "In the first telling, Ravana comes to the hut as a holy man begging for food and Sita steps over the line; in a picture book we had, Ravana arrives dressed as a wandering monk and carries Sita away."
  }
 ],
 "core": "Sita, Rama, Lakshmana and Ravana are named in both tellings, along with the deer and the hut.",
 "closing": "Notice, next time you hear it told, that Lakshmana draws a line in one telling, and leaves to find Rama in the other."
}

The bridge to Lanka - verdict: cousins

Input:

{
 "task": "compare-tellings",
 "episode": "The bridge to Lanka",
 "telling_a": "Our school play\nA1. Rama's army reaches the sea and cannot cross to Lanka.\nA2. Rama prays to the ocean, and the ocean stays silent.\nA3. Nala and Nila lead the monkeys in building a bridge of stones.\nA4. Each stone floats because Rama's name is written on it.\nA5. The army crosses the bridge to Lanka.",
 "telling_b": "A shadow-puppet show I saw\nB1. The army stops at the shore, and the monkeys stare at the waves.\nB2. A small squirrel rolls in the sand and shakes the sand onto the bridge to help.\nB3. Rama strokes the squirrel's back, leaving three stripes.\nB4. The monkeys carry rocks and trees to the water and build the bridge.\nB5. The whole army marches across the bridge to Lanka.",
 "facts": "M1: A1 + B1\nM2: B2 only\nM3: B3 only\nM4: A2 only\nM5: A3 + B4\nM6: A4 only\nM7: A5 + B5\nshared moments: 3 of 7 (only in the first telling 2, only in the second telling 2)\nverdict: cousins\nnames in both tellings: Rama, Lanka; only in the first telling: Nala, Nila; only in the second telling: none"
}

Reply:

{
 "task": "compare-tellings",
 "headline": "These two tellings are cousins, sharing three of their seven moments while each adds its own details to the bridge to Lanka.",
 "moments": [
  {
   "id": "M1",
   "how": "alike",
   "note": "In our school play, Rama's army reaches the sea and cannot cross to Lanka; in a shadow-puppet show I saw, the army stops at the shore while the monkeys stare at the waves."
  },
  {
   "id": "M2",
   "how": "only-b",
   "note": "In a shadow-puppet show I saw, a small squirrel rolls in the sand and shakes the sand onto the bridge to help."
  },
  {
   "id": "M3",
   "how": "only-b",
   "note": "In a shadow-puppet show I saw, Rama strokes the squirrel's back, leaving three stripes."
  },
  {
   "id": "M4",
   "how": "only-a",
   "note": "In our school play, Rama prays to the ocean, and the ocean stays silent."
  },
  {
   "id": "M5",
   "how": "differs",
   "note": "In our school play, Nala and Nila lead the monkeys in building a bridge of stones; in a shadow-puppet show I saw, the monkeys carry rocks and trees to the water and build the bridge."
  },
  {
   "id": "M6",
   "how": "only-a",
   "note": "In our school play, each stone floats because Rama's name is written on it."
  },
  {
   "id": "M7",
   "how": "alike",
   "note": "In our school play, the army crosses the bridge to Lanka; in a shadow-puppet show I saw, the whole army marches across the bridge to Lanka."
  }
 ],
 "core": "The shared moments bring together the army, the monkeys, and a bridge to Lanka.",
 "closing": "Next time you hear the episode, notice the bridge of stones set against the rocks and trees the monkeys carry to build it."
}

The ring in Lanka - verdict: distant kin

Input:

{
 "task": "compare-tellings",
 "episode": "Hanuman and the ring in Lanka",
 "telling_a": "My aunt's bedtime version\nA1. Hanuman leaps across the sea from the top of a mountain.\nA2. A sea demoness tries to swallow Hanuman in mid-air.\nA3. Hanuman shrinks to the size of a cat to slip into Lanka unseen.\nA4. Hanuman finds Sita in a grove of ashoka trees and gives Sita Rama's ring.",
 "telling_b": "A dance performance\nB1. Hanuman bows before Sita and hands over Rama's ring.\nB2. Ravana's guards catch Hanuman in the garden.\nB3. The guards set Hanuman's tail on fire.\nB4. Hanuman leaps from roof to roof and sets Lanka ablaze.",
 "facts": "M1: A1 only\nM2: A2 only\nM3: A3 only\nM4: A4 + B1\nM5: B2 only\nM6: B3 only\nM7: B4 only\nshared moments: 1 of 7 (only in the first telling 3, only in the second telling 3)\nverdict: distant-kin\nnames in both tellings: Hanuman, Lanka, Sita, Rama; only in the first telling: none; only in the second telling: Ravana"
}

Reply:

{
 "task": "compare-tellings",
 "headline": "My aunt's bedtime version and a dance performance share only one point, making the two distant kin overall.",
 "moments": [
  {
   "id": "M1",
   "how": "only-a",
   "note": "In my aunt's bedtime version, Hanuman leaps across the sea from the top of a mountain."
  },
  {
   "id": "M2",
   "how": "only-a",
   "note": "In my aunt's bedtime version, a sea demoness tries to swallow Hanuman in mid-air."
  },
  {
   "id": "M3",
   "how": "only-a",
   "note": "In my aunt's bedtime version, Hanuman shrinks to the size of a cat to slip into Lanka unseen."
  },
  {
   "id": "M4",
   "how": "alike",
   "note": "Both tellings have Hanuman giving Rama's ring to Sita, as my aunt's bedtime version has Hanuman give the ring and a dance performance has Hanuman bow before Sita and hand over the ring."
  },
  {
   "id": "M5",
   "how": "only-b",
   "note": "In a dance performance, Ravana's guards catch Hanuman in the garden."
  },
  {
   "id": "M6",
   "how": "only-b",
   "note": "In a dance performance, the guards set Hanuman's tail on fire."
  },
  {
   "id": "M7",
   "how": "only-b",
   "note": "In a dance performance, Hanuman leaps from roof to roof and sets Lanka ablaze."
  }
 ],
 "core": "Hanuman, Sita and Rama's ring are named in the lines both tellings share.",
 "closing": "Next time the episode is told, notice that a dance performance alone has Ravana's guards catch Hanuman in the garden."
}

Build the input yourself

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

const K = TellKit;
const form = K.form({ episode: "The golden deer",
  a: { label: "How my grandmother told it", beats: [{ text: "Sita sees a golden deer near the hut." }, { text: "Rama chases the deer into the forest." }] },
  b: { label: "A picture book we had", beats: [{ text: "A golden deer plays near the hut.", match: "auto" }, { text: "Rama follows the deer into the forest.", match: "2" }] } });
const built = K.buildInput(form);       // built.facts holds the moments, the shared count and the verdict
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.md(built, reply, K.title(form))    // the comparison as Markdown

A second-telling line's match is auto (shared words), none (only in this telling) or a first-telling line number as a string.

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://two-tellings.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":"two-tellings"}' | 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": "compare-tellings", "moments": "1. The golden deer | what happened: ... | ...", "facts": "...", ...}
# - 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 two-tellings:<task>:<inputHash>:a<attempt> (the one task is compare-tellings), 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="two-tellings:compare-tellings:$(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: two-tellings:compare-tellings:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json