← Goo Forward
API

Drive Goo Forward from your own code

The one metered task, grow, is available over HTTP. It takes a vague aim, up to six lines each of who you are, what you know and whom you know, the hours and money you can afford to lose, a currency sign and a tone (playful or plain), plus page_facts - the page's numbered list of those means with their counts and caps. It returns a verdict (spread, probe or reshape) and 2 to 4 first moves, or a polite decline.

The division of labour first: the page numbers the means and works out its own verdict; the model grows the moves and writes the words; the page then re-checks every move - means that exist, hours and money under the caps, people asked from the list, at least two people asked in a spread, and the move to grow first (most means, then fewer hours, then less money, then earlier). Build page_facts the same way (see "Build the input yourself") so the numbers line up.

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":"goo-forward"}), 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, aim, i_am, i_know, i_know_people, loss_hours, loss_money, currency, tone, page_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: growing moves 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 status and the verdict

Every run sends task: grow (the one task is grow); a missing or different one is still answered as grow. The checks run in this order and stop at the first match: status declined when the aim involves harm, anything illegal, health treatment, gambling, investing or trading, crypto, borrowing or lending money, or handling other people's money; else verdict reshape when the aim names quitting a job, selling or remortgaging a home, moving to another country, or an amount above a cap; else probe with fewer than 3 means or no people; else spread.

Input (every field is a string)

FieldRequiredWhat it holds
taskyesAlways grow.
aimyesThe vague aim, up to 160 characters.
i_am, i_know, i_know_peopleyes (may be empty)Who you are, what you know, whom you know: one item per line, up to six each, repeats dropped, each up to 120 characters. At least one means in all.
loss_hours, loss_moneyyesWhole numbers: the hours (1 or more) and money (0 or more) you can afford to lose across all moves together.
currencyyesOne of $, £, €, ¥, ₹.
toneyesplayful (one slime-mold image allowed in the closing) or plain.
page_factsyesThe means numbered from 1 in the order who / know / whom, the counts, and the caps.
clip_notenoWhat the page left out, if anything.
retry_notenoOnly on the page's one reformat retry, after an unusable reply.

Reply (one JSON object)

{"task": "grow", "status": "ok", "reason": "", "verdict": "spread", "aim_label": "...", "smaller_aim": "",
 "moves": [{"title": "...", "uses": [2, 3, 6], "hours": 2, "money": 0, "ask": 6, "commitment": "...", "why": "..."}, ...],
 "thicken": 3, "thicken_why": "...", "pruned": [{"idea": "...", "reason": "needs_forecast"}],
 "first_step": "...", "closing": "..."}
