← Nuxt Clinic
API
Tokens

Run Nuxt Clinic from your own code

Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page scans the .vue file for free - every hydration hazard, data-fetching mistake, plain internal link and eagerly loaded lazy component against the nuxt4-patterns skill, each with its rule id, line and fix, plus the route rule that matches the page, checked against what kind of page it is - in nxscan.js. The run writes the review or fixes the file; the page then holds the reply to its own scan, re-scans any fixed file and checks the route rule the reply proposes (nxreply.js). Not affiliated with or endorsed by the Nuxt team.

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":"nuxt-clinic"}), 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, code, kind, facts) is missing or not a string. Send every field as a string; route and config 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 - review (judge the page: a verdict, findings with fixes, a call on every scan finding and the route rule it should have) and fix (return the corrected .vue, behaviour-preserving apart from the fixes, with its route rule). A missing or unknown task is answered as review, with a caveat saying so. Both lanes send the same fields.

The input

Every field is a string.

FieldRequiredWhat
taskyesreview or fix. The model answers that task and only that task.
codeyesthe page or component, the whole .vue file. The page clips a long file (over 24,000 characters) by keeping the head and the tail and putting an <!-- ... N lines (a-b) omitted by Nuxt Clinic ... --> comment in the middle; the model keeps that comment where it was and will not guess what it hid.
routeno (may be "")the route the page serves, such as /products/[id]; empty for a component. The route rule either lane proposes must match it.
kindyeswhat kind of page it is: marketing, content, catalog (all three need SEO and server HTML), dashboard, admin (personalised - never served from a shared cache) or other.
configno (may be "")the routeRules part of nuxt.config (the whole defineNuxtConfig call works too). The page clips it past 6,000 characters.
factsyesthe page's own scan of the same code and config: the kind, the route, the matching route rule, a prescan verdict, the public API to keep (props, emits, exposed names, definePageMeta), and every finding as rule | severity | file | line | text. See the facts format.
focusnowhat the developer wants weighed, for example "customers complain the list is sometimes someone else's". May be empty; when given, the summary says how it changed the answer.
retry_notenopresent only on a reformat retry: the previous reply could not be read, and this says how to answer again. Leave it out otherwise.

The facts format

facts is the page's scan, written as plain text. An API caller may send their own short summary of the page instead, but the scan's format gets the most out of a run: the review answers every error and warn rule id it finds there (one prescan entry per id), and the fix keeps every prop, emit and exposed name listed under "Public API to keep". The format, line by line:

Page kind: <kind> (<description>)< - needs SEO and server HTML. | - personalised; must never be served from a shared cache. | .>
Route: <route> (matched as <path>).            or: Route: (not given).
Route rules: <n> given; the matching rule is '<pattern>' -> <options as JSON>.   or: ...; none matches this route.   or: none given.
Prescan verdict: <ship|refactor|rework> (<n> errors, <n> warnings, <n> notes).
Public API to keep: props <a, b>; emits <c>; exposed <d><; definePageMeta present>.
Clipped: <only when code or config was clipped>
Findings (rule | severity | file | line | text):
- <rule id> | <error|warn|info> | <vue|nuxt.config> | line <n> | <title> | <the source line>

An empty list is written (none), and a page with no findings gets the single line - none. The prescan verdict is rework at three errors or more, or when errors and warnings land in three of the four areas (hydration, data fetching, links and lazy loading, route rules); refactor on any error or warning; ship otherwise. A real example - the facts the page sends for the sample used in the worked examples below:

Page kind: dashboard (Signed-in user page (account, dashboard)) - personalised; must never be served from a shared cache.
Route: /account/orders (matched as /account/orders).
Route rules: 3 given; the matching rule is '/account/**' -> {"swr":"600"}.
Prescan verdict: refactor (1 error, 3 warnings, 0 notes).
Public API to keep: props pageSize; emits open; exposed (none).
Findings (rule | severity | file | line | text):
- NR-PRIVATECACHE | error | nuxt.config | line 4 | Shared cache on a personalised page | '/account/**' {"swr":"600"}
- NX-NOKEY | warn | vue | line 8 | useAsyncData without a stable key | const { data: orders } = await useAsyncData(async () => {
- NX-SIDEEFFECT | warn | vue | line 8 | Side effect inside a data handler | const { data: orders } = await useAsyncData(async () => {
- NX-LAZYNOPENDING | warn | vue | line 14 | Lazy data with no loading state | const { data: tips } = await useFetch('/api/me/tips', { lazy: true })

Rule ids the scan uses, with their severity (NX- rules read the .vue file, NR- rules read the route rule):

RuleSeverityAreaTitle
NX-NONDETerrorHydrationNon-deterministic value in the first render
NX-BROWSERAPIerrorHydrationBrowser-only API during SSR
NX-VUEROUTERwarnHydrationuseRoute from vue-router
NX-FULLPATHwarnHydrationroute.fullPath / hash drives markup
NX-TOPFETCHerrorData fetchingTop-level $fetch for page data
NX-NOKEYwarnData fetchinguseAsyncData without a stable key
NX-SIDEEFFECTwarnData fetchingSide effect inside a data handler
NX-NOAWAITwarnData fetchinguseFetch / useAsyncData not awaited
NX-LAZYNOPENDINGwarnData fetchingLazy data with no loading state
NX-SERVERFALSEinfoData fetchingserver: false on a page that needs SEO
NX-MOUNTFETCHinfoData fetchingData fetched in onMounted
NX-AXIOSinfoData fetchingaxios / fetch() for app data
NX-ATAGwarnLinks and lazy loadingPlain <a> for an internal link
NX-LAZYEAGERinfoLinks and lazy loadingLazy component rendered unconditionally
NX-LAZYPROPSinfoLinks and lazy loadingLazily hydrated component with reactive props
NX-CLIENTONLYinfoHydration<ClientOnly> content
NR-NONEinfoRoute rulesNo route rule matches this page
NR-SEOCSRerrorRoute rulesssr: false on a page that needs SEO
NR-PRIVATECACHEerrorRoute rulesShared cache on a personalised page
NR-GLOBALwarnRoute rulesOne rendering rule for every route
NR-DYNPRERENDERinfoRoute rulesPrerender on a dynamic route
NR-UNKNOWNwarnRoute rulesUnknown route rule option

A finding the scan cannot see comes back with the rule NEW.

The standard the model applies

The source skill, @affaan-m/nuxt4-patterns, is the only standard:

House rules

The replies

Both lanes carry a route object - the rule this page should have: pattern (such as /products/**), rule (a JSON object of routeRules options, such as {"swr": 3600}) and why.

review

verdict is ship (renders the same on server and client, fetches SSR-safely, right route rule), refactor (one or two real problems) or rework (mismatches or wrong data fetching throughout, or a route rule that leaks or hides the page). A scan error the model agrees with rules out ship.

  1. summary - two or three sentences: what the page does, the verdict and the first thing to fix.
  2. findings - most severe first: rule (the facts' rule id, or NEW), severity (high, medium or low), title, where (such as line 9 or nuxt.config line 3), why (one or two sentences about this page), fix (the corrected lines).
  3. prescan - one entry per error and warn rule id in the facts: rule, call (agree or disagree), note (why, one sentence).
  4. route - the route rule this page should have.
  5. caveats - what could not be seen or verified.
{
  "task": "review",
  "verdict": "refactor",
  "summary": "",
  "findings": [ { "rule": "NX-TOPFETCH", "severity": "high", "title": "", "where": "line 9", "why": "", "fix": "" } ],
  "prescan": [ { "rule": "NX-TOPFETCH", "call": "agree", "note": "" } ],
  "route": { "pattern": "/products/**", "rule": { "swr": 3600 }, "why": "" },
  "caveats": [ "" ]
}

The page holds it to its own scan: every scan error and warning answered by rule id (in findings or prescan), no rule id the scan did not report other than NEW, no ship over an undisputed error, no rework on a clean scan without a high-severity finding, a route rule that passes the checks above, and no claim that the page was run or checked. Each miss is shown next to the reply as a note.

fix

The corrected file, behaviour-preserving apart from the fixes. Every prop, emit and exposed name listed under "Public API to keep" survives, as does definePageMeta.

  1. summary - two sentences.
  2. file - name (such as pages/products/[id].vue) and code: the complete .vue, including what did not change. A clipped input keeps its omitted-lines comment.
  3. route - as in review, the rule this page should have.
  4. changes - one per change: rule (the rule id it answers, or NEW), what.
  5. kept - anything deliberately left: rule, what, why.
  6. caveats - what could not be seen.
{
  "task": "fix",
  "summary": "",
  "file": { "name": "pages/products/[id].vue", "code": "" },
  "route": { "pattern": "/products/**", "rule": { "swr": 3600 }, "why": "" },
  "changes": [ { "rule": "NX-TOPFETCH", "what": "" } ],
  "kept": [ { "rule": "", "what": "", "why": "" } ],
  "caveats": [ "" ]
}

The page re-scans the fixed file: a scan error or warning still present and not listed in kept, a new problem introduced, a prop, emit or exposed name that is no longer declared, a dropped definePageMeta, or a missing omitted-lines comment on a clipped input is named. The proposed route rule gets the same checks as in review. It shows the diff and offers a .patch; the nuxt.config change is applied by hand.

Worked examples - both lanes, one page

One signed-in orders page (/account/orders, kind dashboard) whose routeRules cache /account/** with swr: 600 - a scan error and three warnings, and a focus on customers seeing someone else's list - sent once to each lane. Each input is exactly what the page sends; each reply is a real reply, abbreviated for reading: long strings end in ... and the fixed file shows only its first lines. The real replies are complete JSON with every field in full. The page's reply check found nothing to flag in either.

Orders page (dashboard) - review

Input:

{
 "task": "review",
 "code": "<script setup lang=\"ts\">\nconst props = defineProps<{ pageSize?: number }>()\nconst emit = defineEmits<{ (e: 'open', id: string): void }>()\nconst route = useRoute()\nconst page = computed(() => Number(route.query.page || 1))\nconst lastSeen = useState('lastSeen', () => '')\n\nconst { data: orders } = await useAsyncData(async () => {\n  const res = await $fetch('/api/me/orders', { query: { page: page.value, size: props.pageSize ?? 20 } })\n  lastSeen.value = res.items[0]?.id ?? ''\n  return res\n})\n\nconst { data: tips } = await useFetch('/api/me/tips', { lazy: true })\n</script>\n\n<template>\n  <section>\n    <h1>Your orders</h1>\n    <ul>\n      <li v-for=\"o in orders?.items\" :key=\"o.id\" @click=\"emit('open', o.id)\">{{ o.number }} - {{ o.total }}</li>\n    </ul>\n    <NuxtLink :to=\"{ query: { page: page + 1 } }\">Next page</NuxtLink>\n    <aside v-if=\"tips\">{{ tips.text }}</aside>\n  </section>\n</template>",
 "route": "/account/orders",
 "kind": "dashboard",
 "config": "export default defineNuxtConfig({\n  routeRules: {\n    '/': { prerender: true },\n    '/account/**': { swr: 600 },\n    '/api/**': { cors: true }\n  }\n})",
 "facts": "Page kind: dashboard (Signed-in user page (account, dashboard)) - personalised; must never be served from a shared cache.\nRoute: /account/orders (matched as /account/orders).\nRoute rules: 3 given; the matching rule is '/account/**' -> {\"swr\":\"600\"}.\nPrescan verdict: refactor (1 error, 3 warnings, 0 notes).\nPublic API to keep: props pageSize; emits open; exposed (none).\nFindings (rule | severity | file | line | text):\n- NR-PRIVATECACHE | error | nuxt.config | line 4 | Shared cache on a personalised page | '/account/**' {\"swr\":\"600\"}\n- NX-NOKEY | warn | vue | line 8 | useAsyncData without a stable key | const { data: orders } = await useAsyncData(async () => {\n- NX-SIDEEFFECT | warn | vue | line 8 | Side effect inside a data handler | const { data: orders } = await useAsyncData(async () => {\n- NX-LAZYNOPENDING | warn | vue | line 14 | Lazy data with no loading state | const { data: tips } = await useFetch('/api/me/tips', { lazy: true })",
 "focus": "Orders change often; customers complain the list is sometimes someone else's."
}

Reply (abbreviated). The prescan said refactor; the review agreed with all four scan findings, added three NEW findings (no error UI, a clickable li that a keyboard cannot reach, a Next page link that drops the other query params) and, weighing the focus on someone else's list, moved the verdict to rework and replaced the cache with a private, no-store header:

{
 "task": "review",
 "verdict": "rework",
 "summary": "A signed-in orders list that loads /api/me/orders for the current page and lazily shows tips. Rework: the '/account/**' rule caches it with swr: 600, which is exactly how a customer can be served someone else's order list - that is the first fix, ahead of the unkeyed useAsyncData that never refetches when ?page changes. Your focus on 'someone else's list' made the route rule the lead finding and ruled out any caching option, even a short one, despite the orders changing often.",
 "findings": [
  {
   "rule": "NR-PRIVATECACHE",
   "severity": "high",
   "title": "swr cache on a per-user page",
   "where": "nuxt.config line 4",
   "why": "swr: 600 stores the rendered HTML (orders and payload) for /account/orders keyed by URL, not by ...",
   "fix": "'/account/**': { headers: { 'cache-control': 'private, no-store' } }"
  },
  {
   "rule": "NX-NOKEY",
   "severity": "high",
   "title": "No key tied to page and size",
   "where": "line 8",
   "why": "Without a key Nuxt derives one from file and line, so it never changes with route.query.page or ...",
   "fix": "const { data: orders, status, error } = await useAsyncData( ..."
  },
  {
   "rule": "NX-SIDEEFFECT",
   "severity": "medium",
   "title": "Handler writes useState",
   "where": "line 10",
   "why": "The handler sets lastSeen while fetching; handlers run on the server and are skipped on hydration ...",
   "fix": "watch(orders, (res) => { ..."
  },
  {
   "rule": "NX-LAZYNOPENDING",
   "severity": "low",
   "title": "Lazy tips with no pending state",
   "where": "line 14",
   "why": "tips is lazy, so on client navigation it is null until it resolves and the aside just pops in with ...",
   "fix": "const { data: tips, status: tipsStatus } = await useFetch('/api/me/tips', { lazy: true }) ..."
  },
  {
   "rule": "NEW",
   "severity": "medium",
   "title": "No error UI for orders",
   "where": "line 8, line 20",
   "why": "If /api/me/orders fails (expired session, 500) error is ignored and the user sees an empty list ...",
   "fix": "<p v-if=\"error\" role=\"alert\">Your orders could not be loaded. Please try again.</p> ..."
  },
  {
   "rule": "NEW",
   "severity": "low",
   "title": "Clickable li is not keyboard-accessible",
   "where": "line 21",
   "why": "A click handler on an li is not focusable or announced as a control, so keyboard and screen-reader ...",
   "fix": "<li v-for=\"o in orders?.items\" :key=\"o.id\"> ..."
  },
  {
   "rule": "NEW",
   "severity": "low",
   "title": "Next page drops other query params",
   "where": "line 23",
   "why": "{ query: { page: page + 1 } } replaces the whole query, so any other filter in the URL is lost on ...",
   "fix": "<NuxtLink :to=\"{ query: { ...route.query, page: page + 1 } }\">Next page</NuxtLink>"
  }
 ],
 "prescan": [
  {
   "rule": "NR-PRIVATECACHE",
   "call": "agree",
   "note": "swr: 600 on '/account/**' shares one user's rendered orders with others; remove it and send ..."
  },
  {
   "rule": "NX-NOKEY",
   "call": "agree",
   "note": "The key must include page and pageSize or paging never refetches and pages share a payload entry."
  },
  {
   "rule": "NX-SIDEEFFECT",
   "call": "agree",
   "note": "lastSeen is written inside the handler; derive it with a watch on orders."
  },
  {
   "rule": "NX-LAZYNOPENDING",
   "call": "agree",
   "note": "The lazy tips need a visible status === 'pending' state."
  }
 ],
 "route": {
  "pattern": "/account/**",
  "rule": {
   "headers": {
    "cache-control": "private, no-store"
   }
  },
  "why": "Account pages are per-user: drop swr: 600 and render them on every request, with a private ..."
 },
 "caveats": [
  "The /api/me/orders and /api/me/tips handlers are not shown: I assume they read the session cookie ...",
  "Whatever else reads useState('lastSeen') is not shown.",
  "The edge or CDN in front of Nitro is not shown; make sure it honours Cache-Control and does not ..."
 ]
}

Orders page (dashboard) - fix

Input - the same fields, with task set to fix:

{
 "task": "fix",
 "code": "<script setup lang=\"ts\">\nconst props = defineProps<{ pageSize?: number }>()\nconst emit = defineEmits<{ (e: 'open', id: string): void }>()\nconst route = useRoute()\nconst page = computed(() => Number(route.query.page || 1))\nconst lastSeen = useState('lastSeen', () => '')\n\nconst { data: orders } = await useAsyncData(async () => {\n  const res = await $fetch('/api/me/orders', { query: { page: page.value, size: props.pageSize ?? 20 } })\n  lastSeen.value = res.items[0]?.id ?? ''\n  return res\n})\n\nconst { data: tips } = await useFetch('/api/me/tips', { lazy: true })\n</script>\n\n<template>\n  <section>\n    <h1>Your orders</h1>\n    <ul>\n      <li v-for=\"o in orders?.items\" :key=\"o.id\" @click=\"emit('open', o.id)\">{{ o.number }} - {{ o.total }}</li>\n    </ul>\n    <NuxtLink :to=\"{ query: { page: page + 1 } }\">Next page</NuxtLink>\n    <aside v-if=\"tips\">{{ tips.text }}</aside>\n  </section>\n</template>",
 "route": "/account/orders",
 "kind": "dashboard",
 "config": "export default defineNuxtConfig({\n  routeRules: {\n    '/': { prerender: true },\n    '/account/**': { swr: 600 },\n    '/api/**': { cors: true }\n  }\n})",
 "facts": "Page kind: dashboard (Signed-in user page (account, dashboard)) - personalised; must never be served from a shared cache.\nRoute: /account/orders (matched as /account/orders).\nRoute rules: 3 given; the matching rule is '/account/**' -> {\"swr\":\"600\"}.\nPrescan verdict: refactor (1 error, 3 warnings, 0 notes).\nPublic API to keep: props pageSize; emits open; exposed (none).\nFindings (rule | severity | file | line | text):\n- NR-PRIVATECACHE | error | nuxt.config | line 4 | Shared cache on a personalised page | '/account/**' {\"swr\":\"600\"}\n- NX-NOKEY | warn | vue | line 8 | useAsyncData without a stable key | const { data: orders } = await useAsyncData(async () => {\n- NX-SIDEEFFECT | warn | vue | line 8 | Side effect inside a data handler | const { data: orders } = await useAsyncData(async () => {\n- NX-LAZYNOPENDING | warn | vue | line 14 | Lazy data with no loading state | const { data: tips } = await useFetch('/api/me/tips', { lazy: true })",
 "focus": "Orders change often; customers complain the list is sometimes someone else's."
}

Reply (abbreviated). The data now has a key built from the page and the page size, lastSeen is derived by a watch instead of inside the handler, the lazy tips show a pending state, and pageSize and open survive; the route rule is the same one the review proposed:

{
 "task": "fix",
 "summary": "The orders list gets a key built from page and pageSize so paging refetches, lastSeen is derived by a watch instead of inside the handler, and the lazy tips show a pending state; error UI and a keyboard-accessible order button are added. The '/account/**' rule drops swr: 600 for a private, no-store header because the focus - customers seeing someone else's list - is that shared cache.",
 "file": {
  "name": "pages/account/orders.vue",
  "code": "<script setup lang=\"ts\">\nconst props = defineProps<{ pageSize?: number }>()\nconst emit = defineEmits<{ (e: 'open', id: string): void }>()\nconst route = useRoute()\nconst page = computed(() => Number(route.query.page || 1))\nconst size = computed(() => props.pageSize ?? 20)\nconst lastSeen = useState('lastSeen', () => '')\n\nconst { data: orders, status, error } = await useAsyncData(\n  () => `account-orders:${page.value}:${size.value}`,\n  () => $fetch('/api/me/orders', { query: { page: page.value, size: size.value } })\n)\n..."
 },
 "route": {
  "pattern": "/account/**",
  "rule": {
   "headers": {
    "cache-control": "private, no-store"
   }
  },
  "why": "Account pages are per-user: drop swr: 600 and render them on every request, with a private ..."
 },
 "changes": [
  {
   "rule": "NR-PRIVATECACHE",
   "what": "nuxt.config: replace '/account/**': { swr: 600 } with { headers: { 'cache-control': 'private ..."
  },
  {
   "rule": "NX-NOKEY",
   "what": "useAsyncData now takes the key getter () => `account-orders:${page.value}:${size.value}`, so ..."
  },
  {
   "rule": "NX-SIDEEFFECT",
   "what": "The handler is now a plain $fetch; lastSeen is set from orders by watch(..., { immediate: true })."
  },
  {
   "rule": "NX-LAZYNOPENDING",
   "what": "Tips expose status as tipsStatus and render a 'Loading tips...' aside while pending."
  },
  {
   "rule": "NEW",
   "what": "Orders error is read and shown as a role=alert message instead of an empty list; the list carries ..."
  },
  {
   "rule": "NEW",
   "what": "Each order is a button inside the li, still emitting open with o.id."
  },
  {
   "rule": "NEW",
   "what": "Next page keeps the other query params via { ...route.query, page: page + 1 }."
  }
 ],
 "kept": [
  {
   "rule": "NEW",
   "what": "useState('lastSeen') and the props/emits",
   "why": "Other code may read lastSeen; pageSize and open are the public API."
  },
  {
   "rule": "NEW",
   "what": "lazy tips fetched during SSR",
   "why": "Tips are non-critical but cheap; server: false was not needed to fix the finding."
  }
 ],
 "caveats": [
  "The API handlers and their response shapes ({ items: [{ id, number, total }] }, { text }) are ...",
  "The nuxt.config change is outside the .vue: apply it by hand, and check any CDN in front honours ...",
  "Run `nuxi dev`, page through ?page=1 and ?page=2, and compare server HTML and response headers for ..."
 ]
}

Build the input yourself

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

const S = NxScan, R = NxReply;
const st = { code: fs.readFileSync("orders.vue", "utf8"), route: "/account/orders", kind: "dashboard",
  config: fs.readFileSync("nuxt.config.ts", "utf8"), focus: "Orders change often; ..." };
const scan = S.scan(st);                        // kind, verdict, counts, api, findings, match (the route rule)
const b = S.buildInput(st, "review");           // or "fix"; clips long files and writes the facts
const wire = S.toWire(b.input);                 // every field a string - POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "review");
// R.check(reply, b)                            // {notes, warn}: anything the reply missed or got wrong

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://nuxt-clinic.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":"nuxt-clinic"}' | 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 one of the worked-example inputs above as input.json to follow along.

# input.json: the run input object - {"task": "review", "code": "<script setup lang=\"ts\"> ...", "route": "/account/orders", "kind": "dashboard", "config": "export default defineNuxtConfig({ ...", "facts": "Page kind: dashboard ...", "focus": "..."}
# - 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 nuxt-clinic:<task>:<inputHash>:a<attempt> - the lane is part of the key, so a review and a fix of the same page 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. A review carries verdict, summary, findings, prescan, route and caveats; a fix carries summary, file, route, changes, kept and caveats. The code below prints the headline parts of either: for a review the verdict, the summary and each finding's severity and title; for a fix the summary and the file name; for both the proposed route pattern.

KEY="nuxt-clinic:$(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 == "fix"
  then .summary, "file: \(.file.name)"
  else "verdict: \(.verdict)", .summary, (.findings[] | "[\(.severity)] \(.title)") end,
  "route: \(.route.pattern)"' reply.json

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