← Most Obedient
API
Tokens

Drive Most Obedient from your own code

The one metered task, letter, is available over HTTP. It takes who the letter is to and from, the relationship, a flourish and the message split into numbered sentences, and returns a verdict - proper, forward, scandalous or wants-more - with the letter (salutation, paragraphs, closing, signature, postscript), every modern word and its period stand-in, and every struck line. A natural use is a "send it as a letter" button in a chat or note-taking tool, or a stationery shop that prints the fair copy.

The division of labour first: the page owns the sweep, the date and the sheet; the model writes the words; the page re-checks the reply. The sentence splitter, the flags and the clipping come from letterkit.js, which runs in node as well as in the browser. The dateline is never asked of the model - LetterKit.dateline(ms) writes it ("Wednesday, the 21st of October, 2026"). LetterKit.check holds each reply to the message: salutation and closing present, sentence ids that exist, struck lines really left out, every stand-in found in the letter, no modern word left in it, and no capitalised name, number or pronoun that the message does not hold.

Get a token on the token page (step 2 below also shows how to mint a guest token in code).

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":"most-obedient"}), 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, message) 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: writing the letter 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 and the verdicts

Every run sends task: letter (the only task; a missing or unknown one is treated as letter). The verdict is decided by ordered, non-overlapping rules - the first that applies wins:

OrderVerdictWhen
1wants-moreNo sentence carries a point (news, a plan, a question, a request, thanks or an apology for something named, a feeling about something named). No letter; asks holds two or three questions.
2scandalousA sentence insults, mocks or gossips about a person. Each such sentence is listed in struck and left out of the letter entirely.
3forwardA sentence swears, shouts in capitals or gives an order with no please. The letter keeps every point with period manners; every softened word is a swap.
4properOtherwise.

The declared input schema carries scalars only, so the array-valued fields (message, page_flags) travel JSON-encoded as strings (LetterKit.toWire does this). Every value is a string.

Input fields

FieldTypeNotes
taskstringletter. Required.
to, fromstringAs typed, up to 60 characters each; either may be empty. The salutation uses to; the signature is from exactly.
relationstringfriend, family, sweetheart, acquaintance or business - sets the salutation and the closing.
flourishstringplain, genteel or effusive.
messagestring (JSON)[{id, text}] - ids S1.., up to 30 sentences of up to 400 characters. Required.
message_wordsstringThe message's total words - the letter runs at least this long, at most four times it and at most 220 words.
page_flagsstring (JSON)[{ref, flag}] with flags modern:<word>, slang:<word>, emoji, shout, crude:<word>, unkind:<word>, command - the page's guesses, which the model judges for itself.
clip_note, retry_notestringWhat the page cut, when it cut something; and only on a reformat retry, what was wrong.

The reply

The job's output.output is a string holding one JSON object: task, verdict, title, summary, salutation, paragraphs ([{text, uses}] - one to four, uses the sentence ids each is built from; [] for wants-more), closing, signature, postscript ({text, uses}, text without the "P.S." the page adds), swaps ([{ref, modern, period}] - period "" when a meaningless word was simply dropped), struck ([{ref, why}], scandalous only) and asks (wants-more only). Parse with LetterKit.parseReply, then normalize(reply), then check(reply, form), which returns the page's notes; letterText(reply, ms) gives the plain-text fair copy with the dateline.

Worked examples

Replies produced against the app's prompt for the five sample messages (made-up people; long fields shortened; the full replies ship in example-result.js). Together they cover every verdict.

a thank-you for a scarf and an invitation to tea, to a friend: proper

Input:

{
 "task": "letter",
 "to": "Harriet",
 "from": "Eleanor",
 "relation": "friend",
 "flourish": "genteel",
 "message": "[{\"id\":\"S1\",\"text\":\"Thank you so much for the scarf you knitted me, I wore it to work today and got three compliments!\"},{\"id\":\"S2\",\"text\":\"Are you free for tea next Saturday?\"},{\"id\":\"S3\",\"text\":\"I'll bring the lemon cake you liked last time.\"}]",
 "message_words": "36",
 "page_flags": "[]"
}

