← Trial Protocol Desk
API
Tokens

Run Trial Protocol Desk from your own code

Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page reads the synopsis and works out the sample size for free - the evaluable and enrolled numbers by a two-sample t-test or a two-proportion z-test, the sensitivity at 90% power and at a 10% smaller effect, and the power the planned enrolment really buys - and checks the synopsis for what a protocol needs: a primary endpoint with a timepoint that is a scheduled visit, a comparator, randomisation and blinding, a non-inferiority margin, sample-size assumptions, measurable criteria, a visit schedule, safety monitoring, a data monitoring committee, an analysis plan and no promotional claims. That is tpscan.js, and every finding carries its rule id, line and fix. The run critiques the design ("critique") or drafts the core protocol sections ("draft"); the page then holds the reply to its own reading (tpreply.js): every rule answered, every sample-size number one of its own, no invented trial numbers or guidance titles.

A drafting aid, not advice. Nothing here or in a reply is medical, statistical or regulatory advice. The sample size is preliminary and needs a biostatistician's review before it goes into a protocol, and the protocol itself needs the sponsor's clinical, regulatory and ethics review.

Base URL and the response shape

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

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

The token is minted for this app (the guest call takes {"slug":"trial-protocol-desk"}), so no slug header is needed afterwards. Send it as Authorization: Bearer .... The run body is the input object, sent bare.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or a field the run needs (task, synopsis, facts) is missing or not a string. Send every field as a string; focus may be "".
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: both lanes are metered and need a personal sign-in.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. Retry with the same Idempotency-Key so a retry never double-bills.

The task field comes first

Two lanes - "critique" (judge the design before anyone writes the protocol: a verdict, findings with fixes, a call on every page finding, a review of seven design topics, a word on the sample size and the questions the team must settle) and "draft" (the eight core protocol sections drafted from the synopsis, fixing what the page flagged where the synopsis allows and raising the rest as open questions). The TP- rules belong to both. A missing or unknown task is answered as critique, with a caveat saying so. Both lanes send the same fields.

The input

Every field is a string.

FieldRequiredWhat
taskyes"critique" or "draft". The model answers that task and only that task.
synopsisyesthe study synopsis as pasted, in the synopsis format. The page sends up to 16,000 characters - the synopsis, not the full protocol - and needs at least an Intervention:, Design: or Primary endpoint: line.
focusnowhat the team wants weighed: timelines, sites, a regulator's feedback - or a critique to apply in a draft (see critique, then draft). Up to 1,500 characters on the page. May be ""; when given, the summary says how it changed the answer.
factsyesthe page's own reading of the same synopsis: the design it read, the primary endpoint and its timepoint, the sample size with every assumption, the Schedule of Activities, and every finding as rule | severity | line | text. See the facts format.
retry_notenopresent only on a reformat retry: the previous reply could not be read as one JSON object, and this says how to answer again. Leave it out otherwise.

The synopsis format

synopsis is plain text: Key: value lines, one field per line, with lists as bullets (- or 1. ) on the lines under their key. The keys the page reads:

Title: A randomised trial of a digital CBT-I program for chronic insomnia
Design: randomized 1:1, SleepPath versus sleep-hygiene education app, parallel group
Primary endpoint: insomnia remission (Insomnia Severity Index below 8) at week 8
Endpoint type: binary
Control rate: 20%
Treatment rate: 35%
Dropout: 20%
Planned enrolment: 380
Inclusion:
- Adults 18 to 70 meeting DSM-5 criteria for insomnia disorder for at least 3 months
Visits: Screening, Baseline, Week 4, Week 8, Week 24
Assessments:
- Sleep diary: Baseline, Week 8
- PHQ-9 suicidality item: all

A field the synopsis does not give is simply left out; the page reports what that costs as a finding.

The free sample-size calculation

The page computes the sample size in the browser, for free, from the synopsis's own assumptions - the formulas of the source skill's sample_size_calculator.py, identical to it at 1:1 allocation:

The result goes into facts as one line, and the model is told to use only the numbers in that line: every evaluable, enrolled, per-arm or power figure in a reply must be one of them, and with no line it gives no number at all. So an API client must compute the sample size itself and send its own sample-size line in facts - with tpscan.js under node (Build the input yourself), or with your own calculator written in the same form:

Sample size (page): <binary|continuous> endpoint, <superiority|non-inferiority>, <test>, <two-sided|one-sided> alpha <a>, power <p>, allocation <1:1>, dropout <d>; <control X%, treatment Y%|difference D, SD S>[, margin M][ (defaults used: ...)]. Evaluable: <t> treatment + <c> control = <n>; to enrol after dropout: <N> (<t> + <c>). Sensitivity: 90% power needs <N90> enrolled; a 10% smaller effect needs <N10>. Planned enrolment <P> gives <power> power.
Sample size: not computed - <what is missing>.

The second form is for a synopsis without usable assumptions; the reply then says what is missing and gives no number. Whichever calculator you use, the figure is preliminary: have a biostatistician review it.

The facts format

facts is the page's own reading, written as plain text. The page builds it from the synopsis with tpscan.js, and the model answers against it. A client calling the API should send the same format (from tpscan.js run under node) or at least its own summary - the design as you read it, the primary endpoint and its timepoint, your sample-size line, and what you already know is missing. The check's format gets the most out of a run: the model answers every error and warn rule id it finds there with one prescan entry per id. The format, line by line:

<Title|Intervention|Product type|Phase|Indication|Design|Control|Blinding>: <as written - only the lines the synopsis gives>
Read by the page: <randomised|randomisation not stated>, <controlled|single-arm|no comparator named>, <blinding stated|blinding not stated>, <superiority|non-inferiority>, allocation <1:1>.
Primary endpoint: <endpoint[ | endpoint]|none>[ (timepoint read: week 8)]
Secondary endpoints: <a | b>
<the sample-size line>
Criteria: <n> inclusion criteria, <n> exclusion criteria. Visits: <visits|none>.
Schedule of Activities: <assessment> at <visits>; ...
Safety: ... / Data monitoring: ... / Analysis: ...   (each on its own line, when given)
Prescan verdict: <ready|revise|rework> (<n> errors, <n> warnings, <n> notes).
Findings (rule | severity | line | text):
- <rule id> | <error|warn|info> | line <n> | <title>[ (note)][ | the synopsis text]

A synopsis with no findings gets the single line - none. The prescan verdict is rework at two or more errors, or with errors or warnings in four or more areas (endpoints, design, statistics, population, operations, safety, regulatory); revise on any error or warning; ready otherwise. A real example - the facts the page sends for the insomnia trial used in the worked example below:

Title: A randomised trial of a digital CBT-I program for chronic insomnia
Intervention: SleepPath, an app delivering six weekly sessions of cognitive behavioural therapy for insomnia
Product type: device (software)
Phase: pivotal
Indication: chronic insomnia disorder (DSM-5)
Design: randomized 1:1, SleepPath versus sleep-hygiene education app, parallel group
Control: sleep-hygiene education app with matched screen time
Blinding: participants are told both apps are active; outcome assessors are blinded
Read by the page: randomised, controlled, blinding stated, superiority, allocation 1:1.
Primary endpoint: insomnia remission (Insomnia Severity Index below 8) at week 8 (timepoint read: week 8)
Secondary endpoints: Change in Insomnia Severity Index at week 8 and week 24 | Sleep efficiency from a sleep diary at week 8
Sample size (page): binary endpoint, superiority, two-proportion z-test, two-sided alpha 0.05, power 80%, allocation 1:1, dropout 20%; control 20%, treatment 35%. Evaluable: 138 treatment + 138 control = 276; to enrol after dropout: 345 (173 + 173). Sensitivity: 90% power needs 463 enrolled; a 10% smaller effect needs 420. Planned enrolment 380 gives 83.7% power.
Criteria: 3 inclusion criteria, 4 exclusion criteria. Visits: Screening, Baseline, Week 4, Week 8, Week 24.
Schedule of Activities: Insomnia Severity Index at Screening, Baseline, Week 4, Week 8, Week 24; Sleep diary at Baseline, Week 8; PHQ-9 suicidality item at Screening, Baseline, Week 4, Week 8, Week 24.
Safety: adverse events and any PHQ-9 item 9 response above 0 reviewed by the study clinician within 24 hours
Data monitoring: an independent DSMB reviews unblinded safety data every 6 months; no efficacy stopping
Analysis: logistic regression of remission on arm, adjusted for baseline ISI, in the intention-to-treat population; missing week-8 data counted as non-remission
Prescan verdict: ready (0 errors, 0 warnings, 0 notes).
Findings (rule | severity | line | text):
- none

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

