← Link a Day
Link a Day API
Tokens

Drive Link a Day from your own code

The one metered task, chain, is available over HTTP. It takes a group and its December as the page's 24-day layout - every day from 1 to 24 December with its kind, its minute limit, its share of the budget and any note - and returns two or three threads (recurring rituals, each with a purpose) and 24 links: one small thing to do together per day, with minutes, an estimated cost, what to get ready ahead and who starts it, plus the links that are safe to skip and how to pick up after a missed day.

The division of labour first: the page owns the calendar and the rule; the model owns the rituals and the words; the page re-checks the reply. The day kinds, the limits, the budget shares and the verdict come from chainkit.js, which you can run yourself (see Build the input yourself).

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":"link-a-day"}), 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, crew_type, days, figures, page_verdict, open_days) 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: fitting the chain 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 verdict rule

Every run sends task: chain (fit the chain); a missing or different one is still answered as chain. Each day gets one kind in a fixed order: planned (a date on the planned list), day off (a weekend day when weekends are off), busy (a busy date or busy weekday), free (any other weekend day), else an ordinary weekday. The verdict follows a fixed rule on the light days - busy, planned or off:

  1. roomy - up to 6 light days. Three threads, no link marked safe to skip.
  2. snug - 7 to 12. Three threads, exactly one link safe to skip.
  3. packed - 13 or more. Two threads, exactly two links safe to skip.

The reply must copy page_verdict; the page flags any reply that does not.

The run input

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

FieldRequiredWhat it holds
taskyesAlways chain.
crew_typeyesfamily with kids, couple, flatmates, friends or remote team.
crewnoWho is in the group, in the person's words (at most 200 characters), or not described.
likes / avoidnoWhat the group enjoys (600 characters) and what to leave out (400), or nothing listed.
tone / faithnocosy, playful, reflective or mixed; secular or include faith moments.
currency / budgetnoThe money symbol (or none) and the whole chain's budget as a whole number.
daysyes24 lines: <day> | <date> | <kind> | up to <N> min | up to <M> | <note>, kinds quick, standard, big, pinned, rest.
figuresyesThe page's counts of each kind of day, the light days, the budget and the verdict.
page_verdictyesroomy, snug or packed, from the rule above.
open_daysyesThe busy and ordinary weekdays, the only days that may be marked safe to skip.
clip_notenoWhat the page shortened (long text, long day notes), 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 ChainKit.parseReply and ChainKit.normalize (a cut-off stream is closed by ChainKit.closeJson and shown as "N of 7 sections recovered", with starter links on the days that did not arrive).

{"task": "chain", "verdict": "roomy | snug | packed", "title": "...", "summary": "...",
 "threads":  [{"id": "a", "name": "...", "purpose": "..."}],
 "links":    [{"day": 1, "thread": "a", "title": "...", "do": "...", "minutes": 5, "cost": 0, "prep": "", "lead": "..."}],
 "skip_ok":  [3],
 "keep_going": "..."}
FieldContract
threadsThree (roomy, snug) or two (packed), ids a, b, c; each holds at least four links.
linksExactly 24, day 1 to 24 in order. minutes from 1 to the day's limit, cost from 0 to the day's share; planned days and days off cost nothing and need no prep; day 24 looks back over the chain.
skip_okBy verdict (above), only from open_days, never day 24.
keep_goingOne sentence on picking up after a missed day.

The page then checks the reply against its own layout (ChainKit.check): the verdict, the thread count, missing or doubled days, unknown thread ids, minutes and costs over each day's limit, the budget total, free planned days and days off, the skip count and which days were skipped, field names leaking into the wording, weekdays or dates in the wording, faith content in a secular chain and anything on the avoid list. What to get ready ahead is added up by the page from the links' prep, not by the model.

Worked examples, one per verdict

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

A couple with lots of free evenings - roomy

Input:

