← FBA Desk / API
Tokens

Drive FBA Desk from your own code

A reading of the model, not of the organism. The model reads the flux balance analysis your browser (or your script) computed; it never recomputes a number, and it is never sent your model file. FBA says what this model allows at the optimum under these bounds, not what the cell does.

Everything the web page does is available over HTTP. Analyse your model with the page's own fbakit.js (cobrapy 0.32 semantics, checked against cobrapy 0.32.1 with GLPK: FBA with slim_optimize, pFBA, flux variability at a fraction of the optimum, blocked reactions, single reaction and gene deletions with the 1% essentiality rule, dead-end metabolites, element and charge balance, the optimum with every default bound raised tenfold, and the gain from opening each limited uptake by one unit), send the facts, and get back a verdict (sound, caveated, unreliable) and either a reading of every metric and the predicted phenotype or a cobrapy script that reproduces every number and applies the fixes. The natural loop: analyse, read, script, fix the bounds, re-check.

Two lanes: the task field

taskwhat you getextra input
interpretA reading of each metric (M1.., in order: objective value, pFBA total flux, active, variable and blocked reactions, dead ends, essential reactions and genes, the optimum at 10x the default bound, and one gain_ metric per limited uptake), the predicted phenotype (what the model takes up and secretes, the largest fluxes, the deletions it cannot survive), your claims judged against the facts, and what the analysis cannot show.none
scriptThe fixes (tightening or closing a named uptake, relaxing a bound that forces flux, stating the objective, loopless FVA, FVA at a lower fraction, removing blocked reactions, listing unbalanced reactions) and one complete Python script: MODEL_PATH = "model.json" loaded with cobra.io.load_json_model, an EXPECTED dict of every browser value checked with math.isclose, then the fixes, all inside main().decision: the text of an earlier interpret run (optional)

Both lanes return the same envelope: lane, verdict, headline, tldr, the lane body, next_steps and prescan_responses. Worked requests: interpret, script, the other examples. The reply shape: output contract.

Input fields

Every field is a string.

fieldrequiredmeaning
taskyesinterpret or script.
factsyesA JSON-encoded string holding the browser's analysis - see below. The page builds it with FbaKit.buildInput; an API caller builds it too.
titlenoA label for the analysis, up to 160 characters.
contextnoYour notes: the organism, the condition, what the objective stands for, what you want to conclude. Up to 3,000 characters.
decisionscript onlyPlain text of an earlier interpret run (the page builds it with Recon.decisionText: "Verdict: ...", the headline, one line per metric reading and phenotype, then the next steps). Up to 5,000 characters.
questionnoAnswered in tldr as a bullet starting "Answer:". Up to 800 characters.
retry_notenoOnly on a retry after a malformed reply.

The facts string

facts is a JSON string, not an object: the browser solves your model, serialises the result with JSON.stringify and sends that text. The model file itself is never sent, so over the API you build the facts yourself. It holds: settings (objective as a list of {id, coef}, direction, fraction_of_optimum, default_bound 1000, tolerance, the essentiality rule, format, model_id, the solver); model (counts of reactions, metabolites, genes, boundary and internal reactions, compartments, formulas and rules given, lines_not_read); fba (status optimal / infeasible / unbounded, objective_value); pfba (total_flux); medium (the uptakes the bounds allow: id, metabolite, bound, unlimited, flux); exchanges (boundary reactions carrying flux in the pFBA state, with direction uptake or secretion); top_fluxes (the largest internal pFBA fluxes); fva (fraction_of_optimum, rows of id, min, max, boundary, and rows_total); blocked (count, ids); dead_ends; knockouts (threshold, and for reactions and genes: tested, essential, reduced_not_essential, each {id, growth, status}, or not_run with the reason); mass_balance (checked, imbalanced, objective_reaction_imbalanced, not_checked_missing_formula); metrics (M1.., each metric, value, sometimes fraction or reaction and side, and basis); flags (F1.. with severity high / medium / low, category, message and refs); browser_verdict; expected (the exact values a cobrapy reproduction must match) and expected_count; and clipped.

