← On a Field
API

Drive On a Field from your own code

The one metered task, grant, is available over HTTP. It takes what someone stands for (one to three lines), one to three everyday quirks, and optionally an everyday object and a favourite colour, and returns mock arms: a field tincture, an ordinary (the band), a charge and its count, a blazon, a short plain-English motto and, for each part, the line of the input it came from.

The division of labour first: the page decides the mechanics; the model does the choosing. The page works out the tincture named by the colour words (colour_tincture) and the number of charges (one per quirk, charge_count), and after the reply it re-checks the rule of tincture (metal on colour, colour on metal), writes the blazon itself for the same arms and confirms every quote is one of the input lines. Mock arms made for fun, not a grant from any heraldic authority.

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":"on-a-field"}), 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, values, quirks, charge_count) 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 grant is metered and needs 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 rules

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

FieldRequiredWhat it holds
taskyesAlways grant.
holderyesWho the arms are for, up to 40 characters; you when empty.
valuesyesOne to three numbered lines (1. kindness): what they stand for. Line 1 sets the field, line 2 the ordinary.
quirksyesOne to three numbered lines: everyday quirks. One charge per quirk; line 1 picks the charge unless the object does.
objectyesAn everyday object, or none.
colouryesA favourite colour in their words, or none.
colour_tinctureyesThe tincture the colour words name (gold is or, green is vert...), or none. Build it with ArmsKit.buildInput.
charge_countyesThe number of quirks, as a string.
retry_notenoOnly on a reformat retry: why the previous reply was unusable.

Vocabulary. Tinctures: metals or, argent; colours gules, azure, vert, purpure, sable. Ordinaries: none, fess, pale, bend, chevron, cross, saltire, chief. Charges: mullet, roundel, lozenge, crescent, heart, key, cup, book, tree, bell, anchor. One value line means no ordinary; the ordinary and the charges must each contrast with what they lie on (the rule of tincture).

The reply

One JSON object, no prose around it:

{"task": "grant", "headline": "...", "arms": {"field": "azure", "ordinary": "fess", "ordinary_tincture": "or", "charge": "bell", "charge_count": 2, "charge_tincture": "gules"}, "blazon": "Azure, on a fess Or two bells Gules", "reasons": [{"part": "field", "from": "...", "because": "..."}], "motto": "...", "motto_from": "..."}
FieldWhat it holds
headlineOne sentence describing the arms in plain colour words.
armsThe six choices, ids from the vocabulary; ordinary_tincture is none when there is no ordinary.
blazonThe arms in heraldic shorthand, for example Vert, two books in pale Or.
reasons[]Three entries - field, ordinary, charge - each with from (an exact input line) and because.
motto / motto_fromTwo to six plain-English words built from one input line, and that line.

The page reads the reply with ReplyKit.normalize and checks it with ReplyKit.check, which lists, under "The page's check", a field that ignores the colour, an ordinary on a one-value shield, a charge count that is not one per quirk, a break of the rule of tincture, a blazon that differs from the page's own, a quote that is not one of the input lines, and motto words not drawn from its line.

Worked examples - every sample

These are the free samples on the page: the exact input the page sends and the reply that replays for free. Together they cover a plain field and a field with a band, a field taken from a favourite colour, a charge taken from an object, and two and three charges.

The late-night reader - Vert, two books in pale Or

Input:

{
 "task": "grant",
 "holder": "me",
 "values": "1. kindness",
 "quirks": "1. reads in bed far too late\n2. names every houseplant",
 "object": "a battered paperback",
 "colour": "dark green",
 "colour_tincture": "vert",
 "charge_count": "2"
}

Reply:

{
 "task": "grant",
 "headline": "Your arms show a green field with two gold books.",
 "arms": {
  "field": "vert",
  "ordinary": "none",
  "ordinary_tincture": "none",
  "charge": "book",
  "charge_count": 2,
  "charge_tincture": "or"
 },
 "blazon": "Vert, two books in pale Or",
 "reasons": [
  {
   "part": "field",
   "from": "dark green",
   "because": "Green is your colour, so it becomes the field itself."
  },
  {
   "part": "ordinary",
   "from": "none",
   "because": "With just one line to work from, a plain field keeps the focus on the charges."
  },
  {
   "part": "charge",
   "from": "a battered paperback",
   "because": "A battered paperback is a book at heart, so it stands as the charge for reading and learning."
  }
 ],
 "motto": "Be kind",
 "motto_from": "kindness"
}

