← Rent Free
API

Drive Rent Free from your own code

The one metered task, sort-loops, is available over HTTP. It takes 2 to 8 loose ends - unfinished things that keep popping back into your head - each with a name, where it stands, how often it pops up, the size of its next step, whether it is up to you, whether you still want it done and whether the next step is written down, plus an optional focus (what you would like more head space for). It returns a plain-words reason for each loose end's call and one worded step for those to close now, park or let go.

The division of labour first: the page decides; the model explains. The page scores each loose end's rent (how hard it nags, 0-6), picks its call and the first move before the run, and sends all of it in facts. The model copies each call verbatim and never changes one.

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":"rent-free"}), 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, loops, focus, 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 calls

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

FieldRequiredWhat it holds
taskyesAlways sort-loops.
loopsyesOne numbered line per loose end (2 to 8): name | where it stands: ... | pops into your head: ... | next step: ... | up to you: ... | still want it done: ... | next step written down: .... Pops up is one of Rarely; Sometimes; Often. Next step: Under 2 minutes; Under an hour; Longer than that; Not sure what it is. Up to you: Up to me; Partly up to me; Waiting on someone or something. Still want it: Yes; Not sure; No. Written down: Only in my head; Written down where I will see it.
focusyesWhat you would like more head space for, up to 120 characters, or not given.
factsyesThe page's lines: per loose end its rent (0-6) with its three parts and its verdict; then the count of each verdict; then the first move.
retry_notenoOnly on the page's one reformat retry.

Rent is pops-up (Often 4, Sometimes 2, Rarely 0) plus 1 when the next step is only in your head plus 1 when the next step is not known. The call, first match wins: still want it No is let-go; a next step under 2 minutes that is up to you is close-now; a written-down, known next step that does not pop up often is leave-open; everything else is park. The first move is the first close-now in list order, else the park with the most rent (ties to the earlier one), else the first let-go, else none.

The reply

