← Retire Runway
API
Tokens

Run Retire Runway from your own code

A planning model, not financial advice. Retire Runway projects one household's retirement plan and writes about it in plain language. It never names a fund, a product, a provider or a security, never promises an outcome, and frames every action as an option to discuss. The intake lane never invents a number - every figure on the sheet comes from the notes (or a month-to-year conversion), and anything unclear comes back as a question. The review lane never computes a new figure - every number it quotes is one the page printed in facts.

Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page does the arithmetic for free - it reads the plan sheet and projects it year by year in today's dollars (pay with real raises, Social Security adjusted for the claiming age, pensions, a fixed mortgage, college costs, one-off events, withdrawals in the order cash, taxable, pre-tax, Roth, required minimum distributions), runs a seeded 1,000-path Monte Carlo for the probability of success against an 85% target, finds the sustainable retirement spending at 85%, and runs the stress test and the Social Security claiming table on the same paths. That is rrplan.js, and every finding carries its rule id, line and fix. The run either turns first-meeting notes into the plan sheet ("intake") or writes the plan review from the page's numbers ("review"); the page then holds the reply to its sources (rrreply.js): every number grounded, the verdict in the page's band, every scenario read, every error and warning answered.

Base URL and the response shape

Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response has one shape:

{ "ok": true,  "data":  { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }

The token is minted for this app (the guest call takes {"slug":"retire-runway"}), 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 field the run needs (task, plan, facts) is missing or not a string. Send every field as a string; focus may be "".
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 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 comes first

Two lanes - "intake" (a first meeting's notes in plan, written in anyone's words, turned into the plan sheet the page projects: people, accounts, spending, Social Security, pensions, mortgage, education, events, assumptions, insurance and estate, plus the questions the household must answer and anything the sheet has no place for) and "review" (a plan sheet in plan plus the page's projection of it in facts, turned into the plan review: a verdict, the drivers, a reading of every stress test and the claiming table, prioritised options to discuss, education, protection, questions, findings and answers to the page's own findings). The RR- rules belong to the review. A missing or unknown task is answered as intake when plan has no People: section and as review otherwise, with a caveat saying so. Both lanes send the same fields.

The input

Every field is a string.

FieldRequiredWhat
taskyes"intake" or "review". The model answers that task and only that task.
planyesfor "intake", the notes as written (free text); for "review", a plan sheet in the sheet format that the page can project (each person with an age, at least one account with a balance, the yearly spending). The page sends up to 12,000 characters - the numbers, not whole statements.
focusnowhat the household wants weighed, such as "we want to stop at 60"; the page clips it to 600 characters. Send "" when there is none.
factsyesthe page's work on the same plan, built in the browser by rrplan.js (RrPlan.facts(), via buildInput). For "intake": what the page could read from the notes and the defaults it uses for missing values. For "review": the whole projection - see the facts format.
retry_notenopresent only on a reformat retry: the previous reply could not be read as one JSON object, and this says how to answer again. Leave it out otherwise.

The plan sheet format

plan is plain text. The sheet the page reads (and the one "intake" produces) has header lines and sections: Plan: and Year:, then People: (name, age, retire at, plan to, salary), Accounts: (name, balance, type - pre-tax for a 401(k), 403(b), 457, TSP or traditional IRA, roth, taxable or cash - and what is added a year), Spending: and Retirement spending:, Social Security: (the benefit at full retirement age, 67, and the claiming age), Pensions:, Mortgage:, Education:, Events:, Assumptions: (inflation, raises, stocks %, stock return, bond return, tax rate), Insurance: and Estate:. All money is in today's dollars.

Plan: The Shah household
Year: 2026

People:
- Priya: age 52, retire at 62, plan to 95, salary 96,000
- Tom: age 55, retire at 63, plan to 95, salary 72,000

Accounts:
- Priya 401(k): 410,000 pre-tax, adding 23,000 a year
- Joint brokerage: 140,000 taxable, adding 6,000 a year
- Savings account: 35,000 cash

Spending: 84,000 a year
Retirement spending: 80,000 a year

Social Security:
- Tom: 2,400 a month at 67, claim at 62

Mortgage: 2,100 a month until 2034

Education:
- Maya: age 14, 32,000 a year for 4 years from 18, 529 balance 48,000, adding 3,000 a year

Assumptions:
- Stocks: 80%
- Stock return: 9%

Estate:
- Wills: 2015
- Beneficiaries reviewed: 2019

A value the sheet leaves out takes the page's default: inflation 2.5%, raises 3%, stocks 60%, stock return 7%, bond return 4%, tax rate 15%, retire at 67, plan to 95, Social Security claimed at 67. Free notes ("Chidi is 47, Ngozi 45 ... 401k (Chidi) around 305k") are fine for "intake": the page finds no sheet sections in them and the model builds the sheet. Every household, name and number on this page is invented for the samples.

The facts format

facts is the page's own work, written as plain text. The page builds it in the browser with rrplan.js (RrPlan.buildInput(state, task) calls RrPlan.facts(scan, task)), and the model may quote only the numbers it prints: the review answers every error and warn rule id it finds there and the page then holds the reply to the same text. So an API client should send facts built the same way - from rrplan.js run under node (Build the input yourself; the Monte Carlo is seeded, so the same sheet gives the same text), or the text the page shows for the same plan - not an empty string and not numbers of your own.

For "intake" it is short: what the page could read, then the defaults line.

The notes are free text - the page found no plan sheet in them.
Defaults the page uses when a value is missing (do NOT copy them into the sheet): inflation 2.5%, raises 3%, stocks 60%, stock return 7%, bond return 4%, tax rate 15%, retire at 67, plan to 95, Social Security claimed at 67.

When the notes do hold part of a sheet, the first line reads The page read part of a plan sheet from the notes: followed by - person | <name> | age <n|->, - account | <name> | <type> | <$balance> and - spending | <$n> a year lines. For "review" the facts carry the whole projection, line by line:

Plan: <name>. Year <year>. All money in today's dollars (inflation <x>% taken out).
People:  - <name> | age <n> | retires at <n> (<year>) | plan to age <n> | salary $<n>
Accounts (total $<n>):  - <name> | <pretax|roth|taxable|cash> | $<n> | adding $<n> a year
Spending: ... ; Social Security: ... claiming at <age> = $<n> a month. ; Pension: ... ; Mortgage: ... ; Event: ...
Assumptions used: inflation, raises, stocks (return), bonds (return), cash return, tax rate; average real return, volatility.
Projection (base case, average returns):  portfolio at retirement, first-year withdrawal rate, runs short or lasts, balances at 65/75/85/90
Monte Carlo (1,000 market paths): probability of success <x>% (target 85%); 10th percentile, median, 90th percentile at 90.
Sustainable retirement spending at 85% success: $<n> a year (the plan has $<n>).
Scenarios (name | success | median portfolio at 90 | average-return path runs short at age):  base case + five stress tests
Social Security claiming age (everyone claims at the same age):  - at 62 / 67 / 70 | monthly | success | median at 90
Education (cost in today's dollars):  - <child> | $<total> | the 529 covers <x>% | about $<n> a month would fund it
Insurance: ... . Estate: ... .
Findings (rule | severity | line | where | text):
- <rule id> | <error|warn|info> | line <n> | <where|-> | <title>[ (note)]
Simplifications: one effective tax rate ...; withdrawals in the order cash, taxable, pre-tax, Roth; ...

A plan with no findings gets the single line - none. The scenarios are always the same five stress tests from the source skill: retire 2 years early, a 20% market drop in the first retirement year, spending +20%, living 5 years longer (to 100), and long-term care at $100,000 a year for 3 years from 85. A real one, shortened - the facts the page sends for its "couple at 52 and 55 with a child" sample (the Shah household, invented) on the "review" lane:

Plan: The Shah household. Year 2026. All money in today's dollars (inflation 2.5% taken out).
People:
- Priya | age 52 | retires at 62 (2036) | plan to age 95 | salary $96,000
- Tom | age 55 | retires at 63 (2034) | plan to age 95 | salary $72,000
Accounts (total $832,000):
- Priya 401(k) | pretax | $410,000 | adding $23,000 a year
...
Projection (base case, average returns):
- portfolio when everyone has retired (2036): $1,719,000
- first-year withdrawal rate: 3.3%
- the money lasts to the end of the plan; $5,328,000 left at the end
...
Monte Carlo (1,000 market paths): probability of success 88.3% (target 85%); portfolio when Priya is 90: 10th percentile $43,000, median $2,735,000, 90th percentile $10,335,000.
Sustainable retirement spending at 85% success: $83,000 a year (the plan has $80,000).
Scenarios (name | success | median portfolio when Priya is 90 | average-return path runs short at Priya's age):
- Base case | 88.3% | $2,735,000 | never
- Retire 2 years early | 75.9% | $1,467,000 | never
- 20% market drop in the first retirement year | 80.4% | $1,551,000 | never
- Spending +20% | 60.3% | $770,000 | never
- Living 5 years longer (to 100) | 86.3% | $2,735,000 | never
- Long-term care: $100,000 a year for 3 years from age 85 | 82.9% | $2,368,000 | never
Social Security claiming age (everyone claims at the same age):
- at 62 | Priya $2,030, Tom $1,680 a month | success 85.8% | median at 90 $2,746,000
- at 67 | Priya $2,900, Tom $2,400 a month | success 90.2% | median at 90 $2,811,000
- at 70 | Priya $3,596, Tom $2,976 a month | success 93.1% | median at 90 $2,809,000
Education (cost in today's dollars):
- Maya | $128,000 in total | the 529 covers 59% | about $930 a month would fund it (now $250)
...
Findings (rule | severity | line | where | text):
- RR-CASHFLOW | warn | line 15 | 2026 | Working years spend more than they earn (after-tax pay $138,000 vs spending, mortgage, education and savings $152,000)
- RR-EDU | warn | line 28 | Maya | Education goal underfunded (59% funded from the 529; about $930 a month would fund it)
- RR-RETURNS | warn | line 37 | Assumptions | Optimistic return assumption (stocks 9%, bonds 4%)
- RR-WILL | warn | line 45 | Estate | Will missing or not reviewed in five years (last reviewed 2015)
- RR-BENEF | warn | line 46 | Estate | Beneficiaries not reviewed in three years (last reviewed 2019)
- RR-SSEARLY | info | line 20 | Tom | Social Security claimed before full retirement age (claim at 62: 70% of the full benefit)
- RR-NOLTC | info | line 41 | Insurance | No long-term-care plan listed
Simplifications: one effective tax rate on pay, Social Security, pensions and pre-tax withdrawals; ...

Rule ids the page uses, with their severity and area:

RuleSeverityAreaTitle
RR-MISSINGerrorhouseholdThe plan cannot be projected yet
RR-DEPLETEerroroutcomeThe money runs out in the base case
RR-SUCCESSwarnoutcomeProbability of success under the 85% target
RR-WITHDRAWwarnoutcomeHigh first-year withdrawal rate
RR-SEQwarnoutcomeFragile to a market drop at retirement
RR-RETURNSwarnassumptionsOptimistic return assumption
RR-INFLATIONwarnassumptionsInflation assumption out of the usual range
RR-DEFAULTSinfoassumptionsDefaults used for missing assumptions
RR-CASHFLOWwarncashflowWorking years spend more than they earn
RR-ALLOCinfoassumptionsHeavy in stocks close to retirement
RR-SSEARLYinfoincomeSocial Security claimed before full retirement age
RR-EDUwarngoalsEducation goal underfunded
RR-NOLIFEwarnprotectionDependants and no life cover listed
RR-NOLTCinfoprotectionNo long-term-care plan listed
RR-WILLwarnestateWill missing or not reviewed in five years
RR-BENEFwarnestateBeneficiaries not reviewed in three years
RR-NOYEARinfohouseholdNo plan year (shown on the page, left out of facts)

A point the page cannot see (a spending figure that looks too low for the household, an assumption that contradicts the notes) comes back as a finding with the rule NEW.

The standard the model applies

The source skill - @anthropics/financial-plan (anthropics/financial-services, Apache-2.0) - is the only standard:

House rules

The replies

Both lanes carry task, summary, questions (strings) and caveats (strings). The rest differs by lane. The shapes below are what the page reads after RrReply.normalize(): a missing list comes back empty, an unknown area becomes other, an unknown severity becomes medium, and actions are sorted by priority.

intake

The notes written down as the plan sheet. The intake writes down the household; it does not judge the plan.

  1. summary - one or two sentences: whose plan this is and what the notes leave open.
  2. sheet - plan, year (only if the notes give one), people (each name, age, retire_at - an age or "retired" - plan_to, salary), accounts (each name, owner, type - pretax, roth, taxable or cash - balance, adding a year), spending and retirement_spending a year, social_security (each name, monthly at full retirement age, claim_at), pensions (each name, monthly, from, indexed - a boolean), mortgage (monthly, until), education (each name, age, per_year, years, start_at, balance, adding), events (each year, what, amount - negative for a cost), assumptions (only rates the notes state: inflation, raises, stocks, stock_return, bond_return, tax_rate), insurance (strings) and estate (wills, beneficiaries, trust - a year or "none"). Numbers are plain figures without currency signs; a field the notes do not give is left empty.
  3. questions - one per thing the household must confirm or supply.
  4. left_out - anything the sheet has no place for, each item and why.
  5. caveats.
{
  "task": "intake",
  "summary": "",
  "sheet": {"plan": "", "year": "", "people": [{"name": "", "age": "", "retire_at": "", "plan_to": "", "salary": ""}], "accounts": [{"name": "", "owner": "", "type": "pretax", "balance": "", "adding": ""}], "spending": "", "retirement_spending": "", "social_security": [{"name": "", "monthly": "", "claim_at": ""}], "pensions": [{"name": "", "monthly": "", "from": "", "indexed": false}], "mortgage": {"monthly": "", "until": ""}, "education": [{"name": "", "age": "", "per_year": "", "years": "", "start_at": "", "balance": "", "adding": ""}], "events": [{"year": "", "what": "", "amount": ""}], "assumptions": {"inflation": "", "raises": "", "stocks": "", "stock_return": "", "bond_return": "", "tax_rate": ""}, "insurance": [""], "estate": {"wills": "", "beneficiaries": "", "trust": ""}},
  "questions": [""],
  "left_out": [{"item": "", "why": ""}],
  "caveats": [""]
}

review

The plan review, written from the facts.

  1. verdict - on_track (probability of success 85% or more), borderline (70% to under 85%) or off_track (under 70%), from the facts' probability.
  2. summary - three or four sentences: where the plan stands, the biggest risk, the strongest lever.
  3. drivers - the three to five things that move this plan most, each factor and effect, citing the page's numbers.
  4. scenarios - every stress test in the facts (not the base case), each name and reading, then the Social Security claiming table as one more entry.
  5. actions - three to seven prioritised options to discuss, each priority (1, 2, 3 ...), area (savings, spending, retirement_age, social_security, allocation, tax, insurance, estate, beneficiaries, education or cash_flow), action, why and effect (the scenario or table row that shows its size, or "not modelled by the page").
  6. education and protection - strings: each education goal; each insurance and estate gap.
  7. questions; findings (points the page cannot see: rule NEW, severity high / medium / low, title, where, why, fix); prescan (one entry per distinct error or warn rule id in the facts: rule, call agree or disagree, note); caveats (the page's simplifications that matter here, and that this is a planning model, not financial advice).
{
  "task": "review",
  "verdict": "on_track",
  "summary": "",
  "drivers": [{"factor": "", "effect": ""}],
  "scenarios": [{"name": "", "reading": ""}],
  "actions": [{"priority": 1, "area": "savings", "action": "", "why": "", "effect": ""}],
  "education": "",
  "protection": "",
  "questions": [""],
  "findings": [{"rule": "NEW", "severity": "medium", "title": "", "where": "", "why": "", "fix": ""}],
  "prescan": [{"rule": "RR-SUCCESS", "call": "agree", "note": ""}],
  "caveats": [""]
}

The page holds either reply to its sources and shows each miss next to the reply as a note. For "intake" it wants every number on the sheet found in the notes (as written, or times or divided by 12), every person named in the notes, the sheet to read back through the page's parser with the same number of people and accounts, spending on the sheet or asked for, an age for each person or a question asking for it, and no advice. For "review" it wants every error and warning answered by rule id, no rule id the page did not report other than NEW, the verdict in the band of the page's probability, every number quoted found in the facts (within 3% for large figures), every stress test read, at least three actions, no product or provider named and no promise, a claiming-age driver or action when Social Security is claimed early, and non-empty education or protection when the page found an education, insurance or estate gap.

Reading the reply

The job's output.output is text that should be exactly one JSON object. Parse it; if a model ever wraps it in prose or a fence, take the text from the first { to the last } and parse that (the page does the same in rrreply.js, and closes a stream that stopped mid-object). If it still does not parse, send the same input again with a retry_note such as "Your previous reply was not usable. Reply again with ONLY the JSON object for task 'review' - no prose, no code fences." and a new attempt number in the key (see step 5). Step 5 prints the sheet's people and accounts, the questions and what was left out (intake), or the verdict, the stress-test readings and the prioritised actions (review). Have the household read the review with the plan in front of them; it is a planning model, not financial advice.

Intake, then review

The two lanes chain. An "intake" reply's sheet is written back as a plan sheet (RrPlan.sheet(reply.sheet): Plan:, Year:, then People:, Accounts:, Spending: and the other sections) and becomes the new plan. The page projects that sheet for free with its own defaults for anything left empty; once the household has answered the questions, the same sheet goes to "review" with fresh facts built from it.

Worked samples

Two of the page's own samples, with the real saved replies it shows for them. Every household, name and number is invented.

Sample 1 - intake: the Okafors' first meeting

Messy notes from a first meeting: two earners at 47 and 45, six accounts, a mortgage, two children heading for college, Social Security statements, no wills. The page finds no sheet in them, so facts is just the defaults line. The input is exactly what the page sends:

{
 "task": "intake",
 "plan": "First meeting with Chidi and Ngozi Okafor, 14 Oct.\nChidi is 47, Ngozi 45. Both want to stop at 60 if possible, Ngozi maybe later (she likes her job).\nChidi earns about 118k, Ngozi 64k. Raises have been roughly 3% a year.\n401k (Chidi) around 305k, he puts in 19,500/yr. Ngozi has a 403b of 88k and adds 6k.\nRoth IRAs: Chidi 34k, Ngozi 29k - not adding anything right now.\nBrokerage about 52k. Emergency fund 30k in a high-yield savings account.\nThey spend roughly $7,500 a month, mortgage included? (Ngozi thinks not - mortgage is 2,400/mo on top, paid off 2041.)\nTwo kids: Ada 11 and Emeka 8. Want 4 years of college for both, figure 28k a year each. 529s: Ada 21k, Emeka 12k, 250/mo each.\nSS statements: Chidi 3,100/mo at FRA, Ngozi 2,050/mo. No pensions.\nLife insurance through work only (2x salary). No wills yet - 'we keep meaning to'.\nNgozi's mother may move in at some point.",
 "focus": "",
 "facts": "The notes are free text - the page found no plan sheet in them.\nDefaults the page uses when a value is missing (do NOT copy them into the sheet): inflation 2.5%, raises 3%, stocks 60%, stock return 7%, bond return 4%, tax rate 15%, retire at 67, plan to 95, Social Security claimed at 67."
}

Reply - the saved reply, abbreviated: long strings are cut and end in ..., accounts keeps 3 of 6, and questions keeps 3 of 22 (the full reply also has 3 left_out items and 4 caveats):

{
 "task": "intake",
 "summary": "This is the plan for Chidi (47) and Ngozi (45) Okafor, who both hope to stop work at 60, with two children, Ada and Emeka, headed for college. The ...",
 "sheet": {
   "plan": "The Okafor household",
   "year": "",
   "people": [
     {"name": "Chidi", "age": "47", "retire_at": "60", "plan_to": "", "salary": "118000"},
     {"name": "Ngozi", "age": "45", "retire_at": "60", "plan_to": "", "salary": "64000"}
   ],
   "accounts": [
     {"name": "Chidi 401(k)", "owner": "Chidi", "type": "pretax", "balance": "305000", "adding": "19500"},
     {"name": "Ngozi 403(b)", "owner": "Ngozi", "type": "pretax", "balance": "88000", "adding": "6000"},
     {"name": "Chidi Roth IRA", "owner": "Chidi", "type": "roth", "balance": "34000", "adding": "0"}
   ],
   "spending": "90000",
   "retirement_spending": "",
   "social_security": [
     {"name": "Chidi", "monthly": "3100", "claim_at": ""},
     {"name": "Ngozi", "monthly": "2050", "claim_at": ""}
   ],
   "pensions": [],
   "mortgage": {"monthly": "2400", "until": "2041"},
   "education": [
     {"name": "Ada", "age": "11", "per_year": "28000", "years": "4", "start_at": "", "balance": "21000", "adding": "3000"},
     {"name": "Emeka", "age": "8", "per_year": "28000", "years": "4", "start_at": "", "balance": "12000", "adding": "3000"}
   ],
   "events": [],
   "assumptions": {
     "inflation": "",
     "raises": "3",
     "stocks": "",
     "stock_return": "",
     "bond_return": "",
     "tax_rate": ""
   },
   "insurance": ["Life insurance through work only (2x salary); no individual policy mentioned"],
   "estate": {"wills": "none", "beneficiaries": "", "trust": ""}
 },
 "questions": [
   "Does the roughly $7,500 a month of spending include the $2,400 a month mortgage, or is the mortgage on top of it (Ngozi thinks it is on top)?",
   "Spending is described as roughly $7,500 a month - what is the closer figure?",
   "Ngozi may work past 60 because she likes her job - at what age would she like to plan to stop?"
 ],
 "left_out": [
   {"item": "Ngozi's mother may move in at some point", "why": "No timing or cost is given, and the sheet has no place for a possible care or household change; it may become an event once a year and cost are known."}
 ],
 "caveats": [
   "Monthly figures were converted to yearly (spending 7,500 a month to 90000; 529 contributions 250 a month to 3000 each). No other ..."
 ]
}

"About 118k" arrives as 118000 and "roughly $7,500 a month" as 90000 a year (the one conversion allowed); the vagueness goes into questions. Nothing the notes do not say is filled in: plan_to, claim_at, start_at and most assumptions are empty, and the page's defaults are not copied in. The page's reply check finds nothing to note.

Sample 2 - review: the Shah household

A couple at 52 and 55 with a child of 14: $832,000 saved, retiring at 62 and 63, Tom claiming Social Security at 62, 80% in stocks at a 9% return, wills from 2015. The page puts the probability of success at 88.3% (on track) and reports five warnings. Input - the plan is exactly what the page sends; facts is shortened here (each ... stands for lines left out - send the whole text the page builds, about 4,000 characters):

{
 "task": "review",
 "plan": "Plan: The Shah household\nYear: 2026\n\nPeople:\n- Priya: age 52, retire at 62, plan to 95, salary 96,000\n- Tom: age 55, retire at 63, plan to 95, salary 72,000\n\nAccounts:\n- Priya 401(k): 410,000 pre-tax, adding 23,000 a year\n- Tom IRA: 185,000 pre-tax, adding 7,000 a year\n- Priya Roth IRA: 62,000 roth, adding 7,000 a year\n- Joint brokerage: 140,000 taxable, adding 6,000 a year\n- Savings account: 35,000 cash\n\nSpending: 84,000 a year\nRetirement spending: 80,000 a year\n\nSocial Security:\n- Priya: 2,900 a month at 67, claim at 67\n- Tom: 2,400 a month at 67, claim at 62\n\nPensions:\n- Tom: 900 a month from 63\n\nMortgage: 2,100 a month until 2034\n\nEducation:\n- Maya: age 14, 32,000 a year for 4 years from 18, 529 balance 48,000, adding 3,000 a year\n\nEvents:\n- 2031: new roof, -25,000\n\nAssumptions:\n- Inflation: 2.5%\n- Raises: 3%\n- Stocks: 80%\n- Stock return: 9%\n- Bond return: 4%\n- Tax rate: 18%\n\nInsurance:\n- Priya: term life 750,000 until 2040\n\nEstate:\n- Wills: 2015\n- Beneficiaries reviewed: 2019\n- Trust: none",
 "focus": "",
 "facts": "Plan: The Shah household. Year 2026. All money in today's dollars (inflation 2.5% taken out).\nPeople:\n- Priya | age 52 | retires at 62 (2036) | plan to age 95 | salary $96,000\n- Tom | age 55 | retires at 63 (2034) | plan to age 95 | salary $72,000\nAccounts (total $832,000):\n- Priya 401(k) | pretax | $410,000 | adding $23,000 a year\n...\nMonte Carlo (1,000 market paths): probability of success 88.3% (target 85%); portfolio when Priya is 90: 10th percentile $43,000, median $2,735,000, 90th percentile $10,335,000.\nSustainable retirement spending at 85% success: $83,000 a year (the plan has $80,000).\nScenarios (name | success | median portfolio when Priya is 90 | average-return path runs short at Priya's age):\n- Base case | 88.3% | $2,735,000 | never\n...\n- Spending +20% | 60.3% | $770,000 | never\n...\nFindings (rule | severity | line | where | text):\n- RR-CASHFLOW | warn | line 15 | 2026 | Working years spend more than they earn (after-tax pay $138,000 vs spending, mortgage, education and savings $152,000)\n...\n- RR-RETURNS | warn | line 37 | Assumptions | Optimistic return assumption (stocks 9%, bonds 4%)\n..."
}

Reply - the saved reply, abbreviated: long strings are cut and end in ..., and lists keep their first entries (the full reply has 5 drivers, 6 scenario readings, 7 actions, 7 questions, 4 findings, 5 prescan answers and 5 caveats):

{
 "task": "review",
 "verdict": "on_track",
 "summary": "The Shah plan shows a probability of success of 88.3% against the 85% target, and on average returns the money lasts to the end of the plan, with $1,719,000 in the ...",
 "drivers": [
   {"factor": "Retirement spending level", "effect": "The plan spends $80,000 a year in retirement, close to the sustainable $83,000 at 85% success; the Spending ..."},
   {"factor": "Retirement date", "effect": "Retiring 2 years early drops success to 75.9% and the median at Priya's 90 to $1,467,000, below the 85% ..."}
 ],
 "scenarios": [
   {"name": "Spending +20%", "reading": "Success falls to 60.3% and the median to $770,000, the largest drop in the table; retirement ..."},
   {"name": "Social Security claiming age", "reading": "With everyone claiming at the same age, success is 85.8% at 62, 90.2% at 67 and 93.1% at 70; later ..."}
 ],
 "actions": [
   {"priority": 1, "area": "cash_flow", "action": "Map the working-year budget to see how the gap between pay and outgoings is being covered today.", "why": "After-tax pay is $138,000 against $152,000 of spending, mortgage, education and savings ...", "effect": "not modelled by the page"},
   {"priority": 2, "area": "spending", "action": "Confirm the $80,000 retirement spending figure and discuss keeping it at or below the sustainable level.", "why": "The sustainable retirement spending at 85% success is $83,000 a year, and a spending rise ...", "effect": "Spending +20% scenario: success 60.3%."},
   {"priority": 3, "area": "social_security", "action": "Weigh Tom claiming later than 62, alongside Priya's claim at 67, against the household's need for income in the early retirement years.", "why": "Tom's claim at 62 pays $1,680 a month against $2,400 at 67, a lower benefit for the rest ...", "effect": "Social Security claiming table: success 85.8% at 62, 90.2% at 67, 93.1% at 70."}
 ],
 "education": "Maya's education is estimated at $128,000 in total in today's dollars; the 529 covers 59%, and about $930 a month would ...",
 "protection": "The wills date from 2015 and the beneficiaries were last reviewed in 2019, both flagged by the page. Priya has term ...",
 "questions": [
   "How is the gap between after-tax pay of $138,000 and outgoings of $152,000 being covered today?",
   "Is $80,000 a year a realistic retirement spending figure, and does it include costs such as travel, health care and car replacement?"
 ],
 "findings": [
   {"rule": "NEW", "severity": "high", "title": "Projection counts on savings the cash flow may not support", "where": "Accounts / Spending", "why": "The accounts add yearly contributions (for example $23,000 to Priya's 401(k)) ...", "fix": "Confirm what is actually saved each year and rerun with that figure."}
 ],
 "prescan": [
   {"rule": "RR-CASHFLOW", "call": "agree", "note": "After-tax pay of $138,000 against $152,000 of outgoings means the ..."},
   {"rule": "RR-EDU", "call": "agree", "note": "The 529 covers 59% of Maya's $128,000; about $930 a month would fund ..."}
 ],
 "caveats": [
   "This is a planning model, not financial advice, and we are not your financial adviser; the figures are probabilities, not guarantees."
 ]
}

Every figure in the reply - 88.3%, $83,000, 60.3%, $1,680, 93.1% - is one the facts print; the verdict matches the page's band; all five warnings (RR-CASHFLOW, RR-EDU, RR-RETURNS, RR-WILL, RR-BENEF) are answered in prescan, and the two info ids need no entry. The page's reply check finds nothing to note.

Build the input yourself

The projection and the reply checker are plain scripts that run under node (global.window = global; require("./rrplan.js"); require("./rrreply.js"), from the app at https://retire-runway.skillsafe.ai/rrplan.js and /rrreply.js). The Monte Carlo is seeded, so the same sheet gives the same facts as the page:

const S = RrPlan, R = RrReply;
const st = { plan: fs.readFileSync("plan.txt", "utf8"), focus: "" };
const b = S.buildInput(st, "review");              // or "intake"; b.ok is false (with b.note) when the plan cannot run
console.log(b.scan.success, b.scan.verdict, b.scan.findings.map(f => f.id)); // 88.3 on_track [ ... ]
const wire = S.toWire(b.input);                    // task, plan, focus, facts - all strings; POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "review");
// R.check(reply, b)                               // {notes, warn}: ungrounded numbers, unread scenarios, unanswered findings
// S.sheet(reply.sheet)                            // an intake reply's sheet as plan text - the plan for a review run
// R.md("The Shah household", b, reply)            // the page's numbers, findings and the reply as Markdown

Get a token for the steps below from /tokens.html.

Step 1 - a tiny client helper

One function that sends JSON and unwraps the response. Paste a personal token from /tokens.html in place of YOUR_TOKEN, or leave it empty and mint a guest token in step 2.

BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="YOUR_TOKEN"   # from https://retire-runway.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":"retire-runway"}' | 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. Save a worked-sample input above as input.json (task "intake" with the notes, or "review" with a sheet and the whole facts the page builds for it) to follow along.

# input.json: the run input object - {"task": "review", "plan": "Plan: ...\nPeople:\n- ...\nAccounts:\n- ...\nSpending: ...", "focus": "", "facts": "Plan: ...\nMonte Carlo ...\nFindings ..."}
# - see the input 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 retire-runway:<task>:<inputHash>:a<attempt> - the lane is part of the key, so an intake and a review are two runs - and its one automatic reformat retry (sent with a retry_note, which the hash leaves out) is a new attempt number, a2, named in the page as one extra run. Any stable hash of the input works for your own key. Each run is metered on gpt-terra; the job reports charged_credits, and truncated is true when the available balance cut the reply short. The code below parses the JSON object out of the output and prints, for "intake", the summary, the sheet's people, accounts and spending, the questions and what was left out, and for "review", the verdict, the summary, every scenario reading and the options to discuss in priority order.

KEY="retire-runway:$(jq -r .task input.json):$(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}'
jq -r 'if .task == "intake" then
    .summary, "", "People:", (.sheet.people[]? | "  - \(.name): age \(.age), retire at \(.retire_at), salary \(.salary)"),
    "Accounts:", (.sheet.accounts[]? | "  - \(.name) (\(.type)): \(.balance), adding \(.adding)"),
    "Spending: \(.sheet.spending) a year",
    (.questions[]? | "? \(.)"), (.left_out[]? | "left out: \(.item) - \(.why)")
  else
    "VERDICT: \(.verdict)", .summary, "", "Scenarios:", (.scenarios[]? | "  - \(.name): \(.reading)"),
    "Options to discuss:", (.actions | sort_by(.priority)[]? | "  \(.priority). [\(.area)] \(.action) (\(.effect))"),
    (.questions[]? | "? \(.)")
  end' reply.json

Step 6 - POST /run-stream

The same run as an SSE stream, with the same input.json and the same kind of key. Browsers receive progress ticks rather than text deltas; the final done message carries the job, whose output.output is the reply.

# SSE: tick/delta messages while the job runs, then a final message with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: retire-runway:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json