← Seven Envelopes
API
Tokens

Drive Seven Envelopes from your own code

The one metered task, plan, is available over HTTP. It takes a household's cash lines for a card-down week (a week when cards, phone wallets and banking apps stop working) together with the page's own figures - the seven day envelopes filled from the tin, the day it runs dry, the savings layers in months - and returns a verdict (exposed, thin or ready), the lines that could shrink for a week, what each layer is for, the next moves in time order and what to watch out for. A natural use is a household budgeting sheet that re-checks the tin each season, or a community preparedness page that hands people a printable cash list.

The division of labour first: the page owns the arithmetic; the model owns the judgement and the words; the page re-checks the reply. Reading the lines, landing each on its days, filling the envelopes, the shortfall, the months of essentials and the rule-of-thumb range come from envkit.js, which runs in node as well as in the browser. The model never adds anything up: EnvKit.buildInput hands it every figure. EnvKit.check holds each reply to them: the verdict by the rule, four layers in order, a this-week top-up of the tin first when the tin is short, every trim a real line, and no number, capitalised name, he/she, field name or bank name that the input does not hold.

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":"seven-envelopes"}), 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, lines, figures) 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 the plan 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: plan (the only task; a missing or unknown one is treated as plan). The verdict is decided by a fixed rule on the page's figures, checked in this order - the first that applies wins:

  1. exposed - the tin fully covers fewer than 3 of the 7 days, or the tin plus instant-access savings hold less than one month of essentials.
  2. ready - the tin covers all 7 days and all four layers together reach the low end of the rule-of-thumb range (3-6 months for two or more steady incomes, 6-9 for one steady income or variable pay, 6-12 for self-employed or retired households; estimates, not advice).
  3. thin - everything else.

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

Input fields

FieldTypeNotes
taskstringplan. Required.
currencystring$, £, € or "".
householdstringPeople at home, 1-12.
income, income_labelstringsteady, one_income, variable, self_employed or retired, and its plain label.
linesstring (JSON)[{id, item, amount, when, days, week_total}] - ids L1.., up to 30 lines; amount per time; days are day names Monday-Sunday. Required.
daysstring (JSON)Seven {day, need, covered, lines} - each envelope's need and what the tin covers, filled in order from Monday.
figuresstring (JSON){week_need, cash_now, days_covered, dry_day, shortfall, spare_cash, monthly, instant, notice, longer, total, months_covered, quick, quick_months, target_low, target_high, target_low_amount, target_high_amount, gap_to_low}, every value a string; months are rounded down to one decimal. Required.
notesstringFree text from the household, up to 400 characters.
clip_note, retry_notestringWhat the page cut, when it cut something; and only on a reformat retry, what was wrong.

The reply

The job's output.output is a string holding one JSON object: task, verdict, title, summary, week ({note, trims: [{ref, how}]}), tiers (four {tier, role, advice} in the order tin, instant, notice, longer), moves (2-5 {step, when, tier}, when one of this-week, this-month, next-3-months) and watch_outs (0-3 strings). Parse with EnvKit.parseReply, then normalize(reply), then check(reply, built) where built = EnvKit.buildInput(form); md(reply, built, form) writes the Markdown and csv(built) the printable cash list.

Worked examples

Replies produced against the app's prompt for the three sample households (made up; long fields shortened; the full replies ship in example-result.js). Together they cover every verdict.

a card-only family of four: the tin runs dry on Monday - exposed

Input:

{
 "task": "plan",
 "currency": "$",
 "household": "4",
 "income": "steady",
 "income_label": "Two or more steady incomes",
 "lines": "[{\"id\":\"L1\",\"item\":\"Groceries top-up\",\"amount\":\"35\",\"when\":\"3x (Mon, Wed, Sat)\",\"days\":[\"Monday\",\"Wednesday\",\"Saturday\"],\"week_total\":\"105\"},{\"id\":\"L2\",\"item\":\"School lunch money\",\"amount\":\"8\",\"when\":\"Mon to Fri\",\"days\":[\"Monday\",\"Tuesday\",\"Wednesday\",\"Thursday\",\"Friday\"],\"week_total\":\"40\"},{\"id\":\"L3\",\"item\":\"Bus fares\",\"amount\":\"6.50\",\"when\":\"Mon to Fri\",\"days\":[\"Monday\",\"Tuesday\",\"Wednesday\",\"Thursday\",\"Friday\"],\"w ... (741 characters)",
 "days": "[{\"day\":\"Monday\",\"need\":\"99.50\",\"covered\":\"40\",\"lines\":[\"L1\",\"L2\",\"L3\",\"L4\"]},{\"day\":\"Tuesday\",\"need\":\"14.50\",\"covered\":\"0\",\"lines\":[\"L2\",\"L3\"]},{\"day\":\"Wednesday\",\"need\":\"49.50\",\"covered\":\"0\",\"lines\":[\"L1\",\"L2\",\"L3\"]},{\"day\":\"Thursday\",\"need\":\"32.50\",\"covered\":\"0\",\"lines\":[\"L2\",\"L3\",\"L5\"]},{\"day\":\"Friday\",\"need\":\"14.50\",\"covered\":\"0\",\"lines\":[\"L2\",\"L3\"]},{\"day\":\"Saturday\",\"need\":\"47\",\"covered\":\"0\",\"lines\":[\"L1\",\"L6\" ... (476 characters)",
 "figures": "{\"week_need\":\"257.50\",\"cash_now\":\"40\",\"days_covered\":\"0\",\"dry_day\":\"Monday\",\"shortfall\":\"217.50\",\"spare_cash\":\"0\",\"monthly\":\"3200\",\"instant\":\"1500\",\"notice\":\"0\",\"longer\":\"2000\",\"total\":\"3540\",\"months_covered\":\"1.1\",\"quick\":\"1540\",\"quick_months\":\"0.4\",\"target_low\":\"3\",\"target_high\":\"6\",\"target_low_amount\":\"9600\",\"target_high_amount\":\"19200\",\"gap_to_low\":\"6060\"}",
 "notes": "Two kids at primary school. We pay for almost everything by phone. The pharmacy near us takes cash."
}

Reply:

{
 "task": "plan",
 "verdict": "exposed",
 "title": "The tin runs dry on Monday",
 "summary": "The tin runs dry on Monday, leaving a $217.50 shortfall for the week. Savings cover 1.1 months against the 3 to 6 month range for two or more steady incomes, with a $6,060 gap to the low end.",
 "week": {
  "note": "Groceries top-up is the week's biggest cost at $105, falling on Monday, Wednesday and Saturday.",
  "trims": [
   {
    "ref": "L1",
    "how": "Buy a smaller top-up and use what's already in the cupboard."
   },
   {
    "ref": "L4",
    "how": "Combine trips or walk shorter journeys to cut fuel use this week."
   },
   {
    "ref": "L6",
    "how": "Delay the laundromat trip or wash by hand for the week."
   }
  ]
 },
 "tiers": [
  {
   "tier": "tin",
   "role": "Cash held at home for a week when cards and phone payments stop working.",
   "advice": "Top up with $217.50 in small notes to cover the week's shortfall."
  },
  {
   "tier": "instant",
   "role": "Instant-access savings for the first month if a bigger emergency hits beyond the week.",
   "advice": "Hold $1,500, but it's not yet a full month's essentials against the $3,200 needed."
  },
  {
   "tier": "notice",
   "role": "Notice savings for the months after the first, once instant-access runs out.",
   "advice": "Currently $0 - nothing is held here yet."
  },
  {
   "tier": "longer",
   "role": "Longer-term savings for whatever is needed beyond the target range.",
   "advice": "Hold $2,000 here; the gap to the target range sits in the tin and instant layers first."
  }
 ],
 "moves": [
  {
   "step": "Add $217.50 in small notes to the tin.",
   "when": "this-week",
   "tier": "tin"
  },
  {
   "step": "Add to instant-access savings toward closing the $6,060 gap to the target range.",
   "when": "this-month",
   "tier": "instant"
  },
  {
   "step": "Keep building savings so the total moves toward the 3 to 6 month range.",
   "when": "next-3-months",
   "tier": "notice"
  }
 ],
 "watch_outs": [
  "Fuel and groceries both fall on Monday, the day the tin runs dry.",
  "Most spending is paid by phone, which stops working in a card-down week.",
  "The pharmacy accepts cash, so keep enough for the Thursday prescription."
 ]
}

