Drive Actives Rota from your own code
Both metered steps - the audit of the week and the reaction triage - are available over HTTP. A natural use is a skincare journal or a routine tracker that already knows someone's products: your code sends the shelf and the week, and shows the cited answer.
The split first: the page does the mechanics; the model gives the judgement; the page checks it. Shelf parsing, active-family detection, layering order, the night pattern, the clashes, the flags and the reaction prescan come from rotakit.js, which runs in node as well as in the browser. Its reconcile function then checks that every product or suspect is on the shelf and answered once, that every move has a destination and is not a no-op, that the verdict matches the count of changes, that ticked red flags force a clinician verdict, that a pause of actives really covers every active, that any high-level clash surviving the changes is named, and that every cited id and every strength is on the page. This is general skincare guidance, not medical advice.
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":"actives-rota"}), 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 |
|---|---|---|
rota | A title, a summary; products [{product_id, verdict, to, why, cite}] - one per shelf product, verdict keep, move, pause or patch_test, to (moves only) am, pm, x_nights, r_nights or c_nights; week [{day, night, why, cite}] - only the PM nights that change, night X, R or C; up to 4 cautions lines; caveats. | verdict: ready, adjust, rebuild |
reaction | A title, a summary; suspects [{product_id, likelihood, why, cite}] with likelihood likely, possible or unlikely; pause - product ids to stop now; plan [{when, text, cite}]; red_flags - when to get help; caveats. | verdict: adjust, pause_actives, see_clinician |
A line is always {text, cite}. Cites are the page's ids: products P1 P2…, clashes C1 C2…, flags such as F-recovery F-heavy F-ret-am F-no-spf F-pregnancy F-new-products, days D1…D7 (Mon to Sun), sessions D1-AM D1-PM…, and T-week L-profile R-report. The audit verdict is fixed by its changes: count every move, every pause and every week entry - ready for none, adjust for 1–3, rebuild for 4 or more (patch_test does not count). A reaction with any ticked red flag must be see_clinician; pause_actives pauses every retinoid, acid, benzoyl peroxide, vitamin C, copper peptide and azelaic acid product.
The declared input schema carries scalars only, so object-valued fields travel JSON-encoded as strings (RotaKit.toWire does this). task, profile, shelf, week and checks are required.
Input fields
| Field | Type | Notes |
|---|---|---|
task | string | rota or reaction. |
profile | string (JSON) | {skin, experience, concerns[], pregnant}. |
shelf | string (JSON) | [{id, name, type, families[], when_given, placement, cells, ingredients?, new?, prescribed?, placed_by_you?}]; ingredient lists clipped to 240 characters for the model. |
week | string (JSON) | {cadence, cadence_name, days[{id, day, night, runs_as, am[], pm[]}]} from RotaKit.analyze. |
checks | string (JSON) | {conflicts[{id, level, rule, products[], cells[], text}], flags[{id, level, text, products[]}], nights, runs_as}. |
reaction | string (JSON) | reaction only: {symptoms[], red_flags[], day, started, description, floor, candidates[{product_id, reasons[]}]} from RotaKit.prescan. |
reaction_result | string (JSON) | rota only, optional: {verdict, summary, pause[], suspects[]} from an earlier reaction, so the audit rebuilds around it. |
note | string | Optional, the person's own words; over 800 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 RotaKit.parseReply, then normalize, then check it with reconcile(reply, {stats, products, prescan}). applyRota(state, reply, stats) returns the placements and nights with the audit applied; applyReaction(state, reply, stats) pauses the named products and, for a pause or clinician verdict, makes every night a recovery night.
Worked examples
Replies produced against the app's prompt for the three sample shelves (generic products, 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 shelf, the week and the checks folded.
task: rota - Trend stacker (sample shelf) (rebuild)
Input:
{
"task": "rota",
"profile": "{\"skin\":\"combination\",\"experience\":\"new\",\"concerns\":[\"texture\",\"fine lines\",\"dullness\"],\"pregnant\":false}",
"shelf": "[{\"id\":\"P1\",\"name\":\"Mandelic acid gel wash\",\"type\":\"cleanser\",\"families\":[\"AHA exfoliant\"],\"when_given\":\"both\",\"placement\":\"AM + every PM\",\"cells\":14,\"ingredients\":\"mandelic acid 5%, glycerin, coco-glucoside\"},{\"id\":\"P2\",\"name\":\"Vitamin C 15% serum\",\"type\":\"serum\",\"families\":[\"vitamin C\"],\"when_given\":\"am\",\"placement\":\"AM\",\"cells\":7,\"ingredients\":\"ascorbic acid 15%, ferulic acid, tocopherol\"},\"... 8 more products\"]",
"week": "{\"cadence\":\"custom\",\"cadence_name\":\"Custom - tap a night to change it\",\"days\":[{\"id\":\"D1\",\"day\":\"Mon\",\"night\":\"R\",\"runs_as\":\"R\",\"am\":[\"P1\",\"P4\",\"P2\",\"P3\",\"P5\",\"P8\",\"P9\",\"P10\"],\"pm\":[\"P1\",\"P5\",\"P8\",\"P7\",\"P9\"]},{\"id\":\"D2\",\"day\":\"Tue\",\"night\":\"X\",\"runs_as\":\"X\",\"am\":[\"P1\",\"P4\",\"P2\",\"P3\",\"P5\",\"P8\",\"P9\",\"P10\"],\"pm\":[\"P1\",\"P4\",\"P6\",\"P8\",\"P9\"]},\"... 5 more days\"]}",
"checks": "{\"conflicts\":[{\"id\":\"C1\",\"level\":\"high\",\"rule\":\"retinoid-acid\",\"products\":[\"P4\",\"P5\"],\"cells\":[\"D1-AM\",\"D2-AM\",\"D3-AM\",\"D4-AM\",\"D5-AM\",\"D6-AM\",\"D7-AM\"],\"text\":\"A retinoid and an exfoliating acid in one session (Glycolic acid 7% toning solution, Retinol 0.5% serum) - the classic irritation stack. Split them across nights.\"},\"...\"],\"flags\":[{\"id\":\"F-ret-am\",\"level\":\"warn\",\"text\":\"Retinol 0.5% serum is set for the morning - retinoids are usually a night step, since light breaks some of them down.\",\"products\":[\"P5\"]},\"...\"],\"nights\":{\"X\":3,\"R\":3,\"C\":1},\"runs_as\":{\"X\":3,\"R\":3,\"C\":1}}",
"note": "I only started actives last month and I use most things morning and night to get results faster. I added the copper peptide serum because everyone is talking about it. My skin has felt tight lately."
}
Reply:
{
"task": "rota",
"verdict": "rebuild",
"title": "A calmer start for a new-actives week",
"summary": "Your morning stack already lines up a sensible cleanser, antioxidant, niacinamide, moisturizer and sunscreen routine, and alternating retinoid and acid nights shows you were already trying to space actives out. The week was still asking a lot of your skin: a retinoid, an acid, vitamin C and a new copper peptide were landing in the same morning session, two retinoids shared one night, and only one night a week was pure recovery. This version keeps the glycolic toner and retinol to their own nights, moves the copper peptide to recovery nights away from acids and vitamin C, and pauses the newer retinoid and the extra leave-on acid so only one new active and one retinoid are building up at a time. It also turns the back of the week into three recovery nights so your barrier gets a real break.",
"products": [
{
"product_id": "P1",
"verdict": "keep",
"to": "",
"why": "This wash-off cleanser still overlaps with other actives in the same session, but contact is brief, so the risk stays low. It is doing its job as a daily cleanser.",
"cite": [
"P1",
"C7",
"C9"
]
},
{
"product_id": "P2",
"verdict": "keep",
"to": "",
"why": "Vitamin C stays in the morning, away from acids and the copper peptide now that those move, so it can do its antioxidant job without extra sting.",
"cite": [
"P2",
"C3",
"C5"
]
},
"..."
],
"week": [
{
"day": "D5",
"night": "C",
"why": "Cutting one retinoid night brings you to two a week, the usual starting pace while you're new to actives.",
"cite": [
"D5",
"F-new-start"
]
},
{
"day": "D6",
"night": "C",
"why": "Turning this into a recovery night gives the barrier three nights in a row to settle instead of one.",
"cite": [
"D6",
"F-recovery",
"F-heavy"
]
}
],
"cautions": [
{
"text": "A wash-off acid cleanser still overlaps with the toner on exfoliation nights and with retinol on retinoid nights. Contact is brief, so watch for stinging rather than stopping it outright.",
"cite": [
"C7",
"C9"
]
},
{
"text": "The copper peptide is new to your shelf. Patch test it on its new recovery-night slot and give it a couple of weeks before judging it.",
"cite": [
"P3"
]
},
"..."
],
"caveats": [
"General cosmetic guidance, not medical advice.",
"Not a diagnosis - see a dermatologist or pharmacist for anything that doesn't settle."
]
}
task: reaction - Trend stacker (sample shelf) (pause_actives)
Input:
{
"task": "reaction",
"profile": "{\"skin\":\"combination\",\"experience\":\"new\",\"concerns\":[\"texture\",\"fine lines\",\"dullness\"],\"pregnant\":false}",
"shelf": "[{\"id\":\"P1\",\"name\":\"Mandelic acid gel wash\",\"type\":\"cleanser\",\"families\":[\"AHA exfoliant\"],\"when_given\":\"both\",\"placement\":\"AM + every PM\",\"cells\":14,\"ingredients\":\"mandelic acid 5%, glycerin, coco-glucoside\"},{\"id\":\"P2\",\"name\":\"Vitamin C 15% serum\",\"type\":\"serum\",\"families\":[\"vitamin C\"],\"when_given\":\"am\",\"placement\":\"AM\",\"cells\":7,\"ingredients\":\"ascorbic acid 15%, ferulic acid, tocopherol\"},\"... 8 more products\"]",
"week": "{\"cadence\":\"custom\",\"cadence_name\":\"Custom - tap a night to change it\",\"days\":[{\"id\":\"D1\",\"day\":\"Mon\",\"night\":\"R\",\"runs_as\":\"R\",\"am\":[\"P1\",\"P4\",\"P2\",\"P3\",\"P5\",\"P8\",\"P9\",\"P10\"],\"pm\":[\"P1\",\"P5\",\"P8\",\"P7\",\"P9\"]},{\"id\":\"D2\",\"day\":\"Tue\",\"night\":\"X\",\"runs_as\":\"X\",\"am\":[\"P1\",\"P4\",\"P2\",\"P3\",\"P5\",\"P8\",\"P9\",\"P10\"],\"pm\":[\"P1\",\"P4\",\"P6\",\"P8\",\"P9\"]},\"... 5 more days\"]}",
"checks": "{\"conflicts\":[{\"id\":\"C1\",\"level\":\"high\",\"rule\":\"retinoid-acid\",\"products\":[\"P4\",\"P5\"],\"cells\":[\"D1-AM\",\"D2-AM\",\"D3-AM\",\"D4-AM\",\"D5-AM\",\"D6-AM\",\"D7-AM\"],\"text\":\"A retinoid and an exfoliating acid in one session (Glycolic acid 7% toning solution, Retinol 0.5% serum) - the classic irritation stack. Split them across nights.\"},\"...\"],\"flags\":[{\"id\":\"F-ret-am\",\"level\":\"warn\",\"text\":\"Retinol 0.5% serum is set for the morning - retinoids are usually a night step, since light breaks some of them down.\",\"products\":[\"P5\"]},\"...\"],\"nights\":{\"X\":3,\"R\":3,\"C\":1},\"runs_as\":{\"X\":3,\"R\":3,\"C\":1}}",
"note": "I only started actives last month and I use most things morning and night to get results faster. I added the copper peptide serum because everyone is talking about it. My skin has felt tight lately.",
"reaction": "{\"symptoms\":[\"stinging\",\"redness\",\"peeling\"],\"red_flags\":[],\"day\":\"Thu\",\"started\":\"Thursday night, worse by Friday morning\",\"description\":\"Cheeks and around my nose sting when I put on moisturizer, look pink and are flaking at the edges. No swelling.\",\"floor\":\"adjust\",\"candidates\":[{\"product_id\":\"P3\",\"reasons\":[\"used Thu morning\",\"new to your routine\",\"carries copper peptide\"]},{\"product_id\":\"P7\",\"reasons\":[\"new to your routine\",\"carries retinoid\"]},\"...\"]}"
}
Reply:
{
"task": "reaction",
"verdict": "pause_actives",
"title": "Acid and retinol stacking likely caused this",
"summary": "This reads as irritation from heavy actives use, not an allergic reaction - there is no swelling and nothing is spreading. Thursday's exfoliation night stacked three acid products back to back, and your morning routine pairs a leave-on glycolic toner with a retinol every single day, which the page already calls the classic irritation stack. The glycolic toner and the lactic acid serum you layered on Thursday night are the most likely triggers. Pause every active now, keep your cleanser, moisturizer and sunscreen, and bring products back one at a time once your skin has settled.",
"suspects": [
{
"product_id": "P4",
"likelihood": "likely",
"why": "Glycolic acid 7% toning solution is a leave-on acid you used both Thursday morning and Thursday night, right before the stinging and peeling started. It is also the product tangled up in the most clashes on your week, including the retinoid pairing the page calls the classic irritation stack.",
"cite": [
"P4",
"C1",
"C6",
"D4-AM",
"D4-PM",
"R-report"
]
},
{
"product_id": "P6",
"likelihood": "likely",
"why": "Lactic acid 10% serum is your strongest leave-on exfoliant and you layered it right after the glycolic toner on Thursday night - two leave-on acids in one session is exactly the kind of stacking that leaves cheeks and the sides of the nose stinging and flaking the next day.",
"cite": [
"P6",
"C6",
"D4-PM",
"R-report"
]
},
"..."
],
"pause": [
"P1",
"P2",
"P3",
"P4",
"P5",
"P6",
"P7"
],
"plan": [
{
"when": "Today",
"text": "Stop every active - both leave-on acids, the acid wash, vitamin C, the copper peptide and both retinoids - and switch down to a gentle cleanse, your ceramide moisturizer and sunscreen until the stinging, redness and peeling have settled.",
"cite": [
"P9",
"P10",
"R-report"
]
},
{
"when": "Days 1-7",
"text": "Give your skin a full recovery stretch on that simplified routine. If it is not calmer within a week, or the redness or peeling gets worse instead of better, treat that as a reason to check in with a pharmacist or dermatologist rather than waiting it out.",
"cite": [
"R-report"
]
},
"..."
],
"red_flags": [
"Swelling of the lips, tongue or face, or trouble breathing - get emergency care.",
"Hives, blistering or oozing skin.",
"..."
],
"caveats": [
"General cosmetic guidance, not a diagnosis.",
"See a pharmacist or dermatologist if this does not settle in a week or two."
]
}
task: rota - Barrier first (sample shelf) (ready)
Input:
{
"task": "rota",
"profile": "{\"skin\":\"sensitive\",\"experience\":\"new\",\"concerns\":[\"redness\",\"dryness\"],\"pregnant\":false}",
"shelf": "[{\"id\":\"P1\",\"name\":\"Cream cleanser\",\"type\":\"cleanser\",\"families\":[\"barrier support\"],\"when_given\":\"both\",\"placement\":\"AM + every PM\",\"cells\":14,\"ingredients\":\"glycerin, ceramides, cetearyl alcohol\"},{\"id\":\"P2\",\"name\":\"Azelaic acid 10% cream gel\",\"type\":\"moisturizer\",\"families\":[\"azelaic acid\",\"barrier support\"],\"when_given\":\"am\",\"placement\":\"AM\",\"cells\":7,\"ingredients\":\"azelaic acid 10%, allantoin\"},\"... 4 more products\"]",
"week": "{\"cadence\":\"custom\",\"cadence_name\":\"Custom - tap a night to change it\",\"days\":[{\"id\":\"D1\",\"day\":\"Mon\",\"night\":\"C\",\"runs_as\":\"C\",\"am\":[\"P1\",\"P3\",\"P2\",\"P5\",\"P6\"],\"pm\":[\"P1\",\"P3\",\"P5\"]},{\"id\":\"D2\",\"day\":\"Tue\",\"night\":\"R\",\"runs_as\":\"R\",\"am\":[\"P1\",\"P3\",\"P2\",\"P5\",\"P6\"],\"pm\":[\"P1\",\"P3\",\"P4\",\"P5\"]},\"... 5 more days\"]}",
"checks": "{\"conflicts\":[],\"flags\":[{\"id\":\"F-new-products\",\"level\":\"info\",\"text\":\"Retinal 0.05% night serum is new - a patch test before full use is the usual advice.\",\"products\":[\"P4\"]}],\"nights\":{\"X\":0,\"R\":2,\"C\":5},\"runs_as\":{\"X\":0,\"R\":2,\"C\":5}}",
"note": "Fragrance-free everything. I want to start a retinoid slowly without wrecking my barrier."
}
Reply:
{
"task": "rota",
"verdict": "ready",
"title": "Barrier-First Start Already Working",
"summary": "This week already fits a slow, sensitive-skin start to a retinoid: two retinoid nights and five recovery nights in between, with no clashes between your azelaic acid, niacinamide and retinal. The only change worth making is a patch test on the new retinal serum before you lean on it for both weekly nights. The cleanser, moisturizer and sunscreen can all keep doing their job where they already sit.",
"products": [
{
"product_id": "P1",
"verdict": "keep",
"to": "",
"why": "Your cleanser runs morning and night and isn't flagged by anything, so it can stay where it is.",
"cite": [
"P1"
]
},
{
"product_id": "P2",
"verdict": "keep",
"to": "",
"why": "The azelaic acid stays in the morning, away from the retinal at night, so it can continue as is.",
"cite": [
"P2",
"P4"
]
},
"..."
],
"week": [],
"cautions": [
{
"text": "Watch the skin around your nose and chin on the two retinal nights for stinging, redness or peeling, since this retinal is new to you.",
"cite": [
"P4",
"F-new-products"
]
},
{
"text": "Two retinoid nights a week is a sensible starting pace for someone new to actives - stay here for a few weeks before adding a third night.",
"cite": [
"P4",
"L-profile"
]
},
"..."
],
"caveats": [
"General skincare guidance, not medical advice.",
"See a dermatologist or pharmacist if your skin reacts or your redness or dryness gets worse."
]
}
task: reaction - Barrier first (sample shelf) (adjust)
Input:
{
"task": "reaction",
"profile": "{\"skin\":\"sensitive\",\"experience\":\"new\",\"concerns\":[\"redness\",\"dryness\"],\"pregnant\":false}",
"shelf": "[{\"id\":\"P1\",\"name\":\"Cream cleanser\",\"type\":\"cleanser\",\"families\":[\"barrier support\"],\"when_given\":\"both\",\"placement\":\"AM + every PM\",\"cells\":14,\"ingredients\":\"glycerin, ceramides, cetearyl alcohol\"},{\"id\":\"P2\",\"name\":\"Azelaic acid 10% cream gel\",\"type\":\"moisturizer\",\"families\":[\"azelaic acid\",\"barrier support\"],\"when_given\":\"am\",\"placement\":\"AM\",\"cells\":7,\"ingredients\":\"azelaic acid 10%, allantoin\"},\"... 4 more products\"]",
"week": "{\"cadence\":\"custom\",\"cadence_name\":\"Custom - tap a night to change it\",\"days\":[{\"id\":\"D1\",\"day\":\"Mon\",\"night\":\"C\",\"runs_as\":\"C\",\"am\":[\"P1\",\"P3\",\"P2\",\"P5\",\"P6\"],\"pm\":[\"P1\",\"P3\",\"P5\"]},{\"id\":\"D2\",\"day\":\"Tue\",\"night\":\"R\",\"runs_as\":\"R\",\"am\":[\"P1\",\"P3\",\"P2\",\"P5\",\"P6\"],\"pm\":[\"P1\",\"P3\",\"P4\",\"P5\"]},\"... 5 more days\"]}",
"checks": "{\"conflicts\":[],\"flags\":[{\"id\":\"F-new-products\",\"level\":\"info\",\"text\":\"Retinal 0.05% night serum is new - a patch test before full use is the usual advice.\",\"products\":[\"P4\"]}],\"nights\":{\"X\":0,\"R\":2,\"C\":5},\"runs_as\":{\"X\":0,\"R\":2,\"C\":5}}",
"note": "Fragrance-free everything. I want to start a retinoid slowly without wrecking my barrier.",
"reaction": "{\"symptoms\":[\"dryness or flaking\"],\"red_flags\":[],\"day\":\"Tue\",\"started\":\"Wednesday morning, after the first retinal night\",\"description\":\"A little flaking around my nose and chin. No stinging, no redness to speak of.\",\"floor\":\"adjust\",\"candidates\":[{\"product_id\":\"P4\",\"reasons\":[\"used Tue night\",\"new to your routine\",\"carries retinoid\"]},{\"product_id\":\"P2\",\"reasons\":[\"used Tue morning\",\"carries azelaic acid\"]},\"...\"]}"
}
Reply:
{
"task": "reaction",
"verdict": "adjust",
"title": "Mild flaking after your first retinal night",
"summary": "This looks like ordinary irritation from starting a new active, not a sign of an allergy. The retinal serum - new to your routine and used for the first time Tuesday night - is the most likely cause, with the azelaic acid possibly adding to it. Keep the retinal at its current twice-weekly pace, pause the azelaic acid for a few days, and lean on the ceramide moisturizer while your skin settles.",
"suspects": [
{
"product_id": "P4",
"likelihood": "likely",
"why": "This is a retinoid and it is new to your routine. Mild flaking after the first night is a common, settling response while skin adjusts to a new retinoid.",
"cite": [
"P4",
"D2-PM",
"R-report"
]
},
{
"product_id": "P2",
"likelihood": "possible",
"why": "Azelaic acid can be drying for some people, and you used it the same morning the flaking started - though it is already a regular part of your AM routine, not new.",
"cite": [
"P2",
"D2-AM"
]
},
"..."
],
"pause": [
"P2"
],
"plan": [
{
"when": "Tonight",
"text": "Keep the retinal serum to its current two nights a week - mild flaking after the first use is a common, settling response while skin adjusts to a new retinoid.",
"cite": [
"P4",
"D2-PM",
"R-report"
]
},
{
"when": "Now",
"text": "Pause the azelaic acid cream gel for a few days so your skin is carrying one active instead of two while it settles. Keep using the ceramide moisturizer and sunscreen each morning in its place.",
"cite": [
"P2",
"P5",
"P6"
]
},
"..."
],
"red_flags": [
"Stinging, burning or redness that spreads or does not settle in a day or two.",
"Swelling, hives, blisters or oozing skin.",
"..."
],
"caveats": [
"General cosmetic guidance, not a diagnosis.",
"If anything changes or does not settle, check with a pharmacist or dermatologist."
]
}
task: rota - Breakout shelf (sample shelf) (adjust)
Input:
{
"task": "rota",
"profile": "{\"skin\":\"oily\",\"experience\":\"some\",\"concerns\":[\"breakouts\",\"texture\"],\"pregnant\":false}",
"shelf": "[{\"id\":\"P1\",\"name\":\"Benzoyl peroxide 4% foaming wash\",\"type\":\"cleanser\",\"families\":[\"benzoyl peroxide\"],\"when_given\":\"am\",\"placement\":\"AM\",\"cells\":7,\"ingredients\":\"benzoyl peroxide 4%\"},{\"id\":\"P2\",\"name\":\"Salicylic acid 2% toner\",\"type\":\"toner\",\"families\":[\"BHA exfoliant\"],\"when_given\":\"pm\",\"placement\":\"X nights\",\"cells\":2,\"ingredients\":\"salicylic acid 2%, witch hazel\"},\"... 3 more products\"]",
"week": "{\"cadence\":\"cycle4\",\"cadence_name\":\"Four-night cycle - exfoliate, retinoid, recover, recover\",\"days\":[{\"id\":\"D1\",\"day\":\"Mon\",\"night\":\"X\",\"runs_as\":\"X\",\"am\":[\"P1\",\"P3\",\"P4\",\"P5\"],\"pm\":[\"P2\",\"P4\"]},{\"id\":\"D2\",\"day\":\"Tue\",\"night\":\"R\",\"runs_as\":\"R\",\"am\":[\"P1\",\"P3\",\"P4\",\"P5\"],\"pm\":[\"P3\",\"P4\"]},\"... 5 more days\"]}",
"checks": "{\"conflicts\":[{\"id\":\"C1\",\"level\":\"low\",\"rule\":\"retinoid-bpo\",\"products\":[\"P1\",\"P3\"],\"cells\":[\"D1-AM\",\"D2-AM\",\"D3-AM\",\"D4-AM\",\"D5-AM\",\"D6-AM\",\"D7-AM\"],\"text\":\"Benzoyl peroxide and a retinoid in one session (Benzoyl peroxide 4% foaming wash, Adapalene 0.1% gel) - both dry the skin and benzoyl peroxide can break down some retinoids. One AM, one PM is the usual split.\"}],\"flags\":[{\"id\":\"F-ret-am\",\"level\":\"warn\",\"text\":\"Adapalene 0.1% gel is set for the morning - retinoids are usually a night step, since light breaks some of them down.\",\"products\":[\"P3\"]},\"...\"],\"nights\":{\"X\":2,\"R\":2,\"C\":3},\"runs_as\":{\"X\":2,\"R\":2,\"C\":3}}",
"note": "I put the adapalene on morning and night to clear things faster. The sunscreen is new this week."
}
Reply:
{
"task": "rota",
"verdict": "adjust",
"title": "Retinoid at night only, sunscreen patch-tested",
"summary": "Your four-night cycle already keeps the BHA toner and the retinoid on separate nights and gives you three recovery nights a week, which suits oily, breakout-prone skin that is still building up actives. The one change is the adapalene gel: it was also set for every morning, which put it in the same session as the benzoyl peroxide wash, so it now runs on the R nights only. The new sunscreen lists a fragrance ingredient, so patch test it before you rely on it every day.",
"products": [
{
"product_id": "P1",
"verdict": "keep",
"to": "",
"why": "This benzoyl peroxide wash stays a quick rinse-off step each morning, and once the retinoid only runs at night this session no longer mixes benzoyl peroxide with a retinoid.",
"cite": [
"P1",
"C1"
]
},
{
"product_id": "P2",
"verdict": "keep",
"to": "",
"why": "This BHA toner stays on its two exfoliation nights, well clear of the retinoid nights.",
"cite": [
"P2",
"D1-PM",
"D5-PM"
]
},
"..."
],
"week": [],
"cautions": [
{
"text": "The new sunscreen carries a fragrance ingredient - watch your skin the first week even after a patch test.",
"cite": [
"P5",
"F-fragrance"
]
},
{
"text": "Between the morning benzoyl peroxide wash, the BHA nights and the retinoid nights, this is a full week of actives for oily skin - watch for overall dryness or flaking even though they are spaced across different sessions.",
"cite": [
"P1",
"P2",
"P3",
"T-week"
]
},
"..."
],
"caveats": [
"General skincare guidance, not medical advice.",
"See a dermatologist or pharmacist if irritation doesn't settle or gets worse."
]
}
task: reaction - Breakout shelf (sample shelf) (see_clinician)
Input:
{
"task": "reaction",
"profile": "{\"skin\":\"oily\",\"experience\":\"some\",\"concerns\":[\"breakouts\",\"texture\"],\"pregnant\":false}",
"shelf": "[{\"id\":\"P1\",\"name\":\"Benzoyl peroxide 4% foaming wash\",\"type\":\"cleanser\",\"families\":[\"benzoyl peroxide\"],\"when_given\":\"am\",\"placement\":\"AM\",\"cells\":7,\"ingredients\":\"benzoyl peroxide 4%\"},{\"id\":\"P2\",\"name\":\"Salicylic acid 2% toner\",\"type\":\"toner\",\"families\":[\"BHA exfoliant\"],\"when_given\":\"pm\",\"placement\":\"X nights\",\"cells\":2,\"ingredients\":\"salicylic acid 2%, witch hazel\"},\"... 3 more products\"]",
"week": "{\"cadence\":\"cycle4\",\"cadence_name\":\"Four-night cycle - exfoliate, retinoid, recover, recover\",\"days\":[{\"id\":\"D1\",\"day\":\"Mon\",\"night\":\"X\",\"runs_as\":\"X\",\"am\":[\"P1\",\"P3\",\"P4\",\"P5\"],\"pm\":[\"P2\",\"P4\"]},{\"id\":\"D2\",\"day\":\"Tue\",\"night\":\"R\",\"runs_as\":\"R\",\"am\":[\"P1\",\"P3\",\"P4\",\"P5\"],\"pm\":[\"P3\",\"P4\"]},\"... 5 more days\"]}",
"checks": "{\"conflicts\":[{\"id\":\"C1\",\"level\":\"low\",\"rule\":\"retinoid-bpo\",\"products\":[\"P1\",\"P3\"],\"cells\":[\"D1-AM\",\"D2-AM\",\"D3-AM\",\"D4-AM\",\"D5-AM\",\"D6-AM\",\"D7-AM\"],\"text\":\"Benzoyl peroxide and a retinoid in one session (Benzoyl peroxide 4% foaming wash, Adapalene 0.1% gel) - both dry the skin and benzoyl peroxide can break down some retinoids. One AM, one PM is the usual split.\"}],\"flags\":[{\"id\":\"F-ret-am\",\"level\":\"warn\",\"text\":\"Adapalene 0.1% gel is set for the morning - retinoids are usually a night step, since light breaks some of them down.\",\"products\":[\"P3\"]},\"...\"],\"nights\":{\"X\":2,\"R\":2,\"C\":3},\"runs_as\":{\"X\":2,\"R\":2,\"C\":3}}",
"note": "I put the adapalene on morning and night to clear things faster. The sunscreen is new this week.",
"reaction": "{\"symptoms\":[\"itching\",\"swelling\",\"hives or welts\",\"eye area involved\"],\"red_flags\":[\"swelling\",\"hives or welts\",\"eye area involved\"],\"day\":\"Sun\",\"started\":\"Sunday morning, an hour after sunscreen\",\"description\":\"Itchy raised welts on my forehead and cheeks, and my eyelids look puffy. I can breathe fine.\",\"floor\":\"see_clinician\",\"candidates\":[{\"product_id\":\"P5\",\"reasons\":[\"used Sun morning\",\"new to your routine\",\"lists fragrance\"]},{\"product_id\":\"P1\",\"reasons\":[\"used Sun morning\",\"carries benzoyl peroxide\"]},\"...\"]}"
}
Reply:
{
"task": "reaction",
"verdict": "see_clinician",
"title": "Possible Allergic Reaction To New Sunscreen",
"summary": "The itching, raised welts and puffy eyelids that began about an hour after your new sunscreen look like more than simple irritation - they fit a possible allergic reaction better. The sunscreen is the main suspect, since it is new this week and lists fragrance ingredients, though the benzoyl peroxide wash and the adapalene used that same morning cannot be fully ruled out. Stop the sunscreen and every active today, keep things down to a gentle cleanser and plain moisturizer, and see a doctor, pharmacist or dermatologist promptly given the eye-area swelling. This is not a diagnosis.",
"suspects": [
{
"product_id": "P5",
"likelihood": "likely",
"why": "This sunscreen is new to your shelf this week and lists fragrance ingredients, including a named fragrance allergen. You used it Sunday morning and the welts and eyelid puffiness started about an hour later - that timing and newness point here first.",
"cite": [
"P5",
"F-fragrance",
"F-new-products",
"D7-AM",
"R-report"
]
},
{
"product_id": "P1",
"likelihood": "possible",
"why": "You used this benzoyl peroxide wash the same morning, and it shares a session with the adapalene every day, so it cannot be ruled out. But benzoyl peroxide more often causes dryness or stinging than hives and swollen eyelids.",
"cite": [
"P1",
"C1",
"D7-AM",
"R-report"
]
},
"..."
],
"pause": [
"P1",
"P2",
"P3",
"P5"
],
"plan": [
{
"when": "Today",
"text": "Stop the sunscreen and every active - the benzoyl peroxide wash, the salicylic acid toner and the adapalene gel. Cleanse gently and use only the plain moisturizer until you are seen.",
"cite": [
"P1",
"P2",
"P3",
"P5",
"P4"
]
},
{
"when": "Today",
"text": "See a doctor, pharmacist or dermatologist today - hives with eyelid swelling needs a clinician's look, not a guess. Take the sunscreen, or its ingredient list, with you.",
"cite": [
"R-report",
"P5"
]
},
"..."
],
"red_flags": [
"Swelling of the lips, tongue or face, or trouble breathing - get emergency care.",
"Swelling spreading beyond your forehead and cheeks, or any change in your vision.",
"..."
],
"caveats": [
"This is not a diagnosis.",
"General cosmetic guidance only - for eyelid swelling and hives, see a doctor, pharmacist or dermatologist."
]
}
Build the input yourself
Load the script in node (global.window = {}; require("./rotakit.js")), then:
const K = RotaKit;
const { products } = K.parseShelf("Retinol 0.5% serum | PM | retinol 0.5%\nDaily sunscreen SPF 50 | AM | zinc oxide\n...");
const stats = K.analyze({ products, profile: { skin: "dry", experience: "new" }, cadence: "cycle4" }); // week, clashes, flags
const { input } = K.buildInput("rota", { stats, note: "" });
const wire = K.toWire(input); // POST this to /estimate and /run
// reaction: K.buildInput("reaction", { stats, reaction: { symptoms: ["stinging"], day: "Thu", started: "", description: "" } })
// later: const reply = K.normalize(K.parseReply(job.output.output));
// K.reconcile(reply, { stats, products, prescan: K.prescan(stats, reaction) }) -> [{level, code, text}]
// const next = K.applyRota({ overrides: {}, nights: stats.nights, cadence: stats.cadence }, reply, stats);
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://actives-rota.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://actives-rota.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"]
// actives-rota.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://actives-rota.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://actives-rota.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://actives-rota.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://actives-rota.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://actives-rota.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://actives-rota.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":"actives-rota"}' | 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": "actives-rota"})["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: "actives-rota" })).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": "actives-rota"}, 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", "actives-rota"), 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: "actives-rota" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "actives-rota"])["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 = "actives-rota" }))!["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": "rota", "profile": "{...}", "shelf": "[...]", "week": "{...}", "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": "rota", "profile": "{...}", "shelf": "[...]", "week": "{...}", "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, profile, shelf, week, checks, note} (reaction: + reaction)
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 actives-rota:<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="actives-rota:rota:$(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 = "actives-rota:" + 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 = `actives-rota:${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 := "actives-rota:" + 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 = "actives-rota:" + 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 = "actives-rota:#{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 = "actives-rota:" . $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 = $"actives-rota:{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: actives-rota:rota:$(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": "rota", "profile": "{...}", "shelf": "[...]", "week": "{...}", "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": "actives-rota:rota:" + 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": `actives-rota:rota:${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", "actives-rota:rota:"+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", "actives-rota:rota:" + 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" => "actives-rota:rota:#{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: actives-rota:rota:" . 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", $"actives-rota:rota:{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);