An abbreviated but real facts object for the page's "E. coli core, aerobic glucose" example (e_coli_core as cobrapy bundles it, glucose uptake capped at 10). Entries shown as "..." are cut here for length; the page computes and sends the full object:

{
  "settings": {
    "objective": [{"id": "Biomass_Ecoli_core", "coef": 1}],
    "direction": "max",
    "fraction_of_optimum": 1,
    "default_bound": 1000,
    "tolerance": 1e-07,
    "essential_threshold_rule": "growth below 1% of the optimum, or infeasible",
    "format": "text",
    "model_id": "e_coli_core",
    "solver": "browser bounded simplex, checked against cobrapy 0.32.1 with GLPK"
  },
  "model": {
    "reactions": 95,
    "metabolites": 72,
    "genes": 137,
    "boundary_reactions": 20,
    "internal_reactions": 75,
    "compartments": ["c", "e"],
    "metabolites_with_formula": 72,
    "reactions_with_gpr": 69,
    "lines_not_read": 0
  },
  "fba": {"status": "optimal", "objective_value": 0.873922},
  "pfba": {"total_flux": 518.422},
  "medium": [
    {
      "id": "EX_glc__D_e",
      "metabolite": "glc__D_e",
      "bound": -10,
      "unlimited": false,
      "flux": -10
    },
    {"id": "EX_o2_e", "metabolite": "o2_e", "bound": -1000, "unlimited": true, "flux": -21.7995},
    "... 5 more"
  ],
  "exchanges": [
    {"id": "EX_glc__D_e", "metabolite": "glc__D_e", "flux": -10, "direction": "uptake"},
    {"id": "EX_o2_e", "metabolite": "o2_e", "flux": -21.7995, "direction": "uptake"},
    {"id": "EX_co2_e", "metabolite": "co2_e", "flux": 22.8098, "direction": "secretion"},
    "... 4 more"
  ],
  "top_fluxes": [
    {"id": "ATPS4r", "name": "", "flux": 45.514},
    {"id": "CYTBD", "name": "", "flux": 43.599},
    {"id": "NADH16", "name": "", "flux": 38.5346},
    "... 22 more"
  ],
  "fva": {
    "fraction_of_optimum": 1,
    "rows_total": 9,
    "rows": [
      {"id": "EX_co2_e", "min": 22.8098, "max": 22.8098, "boundary": true},
      {"id": "EX_glc__D_e", "min": -10, "max": -10, "boundary": true},
      "..."
    ]
  },
  "blocked": {
    "count": 8,
    "ids": [
      "EX_fru_e",
      "EX_fum_e",
      "EX_gln__L_e",
      "EX_mal__L_e",
      "FRUpts2",
      "FUMt2_2",
      "GLNabc",
      "MALt2_2"
    ]
  },
  "dead_ends": {"count": 4, "ids": ["fru_e (only consumed)", "fum_e (only consumed)", "..."]},
  "knockouts": {
    "threshold": 0.00873922,
    "reactions": {
      "tested": 95,
      "essential": [
        {"id": "ACONTa", "growth": 0, "status": "optimal"},
        {"id": "ACONTb", "growth": 0, "status": "optimal"},
        "... 16 more"
      ],
      "reduced_not_essential": ["..."]
    },
    "genes": {
      "tested": 137,
      "essential": [
        {"id": "b0720", "growth": 0, "status": "optimal"},
        {"id": "b2779", "growth": 0, "status": "optimal"},
        "... 5 more"
      ],
      "reduced_not_essential": ["..."]
    }
  },
  "mass_balance": {
    "checked": 75,
    "imbalanced": [],
    "objective_reaction_imbalanced": ["Biomass_Ecoli_core"],
    "not_checked_missing_formula": 0
  },
  "metrics": [
    {
      "id": "M1",
      "metric": "objective_value",
      "value": 0.873922,
      "basis": "maximise Biomass_Ecoli_core at steady state under the pasted bounds (model.slim_optimize)"
    },
    {
      "id": "M2",
      "metric": "pfba_total_flux",
      "value": 518.422,
      "basis": "sum of absolute fluxes, minimised with the objective held at its optimum (cobra.flux_analysis.pfba)"
    },
    {
      "id": "M9",
      "metric": "objective_at_10x_default_bound",
      "value": 0.873922,
      "basis": "the optimum again with every bound of magnitude 1000 raised tenfold; equal to M1 means the default bound does not limit the objective"
    },
    {
      "id": "M10",
      "metric": "gain_EX_glc__D_e",
      "value": 0.0916647,
      "basis": "change in the objective when the uptake bound of EX_glc__D_e (lower_bound -10) is opened by 1 unit; above 0 means this uptake limits the objective",
      "reaction": "EX_glc__D_e",
      "side": "lower_bound"
    },
    "... M3-M8"
  ],
  "flags": [
    {
      "id": "F1",
      "severity": "medium",
      "category": "range_at_cap",
      "message": "1 internal reaction(s) reach the default bound in FVA at 100% of the optimum (SUCDi): a thermodynamically infeasible cycle, or an uptake limited only by the default bound, lets them carry flux up to the cap. Those FVA ranges are set by the cap; loopless FVA removes the cycle case, a finite uptake bound the other.",
      "refs": ["SUCDi"]
    },
    "... F2-F5 (low)"
  ],
  "browser_verdict": "caveated",
  "expected": {
    "objective_value": 0.873921507,
    "pfba_total_flux": 518.4220855,
    "n_blocked": 8,
    "n_essential_reactions": 18,
    "n_essential_genes": 7,
    "objective_10x_default_bound": 0.873921507,
    "fva_min_EX_h2o_e": 29.17582714,
    "fva_max_EX_h2o_e": 29.17582714,
    "fva_min_EX_co2_e": 22.80983331,
    "fva_max_EX_co2_e": 22.80983331,
    "fva_min_EX_o2_e": -21.79949266,
    "fva_max_EX_o2_e": -21.79949266,
    "gain_EX_glc__D_e": 0.09166474638
  },
  "expected_count": 13,
  "clipped": []
}

