Check a design over HTTP

Generating an architecture is cheap now. Knowing whether it will survive contact with a GPU is not. POST a model graph and get the verdict back as JSON: no browser, no LLM, deterministic, the same code path that answers the question inside the app.

EndpointAnswersUse it when
POST /api/v1/check The whole verdict: can it train, what would the run cost, where should it serve and how fast, and what is still a human's call. An agent or a script needs the decision, not the rule list.
POST /api/v1/lint Just the structural rules that fired, with stable ids. CI wants an exit code and a diff-able list.

lint is one stage of check. Both are the same verifier the editor runs on every edit, and neither calls a model.

Authentication

Mint a key in the app under Settings → Developer API. It's shown once; copy it then. Send it as a bearer token:

Authorization: Bearer nrk_your_key_here

Keys are hashed at rest (we store only a SHA-256); revoke any key from the same screen.

Use the www host. The apex answers every /api route with a 307 to www.neurarch.com, and both curl -L and fetch drop the Authorization header across that hop. A valid key sent to the apex comes back as a 401 that blames you.

What a keyed call records. Both endpoints write one anonymous structural row for the graph you send: a fingerprint, the layer-type histogram, the edge count, and the {ruleId, severity} pairs that fired. Never the graph, never parameter values or layer names, and no caller identity, not even the account the key belongs to. It is the one thing we record that you cannot switch off, because it is the grade we just computed for you; if a graph cannot leave your network, run the verifier locally instead (neurarch-mcp or the GitHub Action compute the same verdict offline). Full terms in the data policy.

The whole verdict

POST https://www.neurarch.com/api/v1/check

Five stages run in order (pre-flight, data, train, evaluate, deploy) and the response carries what each of them concluded. The walk stops at the first blocker, because every verdict after a graph that cannot forward-pass would be fiction.

Request

{
  "model": {
    "components": [
      { "id": "in",   "type": "input",     "name": "input",
        "params": { "shape": [3, 32, 32] } },
      { "id": "c1",   "type": "conv2d",    "name": "conv1",
        "params": { "inChannels": 3, "outChannels": 32, "kernelSize": 3, "padding": 1 } },
      { "id": "r1",   "type": "relu",      "name": "relu1",   "params": {} },
      { "id": "p1",   "type": "maxpool2d", "name": "pool1",   "params": { "kernelSize": 2 } },
      { "id": "fl",   "type": "flatten",   "name": "flatten", "params": {} },
      { "id": "fc",   "type": "linear",    "name": "fc",
        "params": { "inFeatures": 8192, "outFeatures": 10 } },
      { "id": "out",  "type": "output",    "name": "output",  "params": {} }
    ],
    "connections": [
      { "from": "in", "to": "c1" }, { "from": "c1", "to": "r1" },
      { "from": "r1", "to": "p1" }, { "from": "p1", "to": "fl" },
      { "from": "fl", "to": "fc" }, { "from": "fc", "to": "out" }
    ]
  }
}

Input shapes carry no batch dimension: [3, 32, 32] is C, H, W. A leading 1 is read as the channel axis and will quietly change every shape and cost below it.

Response

