← Kind-Lie Meter
API

Drive Kind-Lie Meter from your own code

The one metered task, meter, is available over HTTP. It takes a small idea, who you plan to ask and the questions you plan to ask them, together with the page's read of each question, and returns why each question draws the answer it does, a rewrite that asks about a real moment, the verdict, the question to start with, what to listen for and one small closing ask.

The division of labour first: the page reads; the model writes. The page gives every question one of six reads from cue words in its wording (or the read you pick) and sends them as facts. The model copies the reads, applies fixed rules for the verdict and the starting question, and writes the words; the page re-checks all of it, including reading every rewrite again.

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":"kind-lie-meter"}), 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, idea, people, questions, hoped_ask, 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 reads

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

FieldRequiredWhat it holds
taskyesAlways meter.
ideayesThe small idea you want to try, in your words.
peopleyesWho you plan to ask, in your words.
questionsyesOne line per question, q1: text to q8: text, in the order you would ask them.
hoped_askyesA small step you hope they will take, or nothing listed.
factsyesOne line per question: its id, its read, and the cue the page found (or that you chose the read).
retry_notenoOnly on a reformat retry: why the previous reply was unusable.

Reads, riskiest first: flattery (asks what they think of the idea), pitch (tells them about it or leads them to agree), someday (asks them to guess what they would do), usually (habits in general), today (how things go now), past (a real moment that already happened). The page checks cue words in the order flattery, pitch, someday, usually, past, today; first match wins.

Verdict, first match wins: no question reads today or past, fishing; every question does, straight; otherwise mixed. start_with is the riskiest question that does not read past, the earlier one on a tie, or empty when every question reads past.

The reply

One JSON object, no prose around it:

{"task": "meter", "headline": "...", "questions": [{"q": "q1", "read": "flattery", "why": "...", "rewrite": "..."}, ...], "verdict": "mixed", "start_with": "q1", "listen_for": "...", "closing_ask": "..."}
FieldWhat it holds
headlineAt most 20 words that fit the verdict.
questions[]One per question in order: q (the id), read (copied from facts), why (the answer it tends to draw and why) and rewrite (a question about the last time something actually happened; empty for a past read).
verdictstraight, mixed or fishing.
start_withThe id of the question to fix first, or an empty string.
listen_forWhat a real answer sounds like, and what a kind one sounds like.
closing_askOne small, easy-to-refuse step to ask for at the end (your hoped_ask when you gave one).

The page then checks the reply (MeterKit.check) - one reading for every question and none for questions not on the list, every read its own, the verdict and the starting question against the rules, a rewrite for every question that does not read past, every rewrite read again by the same cue words (a rewrite that still reads as praise, a pitch, someday or usually is flagged) and no numbers you did not give - and lists anything it finds under "The page's check".

Worked examples - every verdict and every read

These are the 4 free samples on the page: the exact input the page sends and the reply that replays for free. Together they cover every verdict and every read.

A plant swap, asking neighbours - some kind lies likely (flattery, someday, today, past)

Input:

{
 "task": "meter",
 "idea": "a monthly plant swap in the community hall",
 "people": "neighbours on our street",
 "questions": "q1: Do you like the idea of a plant swap?\nq2: Would you bring cuttings if I ran one?\nq3: How do you get new plants at the moment?\nq4: What did you do with the last plant you didn't want?",
 "hoped_ask": "bring one cutting to the first swap",
 "facts": "q1 - read: flattery - the page's cue: \"do you like\"\nq2 - read: someday - the page's cue: \"would\"\nq3 - read: today - the page's cue: \"how do you\"\nq4 - read: past - the page's cue: \"what did\""
}

Reply:

{
 "task": "meter",
 "headline": "Some of your questions about the monthly plant swap ask about real life, others invite a kind answer.",
 "questions": [
  {
   "q": "q1",
   "read": "flattery",
   "why": "Asking if they like the idea of a plant swap invites a warm compliment, since answering kindly is easier than raising doubts.",
   "rewrite": "When did you last trade or give away a plant with a neighbour?"
  },
  {
   "q": "q2",
   "read": "someday",
   "why": "Asking if they would bring cuttings imagines a future event, so kind neighbours tend to agree now rather than picture saying no later.",
   "rewrite": "When did you last bring a plant cutting to share with someone?"
  },
  {
   "q": "q3",
   "read": "today",
   "why": "Asking how they currently get new plants makes a solid opening question, though pointing to a real recent moment would give a steadier answer.",
   "rewrite": "When did you last get a new plant, and where did it come from?"
  },
  {
   "q": "q4",
   "read": "past",
   "why": "Asking what they did with an unwanted plant asks about a real past action, so kind neighbours have no polite guess to fall back on.",
   "rewrite": ""
  }
 ],
 "verdict": "mixed",
 "start_with": "q1",
 "listen_for": "A real answer names a plant they actually gave away or found a new home for. A kind answer stays vaguer, full of warm compliments, soft maybes, or promises with no real plan behind them.",
 "closing_ask": "Would you be happy to bring one cutting to the first swap?"
}

A repair cafe, asking colleagues - kind lies all round (pitch, flattery, someday, usually, flattery)

Input:

{
 "task": "meter",
 "idea": "a repair cafe in the church hall",
 "people": "my colleagues",
 "questions": "q1: Isn't it a shame how much stuff gets thrown away?\nq2: What do you think of a repair cafe?\nq3: Would you bring a wobbly chair or a torn coat along?\nq4: Do you ever fix things yourself?\nq5: Do you like fixing things with other people around?",
 "hoped_ask": "nothing listed",
 "facts": "q1 - read: pitch - the page's cue: \"isn't it\"\nq2 - read: flattery - the page's cue: \"what do you think of\"\nq3 - read: someday - the page's cue: \"would\"\nq4 - read: usually - the page's cue: \"do you ever\"\nq5 - read: flattery - the page's cue: \"do you like\""
}

Reply:

{
 "task": "meter",
 "headline": "Right now, every question about a repair cafe in the church hall invites a kind answer rather than the truth.",
 "questions": [
  {
   "q": "q1",
   "read": "pitch",
   "why": "This leads with your own view, so colleagues will likely agree rather than say what they actually do with stuff they don't want.",
   "rewrite": "What's the last thing you threw away instead of fixing?"
  },
  {
   "q": "q2",
   "read": "flattery",
   "why": "Asking directly for their opinion invites polite praise, since a kind colleague would rather compliment the idea than reveal real interest.",
   "rewrite": "When did you last take something broken to someone else to repair?"
  },
  {
   "q": "q3",
   "read": "someday",
   "why": "This asks them to guess what they would do, which lets a kind colleague offer a cheerful maybe instead of a real commitment.",
   "rewrite": "What's the last wobbly chair or torn coat you actually got mended?"
  },
  {
   "q": "q4",
   "read": "usually",
   "why": "Asking about this in general invites a quick yes about their self-image rather than a specific time they actually fixed something.",
   "rewrite": "What's the last thing you fixed yourself rather than calling someone in?"
  },
  {
   "q": "q5",
   "read": "flattery",
   "why": "Asking whether they like it invites a warm yes about company rather than any account of time spent fixing things with others.",
   "rewrite": "When did you last fix something alongside other people?"
  }
 ],
 "verdict": "fishing",
 "start_with": "q2",
 "listen_for": "A real answer names something they actually fixed, mended or brought in before. A kind answer offers praise for the repair cafe, a vague maybe, or a promise to come along.",
 "closing_ask": "Would you be up for a quick look at the repair cafe?"
}

An office book shelf - straight answers likely (past, today, past)

Input:

{
 "task": "meter",
 "idea": "a book swap shelf in the office kitchen",
 "people": "people on my floor at work",
 "questions": "q1: When did you last finish a book, and what happened to it after?\nq2: Where do you keep books you have already read?\nq3: Tell me about the last time you lent someone a book.",
 "hoped_ask": "add one book to the shelf in its first week",
 "facts": "q1 - read: past - the page's cue: \"when did\"\nq2 - read: today - the page's cue: \"where do you\"\nq3 - read: past - the page's cue: \"tell me about the last time\""
}

Reply:

{
 "task": "meter",
 "headline": "Your questions about the book swap shelf in the office kitchen stay grounded in real life, not opinions.",
 "questions": [
  {
   "q": "q1",
   "read": "past",
   "why": "Because it points to a specific moment that really took place, people on your floor need to recall facts rather than offer a kind guess.",
   "rewrite": ""
  },
  {
   "q": "q2",
   "read": "today",
   "why": "Knowing where they keep books now is a solid opening, though asking about a specific last time would make it stronger.",
   "rewrite": "Where did you put the last book you finished reading?"
  },
  {
   "q": "q3",
   "read": "past",
   "why": "Asking about an actual time they lent a book invites a specific memory instead of a polite guess.",
   "rewrite": ""
  }
 ],
 "verdict": "straight",
 "start_with": "q2",
 "listen_for": "A real answer names a particular book, where it ended up, or someone they lent it to. A kind answer offers praise for the shelf, a vague maybe, or a promise to join in.",
 "closing_ask": "Would you be up for adding one book to the shelf in its first week?"
}

A pottery taster evening - some kind lies likely (flattery, someday, past, usually)

Input:

{
 "task": "meter",
 "idea": "a beginners' pottery evening at my studio",
 "people": "friends who have said they want to try pottery",
 "questions": "q1: My idea is a relaxed taster evening - does that sound fun?\nq2: How much would you pay for a taster evening?\nq3: Have you booked a class or workshop for yourself before?\nq4: What usually stops you from trying something new?",
 "hoped_ask": "come to a free trial evening",
 "facts": "q1 - read: flattery - the page's cue: \"sound fun\"\nq2 - read: someday - the page's cue: \"would\"\nq3 - read: past - chosen by you\nq4 - read: usually - the page's cue: \"usually\""
}

Reply:

{
 "task": "meter",
 "headline": "Some questions about your beginners' pottery evening ask about real life, others open the door to a polite yes.",
 "questions": [
  {
   "q": "q1",
   "read": "flattery",
   "why": "Asking if your idea sounds fun makes it easy for a friend to say yes just to be kind.",
   "rewrite": "What was the last taster evening you went to?"
  },
  {
   "q": "q2",
   "read": "someday",
   "why": "Asking what a friend would pay invites a guess about imagined spending, not a figure tied to money they have actually spent.",
   "rewrite": "What was the last taster evening you paid for?"
  },
  {
   "q": "q3",
   "read": "past",
   "why": "Asking whether a friend has booked a class before asks about something real that already happened, not a guess or a compliment.",
   "rewrite": ""
  },
  {
   "q": "q4",
   "read": "usually",
   "why": "Asking what usually stops a friend from trying something new draws a general impression of their habits rather than a specific real moment.",
   "rewrite": "What was the last new activity you held back from trying?"
  }
 ],
 "verdict": "mixed",
 "start_with": "q1",
 "listen_for": "A real answer points to a pottery evening they already paid for or attended, or a clear reason nothing new happened. A kind answer offers only praise, a maybe, or a loose promise.",
 "closing_ask": "Would you like to come to a free trial evening and see how it feels?"
}

Build the input yourself

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

const K = MeterKit;
const form = K.form({ idea: "a monthly plant swap in the community hall", people: "neighbours on our street", ask: "", questions: [
  { text: "Do you like the idea of a plant swap?" },             // no read: the page reads it from its cue words
  { text: "Is there anywhere near here to swap plants?", read: "today" }] });   // no cue in it, so you pick the read
const built = K.buildInput(form);       // built.facts holds each question's read, the verdict and the starting question
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.script(built, reply)                // the questions to take along: each rewrite, else the original, then the closing ask

Form keys: idea, people, ask (optional) and up to eight questions, each with text and an optional read (flattery, pitch, someday, usually, today or past). A question with no cue word and no read stops buildInput until you pick one.

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://kind-lie-meter.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":"kind-lie-meter"}' | 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": "meter", "idea": "a monthly plant swap in the community hall", "people": "neighbours on our street", "questions": "...", "hoped_ask": "nothing listed", "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 kind-lie-meter:<task>:<inputHash>:a<attempt> (the one task is meter), 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="kind-lie-meter:meter:$(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: kind-lie-meter:meter:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json