RuleSeverityAreaTitle
TP-NOENDPOINTerrorendpointsNo primary endpoint
TP-NOTIMEPOINTwarnendpointsPrimary endpoint without a timepoint
TP-MULTIPRIMARYwarnendpointsMore than one primary endpoint, no multiplicity plan
TP-VISITMISMATCHwarnendpointsPrimary timepoint is not a scheduled visit
TP-NOCONTROLwarndesignNo comparator
TP-NORANDOMwarndesignControlled but randomisation not stated
TP-NOBLINDinfodesignBlinding not stated
TP-NOASSUMPTIONerrorstatisticsNo sample-size assumptions
TP-NIMARGINerrorstatisticsNon-inferiority without a margin
TP-NODROPOUTwarnstatisticsNo dropout assumption
TP-UNDERPOWEREDerrorstatisticsPlanned enrolment is underpowered
TP-NOANALYSISwarnstatisticsNo analysis method or population
TP-NOINCLwarnpopulationNo inclusion criteria
TP-NOEXCLwarnpopulationNo exclusion criteria
TP-VAGUECRITinfopopulationCriterion without a measurable definition
TP-NOVISITSwarnoperationsNo visit schedule
TP-NOSAFETYwarnsafetyNo safety monitoring
TP-NODSMBinfosafetyNo data monitoring committee or stopping rules
TP-PROMOwarnregulatoryPromotional claims about an investigational product

A finding the check cannot see (an endpoint that is not clinically meaningful, a comparator that is not standard of care, a population that cannot be recruited, a sham that unblinds, a missing-data plan that biases the result) comes back with the rule NEW.

The standard the model applies

The source skill - @anthropics/clinical-trial-protocol-skill (anthropics/healthcare), and the NIH/FDA protocol template it follows - is the only standard:

House rules

The replies

Both lanes carry task, summary, findings (rule - a TP- id from the facts or NEW - severity high / medium / low, title, where, why, fix; most severe first), prescan (rule, call agree or disagree, note) and caveats. The rest differs by lane.

critique

verdict on the design: ready (a protocol can be drafted as is), revise (fixable gaps) or rework (the study as designed cannot answer its question - wrong endpoint, no control, badly underpowered, unsafe). A page error the reply agrees with rules out ready.

  1. summary - two or three sentences: what the study tests, the verdict, the first thing to fix.
  2. findings and prescan.
  3. design_review - seven entries in this order: Endpoint, Control and blinding, Population, Sample size, Analysis, Safety oversight, Operations - each with topic, assessment (one or two sentences) and risk low, medium or high.
  4. sample_size - one or two sentences (a string) on the page's calculation: whether its assumptions look defensible and what the planned enrolment buys, using only the page's numbers.
  5. questions - what the team must settle before drafting, each with for (biostatistics, clinical, regulatory or operations) and question.
  6. caveats - what could not be judged from a synopsis.
{
  "task": "critique",
  "verdict": "revise",
  "summary": "",
  "findings": [{"rule": "TP-UNDERPOWERED", "severity": "high", "title": "", "where": "Planned enrolment", "why": "", "fix": ""}],
  "prescan": [{"rule": "TP-UNDERPOWERED", "call": "agree", "note": ""}],
  "design_review": [{"topic": "Endpoint", "assessment": "", "risk": "low"}],
  "sample_size": "",
  "questions": [{"for": "biostatistics", "question": ""}],
  "caveats": [""]
}

draft