The free browser page computes this full object for any model you paste. To copy it without writing code, open a result on the page and press Download .json: the file carries the exact facts object under browser (the page's saved examples replay for free, so this works before any spend). Send it back as a string: json.dumps(facts), JSON.stringify(facts) or your language's equivalent. Keep the keys and values the browser produced: the reply is reconciled against them, and the script lane copies expected into its reproduction check.

Building the body

The simplest way to get a body that matches the page byte for byte is to run the page's own module in Node. fbakit.js needs only lp.js (the page's simplex solver) next to it, and both export themselves with module.exports. Your model can be cobrapy reaction strings in the page's text form (PGI: g6p_c <=> f6p_c [-1000, 1000] gpr: b4025) or a cobrapy JSON model written by cobra.io.save_json_model; the page solves models of up to 800 reactions and 800 metabolites.

// make-body.js - build the exact body the page sends, with the page's own code.
// Save https://fba-desk.skillsafe.ai/fbakit.js and https://fba-desk.skillsafe.ai/lp.js next to this file, then:
//   node make-body.js model.txt interpret "E. coli core on aerobic glucose" "notes" "question" 1 > body.json
// model.txt is cobrapy reaction strings in the page's text form, or a cobrapy JSON model (save_json_model).
const fs = require("fs");
const K = require("./fbakit.js");
const [file, lane = "interpret", title = "", context = "", question = "", fraction = "1", decision = ""] = process.argv.slice(2);
const set = { lane, title, context, question, decision, fraction, model: fs.readFileSync(file, "utf8") };
const X = K.analyze(set);
if (X.empty) throw new Error(X.errors.join("; ") || "no model");
const body = K.mustBeObject(K.buildInput(X, set));
console.error("browser verdict:", X.hint, "| objective:", X.fba.status, X.fba.z, "| flags:", X.flags.map(f => f.id + " " + f.category).join(", "));
console.error("idempotency key: fba-desk:" + body.task + ":" + K.hashInput(body) + ":a1");
process.stdout.write(JSON.stringify(body));
# Or build the body in any language from a facts object you already hold, for example the
# "browser" key of the page's "Download .json" export. facts must go in as a STRING.
import json

