Drive Recast or Not from your own code
The metered read is available over HTTP: send a mortgage, a lump sum, the options on the table and the page's own run of four paths, and get back a recast / prepay / refinance / keep verdict with a reason for every path, what to watch out for, questions for the servicer or lender, next steps and the facts that could change the answer. A natural use is a budgeting or home-finance app: your code sends the loan and the run, and gets back a structured read.
The division of labour first: the page does the arithmetic; the model judges and writes; the page re-checks the reply against the trails. Reading the amounts, the payment, the four month-by-month paths on one budget, the cash cushion, the flags and the page's lean come from recastkit.js; the run input and the reply check come from recastreply.js. Both run in node as well as in the browser. check holds each reply to the run: every drawn path weighed and none invented, no refinance verdict without a quote, no recast verdict on an FHA, VA or USDA loan, no paydown verdict over a cash cushion under 3 months, servicer questions behind a recast, dollar figures that come from the page, and no internal field names in prose. Every figure is an estimate (constant rates, no taxes, escrow or deductions). Not financial advice.
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":"recast-or-not"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare: never wrap it as {"input": {…}}.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or task / paths is missing. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: the read is metered and needs a personal token. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
The task field
Every run sends task: "weigh" - the one task. Verdicts: recast (pay the lump down and re-amortize to a lower payment), prepay (pay it down and keep the payment so the loan ends sooner), refinance (only when a refinance rate was entered), keep (keep the cash - a thin cushion, savings that out-earn the loan, or the best position at the horizon). The declared input schema carries scalars only, so object-valued fields travel JSON-encoded as strings (RecastKit.toWire does this). The schema requires task and paths.
Input fields
| Field | Type | Notes |
|---|---|---|
task, today | string | weigh; YYYY-MM-DD. |
loan | string (JSON) | {balance, rate_percent, months_left, time_left, payment_principal_interest, payment_source, loan_type}. |
lump_sum | string | The amount to place, like $60,000. |
options | string (JSON) | {recast_fee, refinance_rate_percent, refinance_term_years, refinance_costs, savings_rate_percent, stay_years, horizon}; values the page assumed are marked "(page assumption)". |
cushion | string (JSON) | {other_savings, monthly_expenses, target_months}. |
paths | string (JSON) | One item per drawn path: {path, monthly_payment, payment_change, paid_off_in, interest_from_here, interest_saved_vs_keep, months_saved_vs_keep, cash_kept_today, cushion_months, position_vs_keep_at_horizon}; recast adds {fee, usually_available}, refinance adds {closing_costs, new_term_years, breakeven}. |
goal | string | A lower monthly payment, be mortgage-free sooner, most money at the horizon, or not sure. |
page_flags, page_verdict | string (JSON), string | [{level, text}] with level warn / info; recast, prepay, refinance or keep. |
notes | string | The homeowner's own words (may be empty; past 1,500 characters cut on whole lines with a marker). |
retry_note | string | Only on a reformat retry. |
The reply
The job's output.output is a string holding one JSON object: title, verdict, summary, paths [{path, fit, why}] (one per input path; fit best / fair / poor / unavailable), and watch_outs, ask_servicer, steps and missing (strings). Parse it with RecastKit.parseReply, then normalize(reply), then check(reply, state), which returns {notes}: the page's notes (info, warn) shown under "Checked by the page".
Worked examples
Replies produced against the app's prompt for the sample households (made up; lists shortened; the full replies ship in example-result.js). Together they cover every verdict. The page replays each of them free from its sample buttons.
inheritance, wants a lower payment - recast
Input:
{
"task": "weigh",
"today": "2026-10-20",
"loan": "{\"balance\":\"$312,000\",\"rate_percent\":6.875,\"months_left\":324,\"time_left\":\"27 yr\",\"payment_principal_interest\":\"$2,120.62\",\"payment_source\":\"computed from balance, rate and time left\",\"loan_type\":\"conventional\"}",
"lump_sum": "$60,000",
"options": "{\"recast_fee\":\"$250\",\"refinance_rate_percent\":\"\",\"refinance_term_years\":\"\",\"refinance_costs\":\"\",\"savings_rate_percent\":4,\"stay_years\":\"12\",\"horizon\":\"12 yr\"}",
"cushion": "{\"other_savings\":\"$38,000\",\"monthly_expenses\":\"$6,200\",\"target_months\":3}",
"paths": "[{\"path\":\"keep\",\"monthly_payment\":\"$2,120.62\",\"payment_change\":\"$0\",\"paid_off_in\":\"27 yr\",\"interest_from_here\":\"$375,082\",\"interest_saved_vs_keep\":\"$0\",\"months_saved_vs_keep\":0,\"cash_kept_today\":\"$60,000\",\"cushion_months\":\"15.8\",\"position_vs_keep_at_horizon\":\"$0\"},{\"path\":\"recast\",\"monthly_payment\":\"$1,712.81\",\"payment_change\":\"-$408\",\"paid_off_in\":\"27 yr\",\"interest_from_here\":\"$302,951\",\"interest_saved_vs_keep\":\"+$72,131\",\"months_saved_vs_keep\":0,\"cash_kept_today\":\"-$250\",\"cushion_months\":\"6.1\",\"position_vs_keep_at_horizon\":\"+$23,651\",\"fee\":\"$250\",\"usually_available\":true},\"... 1 more\"]",
"goal": "a lower monthly payment",
"page_flags": "[{\"level\":\"info\",\"text\":\"No refinance rate given - the refinance trail is not drawn.\"}]",
"page_verdict": "recast",
"notes": "My aunt left me $60k. Our second kid starts daycare in January and the monthly budget is tight - I'd like the ... (178 characters)"
}
Reply:
{
"task": "weigh",
"title": "$60,000 against a 6.875% mortgage",
"verdict": "recast",
"summary": "The page leans recast: paying the $60,000 to principal and re-amortizing drops the payment from $2,120.62 to $1,712.81, a $408 cut that matches your goal of a lower monthly payment. Recast is estimated to leave you $23,651 ahead of keeping the cash by the 12 yr horizon, for a $250 fee, with your cushion at 6.1 months of expenses. The trade-off: prepay would save more interest from here ($203,173 vs recast's $72,131, the page's estimate) and pay off the loan in 16 yr 8 mo instead of 27 yr, but it would not lower your monthly payment the way recast does.",
"paths": [
{
"path": "keep",
"fit": "poor",
"why": "Keeping the $60,000 in savings holds the payment at $2,120.62, pays off the loan in 27 yr, and leaves $375,082 of interest still to pay from here, the page's estimate. It is the baseline every other path is measured against, so its own position at the horizon is $0."
},
{
"path": "recast",
"fit": "best",
"why": "Recast drops the monthly payment to $1,712.81, a $408 cut, for a $250 fee, while the payoff stays at 27 yr. It is estimated to leave you $23,651 ahead of keeping the cash at the horizon, with $72,131 less interest from here and a 6.1-month cushion."
},
"... 1 more"
],
"watch_outs": [
"Your cushion falls from 15.8 months of expenses (keeping the cash) to 6.1 months under recast or prepay - still above the 3-month target, but check whether the daycare cost starting in January is already in the $6,200 monthly-expenses figure used here.",
"A recast still needs your servicer's written agreement on the $250 fee and terms - the page's estimate assumes it is offered, but no lender has confirmed it yet.",
"... 2 more"
],
"ask_servicer": [
"What is the minimum lump sum you require to recast this loan?",
"What is your recast fee, and does it match the $250 estimate used here?",
"... 3 more"
],
"steps": [
"Call your servicer this week and ask the recast questions listed above.",
"Get the $250 recast fee and minimum lump-sum requirement confirmed in writing before sending the $60,000.",
"... 3 more"
],
"missing": [
"A refinance rate was not entered, so a refinance path could not be compared against recast.",
"The monthly daycare cost starting in January is not given, so it is unclear whether the $6,200 monthly-expenses figure includes it.",
"... 2 more"
]
}
year-end bonus, wants it paid off sooner - prepay
Input:
{
"task": "weigh",
"today": "2026-10-20",
"loan": "{\"balance\":\"$248,500\",\"rate_percent\":7.25,\"months_left\":342,\"time_left\":\"28 yr 6 mo\",\"payment_principal_interest\":\"$1,720.00\",\"payment_source\":\"entered\",\"loan_type\":\"conventional\"}",
"lump_sum": "$25,000",
"options": "{\"recast_fee\":\"$250 (page assumption)\",\"refinance_rate_percent\":\"\",\"refinance_term_years\":\"\",\"refinance_costs\":\"\",\"savings_rate_percent\":3.8,\"stay_years\":\"15\",\"horizon\":\"15 yr\"}",
"cushion": "{\"other_savings\":\"$30,000\",\"monthly_expenses\":\"$4,800\",\"target_months\":3}",
"paths": "[{\"path\":\"keep\",\"monthly_payment\":\"$1,720.00\",\"payment_change\":\"$0\",\"paid_off_in\":\"28 yr 7 mo\",\"interest_from_here\":\"$340,482\",\"interest_saved_vs_keep\":\"$0\",\"months_saved_vs_keep\":0,\"cash_kept_today\":\"$25,000\",\"cushion_months\":\"11.5\",\"position_vs_keep_at_horizon\":\"$0\"},{\"path\":\"recast\",\"monthly_payment\":\"$1,547.55\",\"payment_change\":\"-$172\",\"paid_off_in\":\"28 yr 6 mo\",\"interest_from_here\":\"$305,761\",\"interest_saved_vs_keep\":\"+$34,721\",\"months_saved_vs_keep\":1,\"cash_kept_today\":\"-$250\",\"cushion_months\":\"6.2\",\"position_vs_keep_at_horizon\":\"+$15,208\",\"fee\":\"$250\",\"usually_available\":true},\"... 1 more\"]",
"goal": "be mortgage-free sooner",
"page_flags": "[{\"level\":\"info\",\"text\":\"Recast fee not given - the page assumes $250 (fees commonly run about $150-$500, estimate).\"},{\"level\":\"info\",\"text\":\"No refinance rate given - the refinance trail is not drawn.\"}]",
"page_verdict": "prepay",
"notes": "Bonus landed in October. The payment itself is fine; I just hate the idea of paying this loan into my 60s."
}
Reply:
{
"task": "weigh",
"title": "$25,000 against a 7.25% loan",
"verdict": "prepay",
"summary": "Prepaying the $25,000 lump sum while keeping the same $1,720.00 payment is the page's lead path, matching your goal of being mortgage-free sooner now that you've said the payment itself is already comfortable. It shortens the payoff from 28 yr 7 mo to 21 yr 4 mo and saves $124,972 in interest versus keeping the cash, putting your position $29,762 ahead of keeping the cash by the 15 yr horizon. The trade-off: all of the $25,000 goes to principal today, so your cushion drops to 6.3 months of expenses instead of 11.5 months if you kept the cash - still above the 3-month target.",
"paths": [
{
"path": "keep",
"fit": "poor",
"why": "Keeping the $25,000 in savings keeps the payment at $1,720.00 and the payoff at 28 yr 7 mo, with $340,482 of interest still to pay and no change in your position against keeping the cash ($0)."
},
{
"path": "recast",
"fit": "fair",
"why": "Recasting lowers the payment to $1,547.55, a $172 drop, while the payoff stays near 28 yr 6 mo; it costs a $250 fee but still leaves your position $15,208 ahead of keeping the cash."
},
"... 1 more"
],
"watch_outs": [
"Once the $25,000 goes to principal it's committed - money paid into the house is hard to get back without a sale or a new loan.",
"Your cushion drops to 6.3 months of expenses, down from 11.5 months if you kept the cash - still above the 3-month target, but all of the lump sum is used.",
"... 2 more"
],
"ask_servicer": [
"How do I submit the $25,000 so it's applied to principal, not to next month's payment?",
"Will you confirm in writing once the payment has posted to principal?",
"... 1 more"
],
"steps": [
"Contact the servicer this month and ask how to submit the $25,000 as a principal-only payment.",
"Get written confirmation that the payment posted to principal, not escrow or a future payment.",
"... 3 more"
],
"missing": [
"No refinance rate was entered, so a refinance alternative couldn't be weighed against prepaying.",
"Your tax situation, for example any mortgage-interest deduction, isn't in the input and could change the comparison.",
"... 1 more"
]
}
7.6% loan with a 6.0% quote - refinance
Input:
{
"task": "weigh",
"today": "2026-10-20",
"loan": "{\"balance\":\"$386,000\",\"rate_percent\":7.6,\"months_left\":326,\"time_left\":\"27 yr 2 mo\",\"payment_principal_interest\":\"$2,802.52\",\"payment_source\":\"computed from balance, rate and time left\",\"loan_type\":\"conventional\"}",
"lump_sum": "$20,000",
"options": "{\"recast_fee\":\"$250 (page assumption)\",\"refinance_rate_percent\":6,\"refinance_term_years\":30,\"refinance_costs\":\"$6,500\",\"savings_rate_percent\":4.1,\"stay_years\":\"10\",\"horizon\":\"10 yr\"}",
"cushion": "{\"other_savings\":\"$42,000\",\"monthly_expenses\":\"$7,000\",\"target_months\":3}",
"paths": "[{\"path\":\"keep\",\"monthly_payment\":\"$2,802.52\",\"payment_change\":\"$0\",\"paid_off_in\":\"27 yr 2 mo\",\"interest_from_here\":\"$527,621\",\"interest_saved_vs_keep\":\"$0\",\"months_saved_vs_keep\":0,\"cash_kept_today\":\"$20,000\",\"cushion_months\":\"8.9\",\"position_vs_keep_at_horizon\":\"$0\"},{\"path\":\"recast\",\"monthly_payment\":\"$2,657.31\",\"payment_change\":\"-$145\",\"paid_off_in\":\"27 yr 2 mo\",\"interest_from_here\":\"$500,283\",\"interest_saved_vs_keep\":\"+$27,338\",\"months_saved_vs_keep\":0,\"cash_kept_today\":\"-$250\",\"cushion_months\":\"6.0\",\"position_vs_keep_at_horizon\":\"+$7,686\",\"fee\":\"$250\",\"usually_available\":true},\"... 2 more\"]",
"goal": "not sure - most money at the horizon",
"page_flags": "[{\"level\":\"info\",\"text\":\"Recast fee not given - the page assumes $250 (fees commonly run about $150-$500, estimate).\"},{\"level\":\"info\",\"text\":\"A 30-year refinance restarts the clock: the loan would run 2 yr 10 mo past today's payoff date.\"}]",
"page_verdict": "refinance",
"notes": "Bought in summer 2023 at the peak. A lender quoted 6.0% with $6,500 in costs. Not sure whether to put the $20k ... (137 characters)"
}
Reply:
{
"task": "weigh",
"title": "$20,000, a 7.6% loan, and a refinance offer",
"verdict": "refinance",
"summary": "The page leans refinance, and the figures back it: refinancing to 6% leaves you $61,435 ahead of keeping the cash at the 10 yr horizon, the best position of any path, with the $6,500 in closing costs paid back in 1 yr 2 mo against a 10 yr stay. You'd keep $13,500 of the $20,000 in savings, with a cushion of 7.9 months. The trade-off: the new 30-year loan restarts the clock, running 2 yr 10 mo past today's payoff date, and the plan depends on the lender's 6% quote and $6,500 in costs holding.",
"paths": [
{
"path": "keep",
"fit": "poor",
"why": "Keeping the $20,000 in savings leaves the payment at $2,802.52 and the loan paid off in 27 yr 2 mo, with $527,621 in interest still to pay from here; your cushion stays at 8.9 months, the strongest of the four paths."
},
{
"path": "recast",
"fit": "fair",
"why": "Recasting cuts the payment by $145 to $2,657.31 for a $250 fee, with the payoff date unchanged at 27 yr 2 mo; it saves $27,338 in interest and puts you $7,686 ahead of keeping the cash at the horizon, with a 6.0-month cushion."
},
"... 2 more"
],
"watch_outs": [
"The refinance's 6% rate and $6,500 in costs come from one lender's quote (per your notes) - confirm it in writing before counting on it.",
"A 30-year refinance restarts the clock: the loan would run 2 yr 10 mo past today's 27 yr 2 mo payoff date.",
"... 2 more"
],
"ask_servicer": [
"Can the 6% rate and $6,500 in closing costs be locked and confirmed in writing?",
"Are there any costs beyond the $6,500 quoted?",
"... 2 more"
],
"steps": [
"Get the lender's 6% rate and $6,500 closing-cost quote confirmed in writing, including how long it is locked.",
"Compare the written refinance offer against the current $2,802.52 payment and 27 yr 2 mo payoff before signing anything.",
"... 3 more"
],
"missing": [
"Whether the 6% quote includes any points, and how long the rate is locked.",
"Whether the current loan carries a prepayment penalty that would affect refinancing.",
"... 1 more"
]
}
FHA loan at 3.1%, thin cash cushion - keep
Input:
{
"task": "weigh",
"today": "2026-10-20",
"loan": "{\"balance\":\"$205,000\",\"rate_percent\":3.1,\"months_left\":300,\"time_left\":\"25 yr\",\"payment_principal_interest\":\"$982.83\",\"payment_source\":\"computed from balance, rate and time left\",\"loan_type\":\"FHA\"}",
"lump_sum": "$40,000",
"options": "{\"recast_fee\":\"$250 (page assumption)\",\"refinance_rate_percent\":\"\",\"refinance_term_years\":\"\",\"refinance_costs\":\"\",\"savings_rate_percent\":4.2,\"stay_years\":\"6\",\"horizon\":\"6 yr\"}",
"cushion": "{\"other_savings\":\"$4,000\",\"monthly_expenses\":\"$5,100\",\"target_months\":3}",
"paths": "[{\"path\":\"keep\",\"monthly_payment\":\"$982.83\",\"payment_change\":\"$0\",\"paid_off_in\":\"25 yr\",\"interest_from_here\":\"$89,849\",\"interest_saved_vs_keep\":\"$0\",\"months_saved_vs_keep\":0,\"cash_kept_today\":\"$40,000\",\"cushion_months\":\"8.6\",\"position_vs_keep_at_horizon\":\"$0\"},{\"path\":\"recast\",\"monthly_payment\":\"$791.06\",\"payment_change\":\"-$192\",\"paid_off_in\":\"25 yr\",\"interest_from_here\":\"$72,317\",\"interest_saved_vs_keep\":\"+$17,531\",\"months_saved_vs_keep\":0,\"cash_kept_today\":\"-$250\",\"cushion_months\":\"0.7\",\"position_vs_keep_at_horizon\":\"-$3,079\",\"fee\":\"$250\",\"usually_available\":false},\"... 1 more\"]",
"goal": "a lower monthly payment",
"page_flags": "[{\"level\":\"warn\",\"text\":\"FHA loans usually cannot be recast - ask the servicer before counting on it; the recast trail is drawn for comparison only.\"},{\"level\":\"info\",\"text\":\"Recast fee not given - the page assumes $250 (fees commonly run about $150-$500, estimate).\"},\"... 3 more\"]",
"page_verdict": "keep",
"notes": "Sold my car and some stock, so I have $40k. My contract job ends in March and may not renew. I'd love a lower ... (118 characters)"
}
Reply:
{
"task": "weigh",
"title": "$40,000 against a 3.1% FHA loan",
"verdict": "keep",
"summary": "The page's lean is to keep the $40,000 in savings: your savings rate (4.2%) is at or above the loan's 3.1% rate, so parking the cash likely earns more before tax than paying down the loan saves in interest. Putting it into the house would also drop your cash cushion to 0.8 months of expenses, under the 3-month target, and recast is usually not offered on FHA loans like yours. The trade-off: you keep the $982.83 payment instead of the lower payment a recast would bring, but you hold on to cash in case your contract job doesn't renew.",
"paths": [
{
"path": "keep",
"fit": "best",
"why": "Keeping the cash leaves your payment at $982.83 with $89,849 of interest still to pay; this is the page's baseline position at the horizon ($0)."
},
{
"path": "recast",
"fit": "unavailable",
"why": "Recast would drop the payment to $791.06, a change of -$192, and cut interest from here to $72,317. But it's usually not offered on an FHA loan, and it would leave your cushion at 0.7 months against the 3-month target."
},
"... 1 more"
],
"watch_outs": [
"Your contract job ends in March and may not renew - a thin cash cushion is riskier if income drops.",
"Putting the $40,000 into the house would drop your cushion under the 3-month target: 0.7 months for recast, 0.8 months for prepay.",
"... 2 more"
],
"ask_servicer": [
"If we revisit this later, can our FHA loan ever qualify for a recast, and what would we need (minimum lump sum, fee)?",
"If our situation changes, how long would a recast or prepay request take to process?",
"... 1 more"
],
"steps": [
"Keep the $40,000 in savings rather than putting it into the mortgage now.",
"Work toward the 3-month cushion target before reconsidering recast or prepay.",
"... 2 more"
],
"missing": [
"Whether a refinance rate has been checked - none is in this run.",
"What income would replace the contract job if it ends in March and isn't renewed.",
"... 2 more"
]
}
Build the input yourself
Load both scripts in node (global.window = {}; require("./recastkit.js"); require("./recastreply.js")), then:
const K = RecastKit;
const st = K.state({ balance: "386000", rate: "7.6", years_left: "27.2", payment: "", lump: "20000", loan_type: "conventional", recast_fee: "",
refi_rate: "6.0", refi_costs: "6500", refi_years: "30", savings_rate: "4.1", stay_years: "10", other_cash: "42000", expenses: "7000", goal: "unsure", notes: "" });
console.log(st.order.map(k => [k, K.usd2(st.paths[k].pay), K.ym(st.paths[k].months), K.usd(st.paths[k].vsKeep)]), st.flags, st.verdict);
const wire = K.toWire(K.buildInput(st, { today: "2026-10-20" }).input); // POST this to /estimate and /run
// later: const reply = K.normalize(K.parseReply(job.output.output));
// const { notes } = K.check(reply, st);
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://recast-or-not.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"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://recast-or-not.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// recast-or-not.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://recast-or-not.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "paste-your-token-here" // from https://recast-or-not.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// RecastOrNot.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class RecastOrNot {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://recast-or-not.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://recast-or-not.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "paste-your-token-here"; // from https://recast-or-not.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "paste-your-token-here"; // from https://recast-or-not.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
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":"recast-or-not"}' | jq -r '.data.token')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "recast-or-not"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "recast-or-not" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "recast-or-not"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "recast-or-not"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "recast-or-not" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "recast-or-not"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "recast-or-not" }))!["token"]!.GetValue<string>();
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}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["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": "weigh", "today": "2026-10-20", "loan": "{...}", "lump_sum": "$20,000", "options": "{...}", "cushion": "{...}", "paths": "[...]", "goal": "not sure - most money at the horizon", "page_flags": "[...]", "page_verdict": "refinance", "notes": "Bought in summer 2023 ..."}
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}'
inp = json.load(open("input.json")) # {"task": "weigh", "today": "2026-10-20", "loan": "{...}", "lump_sum": "$20,000", "options": "{...}", "cushion": "{...}", "paths": "[...]", "goal": "not sure - most money at the horizon", "page_flags": "[...]", "page_verdict": "refinance", "notes": "Bought in summer 2023 ..."}
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task: "weigh", today, loan, lump_sum, options, cushion, paths, goal, page_flags, page_verdict, notes (see the field table)}
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
Step 5 — POST /run, then poll the job
Send an Idempotency-Key built from the task and the input: a retry with the same key never bills twice. The page uses recast-or-not:weigh:<inputHash>:a<attempt>, 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="recast-or-not:weigh:$(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}'
import hashlib, time
key = "recast-or-not:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the reply's JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("verdict"))
import { createHash } from "node:crypto";
const key = `recast-or-not:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.verdict);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "recast-or-not:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
fmt.Println(j["charged_credits"], j["truncated"], j["output"].(map[string]any)["output"])
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "recast-or-not:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply);
require "digest"
key = "recast-or-not:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["title"]].inspect
$key = "recast-or-not:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", $reply["title"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"recast-or-not:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>());
Console.WriteLine($"{j["charged_credits"]} {reply}");
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: recast-or-not:weigh:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json
payload = json.load(open("input.json")) # the same run input as input.json: {"task": "weigh", "today": "2026-10-20", "loan": "{...}", "lump_sum": "$20,000", "options": "{...}", "cushion": "{...}", "paths": "[...]", "goal": "not sure - most money at the horizon", "page_flags": "[...]", "page_verdict": "refinance", "notes": "Bought in summer 2023 ..."}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(payload).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "recast-or-not:weigh:" + hashlib.sha256(json.dumps(payload).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const payload = JSON.parse(readFileSync("input.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(payload), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `recast-or-not:weigh:${createHash("sha256").update(JSON.stringify(payload)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("input.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "recast-or-not:weigh:"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String payload = java.nio.file.Files.readString(java.nio.file.Path.of("input.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(payload.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "recast-or-not:weigh:" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(payload)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
payload = File.read("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "recast-or-not:weigh:#{Digest::SHA256.hexdigest(payload)[0, 16]}:a1")
req.body = payload
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$payload = file_get_contents("input.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: recast-or-not:weigh:" . substr(hash("sha256", $payload), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var payload = File.ReadAllText("input.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(payload));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(payload, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"recast-or-not:weigh:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);