← Not For Sale
Listing API
Tokens

Drive Not For Sale from your own code

The one metered task, list, is available over HTTP. It takes the name of a place someone would never sell, its kind, a voice for the agent and a few true facts (one per line), and returns a status (fixer-upper, sold as is, or move-in ready), every fact sorted into one slot with a listing line and its honest truth, a headline, a short description, a rider for the sign, a closing line and a fact box.

The division of labour first: the page owns the rows, the keyword guess and the status rule; the model owns the judgement and the words; the page re-checks the reply. All three come from listkit.js, which you can run yourself (see Build the input yourself). It is a writing game: nothing is for sale, priced or valued.

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":"not-for-sale"}), 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, place, kind, facts, voice, presort) 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 a listing 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, the slots and the status

Every run sends task: list; a missing or different one is still answered as list. Each fact gets one slot, checked in a fixed order:

  1. flaw - something about the place is wrong, worn, broken, stuck, missing, uncomfortable or hard to use (peeling, rusty, damp, creaky, cold, windy, noisy, cramped, steep, or works only one way).
  2. history - a year, an age, a past event, or someone who made, used or did something there before.
  3. view - what you can see, hear or smell from it, its light, or the sky, water or weather around it.
  4. neighbours - people, animals or other places nearby that are not part of the place itself.
  5. feature - everything else: what it is made of, what it holds or fits, what you do there.

The verdict (shown on the page as the status) then follows the flaws, first match wins: fixer_upper if three or more facts are flaw; otherwise as_is if one or two are; otherwise move_in. The page computes the same rule over the reply's own slots (ListKit.verdictFor) and flags a reply that disagrees.

The input

Every value is a string (objects are never sent). app.js builds it with ListKit.buildInput(form).input.

FieldRequiredWhat it holds
taskyesAlways list.
placeyesWhat the person calls the place, or this place. Kept to its first 120 characters.
kindyesStarts with home, room, outdoor, building, vehicle or thing, then a short description in brackets.
factsyesNumbered lines, 1. ..., one true fact per line in the person's words. At most 16 facts are sent; a fact over 220 characters keeps its start and end around [...].
voiceyesStarts with glossy, plain or classified, then a short description in brackets.
presortyesThe page's keyword guess, one line per fact: <n> | <slot>. Only a guess; the slot rules win.
clip_notenoWhat the page shortened, or absent.
retry_notenoSent only on the page's one reformat retry.

The reply

The reply is one JSON object in job.output.output. The page parses it with ListKit.parseReply and ListKit.normalize (a cut-off stream is closed by ListKit.closeJson and shown as "N of 7 sections recovered").

{"task": "list", "verdict": "fixer_upper | as_is | move_in", "verdict_reason": "...", "title": "...", "summary": "...",
 "facts": [{"n": 1, "slot": "flaw | history | view | neighbours | feature", "listing": "...", "truth": "..."}],
 "listing": {"headline": "...", "description": "...", "rider": "...", "closing": "..."},
 "fact_box": [{"label": "...", "value": "...", "from": 1}]}
FieldRule
factsExactly one entry per numbered fact, same numbers and order. listing: the fact as a listing line in the chosen voice, at most 16 words, a flaw covered with an agent phrase but never denied. truth: the same fact said plainly, at most 14 words. Neither adds anything the fact does not say.
listingA headline of at most 9 words; a description of 3 to 5 sentences and at most 90 words that uses the place's name once; a rider of 2 to 4 words for the sign; a closing line of at most 12 words on why it is not for sale.
fact_box3 to 5 rows: a listing-sheet label, a value that only restates one fact (numbers copied exactly), and from, that fact's number.
summaryTwo sentences: the counts per slot (zeros left out), then what the headline leans on.

The page then checks the reply against your facts (ListKit.check): every fact listed exactly once with both lines, the status against the reply's own slots, the keyword guess where it differs, the length rules, no fact-box number its fact does not give, no number anywhere that your facts do not give, no price, offer or contact detail, no "for sale", no property website or brand, and no internal field names in the prose.