{
 "task": "chain",
 "crew_type": "couple",
 "crew": "the two of us, both working from home",
 "likes": "board games, cooking something new, old films, walks by the river, writing letters",
 "avoid": "alcohol, long phone calls",
 "tone": "cosy",
 "faith": "secular",
 "currency": "£",
 "budget": "40",
 "days": "1 | Tue 1 Dec | standard | up to 25 min | up to 2 |\n2 | Wed 2 Dec | standard | up to 25 min | up to 2 |\n3 | Thu 3 Dec | standard | up to 25 min | up to 2 |\n4 | Fri 4 Dec | standard | up to 25 min | up to 2 |\n5 | Sat 5 Dec | big | up to 60 min | up to 3 |\n6 | Sun 6 Dec | big | up to 60 min | up to 3  ... (24 lines, 1314 characters)",
 "figures": "2 busy days; 16 ordinary weekdays; 5 free weekend days; 1 day with something planned; 0 days off; 3 light days (busy, planned or off); budget £40; verdict roomy",
 "page_verdict": "roomy",
 "open_days": "1, 2, 3, 4, 7, 8, 9, 10, 11, 14, 15, 16, 17, 18, 21, 22, 23, 24"
}

Reply (the first six links shown):

{
 "task": "chain",
 "verdict": "roomy",
 "title": "Twenty-four little things to share together",
 "summary": "This chain holds 2 busier evenings, 16 ordinary weekdays, 5 free weekend days and 1 day with something already planned, with 0 days off and a budget of £40 for the whole chain. In plain words, there is plenty of space around each day, so the chain should feel light and easy to keep going.",
 "threads": [
  {
   "id": "a",
   "name": "Games and Films",
   "purpose": "gives you something playful to share and look forward to together"
  },
  {
   "id": "b",
   "name": "New Recipes",
   "purpose": "lets you make something new together and enjoy what you have made"
  },
  {
   "id": "c",
   "name": "Walks and Letters",
   "purpose": "gives you quiet moments to notice the days and put something into words"
  }
 ],
 "links": [
  {
   "day": 1,
   "thread": "a",
   "title": "Pick a board game",
   "do": "Choose a board game together and play through one full round.",
   "minutes": 20,
   "cost": 0,
   "prep": "",
   "lead": "the two of you"
  },
  {
   "day": 2,
   "thread": "c",
   "title": "Write a short letter",
   "do": "Write each other a short letter and put it aside unopened.",
   "minutes": 15,
   "cost": 0,
   "prep": "",
   "lead": "the two of you"
  },
  {
   "day": 3,
   "thread": "b",
   "title": "Try a new recipe",
   "do": "Pick a recipe neither of you has made before and cook it together.",
   "minutes": 25,
   "cost": 2,
   "prep": "ingredients for one new recipe",
   "lead": "the two of you"
  },
  {
   "day": 4,
   "thread": "c",
   "title": "Walk by the river",
   "do": "Walk together along the river and each name one thing you notice.",
   "minutes": 25,
   "cost": 0,
   "prep": "",
   "lead": "the two of you"
  },
  {
   "day": 5,
   "thread": "b",
   "title": "Cook a full new dish",
   "do": "Choose a bigger recipe you have not tried and cook it together from start to finish.",
   "minutes": 55,
   "cost": 3,
   "prep": "ingredients for the recipe",
   "lead": "the two of you"
  },
  {
   "day": 6,
   "thread": "a",
   "title": "Watch an old film",
   "do": "Pick an old film you both love and watch it all the way through together.",
   "minutes": 60,
   "cost": 0,
   "prep": "",
   "lead": "the two of you"
  },
  "... 18 more links, same shape"
 ],
 "skip_ok": [],
 "keep_going": "If a day gets missed, pick it up together next time you are both free and carry on from there."
}

A family with clubs and a school play - snug

Input:

{
 "task": "chain",
 "crew_type": "family with kids",
 "crew": "two adults and two kids, 6 and 9",
 "likes": "drawing, baking, silly songs, walks after dark, building with blocks",
 "avoid": "nuts, anything loud after 8pm",
 "tone": "playful",
 "faith": "include faith moments",
 "currency": "$",
 "budget": "30",
 "days": "1 | Tue 1 Dec | standard | up to 20 min | up to 1 |\n2 | Wed 2 Dec | quick | up to 10 min | up to 1 |\n3 | Thu 3 Dec | standard | up to 20 min | up to 1 |\n4 | Fri 4 Dec | quick | up to 10 min | up to 1 | busy: late shift\n5 | Sat 5 Dec | big | up to 45 min | up to 3 |\n6 | Sun 6 Dec | big | up to 45 min ... (24 lines, 1314 characters)",
 "figures": "9 busy days; 8 ordinary weekdays; 5 free weekend days; 2 days with something planned; 0 days off; 11 light days (busy, planned or off); budget $30; verdict snug",
 "page_verdict": "snug",
 "open_days": "1, 2, 3, 4, 7, 8, 9, 10, 11, 14, 15, 16, 18, 21, 22, 23, 24"
}