{
  "verdict": "ask",
  "outcome": "needs_input",
  "summary": "No dataset is wired yet, so a run would train on random tensors.",
  "stoppedAt": null,
  "findings": [
    { "stage": "evaluate", "severity": "warn", "title": "No test cases",
      "detail": "Nothing would catch a regression between one run and the next.",
      "fix": "Starter cases can be derived from the input contract right now, for free." },
    { "stage": "deploy", "severity": "warn", "title": "Never trained",
      "detail": "This analysis is about the architecture. Nothing has been trained, so there are no weights to ship." }
  ],
  "stages": [
    { "stage": "preflight", "status": "ok",
      "headline": "Ready to train: 5 layers, 0.08M params, about $0.04 / 3m on an A10G (24GB).",
      "data": { "score": 98, "verdict": "ready", "blockers": [], "warnings": [],
                "layers": 5, "params": 82826, "flops": 1001472,
                "gpu": "A10G (24GB)", "estCostUsd": 0.0422, "estTrainSec": 138.3 } },
    { "stage": "data", "status": "needs_input", "headline": "No dataset is wired yet…",
      "data": { "wired": false, "modality": "vision",
                "contract": { "inputShape": [3, 32, 32], "inputDtype": "float32",
                              "outputDim": 10, "taskType": "classification" } } },
    { "stage": "train", "status": "needs_input", "headline": "Not trained yet…",
      "data": { "lastRun": null, "estCostUsd": 0.0422, "gpu": "A10G (24GB)" } },
    { "stage": "evaluate", "status": "attention",
      "headline": "Nothing measured yet: there is no training run to judge.", "data": { "run": null } },
    { "stage": "deploy", "status": "attention",
      "headline": "Best target is Edge GPU (Jetson) (100/100): ~5.0ms per inference, 0.3 MB.",
      "data": { "target": "edge-gpu", "score": 100, "latencyMs": 5.0,
                "sizeMB": 0.316, "sizeMBQuantized": 0.079, "alternatives": [ /* nine scored targets */ ] } }
  ],
  "decision": {
    "question": "Which data should this train on?",
    "because": "Everything else about the run is derived from the graph. This is not: the design says it wants vision input of shape (3, 32, 32), but not which corpus you have.",
    "options": [
      { "label": "CIFAR-10", "value": "hf:cifar10", "hint": "60k 32×32 colour images, 10 classes" },
      { "label": "No data yet, smoke-test it", "value": "synthetic", "hint": "Random tensors matching the input contract" }
    ]
  }
}
FieldMeaning
verdictblock · warn · ask · ok. The worst status any stage reported.
outcomeblocked, needs_input, attention, or ok.
summaryOne sentence, the same one the app leads with. When something is blocked it is priced: "1 blocker would have failed a $0.17 / 10m on an A10G run."
stoppedAtThe stage the walk stopped at, or null if all five ran.
findings[]Only block and warn, each tagged with the stage that raised it. fix is present when there is one specific thing to change.
stages[].dataThe machine-readable payload per stage: params, FLOPs, the GPU it fits, estimated cost and wall-clock, the derived input contract, the best deploy target with its latency and quantized size.
decisionPresent when something is genuinely a human's call, with because stating why it could not be derived. There are exactly two in the whole flow: which dataset, and whether to spend the money. Surface it, do not answer it: a tool that picks a corpus is inventing a premise.

One difference from the app

In the app, a question pauses the walk. Over HTTP it does not: a caller posting a bare graph has no session, so "no dataset is wired" is true of every request and would otherwise hide the deploy analysis behind it. A blocker still stops. Questions come back in decision, and the stages after them still run.

Example: let an agent check its own design

curl -sS https://www.neurarch.com/api/v1/check \
  -H "Authorization: Bearer $NEURARCH_API_KEY" \
  -H "Content-Type: application/json" \
  --data @model.json \
  | jq '{verdict, summary, blockers: [.findings[] | select(.severity=="block") | .title]}'

Loop it: generate a graph, check it, feed findings[].fix back to the model, check again. Nothing in that loop costs GPU time, which is the point of running it before one exists.

Just the structural rules

POST https://www.neurarch.com/api/v1/lint

Request

{
  "model": {
    "components": [
      { "id": "in",   "type": "input",  "name": "input",
        "params": { "shape": [1, 128] } },
      { "id": "attn", "type": "groupedQueryAttention", "name": "attn",
        "params": { "embedDim": 512, "numHeads": 8, "numKVHeads": 3 } },
      { "id": "out",  "type": "output", "name": "output", "params": {} }
    ],
    "connections": [
      { "from": "in",   "to": "attn" },
      { "from": "attn", "to": "out" }
    ]
  }
}

The graph format is the same JSON the app exports (File → Export → JSON), so the easiest way to get a valid payload is to design once in the app and export.

Response

{
  "verdict": "fail",
  "counts": { "block": 1, "warn": 0, "info": 0 },
  "findings": [
    {
      "rule": "gqa-head-divisibility",
      "severity": "block",
      "message": "numHeads (8) must be divisible by numKVHeads (3)…",
      "componentName": "attn",
      "componentType": "groupedQueryAttention"
    }
  ]
}
FieldMeaning
verdictfail if any blocker, else warn if any warning, else pass. Wire it to your CI exit code.
countsFindings by severity.
findings[].ruleStable rule id (e.g. gqa-head-divisibility, full-mha-serving-cost). Catalogue at /rules.html.
findings[].severityblock (won't run) · warn · info.

Example: fail CI on a blocker

curl -sS https://www.neurarch.com/api/v1/lint \
  -H "Authorization: Bearer $NEURARCH_API_KEY" \
  -H "Content-Type: application/json" \
  --data @model.json \
  | tee result.json \
  | grep -q '"verdict":"fail"' && { echo "Structural blockers found"; exit 1; } || true

Errors

Both endpoints answer the same way.

StatusWhen
401Missing or invalid / revoked API key.
400Body isn't { model: { components: [...] } }.
413More than 2000 components.
422The graph couldn't be verified (malformed shapes).