The core protocol sections, drafted from the synopsis: concise, specific to the study, a draft for the team to complete.

  1. summary - two sentences: what was drafted and what is still open.
  2. sections - eight, in this order, each with id, title and body (plain text, numbered lists as 1. , 80 to 300 words): synopsis (Protocol Summary), objectives (Objectives and Endpoints, each with its endpoint and timepoint), design (Study Design), population (numbered inclusion and exclusion criteria), intervention (administration, comparator, compliance, concomitant therapy), schedule (visits with windows and the Schedule of Activities), safety (adverse event definitions, severity scale, reporting timelines, data monitoring and stopping rules) and statistics (hypotheses, the page's sample size with its assumptions and sensitivity, analysis populations, primary analysis, missing data, multiplicity).
  3. sample_size - an object: enrol (the page's total to enrol, or null when the page could not compute one) and statement (the sample-size paragraph's key sentence).
  4. findings, prescan, open_questions (everything the draft needs but the synopsis does not give) and caveats.
{
  "task": "draft",
  "summary": "",
  "sections": [{"id": "synopsis", "title": "Protocol Summary", "body": ""}],
  "sample_size": {"enrol": 264, "statement": ""},
  "findings": [{"rule": "TP-VISITMISMATCH", "severity": "medium", "title": "", "where": "Visits", "why": "", "fix": ""}],
  "prescan": [{"rule": "TP-VISITMISMATCH", "call": "agree", "note": ""}],
  "open_questions": [""],
  "caveats": [""]
}

The page holds either reply to its own reading: every error and warning answered by rule id, no rule id the page did not report other than NEW, every evaluable, enrolled, per-arm or power figure one of the page's (or of the synopsis's own numbers), no NCT number, guidance title or "et al." citation that is not in the synopsis, and no promotional claim about the investigational product. For critique it also wants all seven design-review topics, a word on the sample size, and no ready over an undisputed error. For draft it wants all eight sections in order, sample_size.enrol equal to the page's enrolment and that number in the statistics section, and the primary timepoint named in the synopsis, objectives, schedule and statistics sections. Each miss is shown next to the reply as a note.

Reading the reply

The job's output.output is text that should be exactly one JSON object. Parse it; if a model ever wraps it in prose or a fence, take the text from the first { to the last } and parse that (the page does the same in tpreply.js). If it still does not parse, send the same input again with a retry_note such as "Your previous reply was not usable. Reply again with ONLY the JSON object for task 'critique' - no prose, no code fences." and a new attempt number in the key (see step 5). In "draft", the sections read as one plain-text protocol draft - 1. PROTOCOL SUMMARY, the body, and so on - which is what the page exports and what step 5 prints. Check every number in it against your own calculation before the draft goes to a biostatistician.

Critique, then draft

A critique can be handed to a "draft" run as its focus, so the draft applies it. The page builds that focus from the critique reply, one line each, clipped to 1,500 characters: Apply this critique (Trial Protocol Desk, verdict revise): <summary>, then - <rule> <title>: <fix> per finding and - Open (<for>): <question> per question. The same synopsis and facts go along.

Worked example - critiquing the insomnia trial

A clean pivotal design: SleepPath, a digital CBT-I app, against a sleep-hygiene education app with matched screen time, randomised 1:1, assessor-blinded, with remission (Insomnia Severity Index below 8) at week 8 as the binary primary endpoint. The page reads control 20% and treatment 35% at two-sided alpha 0.05, 80% power and 20% dropout: 276 evaluable, 345 to enrol (173 per arm), 463 at 90% power, 420 for a 10% smaller effect - and the planned 380 gives 83.7% power. It finds nothing wrong. The input below is exactly what the page sends for the "critique" lane; for the "draft" lane only task changes to "draft", and the same facts go along. Input:

{
 "task": "critique",
 "synopsis": "Title: A randomised trial of a digital CBT-I program for chronic insomnia\nIntervention: SleepPath, an app delivering six weekly sessions of cognitive behavioural therapy for insomnia\nProduct type: device (software)\nPhase: pivotal\nIndication: chronic insomnia disorder (DSM-5)\nDesign: randomized 1:1, SleepPath versus sleep-hygiene education app, parallel group\nControl: sleep-hygiene education app with matched screen time\nBlinding: participants are told both apps are active; outcome assessors are blinded\nPrimary endpoint: insomnia remission (Insomnia Severity Index below 8) at week 8\nSecondary endpoints:\n- Change in Insomnia Severity Index at week 8 and week 24\n- Sleep efficiency from a sleep diary at week 8\nEndpoint type: binary\nControl rate: 20%\nTreatment rate: 35%\nAlpha: 0.05\nPower: 80%\nDropout: 20%\nPlanned enrolment: 380\nInclusion:\n- Adults 18 to 70 meeting DSM-5 criteria for insomnia disorder for at least 3 months\n- Insomnia Severity Index of 15 or more at screening\n- Owns a smartphone and can use apps in English\nExclusion:\n- Untreated obstructive sleep apnoea (STOP-Bang 5 or more without a negative sleep study)\n- Bipolar disorder, psychosis or a seizure disorder\n- Shift work in the past 3 months\n- Current use of hypnotic medication more than 3 nights a week\nVisits: Screening, Baseline, Week 4, Week 8, Week 24\nAssessments:\n- Insomnia Severity Index: Screening, Baseline, Week 4, Week 8, Week 24\n- Sleep diary: Baseline, Week 8\n- PHQ-9 suicidality item: all\nSafety: adverse events and any PHQ-9 item 9 response above 0 reviewed by the study clinician within 24 hours\nDSMB: an independent DSMB reviews unblinded safety data every 6 months; no efficacy stopping\nAnalysis: logistic regression of remission on arm, adjusted for baseline ISI, in the intention-to-treat population; missing week-8 data counted as non-remission",
 "focus": "",
 "facts": "Title: A randomised trial of a digital CBT-I program for chronic insomnia\nIntervention: SleepPath, an app delivering six weekly sessions of cognitive behavioural therapy for insomnia\nProduct type: device (software)\nPhase: pivotal\nIndication: chronic insomnia disorder (DSM-5)\nDesign: randomized 1:1, SleepPath versus sleep-hygiene education app, parallel group\nControl: sleep-hygiene education app with matched screen time\nBlinding: participants are told both apps are active; outcome assessors are blinded\nRead by the page: randomised, controlled, blinding stated, superiority, allocation 1:1.\nPrimary endpoint: insomnia remission (Insomnia Severity Index below 8) at week 8 (timepoint read: week 8)\nSecondary endpoints: Change in Insomnia Severity Index at week 8 and week 24 | Sleep efficiency from a sleep diary at week 8\nSample size (page): binary endpoint, superiority, two-proportion z-test, two-sided alpha 0.05, power 80%, allocation 1:1, dropout 20%; control 20%, treatment 35%. Evaluable: 138 treatment + 138 control = 276; to enrol after dropout: 345 (173 + 173). Sensitivity: 90% power needs 463 enrolled; a 10% smaller effect needs 420. Planned enrolment 380 gives 83.7% power.\nCriteria: 3 inclusion criteria, 4 exclusion criteria. Visits: Screening, Baseline, Week 4, Week 8, Week 24.\nSchedule of Activities: Insomnia Severity Index at Screening, Baseline, Week 4, Week 8, Week 24; Sleep diary at Baseline, Week 8; PHQ-9 suicidality item at Screening, Baseline, Week 4, Week 8, Week 24.\nSafety: adverse events and any PHQ-9 item 9 response above 0 reviewed by the study clinician within 24 hours\nData monitoring: an independent DSMB reviews unblinded safety data every 6 months; no efficacy stopping\nAnalysis: logistic regression of remission on arm, adjusted for baseline ISI, in the intention-to-treat population; missing week-8 data counted as non-remission\nPrescan verdict: ready (0 errors, 0 warnings, 0 notes).\nFindings (rule | severity | line | text):\n- none"
}

Reply - the real saved reply the page shows for this sample, abbreviated: long strings are cut and end in ..., findings and questions keep only their first entry, design_review its first two and caveats its last (the full reply has 5 findings, 7 design-review topics, 7 questions and 4 caveats). prescan is empty because the page reported no findings to answer:

{
 "task": "critique",
 "verdict": "revise",
 "summary": "This pivotal, assessor-blinded, 1:1 randomised trial tests whether the SleepPath digital CBT-I app produces more insomnia remission (ISI below 8) at week 8 than a sleep-hygiene education app with matched screen time. The page found no gaps and the core design ...",
 "findings": [
  {
   "rule": "NEW",
   "severity": "medium",
   "title": "Sample size and missing-data rule assume different things",
   "where": "Analysis",
   "why": "The page's calculation powers 276 evaluable participants for 20% versus 35% and inflates to 345 for 20% dropout, which treats dropouts as missing ...",
   "fix": "Define the estimand first (treatment-policy or composite with dropout as failure). Then either recompute the sample size with the diluted rates that ..."
  }
 ],
 "prescan": [],
 "design_review": [
  {
   "topic": "Endpoint",
   "assessment": "Remission defined as ISI below 8 at week 8 is specific, measurable, clinically meaningful and consistent across the synopsis, schedule and analysis ...",
   "risk": "low"
  },
  {
   "topic": "Control and blinding",
   "assessment": "An attention-matched sleep-hygiene app is a reasonable comparator and assessor blinding is stated, but participant blinding rests on both apps being ...",
   "risk": "medium"
  }
 ],
 "sample_size": "The page's two-proportion z-test (20% versus 35%, two-sided alpha 0.05, 80% power, 1:1, 20% dropout) needs 276 evaluable and 345 to enrol (173 per arm); 90% power needs 463 and a 10% smaller effect needs 420, and the planned 380 gives 83.7% power. The assumptions are plausible for an attention-controlled CBT-I trial but should ...",
 "questions": [
  {
   "for": "biostatistics",
   "question": "Which estimand is primary, and should the sample size be recalculated with the remission rates diluted by counting dropouts as non-remitters, or ..."
  }
 ],
 "caveats": [
  "The sample-size calculation is preliminary and needs a biostatistician's review."
 ]
}

Every finding is NEW: the page could not see that the analysis counts missing week-8 data as non-remission while the sample size treats dropouts as lost, or that the suicidality screen has no response pathway. Sent to "draft", the same synopsis comes back as all eight sections with sample_size.enrol 345 and the week-8 timepoint throughout, and those two gaps raised as open questions.

Build the input yourself

The checker and the reply checker are plain scripts that run under node (global.window = global; require("./tpscan.js"); require("./tpreply.js")), then:

const S = TpScan, R = TpReply;
const st = { synopsis: fs.readFileSync("synopsis.txt", "utf8"), focus: "" };
const scan = S.scan(st);                        // findings, counts, design, timepoint, sampleSize, soa, verdict
console.log(S.ssLine(scan));                    // the sample-size line that goes into facts
const b = S.buildInput(st, "critique");         // or "draft"; b.ok is false (with b.note) when the synopsis cannot run
const wire = S.toWire(b.input);                 // task, synopsis, focus, facts - all strings; POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "critique");
// R.check(reply, b)                            // {notes, warn}: anything the reply missed or got wrong
// R.protocolText(reply)                        // a draft reply's sections as plain text
// R.handoff(reply)                             // a critique reply as the focus of a draft run

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

Step 1 - a tiny client helper

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

BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="YOUR_TOKEN"   # from https://trial-protocol-desk.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
  curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
    ${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}

Step 2 - a token

A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.

# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"trial-protocol-desk"}' | jq -r '.data.token')

Step 3 - GET /me

Who the token is (subject_type: user or guest) and the credit balance.

api GET /me | jq '.data | {subject_type, credits}'

Step 4 - POST /estimate

Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra. Save the worked-example input above as input.json (with task set to "critique" or "draft") to follow along.

# input.json: the run input object - {"task": "critique", "synopsis": "Title: ...\nIntervention: ...", "focus": "", "facts": "Title: ...\nSample size (page): ..."}
# - see the input table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}'

Step 5 - POST /run, then poll the job

Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses trial-protocol-desk:<task>:<inputHash>:a<attempt> - the lane is part of the key, so a critique and a draft of the same synopsis are two runs - and its one automatic reformat retry (sent with a retry_note, which the hash leaves out) is a new attempt number, a2, named in the page as one extra run. Any stable hash of the input works for your own key. The code below parses the JSON object out of the output; for "critique" it prints the verdict, the summary, each design-review topic with its risk, the sample-size word and the questions, and for "draft" the summary, the enrolment and the eight sections as plain text.

KEY="trial-protocol-desk:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
  J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
  case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json   # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}'
jq -r 'if .task == "draft" then
    .summary, "enrol: \(.sample_size.enrol)", .sample_size.statement,
    (.sections // [] | to_entries[] | "", "\(.key + 1). \(.value.title | ascii_upcase)", "", .value.body)
  else
    "verdict: \(.verdict)", .summary,
    (.design_review[]? | "\(.topic): \(.risk) - \(.assessment)"),
    "Sample size: \(.sample_size)",
    (.questions[]? | "[\(.for)] \(.question)")
  end' reply.json   # draft: the protocol sections as plain text

Step 6 - POST /run-stream

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

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