Reply (the first six links shown):

{
 "task": "chain",
 "verdict": "snug",
 "title": "Twenty-four small things to share together",
 "summary": "The chain has 9 busy days, 8 ordinary weekdays, 5 free weekend days, 2 days with something planned, 0 days off and a budget of 30. That makes for a fairly full month, with just enough room to enjoy each small ritual without feeling rushed.",
 "threads": [
  {
   "id": "a",
   "name": "Make Together",
   "purpose": "Gives everyone a chance to make something with their hands."
  },
  {
   "id": "b",
   "name": "Silly Together",
   "purpose": "Brings you together through shared laughter and silly fun."
  },
  {
   "id": "c",
   "name": "Quiet Together",
   "purpose": "Gives you space to pause, notice and be thankful."
  }
 ],
 "links": [
  {
   "day": 1,
   "thread": "a",
   "title": "Draw A Link Together",
   "do": "Draw a small picture together for the chain.",
   "minutes": 15,
   "cost": 0,
   "prep": "",
   "lead": "the kids"
  },
  {
   "day": 2,
   "thread": "c",
   "title": "A Quiet Minute Together",
   "do": "Sit together for a quiet minute and each say one good thing about the day.",
   "minutes": 5,
   "cost": 0,
   "prep": "",
   "lead": "everyone"
  },
  {
   "day": 3,
   "thread": "b",
   "title": "Silly Song Swap",
   "do": "Take turns making up a silly song about something that happened today.",
   "minutes": 15,
   "cost": 0,
   "prep": "",
   "lead": "anyone"
  },
  {
   "day": 4,
   "thread": "c",
   "title": "A Short Quiet Pause",
   "do": "Pause together for a short prayer or reading of your choosing, in your own words.",
   "minutes": 5,
   "cost": 0,
   "prep": "",
   "lead": "everyone"
  },
  {
   "day": 5,
   "thread": "a",
   "title": "Bake Something Together",
   "do": "Bake a treat together from start to finish, taking turns with each step.",
   "minutes": 40,
   "cost": 3,
   "prep": "baking ingredients",
   "lead": "everyone"
  },
  {
   "day": 6,
   "thread": "b",
   "title": "Walk And Sing After Dark",
   "do": "Go for a walk together after dark, making up silly songs as you go.",
   "minutes": 40,
   "cost": 0,
   "prep": "",
   "lead": "everyone"
  },
  "... 18 more links, same shape"
 ],
 "skip_ok": [
  9
 ],
 "keep_going": "If a day gets missed, just tear off two links the next day and carry on without worry."
}

A remote team, weekends off - packed

Input:

{
 "task": "chain",
 "crew_type": "remote team",
 "crew": "a remote team of seven across three time zones",
 "likes": "music, show and tell, quick quizzes, sharing photos",
 "avoid": "anything that needs cameras on, food or drink",
 "tone": "mixed",
 "faith": "secular",
 "currency": "none",
 "budget": "0",
 "days": "1 | Tue 1 Dec | standard | up to 15 min | up to 0 |\n2 | Wed 2 Dec | standard | up to 15 min | up to 0 |\n3 | Thu 3 Dec | quick | up to 5 min | up to 0 |\n4 | Fri 4 Dec | standard | up to 15 min | up to 0 |\n5 | Sat 5 Dec | rest | up to 2 min | up to 0 |\n6 | Sun 6 Dec | rest | up to 2 min | up to 0 |\n7  ... (24 lines, 1317 characters)",
 "figures": "8 busy days; 8 ordinary weekdays; 0 free weekend days; 2 days with something planned; 6 days off; 16 light days (busy, planned or off); budget 0 (every link free); verdict packed",
 "page_verdict": "packed",
 "open_days": "1, 2, 3, 4, 7, 8, 9, 10, 14, 15, 16, 17, 21, 22, 23, 24"
}

