← Tonguewright / API
Tokens

Drive Tonguewright from your own code

Everything the workbench asks the model is available over HTTP. A natural use is a language kept as data - a worldbuilding wiki's conlang, a course's exercise languages, a game's naming tables - where a script sends the sound system and a lexicon sample and asks for a grammar section, example sentences, etymologies, or new forms after a rule change.

One thing to be clear about first: the model is checked, not trusted. In the workbench every form the model writes is re-validated against the inventory, the syllable template and the constraints; spellings are recomputed from the IPA; glosses are checked for one-piece-per-morpheme alignment. Over the API you get the reply as JSON and the checks are yours to run - the workbench's own logic ships as plain scripts you can load in node; see the end of the examples.

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":"tonguewright"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object: never wrap it as {"input": {…}}; /estimate warns unknown field 'input'.

Errors:

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: the model lanes 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

One system prompt serves four tasks. task picks the contract; a missing or unknown task is answered as sketch, and the reply's task names the lane it answered.

taskWhat it doesrequest (a JSON string)
sketchOne section of a reference grammar: prose, paradigm tables, interlinear examples, coinages.{"section":"pronouns","section_label":"Pronouns","ask":"...","written_sections":[]} - sections: word_order, nouns, verbs, pronouns, numerals, paradigm
glossInterlinear glossed sentences (morphemes, Leipzig glosses, free translation).{"count":4,"topic":"...","sentences":["English to translate"],"grammar_known":[]}
etymEtymologies for chosen entries, and comments on the ordered sound changes.{"entries":[["hana","canoe","noun"]],"daughter":{"name":"Late Aru","rules":["k > ʔ"],"reflexes":[["hana","hana"]]},"ask":"..."}
recoinReplacements for forms that broke when the sound system changed.{"forms":[{"form":"ʂuma","gloss":"read","where":"lexicon","broken":"/ʂ/ is not in the inventory"}],"change":"..."}

Every run body is a JSON object of string fields (the declared input schema is scalar, so object-valued fields are JSON-encoded strings; task, language and request are required, and /estimate warns when one is missing).

FieldWhat it holds
task"sketch", "gloss", "etym" or "recoin".
languageJSON string: {"name","feel","culture","purpose","consonants":[...],"vowels":[...],"classes":{"C":"p t k ...","N":"m n"},"template":"(C)V(N)","constraints":["R1: never # ŋ (# = word edge)"],"romanisation":{"ŋ":"ng"},"lexicon":[["ipa","gloss","pos"]],"lexicon_total":28}. The page sends at most 60 lexicon rows - pronouns, numerals, affixes and function words first - and says so in clipped.
requestJSON string, per task (table above).
clippedOptional: what was left out of the input.
retry_noteOptional: sent once when a previous reply was not valid JSON.

The reply text is one JSON object. Every lane carries task, coinages ([{"ipa","gloss","pos","note"}], affixes with their hyphen) and notes; forms in prose and tables are written between slashes.

// sketch
{ "task": "sketch", "section": "pronouns", "title": "...", "summary": "...", "prose": ["... /mo/ ..."],
  "tables": [{ "caption": "...", "columns": ["", "sg", "pl"], "rows": [["1", "/mo/", "/mu/"]] }],
  "examples": [{ "morphemes": ["mo=hana"], "glosses": ["1SG=canoe"], "translation": "my canoe" }], "coinages": [...], "notes": [...] }
// gloss
{ "task": "gloss", "sentences": [{ "morphemes": ["..."], "glosses": ["..."], "translation": "...", "note": "..." }], "coinages": [...], "notes": [...] }
// etym
{ "task": "etym", "entries": [{ "word": "hana", "kind": "compound|derivation|root|shift|borrowing", "parts": ["ha", "na"], "parts_gloss": ["...", "..."], "story": "..." }],
  "rule_notes": [{ "rule": 1, "comment": "..." }], "coinages": [...], "notes": [...] }
// recoin
{ "task": "recoin", "recoined": [{ "old": "ʂuma", "new": "suma", "gloss": "read", "reason": "..." }], "coinages": [], "notes": [...] }

Worked examples

These are the bundled examples on the page: the exact run body the page builds and a reply produced against the shipped prompt (the page replays the same replies for free). Every form in them passes the language's own rules.

sketch: Aru, an open-syllable island language - the pronoun section

Input:

{
 "task": "sketch",
 "language": "(a JSON string - decoded below)",
 "request": "(a JSON string - decoded below)"
}

language, decoded:

{
 "name": "Aru",
 "feel": "open syllables, soft and vowel-heavy, a sea language",
 "culture": "island fishers who name everything after tides and winds",
 "purpose": "names and short songs in a novel",
 "consonants": [
  "p",
  "t",
  "k",
  "m",
  "n",
  "ŋ",
  "h",
  "w",
  "l",
  "ʔ"
 ],
 "vowels": [
  "a",
  "e",
  "i",
  "o",
  "u"
 ],
 "classes": {
  "C": "p t k m n ŋ h w l ʔ",
  "V": "a e i o u"
 },
 "template": "(C)V",
 "constraints": [
  "R1: words have 1 to 4 syllables - short words",
  "R2: never # ŋ (# = word edge) - no word starts with ng",
  "R3: no segment may be doubled (no aa, no tt)"
 ],
 "romanisation": {
  "p": "p",
  "t": "t",
  "k": "k",
  "m": "m",
  "n": "n",
  "ŋ": "ng",
  "h": "h",
  "w": "w",
  "l": "l",
  "ʔ": "'",
  "a": "a",
  "e": "e",
  "i": "i",
  "o": "o",
  "u": "u"
 },
 "lexicon": [
  [
   "tai",
   "sea",
   "noun"
  ],
  [
   "hana",
   "canoe",
   "noun"
  ],
  [
   "ʔulu",
   "head; leader",
   "noun"
  ],
  [
   "mana",
   "wind",
   "noun"
  ],
  [
   "kai",
   "eat",
   "verb"
  ],
  [
   "lani",
   "sky",
   "noun"
  ],
  [
   "wae",
   "go",
   "verb"
  ],
  [
   "aŋa",
   "fish",
   "noun"
  ],
  [
   "pou",
   "post; pillar",
   "noun"
  ],
  [
   "tama",
   "child",
   "noun"
  ],
  [
   "ʔoa",
   "net",
   "noun"
  ],
  [
   "kelo",
   "see",
   "verb"
  ],
  [
   "nui",
   "big",
   "adj"
  ],
  [
   "iki",
   "small",
   "adj"
  ],
  [
   "hou",
   "new",
   "adj"
  ],
  [
   "wai",
   "fresh water",
   "noun"
  ],
  [
   "pune",
   "spring (season)",
   "noun"
  ],
  [
   "tolu",
   "three",
   "num"
  ],
  [
   "lua",
   "two",
   "num"
  ],
  [
   "tahi",
   "one",
   "num"
  ],
  [
   "maŋu",
   "bird",
   "noun"
  ],
  [
   "hiku",
   "tail",
   "noun"
  ],
  [
   "kamu",
   "give",
   "verb"
  ],
  [
   "noho",
   "sit; stay",
   "verb"
  ],
  [
   "ʔoko",
   "rope",
   "noun"
  ],
  [
   "u",
   "and",
   "conj"
  ],
  [
   "i",
   "at, to",
   "adp"
  ],
  [
   "kona",
   "south",
   "noun"
  ]
 ],
 "lexicon_total": 28
}

request, decoded:

{
 "section": "pronouns",
 "section_label": "Pronouns",
 "ask": "Distinguish inclusive and exclusive we. Keep pronouns to one or two syllables.",
 "written_sections": []
}

Reply:

{
 "task": "sketch",
 "section": "pronouns",
 "title": "Pronouns",
 "summary": "Aru pronouns are monosyllabic (C)V words whose final vowel marks number (singular -o, plural -u), with a suppletive inclusive /ta/ that has no singular counterpart.",
 "prose": [
  "Aru has seven independent pronouns, each a single (C)V syllable. The onset consonant marks person - m for first-person exclusive, t for first-person inclusive, h for second person, l for third person - and the vowel marks number, o for singular and u for plural. The pattern is regular except for the inclusive, which has no singular: /mo/ 'I', /mu/ 'we (excl.)', /ho/ 'you (sg.)', /hu/ 'you (pl.)', /lo/ 'he, she, or it', /lu/ 'they', and the suppletive /ta/ 'we (incl.)'.",
  "Pronouns stand as full subject or object words; Aru verbs carry no person or number marking of their own, so the pronoun alone signals who acts. /mo/ /kai/ means 'I eat' and /hu/ /kamu/ /aŋa/ means 'you (pl.) give the fish', with the pronoun simply standing where the subject belongs and no change to the verb /kai/ 'eat' or /kamu/ 'give'.",
  "The same seven forms double as possessive proclitics, bound to the following noun with the clitic boundary =: /mo=hana/ 'my canoe', /ta=hana/ 'our (incl.) canoe', /mu=hana/ 'our (excl.) canoe'. Each piece is still checked on its own against the template, since a clitic does not fuse into a single phonological word with its host the way a suffix does.",
  "The split between inclusive and exclusive matters most when a catch is divided: /ta/ names everyone the speaker is currently addressing, as when a steersman tells the whole crew what they will do together with the net, while /mu/ sets the speaker's own canoe or household apart from whoever is being addressed, as when describing what will be brought home to kin left on shore."
 ],
 "tables": [
  {
   "caption": "Independent pronouns",
   "columns": [
    "Person",
    "Singular",
    "Plural"
   ],
   "rows": [
    [
     "1 incl.",
     "—",
     "/ta/"
    ],
    [
     "1 excl.",
     "/mo/",
     "/mu/"
    ],
    [
     "2",
     "/ho/",
     "/hu/"
    ],
    [
     "3",
     "/lo/",
     "/lu/"
    ]
   ]
  }
 ],
 "examples": [
  {
   "morphemes": [
    "mo",
    "kai"
   ],
   "glosses": [
    "1SG",
    "eat"
   ],
   "translation": "I eat."
  },
  {
   "morphemes": [
    "ta",
    "kai",
    "mu",
    "noho"
   ],
   "glosses": [
    "1PL.INCL",
    "eat",
    "1PL.EXCL",
    "stay"
   ],
   "translation": "We (you and I) eat; we (not you) stay."
  },
  {
   "morphemes": [
    "mo=hana",
    "nui"
   ],
   "glosses": [
    "1SG=canoe",
    "big"
   ],
   "translation": "My canoe is big."
  },
  {
   "morphemes": [
    "hu",
    "kamu",
    "aŋa"
   ],
   "glosses": [
    "2PL",
    "give",
    "fish"
   ],
   "translation": "You (all) give the fish."
  }
 ],
 "coinages": [
  {
   "ipa": "mo",
   "gloss": "1SG",
   "pos": "pron",
   "note": "(C)V, 1 syllable (R1 ok); onset m = 1st exclusive, vowel o = singular; no doubled segment (R3); does not start with ŋ (R2)."
  },
  {
   "ipa": "mu",
   "gloss": "1PL.EXCL",
   "pos": "pron",
   "note": "(C)V, 1 syllable; m = 1st exclusive, u = plural, pairs with /mo/."
  },
  {
   "ipa": "ta",
   "gloss": "1PL.INCL",
   "pos": "pron",
   "note": "(C)V, 1 syllable; suppletive, no singular slot since inclusive 'we' needs speaker+addressee."
  },
  {
   "ipa": "ho",
   "gloss": "2SG",
   "pos": "pron",
   "note": "(C)V, 1 syllable; h = 2nd person, o = singular."
  },
  {
   "ipa": "hu",
   "gloss": "2PL",
   "pos": "pron",
   "note": "(C)V, 1 syllable; h = 2nd person, u = plural, pairs with /ho/."
  },
  {
   "ipa": "lo",
   "gloss": "3SG",
   "pos": "pron",
   "note": "(C)V, 1 syllable; l = 3rd person, o = singular."
  },
  {
   "ipa": "lu",
   "gloss": "3PL",
   "pos": "pron",
   "note": "(C)V, 1 syllable; l = 3rd person, u = plural, pairs with /lo/."
  }
 ],
 "notes": [
  "Pronoun set uses a transparent onset-for-person and vowel-for-number pattern (o = SG, u = PL), with /ta/ 'we (incl.)' as the one suppletive, singular-less form.",
  "Same 7 forms serve as possessive proclitics on a following noun, e.g. /mo=hana/ 'my canoe'.",
  "Only singular and plural are marked, not dual, keeping every pronoun to one syllable as asked."
 ]
}

gloss: Kethri, a clustered highland language - four sentences about a lost goat

Input:

{
 "task": "gloss",
 "language": "(a JSON string - decoded below)",
 "request": "(a JSON string - decoded below)"
}

language, decoded:

{
 "name": "Kethri",
 "feel": "harsh and clustered, back of the mouth, few vowels",
 "culture": "highland herders, terse and ceremonial",
 "purpose": "a game's mountain clans",
 "consonants": [
  "p",
  "t",
  "k",
  "q",
  "b",
  "d",
  "ɡ",
  "t͡s",
  "t͡ʃ",
  "s",
  "ʃ",
  "x",
  "ʁ",
  "m",
  "n",
  "r",
  "l",
  "j"
 ],
 "vowels": [
  "a",
  "e",
  "i",
  "o",
  "u",
  "ə"
 ],
 "classes": {
  "C": "p t k q b d ɡ t͡s t͡ʃ s ʃ x ʁ m n r l j",
  "D": "b d ɡ",
  "F": "s ʃ x ʁ",
  "K": "p t k q",
  "L": "l",
  "N": "m n",
  "R": "r",
  "V": "a e i o u ə"
 },
 "template": "(C)([RL])V(C)",
 "constraints": [
  "R1: a syllable coda may only be N or R or L or F - codas are sonorants or fricatives",
  "R2: never K K (# = word edge)",
  "R3: never D # (# = word edge) - no voiced stop ends a word",
  "R4: no segment may be doubled (no aa, no tt)"
 ],
 "romanisation": {
  "p": "p",
  "t": "t",
  "k": "k",
  "q": "q",
  "b": "b",
  "d": "d",
  "ɡ": "g",
  "t͡s": "ts",
  "t͡ʃ": "ch",
  "s": "s",
  "ʃ": "sh",
  "x": "kh",
  "ʁ": "rh",
  "m": "m",
  "n": "n",
  "r": "r",
  "l": "l",
  "j": "y",
  "a": "a",
  "e": "e",
  "i": "i",
  "o": "o",
  "u": "u",
  "ə": "ë"
 },
 "lexicon": [
  [
   "qral",
   "stone",
   "noun"
  ],
  [
   "t͡ʃəx",
   "goat",
   "noun"
  ],
  [
   "brəs",
   "fire",
   "noun"
  ],
  [
   "klan",
   "kin",
   "noun"
  ],
  [
   "ʁor",
   "hill",
   "noun"
  ],
  [
   "dəl",
   "go",
   "verb"
  ],
  [
   "t͡sər",
   "strike",
   "verb"
  ],
  [
   "xem",
   "eat",
   "verb"
  ],
  [
   "ʃal",
   "salt",
   "noun"
  ],
  [
   "mən",
   "I",
   "pron"
  ],
  [
   "tən",
   "you (sg)",
   "pron"
  ],
  [
   "jos",
   "he, she",
   "pron"
  ],
  [
   "-ən",
   "PL (plural)",
   "affix"
  ],
  [
   "-ux",
   "PST (past)",
   "affix"
  ],
  [
   "-ʃ",
   "ERG (ergative)",
   "affix"
  ],
  [
   "ɡrem",
   "snow",
   "noun"
  ],
  [
   "brul",
   "high",
   "adj"
  ],
  [
   "kos",
   "one",
   "num"
  ],
  [
   "dra",
   "two",
   "num"
  ],
  [
   "ʁal",
   "see",
   "verb"
  ]
 ],
 "lexicon_total": 20
}

request, decoded:

{
 "count": 4,
 "topic": "a herder loses a goat in the snow and her kin search the hill",
 "sentences": [],
 "grammar_known": []
}

Reply:

{
 "task": "gloss",
 "sentences": [
  {
   "morphemes": [
    "t͡ʃəx",
    "ɡrem-qo",
    "dəl-ux"
   ],
   "glosses": [
    "goat",
    "snow-LOC",
    "go-PST"
   ],
   "translation": "The goat wandered off into the snow.",
   "note": "Intransitive clause; the absolutive subject /t͡ʃəx/ is unmarked, verb-final order (OV)."
  },
  {
   "morphemes": [
    "t͡ʃəx-to-ʃ",
    "t͡ʃəx",
    "mox-ux"
   ],
   "glosses": [
    "goat-AGT-ERG",
    "goat",
    "lose-PST"
   ],
   "translation": "The goatherd lost the goat.",
   "note": "Transitive, ergative-absolutive; -ʃ ERG surfaces only because the agent noun ends in the vowel-final AGT suffix /-to/."
  },
  {
   "morphemes": [
    "klan-ən",
    "ʁor",
    "nəqor-ux"
   ],
   "glosses": [
    "kin-PL",
    "hill",
    "search-PST"
   ],
   "translation": "The kinfolk searched the hill.",
   "note": "Agent /klan-ən/ is consonant-final, so ERG -ʃ cannot attach without an illegal second coda; ergative goes unmarked here, recovered from agent-patient-verb order."
  },
  {
   "morphemes": [
    "klan-ən",
    "ʁor-qo",
    "t͡ʃəx",
    "ʁal-ux"
   ],
   "glosses": [
    "kin-PL",
    "hill-LOC",
    "goat",
    "see-PST"
   ],
   "translation": "The kinfolk found the goat on the hill.",
   "note": "/ʁal/ 'see' covers 'find (by sight)'; ergative again unmarked on consonant-final /klan-ən/, same gap as above."
  }
 ],
 "coinages": [
  {
   "ipa": "-to",
   "gloss": "AGT",
   "pos": "affix",
   "note": "Agentive nominalizer ('one who tends/does X'), coined because the lexicon had no agent suffix. Deliberately vowel-final so ERG -ʃ can attach afterward: checked /t͡ʃəxto/ and /t͡ʃəxtoʃ/ both syllabify as (C)([RL])V(C) sequences with codas restricted to N/R/L/F (R1), no adjacent K-K (R2), word-final segment never D (R3), no doubled adjacent segment (R4)."
  },
  {
   "ipa": "mox",
   "gloss": "lose",
   "pos": "verb",
   "note": "'Lose, misplace' (transitive). Checked /moxux/ = mox.ux: coda x is F (R1 ok), no K adjacent to another K (R2), final segment x not D (R3), no doubled segments (R4)."
  },
  {
   "ipa": "nəqor",
   "gloss": "search",
   "pos": "verb",
   "note": "'Search, seek, look for' (transitive). Checked /nəqorux/ = nə.qor.ux: codas r=R and x=F both allowed (R1), the single K (/q/) is not adjacent to another K (R2), word ends in x not D (R3), no doubled segments (R4)."
  },
  {
   "ipa": "-qo",
   "gloss": "LOC",
   "pos": "affix",
   "note": "Locative suffix 'at/in/on', coined since the lexicon marks grammar by suffix (PL, PST, ERG) rather than adposition. Vowel-final so it never forces an illegal coda. Checked /ɡremqo/ = ɡrem.qo and /ʁorqo/ = ʁor.qo against R1-R4."
  }
 ],
 "notes": [
  "Ergative -ʃ has no vowel of its own, so it can only attach to a stem whose final syllable has no coda already filled; it is overtly realised only on the coined vowel-final AGT noun /t͡ʃəxto/, and left unmarked on consonant-final nouns/pronouns (klan, mən, tən, jos) since stacking it there would need a second coda consonant, which the template (C)([RL])V(C) forbids.",
  "Word order is agent-patient-verb (OV, ergative-absolutive alignment), which disambiguates roles when ergative marking cannot surface; this fits the brief's terse, ceremonial register.",
  "Reused /ʁal/ 'see' for 'find' rather than coining a new verb, per house rule 3 (reuse before coining).",
  "/t͡ʃəxto/ 'goatherd' literally contains the root for 'goat' (goat-AGT), which is intentional: a herder of goats is named from the animal, fitting a highland herding culture."
 ]
}

etym: Aru - six etymologies against four sound changes to Late Aru

Input:

{
 "task": "etym",
 "language": "(a JSON string - decoded below)",
 "request": "(a JSON string - decoded below)"
}

language, decoded:

{
 "name": "Aru",
 "feel": "open syllables, soft and vowel-heavy, a sea language",
 "culture": "island fishers who name everything after tides and winds",
 "purpose": "names and short songs in a novel",
 "consonants": [
  "p",
  "t",
  "k",
  "m",
  "n",
  "ŋ",
  "h",
  "w",
  "l",
  "ʔ"
 ],
 "vowels": [
  "a",
  "e",
  "i",
  "o",
  "u"
 ],
 "classes": {
  "C": "p t k m n ŋ h w l ʔ",
  "V": "a e i o u"
 },
 "template": "(C)V",
 "constraints": [
  "R1: words have 1 to 4 syllables - short words",
  "R2: never # ŋ (# = word edge) - no word starts with ng",
  "R3: no segment may be doubled (no aa, no tt)"
 ],
 "romanisation": {
  "p": "p",
  "t": "t",
  "k": "k",
  "m": "m",
  "n": "n",
  "ŋ": "ng",
  "h": "h",
  "w": "w",
  "l": "l",
  "ʔ": "'",
  "a": "a",
  "e": "e",
  "i": "i",
  "o": "o",
  "u": "u"
 },
 "lexicon": [
  [
   "tai",
   "sea",
   "noun"
  ],
  [
   "hana",
   "canoe",
   "noun"
  ],
  [
   "ʔulu",
   "head; leader",
   "noun"
  ],
  [
   "mana",
   "wind",
   "noun"
  ],
  [
   "kai",
   "eat",
   "verb"
  ],
  [
   "lani",
   "sky",
   "noun"
  ],
  [
   "wae",
   "go",
   "verb"
  ],
  [
   "aŋa",
   "fish",
   "noun"
  ],
  [
   "pou",
   "post; pillar",
   "noun"
  ],
  [
   "tama",
   "child",
   "noun"
  ],
  [
   "ʔoa",
   "net",
   "noun"
  ],
  [
   "kelo",
   "see",
   "verb"
  ],
  [
   "nui",
   "big",
   "adj"
  ],
  [
   "iki",
   "small",
   "adj"
  ],
  [
   "hou",
   "new",
   "adj"
  ],
  [
   "wai",
   "fresh water",
   "noun"
  ],
  [
   "pune",
   "spring (season)",
   "noun"
  ],
  [
   "tolu",
   "three",
   "num"
  ],
  [
   "lua",
   "two",
   "num"
  ],
  [
   "tahi",
   "one",
   "num"
  ],
  [
   "maŋu",
   "bird",
   "noun"
  ],
  [
   "hiku",
   "tail",
   "noun"
  ],
  [
   "kamu",
   "give",
   "verb"
  ],
  [
   "noho",
   "sit; stay",
   "verb"
  ],
  [
   "ʔoko",
   "rope",
   "noun"
  ],
  [
   "u",
   "and",
   "conj"
  ],
  [
   "i",
   "at, to",
   "adp"
  ],
  [
   "kona",
   "south",
   "noun"
  ]
 ],
 "lexicon_total": 28
}