Worked examples, one per status

These are the three free samples on the page: the exact input and a stand-in model's reply, unedited.

Window seat - move-in ready

Input:

{
 "task": "list",
 "place": "The window seat on the landing",
 "kind": "room (one room or one corner of a home)",
 "facts": "1. Built into the wall when the house was finished in 1931\n2. Looks straight down the street to the hills\n3. The cushion is a folded quilt in blue and white squares\n4. Fits one grown-up with a book and a blanket\n5. The kids across the street wave from their porch\n6. Rain on the glass is the best sound in the house",
 "voice": "plain (a friendly local agent, plain words, warm and a little dry)",
 "presort": "1 | history\n2 | view\n3 | feature\n4 | feature\n5 | neighbours\n6 | view"
}

Reply:

{
 "task": "list",
 "verdict": "move_in",
 "verdict_reason": "You're getting a move-in ready listing because none of your facts point to something broken or worn.",
 "title": "A Move-In Ready Window Seat With An Outlook",
 "summary": "You have one fact about its past, two about the sights and sounds, two about what the place offers, and one about the kids across the street. The headline leans on when it was built and the outlook to the hills.",
 "facts": [
  {
   "n": 1,
   "slot": "history",
   "listing": "Built into the wall when the house was finished, back in 1931.",
   "truth": "It was built into the wall when the house was finished in 1931."
  },
  {
   "n": 2,
   "slot": "view",
   "listing": "A straight line down the street to the hills.",
   "truth": "It looks straight down the street to the hills."
  },
  {
   "n": 3,
   "slot": "feature",
   "listing": "A folded quilt in blue and white squares serves as the cushion.",
   "truth": "The cushion is a folded quilt, blue and white squares."
  },
  {
   "n": 4,
   "slot": "feature",
   "listing": "Room for one grown-up, a book and a blanket.",
   "truth": "It fits one grown-up with a book and a blanket."
  },
  {
   "n": 5,
   "slot": "neighbours",
   "listing": "Across the street, the kids wave from their porch.",
   "truth": "The kids across the street wave from their porch."
  },
  {
   "n": 6,
   "slot": "view",
   "listing": "The best sound in the house: rain on the glass.",
   "truth": "Rain on the glass is the best sound in the house."
  }
 ],
 "listing": {
  "headline": "Built in 1931, with an outlook to the hills",
  "description": "The window seat on the landing was built into the wall when the house was finished in 1931. It looks straight down the street to the hills, with rain on the glass as the best sound in the house. The cushion is a folded quilt in blue and white squares, with room for one grown-up, a book and a blanket. The kids across the street wave from their porch.",
  "rider": "Built in 1931",
  "closing": "The hills, the rain and the quilt make it irreplaceable."
 },
 "fact_box": [
  {
   "label": "Year built",
   "value": "1931",
   "from": 1
  },
  {
   "label": "Outlook",
   "value": "Down the street to the hills",
   "from": 2
  },
  {
   "label": "Cushion",
   "value": "Blue and white quilt",
   "from": 3
  },
  {
   "label": "Seats",
   "value": "One grown-up",
   "from": 4
  },
  {
   "label": "Across the street",
   "value": "Kids wave from the porch",
   "from": 5
  }
 ]
}

Harbour bench - sold as is

Input:

{
 "task": "list",
 "place": "The bench at the end of the harbour wall",
 "kind": "outdoor (a bench, a garden, a spot in a park or on a beach)",
 "facts": "1. Green paint, peeling at both ends\n2. You can watch the fishing boats come in\n3. Someone carved two sets of initials into the left armrest in 1988\n4. Three gulls patrol it for chips\n5. Seats three, or two and a picnic\n6. The wind in winter could take your hat off",
 "voice": "glossy (a breathless luxury agent, big adjectives, one exclamation mark at most)",
 "presort": "1 | flaw\n2 | view\n3 | history\n4 | neighbours\n5 | feature\n6 | flaw"
}