Reply (the first six links shown):

{
 "task": "chain",
 "verdict": "packed",
 "title": "Twenty-four small moments to share together",
 "summary": "Eight days are busy, eight are ordinary weekdays, none are free weekend days, two already have something planned, and six are days off, with a budget of zero so every link is free. With sixteen of the days already light, planned or off, the chain leaves little room to spare, so the shorter links will help it keep moving.",
 "threads": [
  {
   "id": "a",
   "name": "Team Check-In",
   "purpose": "gives everyone a moment to connect across the time zones"
  },
  {
   "id": "b",
   "name": "Spark Moment",
   "purpose": "brings a short burst of music or play to lift a busy day"
  }
 ],
 "links": [
  {
   "day": 1,
   "thread": "a",
   "title": "Share One Small Photo",
   "do": "Post a photo in the team chat that shows something small from your day, no explanation needed.",
   "minutes": 10,
   "cost": 0,
   "prep": "",
   "lead": "anyone"
  },
  {
   "day": 2,
   "thread": "b",
   "title": "Quick Team Quiz",
   "do": "Post three quick questions in the team chat and let everyone answer whenever they are free.",
   "minutes": 10,
   "cost": 0,
   "prep": "",
   "lead": "today's host"
  },
  {
   "day": 3,
   "thread": "b",
   "title": "One Song For Today",
   "do": "Post one song in the team chat that fits your mood right now.",
   "minutes": 5,
   "cost": 0,
   "prep": "",
   "lead": "anyone"
  },
  {
   "day": 4,
   "thread": "a",
   "title": "One Word Check-In",
   "do": "Share one word in the team chat for how your week is going so far.",
   "minutes": 10,
   "cost": 0,
   "prep": "",
   "lead": "anyone"
  },
  {
   "day": 5,
   "thread": "a",
   "title": "A Quiet Thank You",
   "do": "Pause for a moment and send one thank-you message to someone else in the group.",
   "minutes": 2,
   "cost": 0,
   "prep": "",
   "lead": "everyone"
  },
  {
   "day": 6,
   "thread": "a",
   "title": "One Small Gratitude",
   "do": "Take a short pause and send one thing you are grateful for this week.",
   "minutes": 2,
   "cost": 0,
   "prep": "",
   "lead": "everyone"
  },
  "... 18 more links, same shape"
 ],
 "skip_ok": [
  3,
  21
 ],
 "keep_going": "If a day gets missed, carry its idea into the next open link instead of trying to catch it up."
}

Build the input yourself

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

const K = ChainKit;
const form = K.form({ year: 2026, crew_type: "flatmates", crew: "four flatmates", likes: "cooking, films, board games",
  weekends: "together", busy_wd: [2, 4], busy: "9 late shift", planned: "18 flat dinner", budget: 40, currency: "£" });
const L = K.layout(form);                          // L.days: kind, minute limit, budget share per day; L.fig: counts and verdict
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.ics(built, reply)                            // the 24 links as an all-day calendar file

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://link-a-day.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":"link-a-day"}' | 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": "chain", "crew_type": "family with kids", "crew": "two adults and two kids, 6 and 9", "likes": "drawing, baking, silly songs, walks after dark, building with blocks", "avoid": "nuts, anything loud after 8pm", "tone": "playful", "faith": "include faith moments", "currency": "$", "budget": "30", "days": "1 | Tue 1 Dec | standard | up to 20 min | up to 1 |\n2 | Wed 2 Dec | quick | up to 10 min | up to 1 |\n...", "figures": "9 busy days; 8 ordinary weekdays; 5 free weekend days; 2 days with something planned; 0 days off; 11 light days (busy, planned or off); budget $30; verdict snug", "page_verdict": "snug", "open_days": "1, 2, 3, 4, 7, 8, 9, 10, 11, 14, 15, 16, 18, 21, 22, 23, 24"}
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 link-a-day:<task>:<inputHash>:a<attempt> (the one task is chain), 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="link-a-day:chain:$(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: link-a-day:chain:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json