Sam, always early - Azure, on a fess Or two bells Sable

Input:

{
 "task": "grant",
 "holder": "Sam",
 "values": "1. showing up for people\n2. fairness\n3. a good cup of tea",
 "quirks": "1. always early\n2. keeps a spare umbrella by the door",
 "object": "none",
 "colour": "none",
 "colour_tincture": "none",
 "charge_count": "2"
}

Reply:

{
 "task": "grant",
 "headline": "Sam gets a blue shield with a gold band across the middle and two black bells on it.",
 "arms": {
  "field": "azure",
  "ordinary": "fess",
  "ordinary_tincture": "or",
  "charge": "bell",
  "charge_count": 2,
  "charge_tincture": "sable"
 },
 "blazon": "Azure, on a fess Or two bells Sable",
 "reasons": [
  {
   "part": "field",
   "from": "showing up for people",
   "because": "Showing up for people is pretty much what loyalty means, so the field is blue."
  },
  {
   "part": "ordinary",
   "from": "fairness",
   "because": "Fairness is what balance means, so there's a band straight across the middle."
  },
  {
   "part": "charge",
   "from": "always early",
   "because": "Being always early is simply timekeeping, and that same steady habit is why the two charges are black."
  }
 ],
 "motto": "To show up for people",
 "motto_from": "showing up for people"
}

The tinkerer - Or, on a fess Gules three anchors Or

Input:

{
 "task": "grant",
 "holder": "me",
 "values": "1. curiosity\n2. being useful",
 "quirks": "1. fixes things with tape\n2. asks too many questions\n3. never finishes a crossword",
 "object": "my bicycle",
 "colour": "orange",
 "colour_tincture": "none",
 "charge_count": "3"
}

Reply:

{
 "task": "grant",
 "headline": "Your shield is gold, crossed by a red band, with three gold anchors on it.",
 "arms": {
  "field": "or",
  "ordinary": "fess",
  "ordinary_tincture": "gules",
  "charge": "anchor",
  "charge_count": 3,
  "charge_tincture": "or"
 },
 "blazon": "Or, on a fess Gules three anchors Or",
 "reasons": [
  {
   "part": "field",
   "from": "curiosity",
   "because": "Curiosity didn't match any of the colours' meanings, so gold, the first in line, was used instead."
  },
  {
   "part": "ordinary",
   "from": "being useful",
   "because": "None of the band shapes matched being useful, so the first one in line, a band across the middle, was used instead."
  },
  {
   "part": "charge",
   "from": "fixes things with tape",
   "because": "Fixing things with tape makes you someone people can rely on, which is just what the anchor stands for."
  }
 ],
 "motto": "Be curious",
 "motto_from": "curiosity"
}

Build the input yourself

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

const K = ArmsKit, R = ReplyKit;
const built = K.buildInput(K.form({ holder: "Sam", value1: "showing up for people", value2: "fairness",
  quirk1: "always early", quirk2: "keeps a spare umbrella by the door", object: "", colour: "" }));  // built.ok is false, with built.note, until a value and a quirk are given
const wire = K.toWire(built.input);   // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output));
// R.check(reply, built).notes                        // the page's re-check of the grant
// K.blazon(R.fixed(reply, built))                    // the page's own blazon for the same arms

Form keys: holder, value1-value3, quirk1-quirk3, object and colour, each up to 80 characters (40 for the holder).

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://on-a-field.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":"on-a-field"}' | 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": "grant", "holder": "Sam", "values": "1. ...", "quirks": "1. ...", "object": "none", "colour": "none", "colour_tincture": "none", "charge_count": "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 on-a-field:<task>:<inputHash>:a<attempt> (the one task is grant), 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="on-a-field:grant:$(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: on-a-field:grant:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json