a freelancer with savings but little cash: dry on Saturday - thin

Input:

{
 "task": "plan",
 "currency": "£",
 "household": "1",
 "income": "self_employed",
 "income_label": "Self-employed or freelance",
 "lines": "[{\"id\":\"L1\",\"item\":\"Food shop\",\"amount\":\"30\",\"when\":\"2x (Mon, Fri)\",\"days\":[\"Monday\",\"Friday\"],\"week_total\":\"60\"},{\"id\":\"L2\",\"item\":\"Coffee with clients\",\"amount\":\"4.50\",\"when\":\"Mon to Fri\",\"days\":[\"Monday\",\"Tuesday\",\"Wednesday\",\"Thursday\",\"Friday\"],\"week_total\":\"22.50\"},{\"id\":\"L3\",\"item\":\"Train to the studio\",\"amount\":\"9.80\",\"when\":\"Tue, Thu\",\"days\":[\"Tuesday\",\"Thursday\"],\"week_total\":\"19.60\"},{\"id\":\"L4\",\"item\":\"Cat ... (591 characters)",
 "days": "[{\"day\":\"Monday\",\"need\":\"42.50\",\"covered\":\"42.50\",\"lines\":[\"L1\",\"L2\",\"L4\"]},{\"day\":\"Tuesday\",\"need\":\"14.30\",\"covered\":\"14.30\",\"lines\":[\"L2\",\"L3\"]},{\"day\":\"Wednesday\",\"need\":\"4.50\",\"covered\":\"4.50\",\"lines\":[\"L2\"]},{\"day\":\"Thursday\",\"need\":\"14.30\",\"covered\":\"14.30\",\"lines\":[\"L2\",\"L3\"]},{\"day\":\"Friday\",\"need\":\"34.50\",\"covered\":\"34.50\",\"lines\":[\"L1\",\"L2\"]},{\"day\":\"Saturday\",\"need\":\"15\",\"covered\":\"9.90\",\"lines\":[\"L5\"]},{\" ... (471 characters)",
 "figures": "{\"week_need\":\"125.10\",\"cash_now\":\"120\",\"days_covered\":\"5\",\"dry_day\":\"Saturday\",\"shortfall\":\"5.10\",\"spare_cash\":\"0\",\"monthly\":\"1800\",\"instant\":\"6000\",\"notice\":\"5000\",\"longer\":\"0\",\"total\":\"11120\",\"months_covered\":\"6.1\",\"quick\":\"6120\",\"quick_months\":\"3.4\",\"target_low\":\"6\",\"target_high\":\"12\",\"target_low_amount\":\"10800\",\"target_high_amount\":\"21600\",\"gap_to_low\":\"0\"}",
 "notes": "Invoices get paid by bank transfer, usually at the end of the month. I live alone and don't drive."
}

Reply:

{
 "task": "plan",
 "verdict": "thin",
 "title": "Covered most of the week, short on Saturday",
 "summary": "The tin runs short on Saturday, leaving a shortfall of £5.10. The savings layers cover 6.1 months of essentials, within the 6 to 12 month range for self-employed or freelance income.",
 "week": {
  "note": "Food shop is the week's biggest cost, at £60.",
  "trims": [
   {
    "ref": "L2",
    "how": "Skip the coffee with clients for the week."
   },
   {
    "ref": "L5",
    "how": "Skip the takeaway, or make it at home instead."
   }
  ]
 },
 "tiers": [
  {
   "tier": "tin",
   "role": "What the tin covers in a card-down week when cards and banking stop working.",
   "advice": "Add £5.10 to close the shortfall, then keep the tin topped up at this level."
  },
  {
   "tier": "instant",
   "role": "Instant-access savings for the first month of a real emergency after the card-down week.",
   "advice": "Holds £6,000, which together with the tin covers 3.4 months; keep it topped up rather than drawn down."
  },
  {
   "tier": "notice",
   "role": "A notice account for the months after the first month of a real emergency.",
   "advice": "Holds £5,000 in the notice account, taking the total to 6.1 months of essentials, within the 6 to 12 month range; hold it steady."
  },
  {
   "tier": "longer",
   "role": "Longer-term savings for anything beyond the 6 to 12 month range once it's fully covered.",
   "advice": "At £0, nothing needs to move here yet; essentials are already covered by the other layers."
  }
 ],
 "moves": [
  {
   "step": "Add £5.10 in small notes to the tin.",
   "when": "this-week",
   "tier": "tin"
  },
  {
   "step": "Check the instant-access savings stay at £6,000 so a month of essentials stays covered.",
   "when": "this-month",
   "tier": "instant"
  },
  {
   "step": "Keep the notice account at £5,000, keeping the total within the 6 to 12 month range.",
   "when": "next-3-months",
   "tier": "notice"
  }
 ],
 "watch_outs": [
  "Saturday's takeaway is the day the tin runs short, so plan to cover it another way.",
  "Invoices get paid by bank transfer, which won't work in a card-down week."
 ]
}

a retired couple covered all week, layers in range - ready

Input:

{
 "task": "plan",
 "currency": "€",
 "household": "2",
 "income": "retired",
 "income_label": "Retired, on a pension",
 "lines": "[{\"id\":\"L1\",\"item\":\"Market vegetables\",\"amount\":\"20\",\"when\":\"Wed, Sat\",\"days\":[\"Wednesday\",\"Saturday\"],\"week_total\":\"40\"},{\"id\":\"L2\",\"item\":\"Bakery\",\"amount\":\"4\",\"when\":\"every day\",\"days\":[\"Monday\",\"Tuesday\",\"Wednesday\",\"Thursday\",\"Friday\",\"Saturday\",\"Sunday\"],\"week_total\":\"28\"},{\"id\":\"L3\",\"item\":\"Bus pass top-up\",\"amount\":\"15\",\"when\":\"once (Mon)\",\"days\":[\"Monday\"],\"week_total\":\"15\"},{\"id\":\"L4\",\"item\":\"Pharmacy\",\"amo ... (584 characters)",
 "days": "[{\"day\":\"Monday\",\"need\":\"19\",\"covered\":\"19\",\"lines\":[\"L2\",\"L3\"]},{\"day\":\"Tuesday\",\"need\":\"26\",\"covered\":\"26\",\"lines\":[\"L2\",\"L4\"]},{\"day\":\"Wednesday\",\"need\":\"24\",\"covered\":\"24\",\"lines\":[\"L1\",\"L2\"]},{\"day\":\"Thursday\",\"need\":\"4\",\"covered\":\"4\",\"lines\":[\"L2\"]},{\"day\":\"Friday\",\"need\":\"34\",\"covered\":\"34\",\"lines\":[\"L2\",\"L5\"]},{\"day\":\"Saturday\",\"need\":\"24\",\"covered\":\"24\",\"lines\":[\"L1\",\"L2\"]},{\"day\":\"Sunday\",\"need\":\"4\",\"covere ... (443 characters)",
 "figures": "{\"week_need\":\"135\",\"cash_now\":\"400\",\"days_covered\":\"7\",\"dry_day\":\"\",\"shortfall\":\"0\",\"spare_cash\":\"265\",\"monthly\":\"2100\",\"instant\":\"9000\",\"notice\":\"6000\",\"longer\":\"12000\",\"total\":\"27400\",\"months_covered\":\"13\",\"quick\":\"9400\",\"quick_months\":\"4.4\",\"target_low\":\"6\",\"target_high\":\"12\",\"target_low_amount\":\"12600\",\"target_high_amount\":\"25200\",\"gap_to_low\":\"0\"}",
 "notes": "Both pensions arrive on the 1st. One of us does not drive."
}

