← Dropped Hints
API
Tokens

Drive Dropped Hints from your own code

Both metered lanes are available over HTTP. read takes one person's hints - what they said, did and showed, one per line - with a budget and anything ruled out, and returns an empathy map (every hint placed under Says, Thinks, Does or Feels with what it suggests), a verdict - wrap it, pick one or fish first - gift ideas tied to hint ids with rough price estimates, things to avoid and things still to find out. fish takes those things to find out and returns a short conversation plan: a cover line, a warm-up, one question per thing with what to listen for and a follow-up, a wrap-up and what not to say. A natural use is a family gift-list bot, or a notes app that keeps hints about people and asks for a read when the date gets close.

The division of labour first: the page owns the list, the sorting and the budget math; the model reads; the page re-checks the reply. Numbering the hints, the page's guess at each quadrant, spotting direct asks, the ledger and the countdown come from hintkit.js, which runs in node as well as in the browser. Its check function holds each reply to the hints: every hint id exists and is placed once, the verdict follows the rules, a pick-one idea rests on two hints or more, prices sit inside the budget, nothing ruled out comes back, a casual plan carries no giveaway words, and no number in prose is missing from the input.

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":"dropped-hints"}), 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 such as task, name or clues 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: both lanes are metered and need 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

Every run sends task: read or fish. One system prompt routes on it; a missing or unknown task is treated as read. A read verdict is decided by three ordered, non-overlapping rules - the first that applies wins:

OrderVerdictWhen
1wrap-itA hint is a direct ask for one specific thing (they want, need or wish for it, keep eyeing it, or one they own broke), it is not ruled out, and a plain version fits the budget. The first idea is that thing.
2fish-firstOtherwise, fewer than 3 hints, or no idea can be tied to two different hints. No ideas; 3-5 things to find out.
3pick-oneOtherwise: 2-3 ideas, each tied to two hints or more, one picked.

A fish plan's mode follows who alone: them gives casual (no mention of gifts, the occasion, the date or shopping; never asks outright), helper gives straight (it may say it is for a gift and asks the helper to keep it quiet).

The declared input schema carries scalars only, so array- and object-valued fields travel JSON-encoded as strings (HintKit.toWire does this). Every value is a string.

Input fields

FieldTypeNotes
taskstringread or fish. Required.
name, relationstringWho the gift is for, and who they are to the giver (may be empty). name is required.
budget, currencystringA number (0 = no budget) and one of $ £ € ₹ ¥.
occasion, give_datestringFree text and YYYY-MM-DD; either may be empty.
cluesstring (JSON)[{id, text, quad, ask}]: the hints numbered C1.., the page's quadrant guess (says, thinks, does, feels) and whether the wording reads as a direct ask. Up to 30 hints or 2,400 characters; past that the later hints are left out and clues_note says how many. Required.
ruled_outstring (JSON)Array of things given before or already owned.
page_quads, page_asks, page_leanstring (JSON), string (JSON), stringread only: counts per quadrant, the ids read as asks, and the page's verdict from wording alone.
goalsstring (JSON)fish only: 1-5 things to find out, numbered from 1 in order.
who, helper, channelstringfish only: them or helper; the helper's name; in person, text or call.
retry_notestringOnly on a reformat retry.

The replies

The job's output.output is a string holding one JSON object. For read: task, verdict, title, summary, map ({says, thinks, does, feels}, each [{clue, note}]), ideas ([{idea, why, clues, kind, price_low, price_high}]; prices are rough estimates), pick (an index, or null), avoid and fish. For fish: task, mode, title, summary, cover, warm_up, core ([{goal, ask, listen_for, follow_up}], one per goal), wrap_up and dont. Parse with HintKit.parseReply, then normalize(reply, task), then check(reply, form, tag), which returns the page's notes.

Worked examples

Replies produced against the app's prompt for the five sample runs over one made-up gift list (made-up people and hints; long fields and lists shortened; the full replies ship in example-result.js). Together they cover every verdict and both modes.

read - an aunt who said her pasta roller died: wrap it

Input:

{
 "task": "read",
 "name": "Rosa",
 "relation": "my aunt",
 "budget": "60",
 "currency": "$",
 "occasion": "End-of-year holidays",
 "give_date": "2026-12-20",
 "clues": "[{\"id\":\"C1\",\"text\":\"Said in August: \\\"my old pasta roller finally died, I need a new one\\\"\",\"quad\":\"says\",\"ask\":true},{\"id\":\"C2\",\"text\":\"Makes ravioli with the grandkids most weekends\",\"quad\":\"does\",\"ask\":false},{\"id\":\"C3\",\"text\":\"Loves the farmers market\",\"qu ... (455 characters)",
 "ruled_out": "[\"Gave her a cookbook last year\"]",
 "page_quads": "{\"says\":1,\"thinks\":1,\"does\":2,\"feels\":1}",
 "page_asks": "[\"C1\"]",
 "page_lean": "wrap-it"
}

Reply:

{
 "task": "read",
 "verdict": "wrap-it",
 "title": "A new pasta roller for Rosa",
 "summary": "Rosa said her old pasta roller died and she needs a new one, which makes this a clear ask rather than a guess. A plain hand-crank roller stays well within budget and fits how often she makes pasta with her grandkids and how much she values homemade pasta over store-bought.",
 "map": {
  "says": [
   {
    "clue": "C1",
    "note": "She's directly asking for a replacement since her old pasta roller broke."
   }
  ],
  "thinks": [
   {
    "clue": "C4",
    "note": "She holds strong standards for homemade pasta over store-bought."
   }
  ],
  "does": [
   {
    "clue": "C2",
    "note": "She makes ravioli with her grandkids most weekends, so pasta-making gear would see regular use."
   },
   "... (2 in all)"
  ],
  "feels": [
   {
    "clue": "C3",
    "note": "She enjoys the farmers market, which could inspire a gift connected to it."
   }
  ]
 },
 "ideas": [
  {
   "idea": "A hand-crank pasta roller",
   "why": "She said her old pasta roller died and needs a new one, and she makes pasta with her grandkids most weekends.",
   "clues": [
    "C1",
    "C2"
   ],
   "kind": "thing",
   "price_low": 35,
   "price_high": 50
  },
  {
   "idea": "A bag of good pasta flour",
   "why": "She thinks store-bought pasta is a crime and makes ravioli most weekends, so good pasta flour would get plenty of use.",
   "clues": [
    "C4",
    "C2"
   ],
   "kind": "consumable",
   "price_low": 10,
   "price_high": 15
  }
 ],
 "pick": 0,
 "avoid": [
  "another cookbook, since she's already been given one",
  "bulky kitchen gadgets, since her drawers are already full"
 ],
 "fish": [
  "Check whether she's already replaced the roller herself.",
  "Check if she has a size or style preference for the new one."
 ]
}

read - a brother's new climbing habit: pick one

Input:

{
 "task": "read",
 "name": "Dev",
 "relation": "my brother",
 "budget": "50",
 "currency": "$",
 "occasion": "End-of-year holidays",
 "give_date": "2026-12-20",
 "clues": "[{\"id\":\"C1\",\"text\":\"Started bouldering at the climbing gym in spring\",\"quad\":\"does\",\"ask\":false},{\"id\":\"C2\",\"text\":\"Complains his fingertips are wrecked after every session\",\"quad\":\"says\",\"ask\":false},{\"id\":\"C3\",\"text\":\"Goes three times a week, even after late ... (599 characters)",
 "ruled_out": "[\"Already owns climbing shoes and a chalk bag\"]",
 "page_quads": "{\"says\":2,\"thinks\":0,\"does\":3,\"feels\":1}",
 "page_asks": "[]",
 "page_lean": "pick-one"
}

Reply:

{
 "task": "read",
 "verdict": "pick-one",
 "title": "Gift ideas for Dev's climbing routine",
 "summary": "Dev hasn't dropped one clear ask, but a few clues point the same way: he's climbing three times a week even after late shifts, his fingertips take a beating every session, and his gym bag could use some help. That's enough to put together a short list of options rather than one obvious pick, with finger care topping it since it's the one thing he's actually complained about.",
 "map": {
  "says": [
   {
    "clue": "C2",
    "note": "His fingertips take a beating, so something for hand or finger care could help."
   },
   "... (2 in all)"
  ],
  "thinks": [],
  "does": [
   {
    "clue": "C1",
    "note": "He's new to bouldering, so climbing gear or accessories fit this new interest."
   },
   "... (3 in all)"
  ],
  "feels": [
   {
    "clue": "C4",
    "note": "Finishing a hard route makes him proud, so a gift tied to his climbing progress could land well."
   }
  ]
 },
 "ideas": [
  {
   "idea": "Finger care and tape kit",
   "why": "He complains his fingertips are wrecked after every session, and he's at the gym three times a week, so his hands take a beating.",
   "clues": [
    "C2",
    "C3"
   ],
   "kind": "consumable",
   "price_low": 15,
   "price_high": 25
  },
  {
   "idea": "A better gym bag",
   "why": "He joked his gym bag smells like a swamp, and going three times a week means it gets heavy use.",
   "clues": [
    "C6",
    "C3"
   ],
   "kind": "thing",
   "price_low": 25,
   "price_high": 45
  },
  "... (3 in all)"
 ],
 "pick": 0,
 "avoid": [
  "Climbing shoes or a chalk bag - he already has them.",
  "A bag that's hard to clean or air out, given how often it gets used."
 ],
 "fish": [
  "Whether he already uses something for sore fingertips, so you don't duplicate it.",
  "What size or style of gym bag would actually suit him."
 ]
}