{"task": "sort-loops", "headline": "...", "loops": [{"name": "...", "verdict": "park", "why": "...", "step": "..."}], "focus_fit": "...", "closing": "..."}
FieldWhat it holds
headlineAt most 22 words summing up the sort; counts match facts.
loops[]One per input loose end, same order and exact names: verdict copied from facts, why (at most 45 words, from that loose end's own answers) and step (at most 18 words: the small action for close-now, the next step to write down for park, a first-person line for let-go; empty for leave-open).
focus_fitAt most 45 words on how the moves would free head space for the focus; empty when it is not given.
closingOne sentence naming the first move; never claims anything is done.

The page then checks the reply (LoopKit.check) - each call against its own, every loose end present, steps present only where wanted, time words, contact-like details or posting somewhere public, "already done" claims, loose ends it did not list, internal ids in prose, a focus line with no focus, a closing that misses the first move, and word limits - and lists any disagreement under "The page's check". The "what to do" line for each call is fixed text on the page, not model output.

Worked examples - all four calls

These are the 3 free samples on the page: the exact input the page sends and the reply that replays for free. Together they cover all four calls.

Five loose ends, one quick win - calls: close it now, park it, leave it open, let it go

Input:

{
 "task": "sort-loops",
 "loops": "1. Reply to the plumber's quote | where it stands: the quote looks fine, I just need to say yes | pops into your head: Often | next step: Under 2 minutes | up to you: Up to me | still want it done: Yes | next step written down: Only in my head\n2. Return the hiking boots | where it stands: the box is by the door, the receipt is in my inbox | pops into your head: Sometimes | next step: Under an hour | up to you: Up to me | still want it done: Yes | next step written down: Only in my head\n3. Learn the ukulele | where it stands: it is still in its case | pops into your head: Rarely | next step: Not sure what it is | up to you: Up to me | still want it done: No | next step written down: Only in my head\n4. Sort the photo folders | where it stands: not given | pops into your head: Rarely | next step: Longer than that | up to you: Up to me | still want it done: Yes | next step written down: Written down where I will see it\n5. Hear back about the flat viewing | where it stands: the agent said they would ring | pops into your head: Often | next step: Not sure what it is | up to you: Waiting on someone or something | still want it done: Yes | next step written down: Only in my head",
 "focus": "my pottery class",
 "facts": "1. Reply to the plumber's quote: rent 5 of 6 (pops 4, head-only 1, unknown-step 0); verdict close-now\n2. Return the hiking boots: rent 3 of 6 (pops 2, head-only 1, unknown-step 0); verdict park\n3. Learn the ukulele: rent 2 of 6 (pops 0, head-only 1, unknown-step 1); verdict let-go\n4. Sort the photo folders: rent 0 of 6 (pops 0, head-only 0, unknown-step 0); verdict leave-open\n5. Hear back about the flat viewing: rent 6 of 6 (pops 4, head-only 1, unknown-step 1); verdict park\ncounts: close-now 1, park 2, leave-open 1, let-go 1 (of 5 loose ends)\nfirst move: Reply to the plumber's quote"
}

Reply:

{
 "task": "sort-loops",
 "headline": "Of your five loose ends: one to close now, two to park, one to leave open, and one to let go.",
 "loops": [
  {
   "name": "Reply to the plumber's quote",
   "verdict": "close-now",
   "why": "This pops into your head often, the next step takes under 2 minutes, it's up to you, you still want it done, and it's only in your head.",
   "step": "Say yes to the plumber's quote."
  },
  {
   "name": "Return the hiking boots",
   "verdict": "park",
   "why": "This only comes to mind sometimes, the next step takes under an hour, it's up to you, you still want it done, and it's only in your head, not written down anywhere else.",
   "step": "Take the box of boots and the receipt from your inbox to return them."
  },
  {
   "name": "Learn the ukulele",
   "verdict": "let-go",
   "why": "You said you no longer want this done, so letting it go fits - even though it still pops into your head rarely and sits with no clear next step worked out.",
   "step": "I'm choosing to let go of learning the ukulele."
  },
  {
   "name": "Sort the photo folders",
   "verdict": "leave-open",
   "why": "This only pops into your head rarely, you still want it done, and the next step - which will take more than an hour - is already written down where you will see it, so nothing more needs doing with it.",
   "step": ""
  },
  {
   "name": "Hear back about the flat viewing",
   "verdict": "park",
   "why": "This pops into your head often, you still want it done, you are waiting on the agent to get back to you, the next step is not clear, and it is only in your head.",
   "step": "Note that you are waiting for the agent to ring back about the viewing."
  }
 ],
 "focus_fit": "Saying yes to the plumber, returning the boots, noting what you're waiting on with the flat viewing, and letting go of the ukulele would all clear head space for your pottery class once you do them.",
 "closing": "Reply to the plumber's quote before anything else on this list."
}

Half-done projects at home - calls: park it, leave it open, let it go

Input:

{
 "task": "sort-loops",
 "loops": "1. Finish the knitted scarf | where it stands: about a hand's width left | pops into your head: Sometimes | next step: Under an hour | up to you: Up to me | still want it done: Not sure | next step written down: Written down where I will see it\n2. Fix the wobbly chair | where it stands: not given | pops into your head: Often | next step: Not sure what it is | up to you: Up to me | still want it done: Yes | next step written down: Only in my head\n3. Thank-you card for Priya | where it stands: the card is written, it needs a stamp | pops into your head: Sometimes | next step: Under 2 minutes | up to you: Partly up to me | still want it done: Yes | next step written down: Only in my head\n4. Bookkeeping course | where it stands: paid for it, three videos in | pops into your head: Often | next step: Longer than that | up to you: Up to me | still want it done: No | next step written down: Written down where I will see it",
 "focus": "not given",
 "facts": "1. Finish the knitted scarf: rent 2 of 6 (pops 2, head-only 0, unknown-step 0); verdict leave-open\n2. Fix the wobbly chair: rent 6 of 6 (pops 4, head-only 1, unknown-step 1); verdict park\n3. Thank-you card for Priya: rent 3 of 6 (pops 2, head-only 1, unknown-step 0); verdict park\n4. Bookkeeping course: rent 4 of 6 (pops 4, head-only 0, unknown-step 0); verdict let-go\ncounts: close-now 0, park 2, leave-open 1, let-go 1 (of 4 loose ends)\nfirst move: Fix the wobbly chair"
}

Reply:

{
 "task": "sort-loops",
 "headline": "Of your four loose ends, two are ones to park, one you can leave open, and one to let go of.",
 "loops": [
  {
   "name": "Finish the knitted scarf",
   "verdict": "leave-open",
   "why": "You only think about the scarf sometimes, and with a hand's width left to go, you already know the next step is under an hour and it's written down where you'll see it, so it can stay as it is.",
   "step": ""
  },
  {
   "name": "Fix the wobbly chair",
   "verdict": "park",
   "why": "This loose end pops into your head often, and you still want it fixed, but you're not sure what the next step is and it has only lived in your head, so the call is to park it until that step is written down.",
   "step": "Work out the first action for fixing the wobbly chair."
  },
  {
   "name": "Thank-you card for Priya",
   "verdict": "park",
   "why": "The card is already written and only needs a stamp, so the step itself is quick. But it's only partly up to you and it's only been in your head, so you park it instead of closing it now.",
   "step": "Put a stamp on the card for Priya."
  },
  {
   "name": "Bookkeeping course",
   "verdict": "let-go",
   "why": "You no longer want to keep going with the bookkeeping course, even though you paid for it and are three videos in and it pops into your head often, so you can let it go.",
   "step": "I'm choosing to let the bookkeeping course go."
  }
 ],
 "focus_fit": "",
 "closing": "Fix the wobbly chair is the one to start with, so turn to it before the rest of the list."
}

A reply, an inbox and a novel - calls: close it now, park it, leave it open

Input:

{
 "task": "sort-loops",
 "loops": "1. Text Jo back | where it stands: Jo asked if I still want the spare lamp | pops into your head: Often | next step: Under 2 minutes | up to you: Up to me | still want it done: Yes | next step written down: Only in my head\n2. Clear the email inbox | where it stands: about 300 unread | pops into your head: Sometimes | next step: Longer than that | up to you: Up to me | still want it done: Yes | next step written down: Only in my head\n3. Write the novel | where it stands: not given | pops into your head: Rarely | next step: Longer than that | up to you: Up to me | still want it done: Yes | next step written down: Written down where I will see it",
 "focus": "reading again",
 "facts": "1. Text Jo back: rent 5 of 6 (pops 4, head-only 1, unknown-step 0); verdict close-now\n2. Clear the email inbox: rent 3 of 6 (pops 2, head-only 1, unknown-step 0); verdict park\n3. Write the novel: rent 0 of 6 (pops 0, head-only 0, unknown-step 0); verdict leave-open\ncounts: close-now 1, park 1, leave-open 1, let-go 0 (of 3 loose ends)\nfirst move: Text Jo back"
}

Reply:

{
 "task": "sort-loops",
 "headline": "Of three loose ends, one is ready to close it now, one needs a step written down, and one can stay open.",
 "loops": [
  {
   "name": "Text Jo back",
   "verdict": "close-now",
   "why": "This pops into your head often, and because the next step is under 2 minutes and up to you, you can close it now and be done with it.",
   "step": "Text Jo back to say whether you still want the spare lamp."
  },
  {
   "name": "Clear the email inbox",
   "verdict": "park",
   "why": "This pops into your head sometimes, and because the next step is more than an hour and only sits in your head rather than being written down, it is best to park it with a note you can see.",
   "step": "Start clearing about 300 unread emails from your inbox."
  },
  {
   "name": "Write the novel",
   "verdict": "leave-open",
   "why": "This only pops into your head rarely, and the next step is already written down where you will see it, so there is nothing you need to do about it.",
   "step": ""
  }
 ],
 "focus_fit": "Texting Jo back and writing down a step for the email inbox would clear space in your head, leaving more room for reading again.",
 "closing": "Put Text Jo back first, before the others, since that is where to begin."
}

Build the input yourself

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

const K = LoopKit;
const form = K.form({ focus: "", loops: [
  { name: "Reply to the plumber", note: "just need to say yes", pops: "often", step: "tiny", hands: "yes", want: "yes", written: "no" },
  { name: "Sort the photos", note: "", pops: "rarely", step: "big", hands: "yes", want: "yes", written: "yes" }] });
const built = K.buildInput(form);       // built.facts holds each loose end's rent, call and the first move
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, K.title(form))    // the sort 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://rent-free.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":"rent-free"}' | 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": "sort-loops", "loops": "1. Reply to the plumber's quote | where it stands: ... | ...", "focus": "not given", ...}
# - 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 rent-free:<task>:<inputHash>:a<attempt> (the one task is sort-loops), 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="rent-free:sort-loops:$(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: rent-free:sort-loops:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json