Reply:

{
 "task": "plan",
 "verdict": "ready",
 "title": "Covered all week, with room to spare",
 "summary": "The tin covers every day of the card-down week, leaving €265 spare. The savings layers total 13 months of essential spending, above the 6 to 12 month range for a retired household on a pension.",
 "week": {
  "note": "Market vegetables is the week's biggest cash line at €40, and the tin covers it in full.",
  "trims": []
 },
 "tiers": [
  {
   "tier": "tin",
   "role": "The cash in the tin for a card-down week.",
   "advice": "At €400, it already covers the whole week with €265 spare; keep topping it back up."
  },
  {
   "tier": "instant",
   "role": "Instant-access savings for the first month if a bigger emergency hits.",
   "advice": "At €9,000, it holds steady; together with the tin it covers about 4.4 months of essentials."
  },
  {
   "tier": "notice",
   "role": "Notice savings for the months after the first one, if the emergency drags on.",
   "advice": "At €6,000, it's doing its job; no need to add more right now."
  },
  {
   "tier": "longer",
   "role": "Longer-term savings for whatever is left beyond the usual range.",
   "advice": "At €12,000, it's fine to leave as is for now."
  }
 ],
 "moves": [
  {
   "step": "Keep about €400 in the tin, in small notes, ready for a card-down week.",
   "when": "this-week",
   "tier": "tin"
  },
  {
   "step": "Leave the instant-access savings where they are; they're already doing their job.",
   "when": "this-month",
   "tier": "instant"
  },
  {
   "step": "Check in again in a few months to make sure the balance still feels right.",
   "when": "next-3-months",
   "tier": "longer"
  }
 ],
 "watch_outs": [
  "One of you doesn't drive, so check the bus pass top-up is affordable if cash is tight.",
  "Both pensions arrive on the same day, so keep that timing in mind for the tin."
 ]
}

Build the input yourself

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

const K = EnvKit;
const form = K.form({ currency: "$", household: "2", income: "steady", cash: "60", monthly: "2400",
  instant: "3000", week: "Groceries | 30 | 2x\nBus fares | 5 | weekdays" });
const r = K.read(form);                            // r.fill.env: seven envelopes; r.fig: every figure and the 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.csv(built)                                   // a printable cash list, one row per day per line

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://seven-envelopes.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":"seven-envelopes"}' | 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": "plan", "currency": "$", "household": "4", "income": "steady", "lines": "[{\"id\":\"L1\",\"item\":\"Groceries top-up\",\"amount\":\"35\",\"when\":\"3x (Mon, Wed, Sat)\",\"days\":[\"Monday\",\"Wednesday\",\"Saturday\"],\"week_total\":\"105\"},{\"id\":\"L2\",\"item\":\"School lunch money\",\"amount\":\"8\",\"when\":\"Mon to Fri\",\"days\":[\"Monday\",\"Tuesday\",\"Wednesday\",\"Thursday\",\"Friday\"],\"week_total\":\"40\"}, ...]", "days": "[...]", "figures": "{...}", "notes": "Two kids at primary school. We pay for almost everything by phone. The pharmacy near us takes cash."}
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 seven-envelopes:<task>:<inputHash>:a<attempt> (the one task is plan), 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="seven-envelopes:plan:$(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: seven-envelopes:plan:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json