Drive Fix First from your own code
Both metered steps - building the troubleshooting flowchart and making the repair-or-replace call - are available over HTTP. A natural use is a home-maintenance log, a repair-cafe intake form or a warranty tracker that already knows what broke: your code sends the report, shows the flowchart, records the walk, and asks for the call.
The split first: the page does the mechanics; the model gives the judgement; the page checks it. Category and typical lifespan, safety stops and cautions, error codes, the fact ids and the repair ceiling come from fixkit.js, which runs in node as well as in the browser. Its reconcile function then checks that a safety stop forces a stop verdict with no do-it-yourself fix, that the flowchart has no loops, dangling branches or unreachable steps and starts with a low-risk check, that the call agrees with where the walk ended (never "fix it yourself" from a call-a-pro step), that cost ranges are ordered and sit on the right side of the repair ceiling, and that every cited id is on the page. Costs and lifespans are rough estimates, not quotes.
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":"fix-first"}), 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 a required field 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: both tasks are metered and need 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 comes first
Every run sends task, which routes the one system prompt to one of two contracts:
| task | What it returns | Verdict field |
|---|---|---|
tree | A title, a summary, a start node id; nodes [{id, kind, text, how, tools[], minutes, risk, yes, no, cite}] with kind check (a yes/no question; yes and no name the next node), fix, pro or stop, and risk low, medium or high; safety lines; questions (need-info only); caveats. | verdict: ready, need_info, stop_unsafe |
verdict | A title, a summary; cause {text, confidence, cite} with confidence likely, possible or unsure; steps [{text, minutes, cite}]; parts [{name, low, high, cite}]; cost {diy_low, diy_high, pro_low, pro_high, replace_low, replace_high} (whole numbers in the item's currency, or null); guard lines (how to keep it from coming back); stop_if; caveats. | verdict: diy_fix, call_pro, replace, stop_unsafe |
A line is always {text, cite}. Cites are the page's ids: symptoms Y1 Y2…, things already tried T1…, error codes E1…, safety notes S1…, I-item and M-money, plus the flowchart's nodes N1… and, in the call, the walk's answers W1…. A safety note at level stop (gas smell, carbon monoxide, smoke or burning, sparks, a swollen battery, a shock, water near wiring) sets checks.floor to stop_unsafe, and both tasks must then answer stop_unsafe. The repair ceiling is a rule of thumb: up to half the replacement price in the first half of the item's typical life, tapering to a tenth at the end of it.
The declared input schema carries scalars only, so object-valued fields travel JSON-encoded as strings (FixKit.toWire does this). task, item, report and checks are required.
Input fields
| Field | Type | Notes |
|---|---|---|
task | string | tree or verdict. |
item | string (JSON) | {name, category, category_name, age_years, typical_life_years[lo,hi], replace_price, currency, comfort, comfort_name, tools[]}; comfort is none, basic or handy; age and price may be null. |
report | string (JSON) | {symptoms[{id,text}], tried[{id,text}], codes[{id,code}]} from FixKit.prescan; long text keeps its start and end around a marker. |
checks | string (JSON) | {safety[{id,level,text}], floor, money{stage, life_ratio, repair_share, ceiling}}. |
tree | string (JSON) | verdict only: {start, nodes[{id, kind, text, yes?, no?}]} - the flowchart that was walked. |
walk | string (JSON) | verdict only: {steps[{id, node, answer}], leaf, leaf_kind, leaf_text, complete, found?} from FixKit.walk. |
note | string | Optional, the person's own words; over 600 characters keeps the start and the end around a marker. |
retry_note | string | Only on a reformat retry. |
The reply
The job's output.output is a string holding one JSON object. Parse it with FixKit.parseReply, then normalize, then check it with reconcile(reply, {pre, tree, answers}). FixKit.walk(tree, ["yes","no",...]) follows the answers to a leaf; layout(tree) gives node positions for drawing; toMermaid(tree) writes flowchart source.
Worked examples
Replies produced against the app's prompt for the sample repairs (generic items, not real brands; lists shortened; the full replies ship in example-result.js). The page replays each of them free from its sample buttons. Inputs are shown with the report folded.
task: tree - Washer won't drain (ready)
Input:
{
"task": "tree",
"item": "{\"name\":\"Front-load washing machine\",\"category\":\"washer\",\"category_name\":\"Washing machine\",\"age_years\":6,\"typical_life_years\":[10,14],\"replace_price\":750,\"currency\":\"USD\",\"comfort\":\"basic\",\"comfort_name\":\"Basic - can follow a video\",\"tools\":[\"screwdrivers\",\"bucket and towels\",\"flashlight\"]}",
"report": "{\"symptoms\":[{\"id\":\"Y1\",\"text\":\"Finishes the wash but will not drain.\"},{\"id\":\"Y2\",\"text\":\"Water sits in the drum at the end of the cycle.\"},\"... 2 more\"],\"tried\":[{\"id\":\"T1\",\"text\":\"Turned it off at the wall and back on.\"},{\"id\":\"T2\",\"text\":\"Checked the drain hose behind the machine is not kinked.\"}],\"codes\":[{\"id\":\"E1\",\"code\":\"E21\"}]}",
"checks": "{\"safety\":[],\"floor\":\"\",\"money\":{\"stage\":\"mid\",\"life_ratio\":0.5,\"repair_share\":0.5,\"ceiling\":375}}",
"note": "I would like to fix it myself if it is something simple - I have a free Saturday."
}
Reply:
{
"task": "tree",
"verdict": "ready",
"title": "Washer Won't Drain: Hose, Then Pump Filter",
"summary": "This checks the cheap, external cause first - the far end of the drain hose - since the near end behind the machine was already confirmed clear of kinks. If that's fine, it moves to the pump filter, which commonly traps a coin, button, or lint and causes exactly this hum-but-no-water pattern. If the filter turns out clear, it's time to call a technician, since testing the pump itself needs tools you don't have on hand.",
"start": "N1",
"nodes": [
{
"id": "N1",
"kind": "check",
"text": "Is the drain hose's far end (at the sink, standpipe, or floor drain) free of clogs, properly secured, and not pushed in more than a few centimeters?",
"how": "Look at the end of the hose where it empties out. Clear away any trapped lint or debris, and make sure it isn't jammed deep into the drain, which can break the flow or cause a siphon.",
"tools": [
"flashlight"
],
"minutes": 3,
"risk": "low",
"yes": "N2",
"no": "N3",
"cite": [
"Y1",
"Y2",
"T2"
]
},
{
"id": "N2",
"kind": "check",
"text": "With the machine unplugged, the drum empty, and a bucket and towels under the front, does the pump filter (behind a small flap or cap near the bottom front) have lint, a coin, a button, or other debris caught in it or the opening behind it?",
"how": "Unplug the machine, open the small access flap near the bottom front, and put the bucket underneath. Slowly loosen the filter cap to let any trapped water drain out first, then pull the filter and use the flashlight to look into the opening behind it.",
"tools": [
"screwdrivers",
"bucket and towels",
"flashlight"
],
"minutes": 15,
"risk": "medium",
"yes": "N4",
"no": "N5",
"cite": [
"Y3",
"Y4",
"E1",
"I-item"
]
},
"..."
],
"safety": [
{
"text": "Unplug the machine, or switch it off at the socket, before opening the pump filter access panel or disconnecting the drain hose.",
"cite": [
"I-item"
]
}
],
"questions": [],
"caveats": [
"General guidance, not a technician's diagnosis.",
"Error codes vary by manufacturer - confirm what this one means in your manual."
]
}
task: verdict - Washer won't drain (diy_fix)
Input:
{
"task": "verdict",
"item": "{\"name\":\"Front-load washing machine\",\"category\":\"washer\",\"category_name\":\"Washing machine\",\"age_years\":6,\"typical_life_years\":[10,14],\"replace_price\":750,\"currency\":\"USD\",\"comfort\":\"basic\",\"comfort_name\":\"Basic - can follow a video\",\"tools\":[\"screwdrivers\",\"bucket and towels\",\"flashlight\"]}",
"report": "{\"symptoms\":[{\"id\":\"Y1\",\"text\":\"Finishes the wash but will not drain.\"},{\"id\":\"Y2\",\"text\":\"Water sits in the drum at the end of the cycle.\"},\"... 2 more\"],\"tried\":[{\"id\":\"T1\",\"text\":\"Turned it off at the wall and back on.\"},{\"id\":\"T2\",\"text\":\"Checked the drain hose behind the machine is not kinked.\"}],\"codes\":[{\"id\":\"E1\",\"code\":\"E21\"}]}",
"checks": "{\"safety\":[],\"floor\":\"\",\"money\":{\"stage\":\"mid\",\"life_ratio\":0.5,\"repair_share\":0.5,\"ceiling\":375}}",
"note": "I would like to fix it myself if it is something simple - I have a free Saturday.",
"tree": "{\"start\":\"N1\",\"nodes\":[{\"id\":\"N1\",\"kind\":\"check\",\"text\":\"Is the drain hose's far end (at the sink, standpipe, or floor drain) free of clogs, properly secured, and not pushed in more than a few centimeters?\",\"yes\":\"N2\",\"no\":\"N3\"},{\"id\":\"N2\",\"kind\":\"check\",\"text\":\"With the machine unplugged, the drum empty, and a bucket and towels under the front, does the pump filter (behind a small flap or cap near the bottom front) have lint, a coin, a button, or other debris caught in it or the opening behind it?\",\"yes\":\"N4\",\"no\":\"N5\"},\"... 3 more nodes\"]}",
"walk": "{\"steps\":[{\"id\":\"W1\",\"node\":\"N1\",\"answer\":\"yes\"},{\"id\":\"W2\",\"node\":\"N2\",\"answer\":\"yes\"}],\"leaf\":\"N4\",\"leaf_kind\":\"fix\",\"leaf_text\":\"Clean out the pump filter and the chamber behind it\",\"complete\":true,\"found\":\"The hose end at the standpipe was fine. The pump filter had a sock and a lot of lint wrapped around it.\"}"
}
Reply:
{
"task": "verdict",
"verdict": "diy_fix",
"title": "Clear the clogged pump filter",
"summary": "The hum with no water moving and the E21 fault both point to a blocked drain pump, and you confirmed it: a sock and a lot of lint were wrapped around the pump filter. Clearing a filter like this is a simple, low-risk job that fits your basic comfort level and the tools you have, so it is worth doing yourself rather than paying a technician. The likely cost is close to zero, far under the roughly 375 dollar ceiling for a 6-year-old machine - these are rough estimates, not quotes.",
"cause": {
"text": "The drain pump filter was blocked by a sock and lint, which stopped water leaving the drum, made the pump hum without moving water, and tripped the E21 drain fault.",
"confidence": "likely",
"cite": [
"N4",
"W1",
"W2",
"Y1",
"Y3",
"Y4"
]
},
"steps": [
{
"text": "Unplug the washer and turn off its water supply at the taps before touching anything.",
"minutes": 2,
"cite": [
"I-item"
]
},
{
"text": "Put a shallow pan and towels under the pump filter area at the bottom front - water will spill out when you open it.",
"minutes": 3,
"cite": [
"N2",
"N4"
]
},
"..."
],
"parts": [],
"cost": {
"diy_low": 0,
"diy_high": 15,
"pro_low": 90,
"pro_high": 180,
"replace_low": 700,
"replace_high": 800
},
"guard": [
{
"text": "Empty pockets and use a mesh bag for small items like socks before each wash so they cannot reach the pump.",
"cite": [
"Y1",
"Y4"
]
},
{
"text": "Check and clean the pump filter every few months to stop lint building up again.",
"cite": [
"N4"
]
}
],
"stop_if": [
"The filter is cracked, will not turn, or will not seat back in properly",
"You see scorch marks, a burning smell, or exposed wiring near the pump",
"..."
],
"caveats": [
"Costs are rough estimates, not quotes.",
"General guidance, not a technician's diagnosis."
]
}
task: tree - Old fridge running warm (ready)
Input:
{
"task": "tree",
"item": "{\"name\":\"Fridge-freezer, bottom freezer\",\"category\":\"fridge\",\"category_name\":\"Fridge or freezer\",\"age_years\":14,\"typical_life_years\":[10,15],\"replace_price\":1300,\"currency\":\"USD\",\"comfort\":\"basic\",\"comfort_name\":\"Basic - can follow a video\",\"tools\":[\"screwdrivers\",\"flashlight\"]}",
"report": "{\"symptoms\":[{\"id\":\"Y1\",\"text\":\"The fridge section is warm, around room temperature, and the freezer is only just keeping things frozen.\"},{\"id\":\"Y2\",\"text\":\"The compressor at the back clicks on, runs for a few seconds, then clicks off, every few minutes.\"},\"... 1 more\"],\"tried\":[{\"id\":\"T1\",\"text\":\"Vacuumed the condenser coils underneath.\"},{\"id\":\"T2\",\"text\":\"Checked the door seals - they close fine.\"}],\"codes\":[]}",
"checks": "{\"safety\":[{\"id\":\"S1\",\"level\":\"caution\",\"text\":\"Refrigerant and the sealed cooling system are licensed-technician work in most places.\"}],\"floor\":\"\",\"money\":{\"stage\":\"past\",\"life_ratio\":1.12,\"repair_share\":0.1,\"ceiling\":130}}",
"note": "It is fourteen years old. Is it worth saving?"
}
Reply:
{
"task": "tree",
"verdict": "ready",
"title": "Warm fridge, short-cycling compressor: first checks",
"summary": "This tree starts with the cheapest checks - the temperature setting and the clearance around the fridge - since the coils are already clean and the door seals are already confirmed fine. It then listens to how the compressor cycles and checks for ice buildup, to narrow down what's likely going on before anything needs a technician.",
"start": "N1",
"nodes": [
{
"id": "N1",
"kind": "check",
"text": "Is the temperature control set to a normal mid-range cooling level, not turned down low or off?",
"how": "Open the fridge and find the temperature dial or display, usually near the top back wall or on an inside panel, and check it isn't at its lowest setting or off.",
"tools": [],
"minutes": 2,
"risk": "low",
"yes": "N3",
"no": "N2",
"cite": [
"Y1"
]
},
{
"id": "N2",
"kind": "fix",
"text": "Set the control to the recommended mid-range setting.",
"how": "Turn the dial to the middle of its range (or pick the default on a display), close the doors, and check the temperature again after several hours.",
"tools": [],
"minutes": 5,
"risk": "low",
"yes": "",
"no": "",
"cite": [
"Y1"
]
},
"..."
],
"safety": [
{
"text": "Don't open the sealed refrigerant system, replace the compressor, or do your own electrical testing inside the compressor compartment - that's licensed-technician work in most places.",
"cite": [
"S1"
]
},
{
"text": "If you pull the fridge away from the wall to look or listen near the compressor, unplug it first before reaching behind it.",
"cite": [
"S1"
]
}
],
"questions": [],
"caveats": [
"General guidance, not a technician's diagnosis.",
"Every fridge model is a little different, so confirm details in the manual."
]
}
task: verdict - Old fridge running warm (replace)
Input:
{
"task": "verdict",
"item": "{\"name\":\"Fridge-freezer, bottom freezer\",\"category\":\"fridge\",\"category_name\":\"Fridge or freezer\",\"age_years\":14,\"typical_life_years\":[10,15],\"replace_price\":1300,\"currency\":\"USD\",\"comfort\":\"basic\",\"comfort_name\":\"Basic - can follow a video\",\"tools\":[\"screwdrivers\",\"flashlight\"]}",
"report": "{\"symptoms\":[{\"id\":\"Y1\",\"text\":\"The fridge section is warm, around room temperature, and the freezer is only just keeping things frozen.\"},{\"id\":\"Y2\",\"text\":\"The compressor at the back clicks on, runs for a few seconds, then clicks off, every few minutes.\"},\"... 1 more\"],\"tried\":[{\"id\":\"T1\",\"text\":\"Vacuumed the condenser coils underneath.\"},{\"id\":\"T2\",\"text\":\"Checked the door seals - they close fine.\"}],\"codes\":[]}",
"checks": "{\"safety\":[{\"id\":\"S1\",\"level\":\"caution\",\"text\":\"Refrigerant and the sealed cooling system are licensed-technician work in most places.\"}],\"floor\":\"\",\"money\":{\"stage\":\"past\",\"life_ratio\":1.12,\"repair_share\":0.1,\"ceiling\":130}}",
"note": "It is fourteen years old. Is it worth saving?",
"tree": "{\"start\":\"N1\",\"nodes\":[{\"id\":\"N1\",\"kind\":\"check\",\"text\":\"Is the temperature control set to a normal mid-range cooling level, not turned down low or off?\",\"yes\":\"N3\",\"no\":\"N2\"},{\"id\":\"N2\",\"kind\":\"fix\",\"text\":\"Set the control to the recommended mid-range setting.\"},\"... 9 more nodes\"]}",
"walk": "{\"steps\":[{\"id\":\"W1\",\"node\":\"N1\",\"answer\":\"yes\"},{\"id\":\"W2\",\"node\":\"N3\",\"answer\":\"yes\"},{\"id\":\"W3\",\"node\":\"N5\",\"answer\":\"yes\"}],\"leaf\":\"N6\",\"leaf_kind\":\"pro\",\"leaf_text\":\"Likely a failing start relay, start capacitor, or compressor motor.\",\"complete\":true,\"found\":\"The dial was on the normal setting and there is plenty of space around it. The compressor hums and strains for a few seconds before each click.\"}"
}
Reply:
{
"task": "verdict",
"verdict": "replace",
"title": "Compressor fault: replace rather than repair",
"summary": "The compressor hums and strains for a few seconds before cutting out, even though the temperature setting, clearance, and door seals are all fine, which points to a failing start relay, start capacitor, or a tiring compressor motor rather than anything simple. At fourteen years old this fridge is already past its typical lifespan, and the rule-of-thumb repair ceiling at this stage is only about $130. A technician visit plus parts for a compressor-area fault realistically costs well more than that, so the honest call is to replace rather than repair. Treat the old unit as a recycling job, not a repair project, going forward.",
"cause": {
"text": "The compressor is struggling to start - it hums and strains for a few seconds before clicking back off - even though the dial, clearance, and door seals all check out fine. That pattern points to a failing compressor start relay, start capacitor, or the compressor motor itself, not an airflow or frost problem.",
"confidence": "possible",
"cite": [
"N6",
"W3",
"Y2"
]
},
"steps": [
{
"text": "Treat this as a replacement rather than a repair; confirm with a technician first only if you want certainty about which part failed, since the likely cost either way is more than this fridge is worth fixing at this age.",
"minutes": 30,
"cite": [
"N6",
"M-money"
]
},
{
"text": "When shopping for a replacement, pick a size that fits your kitchen opening and check the energy rating, since a newer unit will run cheaper than this old one at this stage of its life.",
"minutes": 60,
"cite": [
"I-item",
"M-money"
]
},
"..."
],
"parts": [],
"cost": {
"diy_low": null,
"diy_high": null,
"pro_low": 180,
"pro_high": 450,
"replace_low": 1150,
"replace_high": 1400
},
"guard": [
{
"text": "On the new fridge, keep clearance around the back and sides and vacuum the condenser coils every six to twelve months, the way you did on this one, to help the compressor run efficiently and last longer.",
"cite": [
"T1",
"I-item"
]
}
],
"stop_if": [
"The compressor smells burnt or you see scorch marks or smoke near it.",
"The circuit breaker trips repeatedly when it tries to start.",
"..."
],
"caveats": [
"Costs are rough estimates, not quotes.",
"This is general guidance, not a technician's diagnosis."
]
}
task: tree - Lukewarm showers (ready)
Input:
{
"task": "tree",
"item": "{\"name\":\"Electric water heater, 50 gallon tank\",\"category\":\"water_heater\",\"category_name\":\"Water heater\",\"age_years\":5,\"typical_life_years\":[8,12],\"replace_price\":900,\"currency\":\"USD\",\"comfort\":\"basic\",\"comfort_name\":\"Basic - can follow a video\",\"tools\":[\"screwdrivers\",\"flashlight\"]}",
"report": "{\"symptoms\":[{\"id\":\"Y1\",\"text\":\"Hot water runs out after about five minutes and never gets properly hot, only lukewarm.\"},{\"id\":\"Y2\",\"text\":\"It started a week ago.\"},\"... 2 more\"],\"tried\":[{\"id\":\"T1\",\"text\":\"Checked the breaker - it is on and has not tripped.\"}],\"codes\":[]}",
"checks": "{\"safety\":[{\"id\":\"S1\",\"level\":\"caution\",\"text\":\"Water heaters run hot and under pressure - turn off power or gas and the cold supply before any work.\"}],\"floor\":\"\",\"money\":{\"stage\":\"mid\",\"life_ratio\":0.5,\"repair_share\":0.5,\"ceiling\":450}}",
"note": "I do not own a multimeter and would rather not touch the wiring."
}
Reply:
{
"task": "tree",
"verdict": "ready",
"title": "Water Heater Not Heating Fully: First Checks",
"summary": "Since the breaker is already confirmed fine, this tree starts by listening for tank noise, then looking - never testing - inside the access panel for obvious damage or a low thermostat setting. It only uses a screwdriver, a flashlight, and your own eyes and ears, with no meter work, and ends in either a simple fix or a call to a technician.",
"start": "N1",
"nodes": [
{
"id": "N1",
"kind": "check",
"text": "Does the tank make rumbling, popping, or knocking sounds while it's heating?",
"how": "Stand near the tank while a heating cycle runs and listen for a minute or two; these noises usually mean sediment has settled at the bottom of the tank.",
"tools": [],
"minutes": 5,
"risk": "low",
"yes": "N2",
"no": "N3",
"cite": [
"Y1"
]
},
{
"id": "N2",
"kind": "fix",
"text": "Flush the tank to clear sediment",
"how": "Turn off the power at the breaker and the cold water inlet, connect a garden hose to the drain valve at the bottom of the tank and run it to a safe drain point, then open the drain valve and a hot tap until the water runs clear before closing everything back up and restoring power.",
"tools": [
"garden hose"
],
"minutes": 30,
"risk": "medium",
"yes": "",
"no": "",
"cite": [
"Y1",
"S1"
]
},
"..."
],
"safety": [
{
"text": "Turn off the breaker for the water heater and let it cool before you open any panel or touch it.",
"cite": [
"S1"
]
},
{
"text": "Even with the power off, don't touch or test any wiring - none of these checks need a meter.",
"cite": [
"S1",
"I-item"
]
}
],
"questions": [],
"caveats": [
"General guidance, not a technician's diagnosis.",
"If anything you find makes you unsure, treat that as a reason to call a professional rather than guess."
]
}
task: verdict - Lukewarm showers (call_pro)
Input:
{
"task": "verdict",
"item": "{\"name\":\"Electric water heater, 50 gallon tank\",\"category\":\"water_heater\",\"category_name\":\"Water heater\",\"age_years\":5,\"typical_life_years\":[8,12],\"replace_price\":900,\"currency\":\"USD\",\"comfort\":\"basic\",\"comfort_name\":\"Basic - can follow a video\",\"tools\":[\"screwdrivers\",\"flashlight\"]}",
"report": "{\"symptoms\":[{\"id\":\"Y1\",\"text\":\"Hot water runs out after about five minutes and never gets properly hot, only lukewarm.\"},{\"id\":\"Y2\",\"text\":\"It started a week ago.\"},\"... 2 more\"],\"tried\":[{\"id\":\"T1\",\"text\":\"Checked the breaker - it is on and has not tripped.\"}],\"codes\":[]}",
"checks": "{\"safety\":[{\"id\":\"S1\",\"level\":\"caution\",\"text\":\"Water heaters run hot and under pressure - turn off power or gas and the cold supply before any work.\"}],\"floor\":\"\",\"money\":{\"stage\":\"mid\",\"life_ratio\":0.5,\"repair_share\":0.5,\"ceiling\":450}}",
"note": "I do not own a multimeter and would rather not touch the wiring.",
"tree": "{\"start\":\"N1\",\"nodes\":[{\"id\":\"N1\",\"kind\":\"check\",\"text\":\"Does the tank make rumbling, popping, or knocking sounds while it's heating?\",\"yes\":\"N2\",\"no\":\"N3\"},{\"id\":\"N2\",\"kind\":\"fix\",\"text\":\"Flush the tank to clear sediment\"},\"... 5 more nodes\"]}",
"walk": "{\"steps\":[{\"id\":\"W1\",\"node\":\"N1\",\"answer\":\"no\"},{\"id\":\"W2\",\"node\":\"N3\",\"answer\":\"no\"},{\"id\":\"W3\",\"node\":\"N5\",\"answer\":\"no\"}],\"leaf\":\"N7\",\"leaf_kind\":\"pro\",\"leaf_text\":\"Call a technician to test the heating element and thermostat\",\"complete\":true,\"found\":\"No rumbling from the tank. Nothing looked burnt behind the panels. Both dials were already at the middle mark.\"}"
}
Reply:
{
"task": "verdict",
"verdict": "call_pro",
"title": "Heating element likely failed; call a technician",
"summary": "Your walk ruled out sediment noise, visible burn damage, and a misset thermostat, which narrows this to a failed heating element or thermostat rather than something fixable with the tools you have on hand. Testing the elements needs a multimeter, and you've said you don't want to touch the wiring yourself, so this is a job for a technician. A typical professional repair for this kind of fault usually costs well under your $450 repair ceiling for a heater at this age, while a full replacement would run closer to the $900 you'd pay new, so repairing is the better value right now. All the figures here are rough estimates, not quotes.",
"cause": {
"text": "One of the two heating elements (or the thermostat controlling it) has most likely failed, so the tank can only partly heat the water before it runs out - the other element and the thermostat dial both check out normal.",
"confidence": "likely",
"cite": [
"N7",
"W1",
"W2",
"W3",
"Y1"
]
},
"steps": [
{
"text": "Turn off the breaker to the water heater now and leave it off until the technician arrives, since the tank stays hot and pressurized.",
"minutes": 2,
"cite": [
"S1"
]
},
{
"text": "Book a technician who services electric water heaters.",
"minutes": 10,
"cite": [
"N7"
]
},
"..."
],
"parts": [
{
"name": "Heating element",
"low": 10,
"high": 25,
"cite": [
"N7"
]
},
{
"name": "Thermostat",
"low": 10,
"high": 20,
"cite": [
"N7"
]
}
],
"cost": {
"diy_low": null,
"diy_high": null,
"pro_low": 150,
"pro_high": 350,
"replace_low": 850,
"replace_high": 950
},
"guard": [
{
"text": "Once it's working again, have it flushed about once a year - sediment buildup makes elements work harder and fail sooner.",
"cite": [
"N1",
"I-item"
]
},
{
"text": "Keep the thermostat at its normal mid-range setting rather than turning it up high to compensate for lukewarm water, which strains the remaining element.",
"cite": [
"N5",
"N6"
]
}
],
"stop_if": [
"You smell burning or see smoke - cut the breaker and call for help right away.",
"The water turns fully cold rather than lukewarm.",
"..."
],
"caveats": [
"Costs are rough estimates, not quotes.",
"General guidance, not a technician's diagnosis."
]
}
task: tree - Laptop bulging (stop_unsafe)
Input:
{
"task": "tree",
"item": "{\"name\":\"Laptop, about 4 years old\",\"category\":\"laptop\",\"category_name\":\"Laptop or computer\",\"age_years\":4,\"typical_life_years\":[4,6],\"replace_price\":1100,\"currency\":\"USD\",\"comfort\":\"handy\",\"comfort_name\":\"Handy - owns tools, has done repairs\",\"tools\":[\"screwdrivers\",\"spudger / pry tools\"]}",
"report": "{\"symptoms\":[{\"id\":\"Y1\",\"text\":\"The trackpad has started lifting and clicks on its own.\"},{\"id\":\"Y2\",\"text\":\"The bottom case rocks on the desk, and the battery looks swollen through the vent gap.\"},\"... 1 more\"],\"tried\":[{\"id\":\"T1\",\"text\":\"Shut it down and unplugged the charger.\"}],\"codes\":[]}",
"checks": "{\"safety\":[{\"id\":\"S1\",\"level\":\"stop\",\"text\":\"A swollen lithium battery can catch fire: stop charging it, do not press, puncture or try to remove it yourself, and keep it on a non-flammable surface until a repair shop or battery recycler takes it.\"}],\"floor\":\"stop_unsafe\",\"money\":{\"stage\":\"mid\",\"life_ratio\":0.8,\"repair_share\":0.26,\"ceiling\":286}}",
"note": "Can I just swap the battery myself? I have pry tools."
}
Reply:
{
"task": "tree",
"verdict": "stop_unsafe",
"title": "Swollen battery: stop, do not open it",
"summary": "The bulge you can see through the vent gap is a swollen lithium battery, which can catch fire if it is pressed, punctured, or pried at. Stop charging and using the laptop and do not try to swap the battery yourself, even with pry tools - this needs a repair shop or battery recycler to remove it safely.",
"start": "N1",
"nodes": [
{
"id": "N1",
"kind": "stop",
"text": "Stop - do not charge, use, or open the laptop yourself.",
"how": "Leave the charger unplugged, close the lid, and set the laptop on a hard, non-flammable surface such as a tile floor or a metal tray, away from paper, fabric or anything else that can burn. Do not press on the case, try to flatten the bulge, or use your pry tools to open it and take the battery out - a swollen lithium cell can catch fire if it is punctured or squeezed. Call a repair shop or a battery/electronics recycler and tell them the battery is swollen so they can collect or receive it safely.",
"tools": [],
"minutes": 5,
"risk": "low",
"yes": "",
"no": "",
"cite": [
"S1",
"Y2"
]
}
],
"safety": [
{
"text": "Do not charge, press, puncture, bend, or try to remove the swollen battery yourself; keep the laptop closed on a non-flammable surface until a repair shop or recycler takes it.",
"cite": [
"S1"
]
}
],
"questions": [],
"caveats": [
"General guidance, not a technician's diagnosis.",
"A swollen battery is a safety stop, not a DIY job, even for someone handy with pry tools."
]
}
task: verdict - Laptop bulging (stop_unsafe)
Input:
{
"task": "verdict",
"item": "{\"name\":\"Laptop, about 4 years old\",\"category\":\"laptop\",\"category_name\":\"Laptop or computer\",\"age_years\":4,\"typical_life_years\":[4,6],\"replace_price\":1100,\"currency\":\"USD\",\"comfort\":\"handy\",\"comfort_name\":\"Handy - owns tools, has done repairs\",\"tools\":[\"screwdrivers\",\"spudger / pry tools\"]}",
"report": "{\"symptoms\":[{\"id\":\"Y1\",\"text\":\"The trackpad has started lifting and clicks on its own.\"},{\"id\":\"Y2\",\"text\":\"The bottom case rocks on the desk, and the battery looks swollen through the vent gap.\"},\"... 1 more\"],\"tried\":[{\"id\":\"T1\",\"text\":\"Shut it down and unplugged the charger.\"}],\"codes\":[]}",
"checks": "{\"safety\":[{\"id\":\"S1\",\"level\":\"stop\",\"text\":\"A swollen lithium battery can catch fire: stop charging it, do not press, puncture or try to remove it yourself, and keep it on a non-flammable surface until a repair shop or battery recycler takes it.\"}],\"floor\":\"stop_unsafe\",\"money\":{\"stage\":\"mid\",\"life_ratio\":0.8,\"repair_share\":0.26,\"ceiling\":286}}",
"note": "Can I just swap the battery myself? I have pry tools.",
"tree": "{\"start\":\"N1\",\"nodes\":[{\"id\":\"N1\",\"kind\":\"stop\",\"text\":\"Stop - do not charge, use, or open the laptop yourself.\"}]}",
"walk": "{\"steps\":[],\"leaf\":\"N1\",\"leaf_kind\":\"stop\",\"leaf_text\":\"Stop - do not charge, use, or open the laptop yourself.\",\"complete\":true}"
}
Reply:
{
"task": "verdict",
"verdict": "stop_unsafe",
"title": "Stop charging: swollen battery is a fire risk",
"summary": "The trackpad lifting and the case rocking are being pushed up by a swollen lithium battery underneath, which is a fire hazard and not something to test, press, or pry open yourself, even with pry tools. Stop charging and using the laptop now, keep it on a hard, non-flammable surface, and let a repair shop or battery recycler take it from here. A professional battery swap commonly runs in the ballpark of $120-$280, which is under the usual $286 repair ceiling for a laptop at this age, so it is worth asking about repair before assuming you need a full $1000-$1150 replacement. All figures are rough estimates, not quotes.",
"cause": {
"text": "The battery cell has swollen, and that swelling is physically pushing up the trackpad and rocking the bottom case; warming while charging is consistent with a failing cell, not a simple settings or connector issue.",
"confidence": "likely",
"cite": [
"N1",
"S1",
"Y1",
"Y2"
]
},
"steps": [
{
"text": "Unplug the charger and leave the laptop switched off - do not turn it back on or try to charge it again.",
"minutes": 1,
"cite": [
"S1",
"T1"
]
},
{
"text": "Set it on a hard, non-flammable surface such as a tile floor, concrete, or a metal tray, clear of paper, fabric, or anything else that could catch fire.",
"minutes": 2,
"cite": [
"S1"
]
},
"..."
],
"parts": [],
"cost": {
"diy_low": null,
"diy_high": null,
"pro_low": 120,
"pro_high": 280,
"replace_low": 1000,
"replace_high": 1150
},
"guard": [],
"stop_if": [
"Smoke, a burning smell, hissing, or popping sounds - move it outside onto a non-flammable surface and call emergency services.",
"The case cracks open or the swelling grows quickly - avoid touching it and get professional help right away."
],
"caveats": [
"General guidance, not a technician's diagnosis.",
"Costs are rough estimates, not quotes."
]
}
task: tree - Printer just broke (need_info)
Input:
{
"task": "tree",
"item": "{\"name\":\"Printer\",\"category\":\"printer\",\"category_name\":\"Printer\",\"age_years\":null,\"typical_life_years\":[4,6],\"replace_price\":null,\"currency\":\"USD\",\"comfort\":\"none\",\"comfort_name\":\"Never opened anything\",\"tools\":[]}",
"report": "{\"symptoms\":[{\"id\":\"Y1\",\"text\":\"It stopped working.\"}],\"tried\":[],\"codes\":[]}",
"checks": "{\"safety\":[],\"floor\":\"\",\"money\":{\"stage\":\"unknown\",\"life_ratio\":null,\"repair_share\":0.5,\"ceiling\":null}}"
}
Reply:
{
"task": "tree",
"verdict": "need_info",
"title": "Printer stopped working: first checks",
"summary": "You've told us the printer stopped working, but that covers very different problems - no power, no printing, a stuck job, or an error light - and each needs a different set of checks. Below are a few universal checks anyone can do with no tools, plus questions that will let us build the full step-by-step tree.",
"start": "N1",
"nodes": [
{
"id": "N1",
"kind": "check",
"text": "Does the printer power on - any lights, a display, or a start-up sound?",
"how": "Press the power button and watch and listen for any sign of life on the printer itself.",
"tools": [],
"minutes": 1,
"risk": "low",
"yes": "N2",
"no": "N3",
"cite": [
"Y1",
"I-item"
]
},
{
"id": "N2",
"kind": "check",
"text": "Is there a paper jam, stuck paper, or a blinking error light or message visible?",
"how": "Open the covers and look inside for jammed paper, and check the control panel for any flashing light or icon.",
"tools": [],
"minutes": 3,
"risk": "low",
"yes": "N4",
"no": "N5",
"cite": [
"Y1",
"I-item"
]
},
"..."
],
"safety": [
{
"text": "Unplug the printer, or switch it off and unplug it, before opening any cover or reaching in to clear a jam.",
"cite": [
"I-item"
]
}
],
"questions": [
"When it \"stopped working\", what exactly happens - no power at all, powers on but will not print, a job stuck partway, or an error message or blinking light?",
"If there is a display or indicator lights, what do they show, including any blinking pattern or code?",
"..."
],
"caveats": [
"General guidance, not a technician's diagnosis.",
"We need a bit more detail before a full check-by-check tree can be built."
]
}
Build the input yourself
Load the script in node (global.window = {}; require("./fixkit.js")), then:
const K = FixKit;
const pre = K.prescan({ item: "Front-load washing machine", age: 6, price: 750, currency: "USD", comfort: "basic",
tools: ["screwdrivers"], symptoms: "Will not drain. Display shows E21.", tried: "Restarted it" });
const { input } = K.buildInput("tree", { pre, note: "" });
const wire = K.toWire(input); // POST this to /estimate and /run
// later: const tree = K.normalize(K.parseReply(job.output.output));
// K.reconcile(tree, { pre }) -> [{level, code, text}]
// const answers = ["yes", "yes"]; // the walk, one answer per check
// const call = K.buildInput("verdict", { pre, tree, answers, found: "lint in the pump filter" }).input;
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://fix-first.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://fix-first.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"]
// fix-first.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://fix-first.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://fix-first.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
}
// CueScroll.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 CueScroll {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://fix-first.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://fix-first.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://fix-first.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://fix-first.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":"fix-first"}' | 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": "fix-first"})["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: "fix-first" })).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": "fix-first"}, 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", "fix-first"), 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: "fix-first" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "fix-first"])["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 = "fix-first" }))!["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": "tree", "item": "{...}", "report": "{...}", "checks": "{...}"}
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": "tree", "item": "{...}", "report": "{...}", "checks": "{...}"}
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, item, report, checks, note} (verdict: + tree, walk)
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 lane and the input: a retry with the same key never bills twice. The page uses fix-first:<task>:<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="fix-first:tree:$(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 = "fix-first:" + 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 lane's JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("verdict"))
import { createHash } from "node:crypto";
const key = `fix-first:${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 := "fix-first:" + 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 = "fix-first:" + 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 = "fix-first:#{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["verdict"]].inspect
$key = "fix-first:" . $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["verdict"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"fix-first:{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: fix-first:tree:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json
rev = json.load(open("input.json")) # the same run input as input.json: {"task": "tree", "item": "{...}", "report": "{...}", "checks": "{...}"}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "fix-first:tree:" + hashlib.sha256(json.dumps(rev).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 rev = JSON.parse(readFileSync("input.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `fix-first:tree:${createHash("sha256").update(JSON.stringify(rev)).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", "fix-first:tree:"+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 rev = java.nio.file.Files.readString(java.nio.file.Path.of("input.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "fix-first:tree:" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "fix-first:tree:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
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
$rev = file_get_contents("input.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: fix-first:tree:" . substr(hash("sha256", $rev), 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 rev = File.ReadAllText("input.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"fix-first:tree:{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);