request, decoded:

{
 "entries": [
  [
   "hana",
   "canoe",
   "noun"
  ],
  [
   "ʔulu",
   "head; leader",
   "noun"
  ],
  [
   "pune",
   "spring (season)",
   "noun"
  ],
  [
   "maŋu",
   "bird",
   "noun"
  ],
  [
   "hiku",
   "tail",
   "noun"
  ],
  [
   "kona",
   "south",
   "noun"
  ]
 ],
 "daughter": {
  "name": "Late Aru",
  "rules": [
   "k > ʔ",
   "t > s / _i",
   "h > ∅ / V_V",
   "u > o / _#"
  ],
  "reflexes": [
   [
    "hana",
    "hana"
   ],
   [
    "ʔulu",
    "ʔulo"
   ],
   [
    "pune",
    "pune"
   ],
   [
    "maŋu",
    "maŋo"
   ],
   [
    "hiku",
    "hiʔo"
   ],
   [
    "kona",
    "ʔona"
   ]
  ]
 },
 "ask": "The islanders were once two peoples; let some words come from the hill people inland."
}

Reply:

{
 "task": "etym",
 "entries": [
  {
   "word": "hana",
   "kind": "compound",
   "parts": [
    "ha",
    "na"
   ],
   "parts_gloss": [
    "hull.wood",
    "vessel.holder"
   ],
   "story": "Canoe builders once spoke of the dugout trunk itself as /ha/, 'hollow wood', and of anything shaped to hold or carry as /na/. The two fused into /ha-na/, the hollowed wood that holds a fisher and the catch: a canoe. Speakers no longer feel the two roots apart; /hana/ sits in the lexicon as one simple word."
  },
  {
   "word": "ʔulu",
   "kind": "shift",
   "parts": [],
   "parts_gloss": [],
   "story": "/ʔulu/ named the head, the body's foremost part, long before it named a person. Fishers who follow one steersman at the prow let the word for the body's leading part slide onto whoever leads the canoe or the village. No new form was ever coined for this; the old root simply widened its reach, so /ʔulu/ now covers both head and leader."
  },
  {
   "word": "pune",
   "kind": "borrowing",
   "parts": [],
   "parts_gloss": [],
   "story": "Islanders read the tides and winds themselves, but the finer signs of the turning season, when inland blossoms open, were first read by the hill people. /pune/ passed into Aru speech whole, as their word for that thaw, because the sign belonged to knowledge the sea people did not keep on their own coasts. It settled into ordinary vowel-final shape and was never reanalysed into smaller Aru parts."
  },
  {
   "word": "maŋu",
   "kind": "derivation",
   "parts": [
    "ma",
    "-ŋu"
   ],
   "parts_gloss": [
    "cry.call",
    "creature.agentive"
   ],
   "story": "An old bound root /ma/, 'to cry out', took the agentive ending /-ŋu/, 'the one that--', to name a creature first known by its call rather than its shape. /ma-ŋu/, 'the one that cries out', settled as the general word for bird, since a fisher sighting storm or shore trusts the call before the wingbeat. The medial /ŋ/ is unremarkable here, since R2 only bars /ŋ/ at the word's edge, not in the middle of a derived word."
  },
  {
   "word": "hiku",
   "kind": "derivation",
   "parts": [
    "hi",
    "-ku"
   ],
   "parts_gloss": [
    "tip.point",
    "hind.rear"
   ],
   "story": "/hi/, an old word for a tip or point, took the old suffix /-ku/, marking the hindmost part of a thing, giving /hi-ku/, 'the point at the back'. The compound fossilized onto fish and birds alike before settling as the general word for tail. Its daughter form /hiʔo/ only shows the regular lenition of /k/ and the final-vowel lowering; the word's shape and story are otherwise untouched."
  },
  {
   "word": "kona",
   "kind": "borrowing",
   "parts": [],
   "parts_gloss": [],
   "story": "Before the two peoples joined, south was simply the direction of the hill country, and the islanders took the hill people's own name for their land as the word for that direction. /kona/ is therefore not built from Aru roots at all; it is a loan that settled into ordinary noun shape and now names the compass point rather than the people who first bore the name."
  }
 ],
 "rule_notes": [
  {
   "rule": 1,
   "comment": "/k/ > /ʔ/, the weakening of the voiceless velar stop to a glottal stop, is a common and phonetically natural lenition. It is unconditioned, so it fires wherever /k/ occurs, word-initial or medial alike, as seen in both /kona/ > /ʔona/ and /hiku/ > /hiʔo/."
  },
  {
   "rule": 2,
   "comment": "/t/ > /s/ / _i is a classic assibilation before a high front vowel and is phonetically natural and narrowly conditioned. None of these six words has a /t/ before /i/, so it is silent in this sample, not blocked by the other rules."
  },
  {
   "rule": 3,
   "comment": "/h/ > ∅ / V_V (loss between vowels) is a natural and common lenition, but its environment is strictly intervocalic. Word-initial /h/, as in /hana/ and /hiku/, falls outside that environment and survives untouched, which is exactly why both reflexes keep their initial /h/."
  },
  {
   "rule": 4,
   "comment": "/u/ > /o/ / _# (final-vowel lowering) is plausible, if a little less cross-linguistically common than the other three. It targets a different segment and position from the others, so in this set it neither feeds nor bleeds them: in /hiku/ > /hiʔo/ it simply applies after /k/ > /ʔ/ to the same word, the two changes acting independently rather than one creating the other's environment."
  }
 ],
 "coinages": [
  {
   "ipa": "ha",
   "gloss": "hull.wood",
   "pos": "noun",
   "note": "archaic root, bound only in /ha-na/ 'canoe'; single CV syllable, fits (C)V, no doubled segment, no word-edge ŋ issue."
  },
  {
   "ipa": "na",
   "gloss": "vessel.holder",
   "pos": "noun",
   "note": "archaic root, bound only in /ha-na/ 'canoe'; single CV syllable, legal under R1-R3."
  },
  {
   "ipa": "ma",
   "gloss": "cry.call",
   "pos": "verb",
   "note": "archaic bound root in /ma-ŋu/ 'bird'; single CV syllable, legal under R1-R3."
  },
  {
   "ipa": "-ŋu",
   "gloss": "creature.agentive",
   "pos": "affix",
   "note": "suffix checked against the phoneme inventory only (ŋ, u both listed); medial in /ma-ŋu/, so the word-edge ban in R2 does not apply."
  },
  {
   "ipa": "hi",
   "gloss": "tip.point",
   "p
... (shortened here; the page shows it whole)

recoin: Sefet lost /ʂ/ and /t͡ɕ/ - three words re-coined

Input:

{
 "task": "recoin",
 "language": "(a JSON string - decoded below)",
 "request": "(a JSON string - decoded below)"
}

language, decoded:

{
 "name": "Sefet",
 "feel": "level and liquid, long vowels, codas only nasal",
 "culture": "river-delta scholars",
 "purpose": "a linguistics class exercise",
 "consonants": [
  "p",
  "t",
  "k",
  "s",
  "h",
  "m",
  "n",
  "ɳ",
  "l",
  "j",
  "w"
 ],
 "vowels": [
  "a",
  "i",
  "u",
  "aː",
  "iː",
  "uː"
 ],
 "classes": {
  "C": "p t k s h m n ɳ l j w",
  "N": "m n ɳ",
  "V": "a i u aː iː uː"
 },
 "template": "(C)V(N)",
 "constraints": [
  "R1: never N N (# = word edge) - no nasal clusters",
  "R2: words have 1 to 3 syllables"
 ],
 "romanisation": {
  "p": "p",
  "t": "t",
  "k": "k",
  "s": "s",
  "h": "h",
  "m": "m",
  "n": "n",
  "ɳ": "rn",
  "l": "l",
  "j": "y",
  "w": "w",
  "a": "a",
  "i": "i",
  "u": "u",
  "aː": "aa",
  "iː": "ii",
  "uː": "uu"
 },
 "lexicon": [
  [
   "saːn",
   "river",
   "noun"
  ],
  [
   "t͡ɕiːm",
   "reed",
   "noun"
  ],
  [
   "ʂuma",
   "read",
   "verb"
  ],
  [
   "lan",
   "mud",
   "noun"
  ],
  [
   "haːɳ",
   "scholar",
   "noun"
  ],
  [
   "wiː",
   "water bird",
   "noun"
  ],
  [
   "kuːn",
   "boat",
   "noun"
  ],
  [
   "jaːt͡ɕi",
   "write",
   "verb"
  ]
 ],
 "lexicon_total": 8
}

request, decoded:

{
 "forms": [
  {
   "form": "t͡ɕiːm",
   "gloss": "reed",
   "where": "lexicon",
   "broken": "/t͡ɕ/ is not in the inventory"
  },
  {
   "form": "ʂuma",
   "gloss": "read",
   "where": "lexicon",
   "broken": "/ʂ/ is not in the inventory"
  },
  {
   "form": "jaːt͡ɕi",
   "gloss": "write",
   "where": "lexicon",
   "broken": "/t͡ɕ/ is not in the inventory"
  }
 ],
 "change": "dropped /ʂ/ and /t͡ɕ/ from the inventory to make the language softer"
}

Reply:

{
 "task": "recoin",
 "recoined": [
  {
   "old": "t͡ɕiːm",
   "new": "siːm",
   "gloss": "reed",
   "reason": "/t͡ɕ/ was dropped; /s/ is the only sibilant left in the inventory so it steps in as onset, and the rest of the skeleton (/iː/, coda /m/) is kept unchanged. /siːm/ = s-iː-m, one syllable matching (C)V(N), coda is a lone nasal so R1 holds, and R2 (1-3 syllables) holds."
  },
  {
   "old": "ʂuma",
   "new": "suma",
   "gloss": "read",
   "reason": "/ʂ/ was dropped; /s/ is the nearest surviving fricative (same manner, voiceless, sibilant) so it replaces the onset while /u/, /m/, /a/ stay exactly as before. /su.ma/ is two syllables, each (C)V, only one nasal so no NN cluster, within the 1-3 syllable limit."
  },
  {
   "old": "jaːt͡ɕi",
   "new": "jaːsi",
   "gloss": "write",
   "reason": "/t͡ɕ/ was dropped; using the same /s/ substitution as 'reed' keeps the softening change consistent across the lexicon (both /ʂ/ and /t͡ɕ/ merge to /s/). /jaː.si/ keeps onset /j/, nucleus /aː/ and /i/ unchanged; two syllables, no nasals at all so R1 is vacuously satisfied, R2 holds."
  }
 ],
 "coinages": [],
 "notes": [
  "Treated the change as a single merger: /ʂ/ and /t͡ɕ/ both collapse to /s/, the only sibilant left in the new inventory - this keeps the three replacements internally consistent rather than picking a different filler for each.",
  "In every case only the illegal consonant was swapped; vowels and nasal codas were left exactly as sent, so the consonant skeleton (and the romanisation of everything but the one sound) is unchanged.",
  "No new roots or affixes were coined - these are repairs of existing lexicon forms, so coinages is empty per the task contract."
 ]
}

The workbench checks; the API does not

Over the API you get the model's forms as JSON. The validation is yours to run, and the workbench's own code is plain scripts you can load in node after global.window = global, in this order: ipa.js, phono.js (Phono.compile(lang), Phono.validate(sys, ipa) - returns the syllabification and every rule broken, Phono.romanise(lang, ipa), Phono.generate(sys, {seed, count})), soundchange.js (SoundChange.daughter(lang, forms, rules)), lang.js (Lang.buildInput(lang, sys, task, opts) builds the exact run body) and reconcile.js (Recon.normalize(Recon.parseResult(text), task) then Recon.reconcile(result, lang, sys, ctx) - the same flags the page shows). A language file saved from the page (Download language) is the lang object.

Step 1 — a tiny client helper

One function that sends JSON and unwraps the envelope.

BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here"   # from https://tonguewright.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":"tonguewright"}' | 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 (not wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model reads gpt-5.6-terra and model_alias gpt-terra.

# input.json: the run input object - {"task": "sketch", "language": "{...}", "request": "{...}"}
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 tonguewright:<task>:<inputHash>:a<attempt>. The job's output.output is the reply text; truncated: true means the balance cut it short.

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