export = json.load(open("e-coli-core-interpret.json"))   # the page's .json download
facts = export["browser"]
body = {
    "task": "interpret",
    "title": "E. coli core on aerobic glucose",
    "context": "The E. coli core model (e_coli_core) exactly as cobrapy bundles it: ...",
    "question": "Which uptake limits growth, and does any single gene deletion stop it?",
    "facts": json.dumps(facts, separators=(",", ":")),
}
json.dump(body, open("body.json", "w"))

Base URL and the envelope

Every endpoint lives under https://api.skillsafe.ai/v1/app-api and every response uses the same envelope, so one helper covers the whole API:

{"ok": true, "data": {"job_id": "job_...", "status": "queued"}}
{"ok": false, "error": {"code": "payment_required", "message": "..."}}

The token is minted for this app (the guest endpoint takes {"slug":"fba-desk"} in its body), so no slug header is needed afterwards. Send it as Authorization: Bearer ….

The input object IS the request body. There is no {"input": …} wrapper. A wrapped body is answered with an unknown field 'input' warning, and the model never sees your text.

Error codes

statuscodewhat to do
400validation_errorA field is missing or the wrong type. Every field is a string: facts must be a JSON-encoded string, not an object.
401unauthorizedThe token is missing, malformed or expired. Get a new one from the token page.
402payment_requiredThe balance is below min_credits. Call /estimate first and top up.
403forbiddenThe token is valid but not for this app, or a guest token tried a metered run. A guest cannot run; sign in for a personal token.
404not_foundUnknown job id, or the app slug does not exist.
409conflictThe same Idempotency-Key was replayed with a different body. Change the key or send the original input.
429rate_limitedToo many requests. Back off and retry; do not tight-loop.
5xxinternalA server-side failure. Retry with the SAME Idempotency-Key so you are not billed twice.

1. A tiny client

One helper that sends the token, unwraps data and raises on ok: false. The token comes from the token page (Copy token or Copy shell export); step 2 covers the kinds of token and minting one from code.

# Every call is the same three things: the base URL, your bearer token,
# and a JSON body. Keep the token in a shell variable.
BASE="https://api.skillsafe.ai/v1/app-api"
SLUG="fba-desk"
TOKEN="$SKILLSAFE_TOKEN"   # from https://fba-desk.skillsafe.ai/tokens.html

call() {                  # call <path> [json-body]
  if [ -n "$2" ]; then
    curl -sS -X POST "$BASE/$1" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d "$2"
  else
    curl -sS "$BASE/$1" -H "Authorization: Bearer $TOKEN"
  fi
}

2. Get a token

The easiest route is the token page: it shows the token this browser already holds, with Copy token and Copy shell export buttons, and a sign-in button for a personal token. A guest token, minted with POST /guest and {"slug":"fba-desk"}, can call /me and /estimate; the run is metered, so /run and /run-stream need a personal token.

# The token page is the shortest path. It shows the token this browser holds and
# hands you a ready-made shell export:
#
#   https://fba-desk.skillsafe.ai/tokens.html
#   export SKILLSAFE_TOKEN="..."
#
# To mint a guest token from the command line instead. A guest token is enough
# for /me and /estimate; a run needs a personal token from signing in.
curl -sS -X POST "https://api.skillsafe.ai/v1/app-api/guest" \
  -H "Content-Type: application/json" -d '{"slug":"fba-desk"}'
# {"ok":true,"data":{"token":"…","subject_type":"guest"}}

3. Check the session and the balance

call me
# {"ok":true,"data":{"subject_type":"user","username":"you","credits":51234}}

4. Price the run (free)