read - an office gift swap with two hints: fish first

Input:

{
 "task": "read",
 "name": "Mara",
 "relation": "coworker (office gift swap)",
 "budget": "25",
 "currency": "$",
 "occasion": "End-of-year holidays",
 "give_date": "2026-12-20",
 "clues": "[{\"id\":\"C1\",\"text\":\"Has a small plant on her desk\",\"quad\":\"does\",\"ask\":false},{\"id\":\"C2\",\"text\":\"Seems to drink a lot of tea\",\"quad\":\"does\",\"ask\":false}]",
 "ruled_out": "[]",
 "page_quads": "{\"says\":0,\"thinks\":0,\"does\":2,\"feels\":0}",
 "page_asks": "[]",
 "page_lean": "fish-first"
}

Reply:

{
 "task": "read",
 "verdict": "fish-first",
 "title": "Not quite enough to choose yet",
 "summary": "There are only two clues so far, which isn't enough to settle on a gift for Mara. She keeps a small plant at her desk and seems to drink a lot of tea, two different directions that haven't been narrowed down yet. A bit more to go on would help land on something she'd really like.",
 "map": {
  "says": [],
  "thinks": [],
  "does": [
   {
    "clue": "C1",
    "note": "A plant on her desk suggests a gift for her workspace could suit her."
   },
   "... (2 in all)"
  ],
  "feels": []
 },
 "ideas": [],
 "pick": null,
 "avoid": [],
 "fish": [
  "Whether she'd welcome another plant or already has enough.",
  "What kind of tea she likes best.",
  "Whether she already has what she needs to care for her plant.",
  "How much room she has on her desk for something new."
 ]
}

fish - asking the coworker yourself: casual

Input:

{
 "task": "fish",
 "name": "Mara",
 "relation": "coworker (office gift swap)",
 "budget": "25",
 "currency": "$",
 "occasion": "End-of-year holidays",
 "give_date": "2026-12-20",
 "clues": "[{\"id\":\"C1\",\"text\":\"Has a small plant on her desk\",\"quad\":\"does\",\"ask\":false},{\"id\":\"C2\",\"text\":\"Seems to drink a lot of tea\",\"quad\":\"does\",\"ask\":false}]",
 "ruled_out": "[]",
 "goals": "[\"Whether she enjoys looking after plants or the one on her desk was a gift\",\"What kind of tea she actually likes\",\"Whether she would rather have something for her desk or something to take home\"]",
 "who": "them",
 "helper": "",
 "channel": "in person"
}

Reply:

{
 "task": "fish",
 "mode": "casual",
 "title": "Casual chat about plants and tea",
 "summary": "You bring this up naturally in person, starting from her desk plant and her tea habit. The aim is to learn whether she enjoys looking after plants, what tea she actually likes, and whether she'd lean toward something for her desk or something to take home.",
 "cover": "I've been thinking about getting a little desk plant of my own.",
 "warm_up": [
  "That plant on your desk is looking good — is it new?",
  "You always seem to have a cup of tea going — long day?"
 ],
 "core": [
  {
   "goal": 1,
   "ask": "Did you pick that plant out yourself, or did it come from someone else?",
   "listen_for": "She says she chose it and enjoys tending it herself, versus it was given to her and plant care isn't really her thing.",
   "follow_up": "Do you like having something to look after at your desk, or is it just nice to have around?"
  },
  {
   "goal": 2,
   "ask": "You always have tea going — what kind are you drinking these days?",
   "listen_for": "A specific type or flavour she actually enjoys, not just tea in general.",
   "follow_up": "Is there a kind you've been meaning to try?"
  },
  "... (3 in all)"
 ],
 "wrap_up": "Anyway, I'll let you get back to it — that tea smells good.",
 "dont": [
  "Don't ask her straight out what she wants for the gift swap.",
  "Don't bring up the gift swap, the holidays, or a budget while asking about the plant or the tea.",
  "Don't fire off all three questions back-to-back — spread them out so it doesn't feel like an interview."
 ]
}

