← React 19 Source Desk
API
Tokens

Run React 19 Source Desk from your own code

Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page masks the JSX, finds every API React 19 removes or changes, and makes the mechanical changes itself - createRoot, ref props, default parameters, useRef(null), the propTypes note, then hands it all to the run as facts. The run writes the review or the rewrite; the page then holds it to its own facts and re-scans the upgraded file with the same scanner (r19reply.js).

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":"react19-source-desk"}), so no slug header is needed afterwards. Send it as Authorization: Bearer YOUR_TOKEN. The run body is the input object, sent bare.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or a required key (task, unit, code, facts) is missing. The page always sends all five of task, unit, code, carry, facts; carry is an empty string for review.
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, chosen by task, in the order the work runs - inspect, then produce:

A missing or unknown task is answered as review. Both lanes send the same five fields. The facts are the page's scan, one finding per line with its id, and a line starting "must:" is a rule the reply has to hold. The page's handoff carries the review's decisions into carry for the rewrite lane. The review sends at most 24,000 characters of code (the middle of a longer file is cut on whole lines and marked; every finding is still in the facts); the rewrite lane takes at most 16,000.

The input

FieldRequiredWhat
taskyesreview or rewrite. If it is missing or neither, do review and set "task": "review".
unityesthe file's path and what it is (a component or hook module, the app's entry point, a shared library file).
codeyesthe file as pasted (before the page's changes). A line starting "// [... N lines cut by the page" marks code the page scanned but did not send.
carrysend it (empty for review)rewrite task only: the review's decisions, one per line as id | confirmed or cleared or extra | where | fix or reason; may be empty.
factsyesthe page's scan: one finding per line as id | area | severity | line | where | evidence | source | what the page did (migrated or left). "migrated" means the page made that change in its own free React 19 copy of the file, which the person already has; the code field above is still the original paste, so judge it as written. A line starting "must:" applies to your reply.
retry_notenooptional; if present, your previous reply was unusable - follow it.

The rules the model applies

From the source skill, @github/react19-source-patterns - the only standard the run applies:

House rules

The replies

review

Confirm or clear each of the page's findings, decide what the page left, add what a scan cannot see, and put the steps in order.

  1. summary - one sentence, at most 40 words: what breaks on React 19 today, what the page already changed, and the first step.
  2. findings - one object per page finding you confirm, in id order: id (the page's id), widget (where it sits, e.g. "Button" or "the entry point"), issue (one sentence: what goes wrong on React 19), fix (one sentence: the change), code (the React 19 lines for that piece only, at most 6; "" when fix says it all).
  3. cleared - one object per page finding that is not a problem here, with id and why (one sentence tied to the code). Every page id appears exactly once, in findings or in cleared.
  4. extra - at most 5 issues the scan cannot see, each tied to a line of the code: id (X1, X2, ...), area (root, dom, refs, props, removed or imports), line (a number), widget, issue, fix. Examples: a render callback that must become an effect, a root created twice (once per hot reload), a default read from props elsewhere, a ref forwarded into a child that is still a class, a typed forwardRef whose props type must gain ref. [] when there are none.
  5. plan - 2 to 10 short strings, the steps in order (what React 19 removes first - the root API, findDOMNode, defaultProps, legacy context and string refs - then refs and the rest), each naming the ids it covers (e.g. "Switch the entry point to createRoot (N1)"). Every confirmed id appears in a step. [] only when nothing is confirmed.
  6. caveats - at most 4 short strings; [] when there is nothing to say.
{
  "task": "review",
  "summary": "one sentence",
  "findings": [ { "id": "N1", "widget": "the entry point", "issue": "one sentence", "fix": "one sentence", "code": "const root = createRoot(container);\nroot.render(<App />);" } ],
  "cleared": [ { "id": "N3", "why": "one sentence" } ],
  "extra": [ { "id": "X1", "area": "root", "line": 15, "widget": "the entry point", "issue": "one sentence", "fix": "one sentence" } ],
  "plan": [ "Switch the entry point to createRoot (N1)" ],
  "caveats": [ ]
}

The page holds it to its own facts (check() in r19reply.js; any warning makes the reply flagged): the reply answers the task it was sent; every page finding id appears exactly once across findings and cleared, and no id the page never found; a cleared id has a reason; a confirmed id has an issue and a fix; at most five extra issues, each on a line inside the paste; a fix order of 2 to 10 steps that names every confirmed id; a summary; no web link in the prose.

rewrite

Rewrite the whole file on React 19.

  1. summary - one sentence, at most 40 words: what the rewrite changed and what it left for the team.
  2. code - the complete React 19 file. Keep every import that is still used, every component and class (by name), every export and prop; change only what the findings name, plus every carried extra whose fix lives in this file (list it in changes; one that needs another file goes in a caveat), plus the imports a change needs, and drop an import the change leaves unused. Never shorten the file with "..." or "unchanged" - write every line. If the code holds a line starting "// [... N lines cut by the page", keep that line exactly where it is. The root - createRoot / hydrateRoot from react-dom/client; keep the root in a const when it is unmounted, and replace unmountComponentAtNode with root.unmount(). findDOMNode - a ref on the element, read as ref.current. forwardRef - ref as a prop (for TypeScript, add ref to the props type: ref?: React.Ref<HTMLDivElement>), useImperativeHandle untouched. Function-component defaultProps - default parameters in the destructured props, the assignment deleted. useRef() - useRef(null); propTypes - kept with the note. Legacy context and string refs - createContext and createRef / useRef(null), saying in a caveat which other files change with it.
  3. changes - one object per page finding you acted on (including the ones the page already migrated, which the rewrite keeps), in id order: id, change (one sentence: what changed, naming where). Extra ids (X1, ...) from the carried review may appear too.
  4. kept - one object per page finding you did not change: id, why (one sentence - the review cleared it, or it needs a file the paste does not hold). Every page id appears exactly once, in changes or in kept; an id the carried review cleared belongs in kept.
  5. caveats - at most 4 short strings; [] when there is nothing to say.
{
  "task": "rewrite",
  "summary": "one sentence",
  "code": "the complete React 19 file",
  "changes": [ { "id": "N1", "change": "one sentence" } ],
  "kept": [ { "id": "N3", "why": "one sentence" } ],
  "caveats": [ ]
}

The page requires: the reply answers the task it was sent; every page finding id appears exactly once across changes and kept; every component survives; the file is not shortened (at least 70% of its lines) and elides nothing; and the rewritten code is scanned again - a rule the changes claim fixed that still fires, or a rule the paste never had, is flagged. A component the rewrite drops or renames is flagged.

Worked examples

Three runs from the page's free samples (invented apps): the review of a component kit on forwardRef and defaultProps, the rewrite of the same file carrying that review, and the review of an entry point still on ReactDOM.render. Each input is exactly what the page sends; each reply is the saved reply the sample replays, shipped unedited - but the code, the facts and long strings are shortened in this display only, and every cut is listed under the example. The last line is the page's own check of the full reply.

review - A component kit on forwardRef and defaultProps

The page read it as: Breaks on React 19 - 1 blocker, 1 major, 4 minor; findings N1 unused-react-import, N2 forward-ref, N3 useref-no-arg, N4 default-props, N5 prop-types, N6 forward-ref.

Input (facts is 1682 characters; the first 1397 are shown):

{
 "task": "review",
 "unit": "File: src/ui/kit.jsx\nKind: a shared component library file",
 "code": "import React, { forwardRef, useRef, useImperativeHandle } from \"react\";\nimport PropTypes from \"prop-types\";\n\nexport const TextField = forwardRef((props, ref) => {\n  const inputRef = useRef();\n  useImperativeHandle(ref, () => ({\n    focus: () => inputRef.current.focus(),\n    clear: () => { inputRef.current.value = \"\"; },\n  }));\n  return <input ref={inputRef} className=\"field\" {...props} />;\n});\n\nexport function Button({ label, variant, disabled, onClick }) {\n  return (\n    <button className={\"btn btn-\" + variant} disabled={disabled} onClick={onClick}>\n      {label}\n    </button>\n  );\n}\n\nButton.defaultProps = {\n  label: \"Save\",\n  variant: \"primary\",\n  disabled: false,\n};\n\nButton.propTypes = {\n  label: PropTypes.string,\n  variant: PropTypes.oneOf([\"primary\", \"ghost\"]),\n  disabled: PropTypes.bool,\n  onClick: PropTypes.func,\n};\n\nexport const Icon = forwardRef(({ name, size }, ref) => (\n  <svg ref={ref} width={size} height={size} aria-hidden=\"true\"><use href={\"#\" + name} /></svg>\n));\n",
 "carry": "",
 "facts": "The page's scan of 37 lines - 3 components (TextField, Button, Icon); 5 of 6 findings migrated by the page; verdict: Breaks on React 19.\nFindings (id | area | severity | line | where | evidence | source | what the page did):\n- N1 | imports | minor | line 1 | the file | The default React import is not used as React.* anywhere in the file. | source: import React, { forwardRef, useRef, useImperativeHandle } from \"react\"; | left\n- N2 | refs | minor | line 4 | TextField | forwardRef wraps TextField - in React 19 a function component takes ref as a plain prop, so the wrapper is no longer needed. | source: export const TextField = forwardRef((props, ref) => { | migrated\n- N3 | refs | major | line 5 | TextField | useRef() with no initial value - React 19's types require an argument, and the ref starts undefined rather than null. | source: const inputRef = useRef(); | migrated\n- N4 | props | blocker | line 21 | Button | Button.defaultProps is ignored for function components in React 19 - every default (label, variant, disabled) silently becomes undefined. | source: Button.defaultProps = { | migrated\n- N5 | props | minor | line 27 | Button | React 19 no longer runs propTypes validation at runtime - the prop-types package still works as a standalone validator, so the block stays. | source: Button.propTypes = { | migrated\n- N6 | refs | minor | line 34 | Icon | forwardRef wraps Icon - in\n\n[... 285 more characters cut from this display ...]"
}

Reply:

{
 "task": "review",
 "summary": "On React 19 the Button defaults vanish and useRef() fails the types; the page already rewrote defaults, refs, forwardRef and the propTypes note, and the first step is moving the Button defaults into its parameters.",
 "findings": [
  {
   "id": "N2",
   "widget": "TextField",
   "issue": "The forwardRef wrapper is no longer needed because a React 19 function component receives ref as a prop.",
   "fix": "Turn TextField into a function that destructures ref from its props and passes it to useImperativeHandle unchanged.",
   "code": "export function TextField({ ref, ...props }) {\n  const inputRef = useRef(null);\n  useImperativeHandle(ref, () => ({\n    focus: () => inputRef.current.focus(),\n    clear: () => { inputRef.current.value = \"\"; },\n  }));"
  },
  {
   "id": "N3",
   "widget": "TextField",
   "issue": "useRef() with no argument starts undefined and no longer satisfies the React 19 types.",
   "fix": "Initialise the input ref with useRef(null).",
   "code": "const inputRef = useRef(null);"
  },
  {
   "id": "N4",
   "widget": "Button",
   "issue": "Button.defaultProps is ignored for function components, so label, variant and disabled arrive undefined.",
   "fix": "Move the three defaults into the destructured parameters and delete the Button.defaultProps assignment.",
   "code": "export function Button({ label = \"Save\", variant = \"primary\", disabled = false, onClick }) {"
  },
  {
   "id": "N5",
   "widget": "Button",
   "issue": "React 19 no longer runs the Button.propTypes checks, so the block only documents the props.",
   "fix": "Keep Button.propTypes and add the React 19 note above it.",
   "code": "// NOTE: React 19 no longer runs propTypes validation at runtime.\n// PropTypes kept for documentation and IDE tooling only.\nButton.propTypes = {"
  },
  {
   "id": "N6",
   "widget": "Icon",
   "issue": "The forwardRef wrapper around Icon is no longer needed because ref arrives as a prop in React 19.",
   "fix": "Turn Icon into a function that destructures name, size and ref and puts ref on the svg.",
   "code": "export function Icon({ name, size, ref }) {\n  return (\n    <svg ref={ref} width={size} height={size} aria-hidden=\"true\"><use href={\"#\" + name} /></svg>\n  );\n}"
  }
 ],
 "cleared": [
  {
   "id": "N1",
   "why": "The file is JSX with no visible build setup, so the default React import may still be what the classic JSX transform needs, and it also carries the named hook imports."
  }
 ],
 "extra": [
  {
   "id": "X1",
   "area": "imports",
   "line": 1,
   "widget": "the file",
   "issue": "After both forwardRef wrappers go, forwardRef stays in the named import with nothing using it.",
   "fix": "Drop forwardRef from the react import and keep useRef and useImperativeHandle."
  }
 ],
 "plan": [
  "Move the Button defaults into its destructured parameters and delete Button.defaultProps (N4)",
  "Switch the TextField input ref to useRef(null) (N3)",
  "Take ref as a prop in TextField and Icon instead of forwardRef (N2, N6)",
  "Remove forwardRef from the react import (X1)",
  "Add the React 19 note above Button.propTypes and keep the block (N5)"
 ],
 "caveats": [
  "Callers that relied on Button.defaultProps being read from props elsewhere should be checked in their own files."
 ]
}

What step 5 prints for it: N2: Turn TextField into a function that destructures ref from its props and passes it to useImperativeHandle unchanged. | N3: Initialise the input ref with useRef(null). | N4: Move the three defaults into the destructured parameters and delete the Button.defaultProps assignment. | N5: Keep Button.propTypes and add the React 19 note above it. | N6: Turn Icon into a function that destructures name, size and ref and puts ref on the svg. | N1: cleared

The page's check: kept - X1 repeats what the page already found on line 1.

rewrite - A component kit on forwardRef and defaultProps

The input carries the saved review (R.laneForm fills carry from the review reply), as the page's handoff does. The page re-scanned the rewritten code: 6 findings before, 1 after.

Input (facts is 1674 characters; the first 1397 are shown):

{
 "task": "rewrite",
 "unit": "File: src/ui/kit.jsx\nKind: a shared component library file",
 "code": "import React, { forwardRef, useRef, useImperativeHandle } from \"react\";\nimport PropTypes from \"prop-types\";\n\nexport const TextField = forwardRef((props, ref) => {\n  const inputRef = useRef();\n  useImperativeHandle(ref, () => ({\n    focus: () => inputRef.current.focus(),\n    clear: () => { inputRef.current.value = \"\"; },\n  }));\n  return <input ref={inputRef} className=\"field\" {...props} />;\n});\n\nexport function Button({ label, variant, disabled, onClick }) {\n  return (\n    <button className={\"btn btn-\" + variant} disabled={disabled} onClick={onClick}>\n      {label}\n    </button>\n  );\n}\n\nButton.defaultProps = {\n  label: \"Save\",\n  variant: \"primary\",\n  disabled: false,\n};\n\nButton.propTypes = {\n  label: PropTypes.string,\n  variant: PropTypes.oneOf([\"primary\", \"ghost\"]),\n  disabled: PropTypes.bool,\n  onClick: PropTypes.func,\n};\n\nexport const Icon = forwardRef(({ name, size }, ref) => (\n  <svg ref={ref} width={size} height={size} aria-hidden=\"true\"><use href={\"#\" + name} /></svg>\n));\n",
 "carry": "N2 | confirmed | TextField | Turn TextField into a function that destructures ref from its props and passes it to useImperativeHandle unchanged.\nN3 | confirmed | TextField | Initialise the input ref with useRef(null).\nN4 | confirmed | Button | Move the three defaults into the destructured parameters and delete the Button.defaultProps assignment.\nN5 | confirmed | Button | Keep Button.propTypes and add the React 19 note above it.\nN6 | confirmed | Icon | Turn Icon into a function that destructures name, size and ref and puts ref on the svg.\nN1 | cleared | - | The file is JSX with no visible build setup, so the default React import may still be what the classic JSX transform needs, and it also carries the named hook imports.\nX1 | extra | the file (line 1) | Drop forwardRef from the react import and keep useRef and useImperativeHandle.",
 "facts": "The page's scan of 37 lines - 3 components (TextField, Button, Icon); 5 of 6 findings migrated by the page; verdict: Breaks on React 19.\nFindings (id | area | severity | line | where | evidence | source | what the page did):\n- N1 | imports | minor | line 1 | the file | The default React import is not used as React.* anywhere in the file. | source: import React, { forwardRef, useRef, useImperativeHandle } from \"react\"; | left\n- N2 | refs | minor | line 4 | TextField | forwardRef wraps TextField - in React 19 a function component takes ref as a plain prop, so the wrapper is no longer needed. | source: export const TextField = forwardRef((props, ref) => { | migrated\n- N3 | refs | major | line 5 | TextField | useRef() with no initial value - React 19's types require an argument, and the ref starts undefined rather than null. | source: const inputRef = useRef(); | migrated\n- N4 | props | blocker | line 21 | Button | Button.defaultProps is ignored for function components in React 19 - every default (label, variant, disabled) silently becomes undefined. | source: Button.defaultProps = { | migrated\n- N5 | props | minor | line 27 | Button | React 19 no longer runs propTypes validation at runtime - the prop-types package still works as a standalone validator, so the block stays. | source: Button.propTypes = { | migrated\n- N6 | refs | minor | line 34 | Icon | forwardRef wraps Icon - in\n\n[... 277 more characters cut from this display ...]"
}

Reply (long strings shortened for display - see the cuts below):

{
 "task": "rewrite",
 "summary": "TextField and Icon take ref as a prop, the input ref starts at null, Button defaults moved into its parameters and propTypes carry the React 19 note; the default React import is left for the team's JSX runtime check.",
 "code": "import React, { useRef, useImperativeHandle } from \"react\";\nimport PropTypes from \"prop-types\";\n\nexport function TextField({ ref, ...props }) {\n  const inputRef = useRef(null);\n  useImperativeHandle(ref, () => ({\n    focus: () => inputRef.current.focus(),\n    clear: () => { inputRef.current.value = \"\"; },\n  })); [... 732 more characters cut from this display]",
 "changes": [
  {
   "id": "N2",
   "change": "TextField is a plain function that takes ref from its props and hands it to useImperativeHandle."
  },
  {
   "id": "N3",
   "change": "The TextField input ref now starts with useRef(null)."
  },
  {
   "id": "N4",
   "change": "Button takes its label, variant and disabled defaults as parameter defaults and Button.defaultProps is gone."
  },
  {
   "id": "N5",
   "change": "Button.propTypes is kept with the React 19 note above it."
  },
  {
   "id": "N6",
   "change": "Icon is a plain function that takes ref with name and size and puts it on the svg."
  },
  {
   "id": "X1",
   "change": "forwardRef was dropped from the react import, which keeps useRef and useImperativeHandle."
  }
 ],
 "kept": [
  {
   "id": "N1",
   "why": "The review cleared it because the default React import may still be needed by a classic JSX transform."
  }
 ],
 "caveats": [
  "Remove the default React import only once the build is confirmed to use the automatic JSX runtime."
 ]
}

Cut from this display only (the saved reply and the check use the full text): code (1045 characters, 313 shown).

What step 5 prints for it: N2: TextField is a plain function that takes ref from its props and hands it to useImperativeHandle. | N3: The TextField input ref now starts with useRef(null). | N4: Button takes its label, variant and disabled defaults as parameter defaults and Button.defaultProps is gone. | N5: Button.propTypes is kept with the React 19 note above it. | N6: Icon is a plain function that takes ref with name and size and puts it on the svg. | X1: forwardRef was dropped from the react import, which keeps useRef and useImperativeHandle.

The page's check: kept - nothing to flag

review - An entry point still on ReactDOM.render

The page read it as: Breaks on React 19 - 2 blocker, 0 major, 0 minor; findings N1 legacy-root, N2 unmount-node.

Input:

{
 "task": "review",
 "unit": "File: src/index.jsx\nKind: the app's entry point",
 "code": "import React from \"react\";\nimport ReactDOM from \"react-dom\";\nimport App from \"./App\";\nimport \"./index.css\";\n\nconst container = document.getElementById(\"root\");\n\nReactDOM.render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>,\n  container\n);\n\nif (module.hot) {\n  module.hot.dispose(() => ReactDOM.unmountComponentAtNode(container));\n}\n",
 "carry": "",
 "facts": "The page's scan of 18 lines - 0 components (none); 1 of 2 findings migrated by the page; verdict: Breaks on React 19.\nFindings (id | area | severity | line | where | evidence | source | what the page did):\n- N1 | root | blocker | line 8 | the entry point | ReactDOM.render(...) is removed in React 19 - every app needs createRoot from react-dom/client. | source: ReactDOM.render( | migrated\n- N2 | root | blocker | line 16 | the entry point | unmountComponentAtNode is removed in React 19 - a root is unmounted through the object createRoot returned. | source: module.hot.dispose(() => ReactDOM.unmountComponentAtNode(container)); | left\nmust: account for every finding id above exactly once - in findings (confirmed) or in cleared (with the reason)."
}

Reply:

{
 "task": "review",
 "summary": "On React 19 the ReactDOM.render call and the hot-reload unmountComponentAtNode both fail; the page already moved rendering to createRoot, and the first step is saving that root so the dispose handler can unmount it.",
 "findings": [
  {
   "id": "N1",
   "widget": "the entry point",
   "issue": "ReactDOM.render no longer exists in React 19, so the app never mounts.",
   "fix": "Create the root with createRoot(container) from react-dom/client and call root.render with the StrictMode tree.",
   "code": "const root = createRoot(container);\nroot.render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>\n);"
  },
  {
   "id": "N2",
   "widget": "the entry point",
   "issue": "ReactDOM.unmountComponentAtNode is gone in React 19, so the hot-reload dispose handler throws.",
   "fix": "Keep the root from createRoot in a const and call root.unmount() inside module.hot.dispose.",
   "code": "if (module.hot) {\n  module.hot.dispose(() => root.unmount());\n}"
  }
 ],
 "cleared": [],
 "extra": [
  {
   "id": "X1",
   "area": "imports",
   "line": 2,
   "widget": "the entry point",
   "issue": "Once both calls move to the client API, the default react-dom import is left with nothing to provide.",
   "fix": "Replace the react-dom import with import { createRoot } from \"react-dom/client\"."
  }
 ],
 "plan": [
  "Import createRoot from react-dom/client in place of the default react-dom import (X1)",
  "Render the StrictMode tree through a root saved in a const (N1)",
  "Unmount that saved root in the hot-reload dispose handler (N2)",
  "Run the app and trigger a hot reload to confirm one root mounts each time (N1, N2)"
 ],
 "caveats": [
  "React stays imported because the file still reads React.StrictMode."
 ]
}

What step 5 prints for it: N1: Create the root with createRoot(container) from react-dom/client and call root.render with the StrictMode tree. | N2: Keep the root from createRoot in a const and call root.unmount() inside module.hot.dispose.

The page's check: kept - nothing to flag

Build the input yourself

Load the engine in node (global.window = global; require("./r19kit.js"); require("./r19reply.js")), then:

const K = R19Kit, R = R19Reply;
const f = K.form({ name: "src/Btn.jsx", targets: "component", carry: "",
  code: "export const Btn = forwardRef((props, ref) => <button ref={ref} {...props} />);\nexport function Tag({ text }) { return <b>{text}</b>; }\nTag.defaultProps = { text: \"new\" };" });
// f.findings (each with .auto when the page migrated it), f.scan.migrated (the React 19 file), f.checklist
// K.findingsCsv(f), K.itemsCsv(f), K.checklistMd(f) - the free exports
const b = K.buildInput(f, "review");                         // or "rewrite" (R.laneForm(form, "rewrite", review) fills carry)
const wire = K.toWire(b.input);                              // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "review");
// R.check(reply, b)                                         // {status: kept or flags, notes}

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, 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://react19-source-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":"react19-source-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.

# input.json: the run input object - {"task": "review", "unit": "File: src/ui/kit.jsx\nKind: a shared component library file", "code": "import React, { forwardRef, useRef, useImperativeHandle } from \"react\";\n...", "carry": "", "facts": "The page's scan of 54 lines - 3 components ...\nFindings (id | area | severity | line | where | evidence | source | what the page did):\n- N1 | imports | minor | line 1 | ..."}
# - 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 react19-source-desk:<task>:<inputHash>:a<attempt> - the lane is part of the key, retry_note is left out of the hash - and its one automatic reformat retry is a new attempt number, named in the page as one extra run. A review reply carries summary, findings, cleared, extra, plan and caveats; a rewrite reply carries summary, code, changes, kept and caveats. The code below prints the gist of either: each confirmed finding and its fix, or each change.

KEY="react19-source-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 == "rewrite" then (.changes[] | "\(.id): \(.change)") 
       else (.findings[] | "\(.id): \(.fix)") end' reply.json

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: react19-source-desk:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json