Reply:

{
 "task": "letter",
 "verdict": "proper",
 "title": "Thanks for the scarf and tea",
 "summary": "Your letter thanks Harriet for the scarf and the compliments it earned you, then asks Harriet to tea next Saturday and offers to bring the lemon cake Harriet liked before. As every line is warm and courteous with nothing unkind or forward, the verdict is proper.",
 "salutation": "My dear Harriet,",
 "paragraphs": [
  {
   "text": "I hope this finds you well, dear Harriet. I must thank you most sincerely for the scarf you knitted for me; I wore it to work today and received three compliments upon it.",
   "uses": [
    "S1"
   ]
  },
  {
   "text": "I write further to ask whether you might be free to take tea with me next Saturday. Should you be at liberty, I shall bring the lemon cake which you liked on the last occasion.",
   "uses": [
    "S2",
    "S3"
   ]
  }
 ],
 "closing": "Believe me, ever your affectionate friend,",
 "signature": "Eleanor",
 "postscript": {
  "text": "",
  "uses": []
 },
 "swaps": [],
 "struck": [],
 "asks": []
}

a leaking kitchen tap, to the landlord (business): proper

Input:

{
 "task": "letter",
 "to": "Mr. Okafor",
 "from": "Rosa Quellbridge",
 "relation": "business",
 "flourish": "genteel",
 "message": "[{\"id\":\"S1\",\"text\":\"The kitchen tap in flat 4 has been leaking since Monday.\"},{\"id\":\"S2\",\"text\":\"Could someone come and fix it this week?\"},{\"id\":\"S3\",\"text\":\"I'm home after 5pm on weekdays, or you can email me to arrange a time.\"},{\"id\":\"S4\",\"text\":\"Thanks for your help.\"}]",
 "message_words": "38",
 "page_flags": "[{\"ref\":\"S3\",\"flag\":\"modern:email\"}]"
}

Reply:

{
 "task": "letter",
 "verdict": "proper",
 "title": "Request to fix the leaking tap",
 "summary": "This turns your note to Mr. Okafor into a courteous business letter about the leaking tap, your wish to have it fixed this week, and the times you are free to arrange it. Every point you raised is included without any unkind or forward language, which is why the verdict is proper.",
 "salutation": "Dear Mr. Okafor,",
 "paragraphs": [
  {
   "text": "I hope this letter finds you well. I write to inform you that the kitchen tap in flat 4 has been leaking since Monday.",
   "uses": [
    "S1"
   ]
  },
  {
   "text": "I should be glad if someone might come and fix it this week.",
   "uses": [
    "S2"
   ]
  },
  {
   "text": "I am at home after five o'clock on weekdays, or you may write to me to arrange a time.",
   "uses": [
    "S3"
   ]
  },
  {
   "text": "Thank you for your help in this matter. I am obliged to you.",
   "uses": [
    "S4"
   ]
  }
 ],
 "closing": "I have the honour to remain, your most obedient servant,",
 "signature": "Rosa Quellbridge",
 "postscript": {
  "text": "",
  "uses": []
 },
 "swaps": [
  {
   "ref": "S3",
   "modern": "email",
   "period": "write"
  }
 ],
 "struck": [],
 "asks": []
}

"Bring my phone charger back ASAP", to family: forward

Input:

{
 "task": "letter",
 "to": "Tom",
 "from": "Ben",
 "relation": "family",
 "flourish": "plain",
 "message": "[{\"id\":\"S1\",\"text\":\"Bring my phone charger back ASAP.\"},{\"id\":\"S2\",\"text\":\"You took it from the kitchen on Sunday.\"},{\"id\":\"S3\",\"text\":\"The party was LIT lol.\"},{\"id\":\"S4\",\"text\":\"Text me when you're on your way.\"}]",
 "message_words": "26",
 "page_flags": "[{\"ref\":\"S1\",\"flag\":\"slang:asap\"},{\"ref\":\"S1\",\"flag\":\"modern:phone\"},{\"ref\":\"S1\",\"flag\":\"modern:charger\"},{\"ref\":\"S1\",\"flag\":\"command\"},{\"ref\":\"S3\",\"flag\":\"slang:lol\"},{\"ref\":\"S3\",\"flag\":\"slang:lit\"},{\"ref\":\"S3\",\"flag\":\"shout\"},{\"ref\":\"S4\",\"flag\":\"modern:text\"},{\"ref\":\"S4\",\"flag\":\"command\"}]"
}

Reply:

{
 "task": "letter",
 "verdict": "forward",
 "title": "Requesting the return of an item",
 "summary": "Your letter asks Tom to return your item without delay, recalls that Tom took it from the kitchen on Sunday, calls the party delightful, and asks Tom to send a line when on the way. Because your message gave Tom direct orders with no softening word and one word in capitals, the letter restates those points in a more polite period style.",
 "salutation": "My dear Tom,",
 "paragraphs": [
  {
   "text": "I hope this finds you well. I should be much obliged if you would return the little apparatus belonging to my pocket instrument without delay, for I recall that you took it from the kitchen on Sunday.",
   "uses": [
    "S1",
    "S2"
   ]
  },
  {
   "text": "I must tell you that the party was most delightful.",
   "uses": [
    "S3"
   ]
  },
  {
   "text": "I should be glad if you would send me a line when you are on your way.",
   "uses": [
    "S4"
   ]
  }
 ],
 "closing": "Your ever affectionate,",
 "signature": "Ben",
 "postscript": {
  "text": "",
  "uses": []
 },
 "swaps": [
  {
   "ref": "S1",
   "modern": "phone charger",
   "period": "the little apparatus belonging to my pocket instrument"
  },
  {
   "ref": "S1",
   "modern": "ASAP",
   "period": "without delay"
  },
  {
   "ref": "S3",
   "modern": "LIT",
   "period": "delightful"
  },
  {
   "ref": "S3",
   "modern": "lol",
   "period": ""
  },
  "... (5 in all)"
 ],
 "struck": [],
 "asks": []
}

the dance on Friday, with a line of gossip, to a friend: scandalous

Input:

{
 "task": "letter",
 "to": "Priya",
 "from": "Jess",
 "relation": "friend",
 "flourish": "effusive",
 "message": "[{\"id\":\"S1\",\"text\":\"OMG the dance on Friday was amazing!!\"},{\"id\":\"S2\",\"text\":\"I danced with the drummer from the band twice.\"},{\"id\":\"S3\",\"text\":\"Honestly Dana looked like a total mess and everyone was laughing at her.\"},{\"id\":\"S4\",\"text\":\"Can you come over on Sunday so I can tell you everything? 💃\"},{\"id\":\"S5\",\"text\":\"PS bring the photos from your phone!\"} ... (361 characters)",
 "message_words": "48",
 "page_flags": "[{\"ref\":\"S1\",\"flag\":\"slang:omg\"},{\"ref\":\"S3\",\"flag\":\"unkind:mess\"},{\"ref\":\"S3\",\"flag\":\"unkind:laughing at\"},{\"ref\":\"S4\",\"flag\":\"emoji\"},{\"ref\":\"S5\",\"flag\":\"modern:phone\"},{\"ref\":\"S5\",\"flag\":\"modern:photos\"}]"
}

Reply:

{
 "task": "letter",
 "verdict": "scandalous",
 "title": "A letter about the Friday dance",
 "summary": "This letter carries your news of Friday's dance, your dancing with the drummer, and your wish for Priya to call on Sunday, written in a warm and courteous style. One line mocking Dana was unkind, so it has been left out of the letter altogether, which is why this reply cannot stand as a proper one.",
 "salutation": "My dear Priya,",
 "paragraphs": [
  {
   "text": "I hope indeed that this letter finds you in very good health. The dance on Friday was quite delightful.",
   "uses": [
    "S1"
   ]
  },
  {
   "text": "I had the pleasure of dancing with the drummer of the band, not once but twice.",
   "uses": [
    "S2"
   ]
  },
  {
   "text": "Will you come and see me on Sunday, that I may tell you everything?",
   "uses": [
    "S4"
   ]
  }
 ],
 "closing": "Believe me, ever your affectionate friend,",
 "signature": "Jess",
 "postscript": {
  "text": "I beg you will bring the pictures taken with your little pocket instrument.",
  "uses": [
   "S5"
  ]
 },
 "swaps": [
  {
   "ref": "S1",
   "modern": "OMG",
   "period": ""
  },
  {
   "ref": "S4",
   "modern": "💃",
   "period": ""
  },
  {
   "ref": "S5",
   "modern": "photos",
   "period": "pictures"
  },
  {
   "ref": "S5",
   "modern": "phone",
   "period": "little pocket instrument"
  }
 ],
 "struck": [
  {
   "ref": "S3",
   "why": "it mocks Dana's appearance and says others were laughing at Dana"
  }
 ],
 "asks": []
}

"hey lol" / "k thx": wants-more

Input:

{
 "task": "letter",
 "to": "Sam",
 "from": "",
 "relation": "friend",
 "flourish": "genteel",
 "message": "[{\"id\":\"S1\",\"text\":\"hey lol\"},{\"id\":\"S2\",\"text\":\"k thx 👍\"}]",
 "message_words": "4",
 "page_flags": "[{\"ref\":\"S1\",\"flag\":\"slang:lol\"},{\"ref\":\"S1\",\"flag\":\"slang:hey\"},{\"ref\":\"S2\",\"flag\":\"slang:thx\"},{\"ref\":\"S2\",\"flag\":\"slang:k\"},{\"ref\":\"S2\",\"flag\":\"emoji\"}]"
}

Reply:

{
 "task": "letter",
 "verdict": "wants-more",
 "title": "A letter in want of news",
 "summary": "Your note to Sam does not yet carry anything to answer - only a greeting and a word of thanks with nothing named. I cannot draw a letter from it until you say what the thanks are for, what news you have, or what you wish to ask. A few questions are offered below to help you say more.",
 "salutation": "",
 "paragraphs": [],
 "closing": "",
 "signature": "",
 "postscript": {
  "text": "",
  "uses": []
 },
 "swaps": [],
 "struck": [],
 "asks": [
  "What would you like to tell Sam?",
  "What are you thanking Sam for?",
  "Is there anything you'd like to ask Sam?"
 ]
}

Build the input yourself

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

const K = LetterKit;
const form = K.form({ to: "Tom", from: "Ben", relation: "family", flourish: "plain",
  message: "Bring my phone charger back ASAP.\nYou took it from the kitchen on Sunday." });
console.log(K.flags(form), K.lean(form), K.preview(form));   // the sweep, the page's lean, the word-for-word preview
const wire = K.toWire(K.buildInput(form).input);            // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// const { notes } = K.check(reply, form);                   // the page's re-check of the reply
// K.letterText(reply, Date.now())                           // the fair copy, with the page's dateline

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://most-obedient.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":"most-obedient"}' | 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": "letter", "to": "Harriet", "from": "Eleanor", "relation": "friend", "flourish": "genteel", "message": "[{\"id\":\"S1\",\"text\":\"Thank you so much for the scarf you knitted me, I wore it to work today and got three compliments!\"},{\"id\":\"S2\",\"text\":\"Are you free for tea next Saturday?\"}, ...]", "message_words": "36"}
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 most-obedient:<task>:<inputHash>:a<attempt> (the one task is letter), 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="most-obedient:letter:$(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: most-obedient:letter:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json