fish - texting a go-between about a grandfather: straight

Input:

{
 "task": "fish",
 "name": "Grandpa Lou",
 "relation": "my grandfather",
 "budget": "40",
 "currency": "$",
 "occasion": "End-of-year holidays",
 "give_date": "2026-12-20",
 "clues": "[{\"id\":\"C1\",\"text\":\"Listens to ball games on an old radio in the garage\",\"quad\":\"does\",\"ask\":false},{\"id\":\"C2\",\"text\":\"Said the radio \\\"only gets one station now\\\"\",\"quad\":\"says\",\"ask\":false},{\"id\":\"C3\",\"text\":\"Fixes everyone's bikes on the street\",\"quad\":\"doe ... (371 characters)",
 "ruled_out": "[\"Socks (every year)\",\"A sweater (gave one in 2024)\"]",
 "goals": "[\"Whether he would use a new radio or is attached to the old one\",\"Whether there is a power outlet in the garage\",\"Whether he still has his fishing gear\"]",
 "who": "helper",
 "helper": "Jen",
 "channel": "text"
}

Reply:

{
 "task": "fish",
 "mode": "straight",
 "title": "Checking details for Grandpa Lou's gift",
 "summary": "You text Jen directly about the garage radio, the power outlet, and Grandpa Lou's fishing gear, then ask them to keep it quiet.",
 "cover": "Keep this between us so it doesn't spoil the surprise.",
 "warm_up": [
  "Hey Jen, quick one for you about Grandpa Lou's gift."
 ],
 "core": [
  {
   "goal": 1,
   "ask": "Would he actually use a new radio, or is he attached to the old one?",
   "listen_for": "a clear yes he'd use a new one, or that he really wants to keep the old radio",
   "follow_up": "If so, I'll look at getting him a new one."
  },
  {
   "goal": 2,
   "ask": "Also - is there a power outlet near where he listens in the garage?",
   "listen_for": "a straight yes or no on whether there's a plug close by",
   "follow_up": "If not, I'll find something that doesn't need power."
  },
  "... (3 in all)"
 ],
 "wrap_up": "Thanks so much, Jen - this really helps me pick something good.",
 "dont": [
  "Don't mention a new radio like it's already decided - you're only checking if he'd want one.",
  "Don't ask about the power outlet where he's within earshot - it would tip him off about the garage.",
  "Don't bring up the fishing gear directly to him - that's the detail most likely to give it away."
 ]
}

Build the input yourself

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

const K = HintKit;
const form = { occasion: "End-of-year holidays", give_date: "2026-12-20", currency: "$" };
const tag = { name: "Rosa", relation: "my aunt", budget: "60",
  clues: "Said in August: \"my old pasta roller finally died, I need a new one\"\nMakes ravioli with the grandkids most weekends",
  ruled: "Gave her a cookbook last year" };
console.log(K.clues(tag));                                  // numbered hints, quadrant guess, ask flag
console.log(K.lean(tag), K.tagFlags(tag, form));            // the page's verdict from wording, and its notes
const wire = K.toWire(K.buildInput("read", form, tag).input);   // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output), "read");
// const { notes } = K.check(reply, form, tag);              // the page's re-check of the reply

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://dropped-hints.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":"dropped-hints"}' | 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": "read", "name": "Rosa", "relation": "my aunt", "budget": "60", "currency": "$", "occasion": "End-of-year holidays", "give_date": "2026-12-20", "clues": "[{\"id\":\"C1\",\"text\":\"Said in August: ...\",\"quad\":\"says\",\"ask\":true}, ...]", "ruled_out": "[\"Gave her a cookbook last year\"]", "page_quads": "{...}", "page_asks": "[\"C1\"]", "page_lean": "wrap-it"}
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 dropped-hints:<task>:<inputHash>:a<attempt> (the lanes are read and fish), 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="dropped-hints:read:$(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: dropped-hints:read:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json