/estimate returns the model binding and the credits a run would reserve. It creates no job and charges nothing. Expect model_alias gpt-terra and markup_bps 1000 (a 10% markup). hold_credits is a reservation, not the price: it is held against your balance while the run executes and released afterwards. min_credits is the least balance that can start a run. What you actually pay is charged_credits, reported on the finished job and in the done event, and it is usually far lower than the hold. The body is the input object itself, with no {"input": …} wrapper. /estimate does not validate the body, so check the shape yourself: an object whose every value is a string, task equal to interpret or script, facts non-empty, and facts a JSON string that parses to an object (this is what the page's own guard, FbaKit.mustBeObject, refuses to spend without).

# body.json is the input object itself - no {"input": ...} wrapper. Build it with
# make-body.js above, or by hand. estimate does not validate it, so check the shape first:
python3 -c 'import json;b=json.load(open("body.json"));assert isinstance(b,dict) and b.get("task") in ("interpret","script") and all(isinstance(v,str) for v in b.values()) and all(b.get(k,"").strip() for k in ("facts",)) and isinstance(json.loads(b["facts"]),dict)'
INPUT=$(cat body.json)

call estimate "$INPUT"
# {"ok":true,"data":{"model":"...","model_alias":"gpt-terra",
#   "markup_bps":1000,"hold_credits":...,"min_credits":...,"sponsor_enabled":false,
#   "warnings":[]}}
#
# estimate creates no job and charges nothing. hold_credits is RESERVED, not the
# price; charged_credits after the run is the actual cost, usually far lower.

5. Run it, then poll

POST /run returns a job_id; poll GET /jobs/{id} until it is terminal. The reply is a string at data.output.output: JSON.parse it (step 7). Send an Idempotency-Key built from the lane, a hash of the input and the attempt number, fba-desk:<lane>:<hash>:a<attempt> (for example fba-desk:interpret:f2j0v11dgeebz:a1), so a retried request returns the same job instead of billing a second run. Use one key per distinct input: a changed model, bounds, fraction of the optimum or notes (so changed facts) or a changed reading are a new hash, the same model in the other lane are a new key, and replaying an old key with a different body is a 409. The page uses FbaKit.hashInput(body) for the hash (it covers task, title, context, facts, decision and question; make-body.js prints the key); any stable digest of the body works from other languages. Leave retry_note out of the hash and bump the attempt instead.

# Always send an Idempotency-Key derived from the input. A retried request with
# the same key returns the SAME job instead of billing a second run.
LANE=$(printf '%s' "$INPUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["task"])')   # interpret or script
KEY="fba-desk:$LANE:$(printf '%s' "$INPUT" | shasum -a 256 | cut -c1-16):a1"

JOB=$(curl -sS -X POST "$BASE/run" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" \
  -d "$INPUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["job_id"])')

while :; do
  OUT=$(call "jobs/$JOB")
  STATUS=$(printf '%s' "$OUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["status"])')
  [ "$STATUS" = "succeeded" ] && break
  [ "$STATUS" = "failed" ] && echo "$OUT" && exit 1
  sleep 2
done

# {"ok":true,"data":{"job_id":"job_...","status":"succeeded",
#   "output":{"output":"{\"lane\":\"interpret\",\"verdict\":\"caveated\",\"headline\":\"...\", ...}"},
#   "charged_credits":...,"truncated":false}}
printf '%s' "$OUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["output"]["output"])' > reply.json

6. Or stream it

POST /run-stream takes the same body and headers and answers with server-sent events: job (the job id), delta (chunks of the reply) and done (the status, charged_credits, truncated and, when present, the full output). A browser page may receive only tick heartbeats and then done, never a delta, so take the reply from done.output.output when it is there, fall back to the concatenated deltas, and fall back again to GET /jobs/{id}.

# Server-sent events. `delta` events carry chunks of the reply; `done` carries the
# status, charged_credits and the truncated flag. Ignore `tick` heartbeats.
curl -N -X POST "$BASE/run-stream" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" \
  -H "Accept: text/event-stream" \
  -d "$INPUT"

# event: job    {"job_id":"job_..."}
# event: delta  {"text":"{\"lane\":\"interpret\",\"verdict\":\"caveated\",\"headline\":\"The"}
# event: done   {"status":"succeeded","charged_credits":...,"truncated":false}

7. Parse the reply

The reply is a JSON object serialised as a string. Parse it, then check the lane.

# The reply is a JSON string inside data.output.output. Pull it out and parse it:
printf '%s' "$JOB" | python3 -c 'import sys,json;r=json.loads(json.load(sys.stdin)["output"]["output"]);print(r["verdict"],r["headline"])'

Invariants worth asserting

The output contract

Every key of the lane's contract is present; empty sections are [].

{
  "lane": "interpret" | "script",
  "verdict": "sound" | "caveated" | "unreliable",
  "headline": "one sentence",
  "tldr": ["2-5 bullets; one starts \"Answer:\" when a question was asked"],
  // interpret:
  "metrics": [{"id": "M1", "reading": "..."}],          // one per facts.metrics item, same order
  "phenotype": ["1-4 strings: uptake and secretion, largest fluxes, lethal deletions"],
  "claims": [{"claim": "...", "support": "supported|partly|not_supported", "why": "..."}],
  "cautions": ["1-4 strings on what the analysis cannot show"],
  // script:
  "fixes": [{"fix": "...", "why": "...", "refs": "F1"}],  // 1-6; refs "" for a plain reproduction step
  "script": "import math\nimport cobra\n...",             // one complete Python script, under 9000 characters
  "assumptions": ["1-4 strings"],
  "checks": ["1-4 strings"],
  // both:
  "next_steps": ["1-5 concrete actions"],
  "prescan_responses": [{"ref": "F1", "verdict": "confirmed|dismissed", "note": "..."}]
}

The script lane's script follows a fixed order: import cobra and set cobra.Configuration().processes = 1; set MODEL_PATH = "model.json" and check it exists; load it (and set model.objective_direction = "min" when settings.direction is min); define EXPECTED; compute each key with cobrapy (objective_value from slim_optimize(), pfba_total_flux from pfba(model).objective_value, n_blocked, n_essential_reactions, n_essential_genes, objective_10x_default_bound, fva_min_<ID> / fva_max_<ID> and gain_<ID>); compare each with math.isclose(got, want, rel_tol=1e-6, abs_tol=1e-6); then apply the fixes. A bound the user did not give is a named constant set to None with an AUTHOR_INPUT_NEEDED comment, and that fix is skipped while it is None.

Worked example: interpret

The page's "E. coli core, aerobic glucose" example. The browser finds an optimum of 0.873922, glucose as the only limiting uptake (gain_EX_glc__D_e 0.0916647, M10), 18 essential reactions and 7 essential genes, and five flags: SUCDi reaching the default bound in FVA (F1, medium) plus four low flags (alternate optima, open uptakes, 8 blocked reactions, 4 dead ends), so its read is caveated. The body, with facts abbreviated (send the full string from make-body.js or the page):

{
 "task": "interpret",
 "title": "E. coli core on aerobic glucose",
 "context": "The E. coli core model (e_coli_core) exactly as cobrapy bundles it: aerobic, glucose uptake capped at 10, the biomass reaction as the objective. I want to say that growth is limited by glucose rather than oxygen, and that no single gene deletion stops growth.",
 "question": "Which uptake limits growth, and does any single gene deletion stop it?",
 "facts": "{\"settings\":{\"objective\":[{\"id\":\"Biomass_Ecoli_core\",\"coef\":1}],\"direction\":\"max\",\"fraction_of_optimum\":1,\"default_bound\":1000,\"model_id\":\"e_coli_core\",\"...\":\"more\"},\"fba\":{\"status\":\"optimal\",\"objective_value\":0.873922},\"pfba\":{\"total_flux\":518.422},\"exchanges\":[{\"id\":\"EX_glc__D_e\",\"metabolite\":\"glc__D_e\",\"flux\":-10,\"direction\":\"uptake\"},{\"id\":\"EX_o2_e\",\"metabolite\":\"o2_e\",\"flux\":-21.7995,\"direction\":\"uptake\"},{\"id\":\"EX_co2_e\",\"metabolite\":\"co2_e\",\"flux\":22.8098,\"direction\":\"secretion\"},\"... 4 more\"],\"...\":\"medium, top_fluxes, fva, blocked, dead_ends, knockouts, mass_balance, metrics, flags as above\",\"browser_verdict\":\"caveated\",\"expected\":{\"objective_value\":0.873921507,\"pfba_total_flux\":518.4220855,\"...\":\"11 more\"},\"expected_count\":13,\"clipped\":[]}"
}

A reply must answer F1 to F5 once each in prescan_responses, read M1 to M10 in order, judge both claims in context (growth limited by glucose rather than oxygen; no single gene deletion stops growth) against knockouts and the gain_ metric, and stay at caveated or tighter unless it dismisses F1.

Worked example: script

The same model with the interpret reading handed over as decision (the page fills it with Recon.decisionText of the earlier reply; it may be empty). No question is sent in this lane. The body, with facts and decision abbreviated:

{
 "task": "script",
 "title": "E. coli core on aerobic glucose",
 "context": "The E. coli core model (e_coli_core) exactly as cobrapy bundles it: aerobic, glucose uptake capped at 10, the biomass reaction as the objective. I want to say that growth is limited by glucose rather than oxygen, and that no single gene deletion stops growth.",
 "question": "",
 "decision": "Verdict: caveated.\n<headline of the interpret reply>\n- M1: <reading>\n- M2: <reading>\n...\nNext steps:\n- ...",
 "facts": "{\"settings\":{\"objective\":[{\"id\":\"Biomass_Ecoli_core\",\"coef\":1}],\"direction\":\"max\",\"fraction_of_optimum\":1,\"default_bound\":1000,\"model_id\":\"e_coli_core\",\"...\":\"more\"},\"fba\":{\"status\":\"optimal\",\"objective_value\":0.873922},\"pfba\":{\"total_flux\":518.422},\"exchanges\":[{\"id\":\"EX_glc__D_e\",\"metabolite\":\"glc__D_e\",\"flux\":-10,\"direction\":\"uptake\"},{\"id\":\"EX_o2_e\",\"metabolite\":\"o2_e\",\"flux\":-21.7995,\"direction\":\"uptake\"},{\"id\":\"EX_co2_e\",\"metabolite\":\"co2_e\",\"flux\":22.8098,\"direction\":\"secretion\"},\"... 4 more\"],\"...\":\"medium, top_fluxes, fva, blocked, dead_ends, knockouts, mass_balance, metrics, flags as above\",\"browser_verdict\":\"caveated\",\"expected\":{\"objective_value\":0.873921507,\"pfba_total_flux\":518.4220855,\"...\":\"11 more\"},\"expected_count\":13,\"clipped\":[]}"
}

The reply's script must put all 13 keys of facts.expected into EXPECTED with these values (objective_value 0.873921507, pfba_total_flux 518.4220855, n_blocked 8, n_essential_reactions 18, n_essential_genes 7, objective_10x_default_bound 0.873921507, the FVA minimum and maximum of EX_h2o_e, EX_co2_e and EX_o2_e, and gain_EX_glc__D_e 0.09166474638), check each with math.isclose, and answer F1 with a fix from the allowed list, typically re-running FVA with loopless=True. Save the page's model.json next to the script and run it with python fba_fix.py.

The other examples

The page carries two more interpret examples; build their bodies the same way (the numbers below are the browser's own):

examplewhat the browser findsbrowser verdict
E. coli core without oxygen (EX_o2_e lower bound 0)Optimum 0.211663; the pFBA state secretes EX_ac_e 8.50359, EX_etoh_e 8.27946 and EX_for_e 17.8047; 23 essential reactions and 10 essential genes; gain_EX_glc__D_e 0.0304591. Same five flag categories as aerobic, F1 (range_at_cap) medium.caveated
A teaching network with open uptakes (toy_fermenter)Optimum 666.667, but 6666.67 with every default bound raised tenfold (M9), so F1 is a high default_bound flag: the optimum reflects cobrapy's arbitrary cap of 1000, not a nutrient limit. Also range_at_cap (medium), alternate optima, open uptakes, a blocked reaction, a dead end and no formulas (low).unreliable

Truncation and partial results

If your balance sits between min_credits and hold_credits, the run still executes with a smaller output cap and the job carries "truncated": true. The JSON may then stop mid-object: close it (the page's Recon.closeJson does this) and show the sections that arrived, saying how many of the lane's sections were recovered, rather than treating a clipped reply as complete. A clipped script is not runnable; re-run instead.