Reply:

{
 "task": "list",
 "verdict": "as_is",
 "verdict_reason": "You have two flaws, the peeling paint and the winter wind, which is why this one is sold as is.",
 "title": "The Peeling Green Bench, Sold As Is",
 "summary": "You have two flaws, one history, one view, one neighbour and one feature. The headline leans on the peeling green paint and the views of the fishing boats.",
 "facts": [
  {
   "n": 1,
   "slot": "flaw",
   "listing": "Rare green paintwork, full of character at both ends.",
   "truth": "Green paint is peeling at both ends."
  },
  {
   "n": 2,
   "slot": "view",
   "listing": "Sought-after views of the fishing boats coming in.",
   "truth": "You can watch the fishing boats come in."
  },
  {
   "n": 3,
   "slot": "history",
   "listing": "Boasting two sets of initials, hand-carved into the left armrest in 1988.",
   "truth": "Someone carved two sets of initials into the left armrest in 1988."
  },
  {
   "n": 4,
   "slot": "neighbours",
   "listing": "Three gulls on constant patrol for chips.",
   "truth": "Three gulls patrol it for chips."
  },
  {
   "n": 5,
   "slot": "feature",
   "listing": "Generous seating for three, or two plus a picnic.",
   "truth": "Seats three, or two and a picnic."
  },
  {
   "n": 6,
   "slot": "flaw",
   "listing": "Naturally ventilated, with winter breezes strong enough to catch your hat.",
   "truth": "The wind in winter could take your hat off."
  }
 ],
 "listing": {
  "headline": "Charming Green Bench, Stunning Fishing-Boat Views!",
  "description": "The bench at the end of the harbour wall offers sought-after views of the fishing boats coming in. Its green paintwork is full of character, peeling at both ends. Two sets of initials were hand-carved into the left armrest back in 1988. Three dedicated gulls patrol for chips, and there is generous seating for three, or two and a picnic. Winter brings bracing breezes brisk enough to catch your hat.",
  "rider": "Carved In 1988",
  "closing": "Initials carved into it in 1988 mean this bench is staying put."
 },
 "fact_box": [
  {
   "label": "Exterior",
   "value": "Green paint, peeling at both ends",
   "from": 1
  },
  {
   "label": "Outlook",
   "value": "Fishing boats coming in",
   "from": 2
  },
  {
   "label": "Year Carved",
   "value": "Initials carved in 1988",
   "from": 3
  },
  {
   "label": "Seats",
   "value": "Three, or two and a picnic",
   "from": 5
  },
  {
   "label": "Residents",
   "value": "Three gulls, patrolling for chips",
   "from": 4
  }
 ]
}

First car - fixer-upper

Input:

{
 "task": "list",
 "place": "The 1997 hatchback",
 "kind": "vehicle (a car, a van, a boat, a bike)",
 "facts": "1. Bought with savings from a summer job in 2004\n2. The passenger door only opens from the inside\n3. The heater has two settings: off and volcano\n4. A cassette has been stuck in the tape deck since 2009, and it still plays\n5. Drove it to the coast and back every summer for six years\n6. The back seat folds flat and fits a mattress\n7. Rust has started around the rear wheel arches",
 "voice": "classified (an old newspaper small ad, clipped phrases and a few abbreviations)",
 "presort": "1 | history\n2 | flaw\n3 | feature\n4 | flaw\n5 | history\n6 | feature\n7 | flaw"
}

Reply:

{
 "task": "list",
 "verdict": "fixer_upper",
 "verdict_reason": "You listed four flaws, so this one counts as a fixer-upper.",
 "title": "The 1997 Hatchback: Rust, Heat, Stuck Tape",
 "summary": "You gave four flaws, two pieces of history and one feature for this one. The headline leans on the stuck cassette and the rusty wheel arches.",
 "facts": [
  {
   "n": 1,
   "slot": "history",
   "listing": "Bought w/ summer-job savings, 2004.",
   "truth": "Bought with savings from a summer job, in 2004."
  },
  {
   "n": 2,
   "slot": "flaw",
   "listing": "Pass. door, interior release only - security feature.",
   "truth": "The passenger door only opens from inside."
  },
  {
   "n": 3,
   "slot": "flaw",
   "listing": "Heating: off or volcano - all character, no half measures.",
   "truth": "The heater only has two settings: off and volcano."
  },
  {
   "n": 4,
   "slot": "flaw",
   "listing": "Cassette lodged in deck since 2009 - a permanent soundtrack, still plays.",
   "truth": "A cassette has been stuck in the tape deck since 2009; it still plays."
  },
  {
   "n": 5,
   "slot": "history",
   "listing": "Coastal round-trip, every summer, six years running.",
   "truth": "Driven to the coast and back every summer for six years."
  },
  {
   "n": 6,
   "slot": "feature",
   "listing": "Back seat folds flat - fits a mattress.",
   "truth": "The back seat folds flat and fits a mattress."
  },
  {
   "n": 7,
   "slot": "flaw",
   "listing": "Rust, rear wheel arches - a project for the right owner.",
   "truth": "Rust has started around the rear wheel arches."
  }
 ],
 "listing": {
  "headline": "STUCK CASSETTE SINCE 2009, RUSTY REAR ARCHES",
  "description": "The 1997 hatchback, bought w/ summer-job savings in 2004, has clocked six summers of coastal round-trips. Pass. door opens from inside only, heating runs off or volcano, and rust has set in nr the rear wheel arches. A cassette's been lodged in the tape deck since 2009 and still plays, w/ the back seat folding flat to fit a mattress.",
  "rider": "Stuck cassette, still plays",
  "closing": "Six summers of coastal trips like this aren't for sale."
 },
 "fact_box": [
  {
   "label": "Doors",
   "value": "Opens from inside only",
   "from": 2
  },
  {
   "label": "Heating",
   "value": "Off or volcano setting",
   "from": 3
  },
  {
   "label": "Tape Deck",
   "value": "Cassette stuck since 2009",
   "from": 4
  },
  {
   "label": "Rear Arches",
   "value": "Rust has started",
   "from": 7
  },
  {
   "label": "Seats",
   "value": "Fold flat, fit mattress",
   "from": 6
  }
 ]
}

Build the input yourself

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

const K = ListKit;
const form = K.form({ place: "The corner table at the old cafe", kind: "building", voice: "plain",
  facts: "The table wobbles unless you fold a napkin under one leg\nYou can see the whole square from it\nThe owner's grandmother painted the mural behind it in 1962" });
K.rows(form);                                    // one row per fact, with the page's slot guess
K.expected(K.rows(form));                        // {verdict: "as_is", why: "1 flaw on file"} - the page's rule over its guesses
K.draft(K.rows(form), form);                     // the free draft sheet: stock agent phrases, your words as the truth
K.decode("Charming and cozy, needs TLC");        // listing-speak, decoded
const built = K.buildInput(form);
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)                            // the listing and every fact as Markdown

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://not-for-sale.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":"not-for-sale"}' | 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": "list", "place": "The bench at the end of the harbour wall", "kind": "outdoor (a bench, a garden, a spot in a park or on a beach)", "facts": "1. Green paint, peeling at both ends\n2. You can watch the fishing boats come in\n3. Someone carved two sets of initials into the left armrest in 1988\n4. Three gulls patrol it for chips\n5. Seats three, or two and a picnic\n6. The wind in winter could take your hat off", "voice": "glossy (a breathless luxury agent, big adjectives, one exclamation mark at most)", "presort": "1 | flaw\n2 | view\n3 | history\n4 | neighbours\n5 | feature\n6 | flaw"}
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 not-for-sale:<task>:<inputHash>:a<attempt> (the one task is list), 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="not-for-sale:list:$(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: not-for-sale:list:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json