FieldWhat it holds
status, reasonok or declined; for declined, one sentence of at most 25 words, and every other field empty.
verdictspread (3 or 4 moves, at least two asking different people), probe (2 moves, one going looking for a new means) or reshape (2 moves toward smaller_aim).
aim_label, smaller_aimThe aim in at most 8 words; a smaller aim of at most 14 words, only for reshape.
movesEach: title (at most 8 words, starts with a verb), uses (means numbers), hours (1 or more), money, ask (a person's means number or 0), commitment (at most 14 words, empty when ask is 0) and why (at most 22 words). Hours and money add up to no more than the caps.
thicken, thicken_whyThe 1-based move to grow first: most uses, then fewer hours, then less money, then earlier; and why, in at most 22 words.
pruned1 or 2 big conventional steps set aside, each with reason over_cap, needs_forecast or no_means.
first_step, closingThe first action of the move to grow first (at most 22 words) and a closing line (at most 18 words).

The page then checks the reply (GooKit.check) against its own verdict, the numbered means and the caps, and lists any disagreement under "The page's check".

Worked examples

These are the four free samples on the page: the exact input and a stand-in model's reply, unedited. Together they cover every outcome - a bike-repair side gig (spread), teaching grown-ups to juggle (probe), a record shop (reshape) and evening crypto trading (declined).

A bike-repair side gig - spread

Input:

{
 "task": "grow",
 "aim": "start a small bike-repair side gig",
 "i_am": "patient at explaining things\nown a full bike toolbox",
 "i_know": "fixing punctures, brakes and gears\nfour years working in a bike shop",
 "i_know_people": "my neighbour who runs the community centre\nmy sister, who rides a cargo bike to work\nmy old manager at the bike shop",
 "loss_hours": "12",
 "loss_money": "60",
 "currency": "$",
 "tone": "playful",
 "page_facts": "Means (numbered):\n1. (who) patient at explaining things\n2. (who) own a full bike toolbox\n3. (know) fixing punctures, brakes and gears\n4. (know) four years working in a bike shop\n5. (whom) my neighbour who runs the community centre\n6. (whom) my sister, who rides a cargo bike to work\n7. (whom) my old manager at the bike shop\nCounts: who you are 2, what you know 2, whom you know (people) 3, total 7.\nCaps: at most 12 hours and $60 in total across all moves."
}

Reply:

{
 "task": "grow",
 "status": "ok",
 "reason": "",
 "verdict": "spread",
 "aim_label": "Start a small bike-repair side gig",
 "smaller_aim": "",
 "moves": [
  {
   "title": "Fix your sister's cargo bike first",
   "uses": [
    2,
    3,
    6
   ],
   "hours": 2,
   "money": 0,
   "ask": 6,
   "commitment": "A yes to being your first practice fix.",
   "why": "Your full toolbox and puncture, brake and gear skills let you tune your sister's cargo bike cheaply."
  },
  {
   "title": "Offer a repair-tips talk at the centre",
   "uses": [
    1,
    3,
    5
   ],
   "hours": 3,
   "money": 5,
   "ask": 5,
   "commitment": "A slot to host a short repair talk.",
   "why": "Your patience explaining things and puncture, brake and gear knowledge suit a short talk your neighbour could host."
  },
  {
   "title": "Message your old manager for a recommendation",
   "uses": [
    4,
    7
   ],
   "hours": 1,
   "money": 0,
   "ask": 7,
   "commitment": "A yes to pointing people your way.",
   "why": "Four years working at the shop and your manager there make this request easy to try."
  }
 ],
 "thicken": 1,
 "thicken_why": "It draws on three means and costs the fewest hours among the tied options, so grow this first.",
 "pruned": [
  {
   "idea": "Renting a dedicated repair shop space",
   "reason": "over_cap"
  },
  {
   "idea": "Forecasting steady demand before starting",
   "reason": "needs_forecast"
  }
 ],
 "first_step": "Message your sister to set up a time to look at her cargo bike.",
 "closing": "These small steps can grow like a slime mold feeling its way outward."
}

Teach grown-ups to juggle - probe

Input:

{
 "task": "grow",
 "aim": "teach grown-ups to juggle",
 "i_am": "can juggle five balls",
 "i_know": "",
 "i_know_people": "",
 "loss_hours": "6",
 "loss_money": "0",
 "currency": "£",
 "tone": "plain",
 "page_facts": "Means (numbered):\n1. (who) can juggle five balls\nCounts: who you are 1, what you know 0, whom you know (people) 0, total 1.\nCaps: at most 6 hours and £0 in total across all moves."
}

Reply:

{
 "task": "grow",
 "status": "ok",
 "reason": "",
 "verdict": "probe",
 "aim_label": "Teach grown-ups to juggle",
 "smaller_aim": "",
 "moves": [
  {
   "title": "Offer a grown-up a juggling taster",
   "uses": [
    1
   ],
   "hours": 2,
   "money": 0,
   "ask": 0,
   "commitment": "",
   "why": "Your five-ball juggling lets you show a grown-up a trick and invite them to try."
  },
  {
   "title": "Practice explaining your juggling technique aloud",
   "uses": [
    1
   ],
   "hours": 3,
   "money": 0,
   "ask": 0,
   "commitment": "",
   "why": "Turning your five-ball juggling into spoken steps builds a teachable method without needing anything else."
  }
 ],
 "thicken": 1,
 "thicken_why": "This move ties on means used but costs the fewest hours, so you grow it first.",
 "pruned": [
  {
   "idea": "Renting a venue for group classes",
   "reason": "over_cap"
  },
  {
   "idea": "Writing a full course curriculum upfront",
   "reason": "needs_forecast"
  }
 ],
 "first_step": "Ask a grown-up to watch you juggle and then try it themselves.",
 "closing": "Small steps to find a first learner and sharpen your teaching before spreading further."
}

A record shop - reshape

Input:

{
 "task": "grow",
 "aim": "quit my job and open a second-hand vinyl record shop",
 "i_am": "own about 900 records\ngood at chatting to strangers",
 "i_know": "how to grade a record's condition\nwhich pressings collectors look for",
 "i_know_people": "a friend who runs a café with a spare corner\nmy cousin who sells at a market every month",
 "loss_hours": "20",
 "loss_money": "150",
 "currency": "£",
 "tone": "playful",
 "page_facts": "Means (numbered):\n1. (who) own about 900 records\n2. (who) good at chatting to strangers\n3. (know) how to grade a record's condition\n4. (know) which pressings collectors look for\n5. (whom) a friend who runs a café with a spare corner\n6. (whom) my cousin who sells at a market every month\nCounts: who you are 2, what you know 2, whom you know (people) 2, total 6.\nCaps: at most 20 hours and £150 in total across all moves."
}

Reply:

{
 "task": "grow",
 "status": "ok",
 "reason": "",
 "verdict": "reshape",
 "aim_label": "Quit your job, open a vinyl record shop",
 "smaller_aim": "Sell some records at the market with your cousin",
 "moves": [
  {
   "title": "Sell records at the market with your cousin",
   "uses": [
    1,
    3,
    4,
    6
   ],
   "hours": 6,
   "money": 0,
   "ask": 6,
   "commitment": "a slot at the market to sell some records",
   "why": "You own records, know how to grade them and which pressings collectors look for, and your cousin sells at a market."
  },
  {
   "title": "Display records and chat at café corner",
   "uses": [
    1,
    2,
    5
   ],
   "hours": 4,
   "money": 0,
   "ask": 5,
   "commitment": "a spare corner to display some records",
   "why": "You own records, are good at chatting to strangers, and your friend runs a café with a spare corner."
  }
 ],
 "thicken": 1,
 "thicken_why": "This move draws on the most you have: your records, their condition, pressings knowledge and your cousin at the market.",
 "pruned": [
  {
   "idea": "Renting a shop before any sales are made",
   "reason": "over_cap"
  },
  {
   "idea": "Stocking wide inventory before knowing what sells",
   "reason": "needs_forecast"
  }
 ],
 "first_step": "Ask your cousin for a market slot to sell some of your records.",
 "closing": "Even slime molds test a thread before committing to the whole dish - try the smaller shape first."
}

Evening crypto trading - declined

Input:

{
 "task": "grow",
 "aim": "make extra money trading crypto in the evenings",
 "i_am": "good with spreadsheets",
 "i_know": "basic chart reading",
 "i_know_people": "a coworker who trades",
 "loss_hours": "10",
 "loss_money": "500",
 "currency": "$",
 "tone": "plain",
 "page_facts": "Means (numbered):\n1. (who) good with spreadsheets\n2. (know) basic chart reading\n3. (whom) a coworker who trades\nCounts: who you are 1, what you know 1, whom you know (people) 1, total 3.\nCaps: at most 10 hours and $500 in total across all moves."
}

Reply:

{
 "task": "grow",
 "status": "declined",
 "reason": "This aim involves trading crypto, which isn't something this approach can help you with.",
 "verdict": "",
 "aim_label": "",
 "smaller_aim": "",
 "moves": [],
 "thicken": 0,
 "thicken_why": "",
 "pruned": [],
 "first_step": "",
 "closing": ""
}

Build the input yourself

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

const K = GooKit, D = GooDish;
const built = K.buildInput({ aim: "start a small bike-repair side gig", am: "own a full bike toolbox",
  know: "fixing punctures, brakes and gears", people: "my neighbour who runs the community centre",
  hours: "12", money: "60", currency: "$", tone: "playful" });
built.parsed.means;              // [{n: 1, group: "am", text: "own a full bike toolbox"}, ...]
built.prescan.verdict;           // the page's own verdict: "probe" here (fewer than 3 means)
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.thickenOf(reply.moves)                     // the move to grow first, by the rule
// D.svg({ means: built.parsed.means, moves: reply.moves, thicken: K.thickenOf(reply.moves), pruned: reply.pruned })
// K.md(built, reply)                           // the grown dish 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://goo-forward.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":"goo-forward"}' | 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": "grow", "aim": "...", "i_am": "...\n...", "i_know": "...", "i_know_people": "...", "loss_hours": "12", "loss_money": "60", "currency": "$", "tone": "playful", "page_facts": "Means (numbered):\n1. (who) ..."} - 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 goo-forward:<task>:<inputHash>:a<attempt> (the one task is grow), 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="goo-forward:grow:$(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: goo-forward:grow:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json