← Script Checker for deepTools / API
Tokens

Drive Script Checker for deepTools from your own code

Script Checker for deepTools checks a deepTools bash script before you run it. In the page, and free, it splits the script into commands, runs every deepTools command line through an emulation of deepTools 3.5.6's own argument parser (all 20 commands) and the argument-only exits in each tool's main(), and adds best-practice checks that quote the deeptools agent skill's SKILL.md. It also ports the skill's workflow_generator.py, so its four templates come out byte for byte.

The metered lanes read what the page found: review judges a script for the experiment you describe and returns a corrected script; plan drafts a pipeline from a prose description. Neither sees your data: nothing opens a BAM or BED file, and no reply may claim a result. The page parses any script in a reply with the same deepTools 3.5.6 checks and shows every error it finds.

Two lanes: the task field

tasksendyou get back
reviewfacts, script; context and question optionalstatus (blocked, fix_first, ready), headline, experiment_read, a stance on every flag (flag_responses), findings (severity, command, issue, why, fix), qc_checkpoints, corrected_script ("" when nothing needs to change) and to_confirm.
planfacts, experiment; context optionalstatus (always draft), headline, assumptions, steps (n, tool, purpose, why), normalization (method, why), the whole script, qc_checkpoints and to_confirm.

Worked example: review

The skill's own ChIP-seq QC template, generated for a paired-end H3K4me3 experiment on GRCh38. The page finds one deepTools error: the template calls plotCorrelation without its required --whatToPlot/-p (and with --whatToShow, a plotHeatmap option), so deepTools 3.5.6 stops at step 2 with an argparse error.

{
 "task": "review",
 "facts": "{\"deeptools\":\"3.5.6\",\"experiment\":{\"assay\":\"chip\",\"layout\":\"paired\",\"assembly\":\"GRCh38\"},\"genome_sizes\":{\"assembly\":\"GRCh38\",\"read_lengths\":[50,75,100,150,200,250],\"deeptools_3_5_6\":{\"nonN\":2913022398,\"by_read_length\":[2701495711,2747877702,2805636231,2862010428,2887553103,2898802627]},\"current_docs\":{\"nonN\":3130250755,\"by_read_length\":[2708135512,2765133945,2820945813,2884242992,2909040054,2919128283]}},\"status\":\"blocked\",\"commands\":[{\"n\":1,\"line\":21,\"tool\":\"multiBamSummary bins\",\"parse\":\"ok\",\"exits\":[]},{\"n\":2,\"line\":27,\"tool\":\"plotCorrelation\",\"parse\":\"the following arguments are required: --whatToPlot/-p\",\"exits\":[]},{\"n\":3,\"line\":35,\"tool\":\"plotPCA\",\"parse\":\"ok\",\"exits\":[]},{\"n\":4,\"line\":42,\"tool\":\"plotCoverage\",\"parse\":\"ok\",\"exits\":[]},{\"n\":5,\"line\":50,\"tool\":\"bamPEFragmentSize\",\"parse\":\"ok\",\"exits\":[]},{\"n\":6,\"line\":57,\"tool\":\"plotFingerprint\",\"parse\":\"ok\",\"exits\":[]}],\"flags\":[{\"id\":\"F1\",\"source\":\"tool\",\"severity\":\"high\",\"code\":\"parse\",\"command\":2,\"text\":\"plotCorrelation: error: the following arguments are required: --whatToPlot/-p (argparse exits 2 before reading any file)\",\"quote\":\"\"}],\"flags_total\":1,\"generator\":{\"workflow\":\"chipseq_qc\",\"command_line\":\"python scripts/workflow_generator.py chipseq_qc -o chipseq_qc.sh --threads 8 --input-bam Input.bam --chip-bams 'H3K4me3_rep1.bam H3K4me3_rep2.bam'\"},\"script_sent\":{\"lines\":67,\"lines_cut\":0,\"chars_cut\":0}}",
 "script": "#!/bin/bash\nset -euo pipefail\n\n# deepTools ChIP-seq Quality Control Workflow\n# Generated by deepTools workflow generator\n\n# Configuration\nINPUT_BAM=Input.bam\nCHIP_BAM=(H3K4me3_rep1.bam H3K4me3_rep2.bam)\nGENOME_SIZE=2913022398\nTHREADS=8\nOUTPUT_DIR=chipseq_qc_output\n\n# Create output directory\nmkdir -p \"$OUTPUT_DIR\"\n\necho \"=== Starting ChIP-seq QC workflow ===\"\n\n# Step 1: Correlation analysis\necho \"Step 1: Computing correlation matrix...\"\nmultiBamSummary bins \\\n    --bamfiles \"$INPUT_BAM\" \"${CHIP_BAM[@]}\" \\\n    -o \"$OUTPUT_DIR/readCounts.npz\" \\\n    --numberOfProcessors \"$THREADS\"\n\necho \"Step 2: Generating correlation heatmap...\"\nplotCorrelation \\\n    -in \"$OUTPUT_DIR/readCounts.npz\" \\\n    --corMethod pearson \\\n    --whatToShow heatmap \\\n    --plotFile \"$OUTPUT_DIR/correlation_heatmap.png\" \\\n    --plotNumbers\n\necho \"Step 3: Generating PCA plot...\"\nplotPCA \\\n    -in \"$OUTPUT_DIR/readCounts.npz\" \\\n    -o \"$OUTPUT_DIR/PCA_plot.png\" \\\n    -T \"PCA of ChIP-seq samples\"\n\n# Step 2: Coverage assessment\necho \"Step 4: Assessing coverage...\"\nplotCoverage \\\n    --bamfiles \"$INPUT_BAM\" \"${CHIP_BAM[@]}\" \\\n    --plotFile \"$OUTPUT_DIR/coverage.png\" \\\n    --ignoreDuplicates \\\n    --numberOfProcessors \"$THREADS\"\n\n# Step 3: Fragment size (for paired-end data)\necho \"Step 5: Analyzing fragment sizes...\"\nbamPEFragmentSize \\\n    --bamfiles \"$INPUT_BAM\" \"${CHIP_BAM[@]}\" \\\n    --histogram \"$OUTPUT_DIR/fragmentSizes.png\" \\\n    --plotTitle \"Fragment Size Distribution\"\n\n# Step 4: ChIP signal strength\necho \"Step 6: Evaluating ChIP enrichment...\"\nplotFingerprint \\\n    --bamfiles \"$INPUT_BAM\" \"${CHIP_BAM[@]}\" \\\n    --plotFile \"$OUTPUT_DIR/fingerprint.png\" \\\n    --extendReads 200 \\\n    --ignoreDuplicates \\\n    --numberOfProcessors \"$THREADS\" \\\n    --outQualityMetrics \"$OUTPUT_DIR/fingerprint_metrics.txt\"\n\necho \"=== ChIP-seq QC workflow complete ===\"\necho \"Results are in: $OUTPUT_DIR\"\n",
 "context": "Human cell line, H3K4me3 ChIP with one input, paired-end 2x50. Generated with the skill's template, not edited yet.",
 "question": "Will this run as-is, and what should I look at in the QC before making tracks?"
}

A trimmed reply (the saved reply on the page is complete; corrected_script is shortened here with a comment line):

{
 "task": "review",
 "status": "blocked",
 "headline": "The template stops at step 2: plotCorrelation needs --whatToPlot, and --whatToShow is not one of its options.",
 "experiment_read": "QC of one H3K4me3 ChIP with two replicates against one input, paired-end 2x50 on GRCh38, before any tracks are made. The script assumes coordinate-sorted, indexed BAMs with the template's file names.",
 "flag_responses": [
  {
   "ref": "F1",
   "stance": "confirmed",
   "note": "deepTools 3.5.6 exits with status 2 at plotCorrelation before reading readCounts.npz, and set -e stops the script there, so steps 3-6 never run. Replace --whatToShow heatmap with --whatToPlot heatmap."
  }
 ],
 "findings": [
  {
   "severity": "low",
   "command": 1,
   "issue": "multiBamSummary counts duplicate reads while plotCoverage and plotFingerprint ignore them.",
   "why": "The correlation and the coverage plots then describe different read sets; the skill removes duplicates in most ChIP-seq analyses.",
   "fix": "Add --ignoreDuplicates to multiBamSummary bins, or remove duplicates before this script."
  }
 ],
 "qc_checkpoints": [
  {
   "after": "plotFingerprint (fingerprint.png, fingerprint_metrics.txt)",
   "look_for": "How far each ChIP curve bends away from the input curve; the skill runs this before any detailed analysis."
  },
  {
   "after": "plotCorrelation (correlation_heatmap.png)",
   "look_for": "Whether the two H3K4me3 replicates are more alike than either is to the input."
  },
  {
   "after": "bamPEFragmentSize (fragmentSizes.png)",
   "look_for": "The fragment-length distribution of each library, and any library that differs from the others."
  }
 ],
 "corrected_script": "#!/bin/bash\nset -euo pipefail\n\n# deepTools ChIP-seq Quality Control Workflow\n# Generated by deepTools workflow generator\n\n# Configuration\nINPUT_BAM=Input.bam\nCHIP_BAM=(H3K4me3_rep1.bam H3K4me3_rep2.bam)\nGENOME_SIZE=2913022398\nTHREADS=8\nOUTPUT_DIR=chipseq_qc_output\n\n# Create output directory\nmkdir -p \"$OUTPUT_DIR\"\n\necho \"=== Starting ChIP-seq QC workflow ===\"\n\n# Step 1: Correlation analysis\necho \"Step 1: Computing correlation matrix...\"\nmultiBamSummary bins \\\n    --bamfiles \"$INPUT_BAM\" \"${CHIP_BAM[@]}\" \\\n    -o \"$OUTPUT_DIR/readCounts.npz\" \\\n    --ignoreDuplicates \\\n    --numberOfProcessors \"$THREADS\"\n\necho \"Step 2: Generating correlation heatmap...\"\nplotCorrelation \\\n    -in \"$OUTPUT_DIR/readCounts.npz\" \\\n    --corMethod pearson \\\n    --whatToPlot heatmap \\\n    --plotFile \"$OUTPUT_DIR/correlation_heatmap.png\" \\\n    --plotNumbers\n\n# ... steps 3-6 (plotPCA, plotCoverage, bamPEFragmentSize, plotFingerprint) unchanged - trimmed in this example\n",
 "to_confirm": [
  "The three BAM files have .bai indexes (samtools index), which the skill checks before any deepTools step."
 ]
}

A source: "tool" flag is what deepTools itself does, so it can only be confirmed, and the status cannot be ready while a high flag stands.

Worked example: plan

An H3K27ac ChIP-seq experiment described in prose. facts carries the assay, layout and assembly the page's selectors set, the effective genome sizes for that assembly, the deepTools 3.5.6 command names and the skill generator's four templates.

{
 "task": "plan",
 "facts": "{\"deeptools\":\"3.5.6\",\"experiment\":{\"assay\":\"chip\",\"layout\":\"single\",\"assembly\":\"GRCm39\"},\"genome_sizes\":{\"assembly\":\"GRCm39\",\"read_lengths\":[50,75,100,150,200,250],\"deeptools_3_5_6\":{\"nonN\":2654621783,\"by_read_length\":[2309746861,2410055689,2468088461,2495461690,2521902382,2538633971]},\"current_docs\":{\"nonN\":2654621783,\"by_read_length\":[2309746861,2410055689,2468088461,2495461690,2521902382,2538633971]}},\"tools\":[\"bamCoverage\",\"bamCompare\",\"multiBamSummary\",\"multiBigwigSummary\",\"plotCorrelation\",\"plotPCA\",\"plotCoverage\",\"plotFingerprint\",\"bamPEFragmentSize\",\"computeGCBias\",\"correctGCBias\",\"alignmentSieve\",\"computeMatrix\",\"plotHeatmap\",\"plotProfile\",\"plotEnrichment\",\"bigwigCompare\",\"bigwigAverage\",\"computeMatrixOperations\",\"estimateReadFiltering\"],\"workflows\":[{\"id\":\"chipseq_qc\",\"name\":\"ChIP-seq Quality Control\",\"description\":\"Complete QC workflow for ChIP-seq experiments\"},{\"id\":\"chipseq_analysis\",\"name\":\"ChIP-seq Complete Analysis\",\"description\":\"Full ChIP-seq analysis from BAM to heatmaps\"},{\"id\":\"rnaseq_coverage\",\"name\":\"RNA-seq Coverage Tracks\",\"description\":\"Generate strand-specific RNA-seq coverage\"},{\"id\":\"atacseq\",\"name\":\"ATAC-seq Analysis\",\"description\":\"ATAC-seq workflow with Tn5 correction\"}]}",
 "experiment": "H3K27ac ChIP-seq in mouse liver, two biological replicates (H3K27ac_rep1.bam, H3K27ac_rep2.bam) and one input (input.bam), single-end 75 bp, aligned to GRCm39 with bowtie2, duplicates not removed yet. I want normalized browser tracks, a check that the ChIP worked, replicate agreement, and heatmaps of signal at promoters (TSS from genes.bed) and at our peaks (H3K27ac_peaks.bed).",
 "context": "Linux server with 16 cores."
}

A trimmed reply (script shortened here with a comment line; the real one is the full pipeline):

{
 "task": "plan",
 "status": "draft",
 "headline": "Index, check enrichment and replicate agreement, then RPGC and log2-over-input tracks and heatmaps at TSS and at your peaks.",
 "assumptions": [
  "BAMs are coordinate-sorted (bowtie2 output sorted by samtools sort).",
  "Duplicates are ignored in every deepTools step with --ignoreDuplicates, since they were not removed.",
  "genes.bed and H3K27ac_peaks.bed use GRCm39 coordinates, like the BAMs."
 ],
 "steps": [
  {
   "n": 1,
   "tool": "samtools index",
   "purpose": "Index the three BAMs.",
   "why": "Every deepTools BAM command needs the .bai index."
  },
  {
   "n": 2,
   "tool": "plotFingerprint",
   "purpose": "Check that the ChIP worked.",
   "why": "The skill runs plotFingerprint before detailed analysis."
  },
  {
   "n": 3,
   "tool": "multiBamSummary",
   "purpose": "Count reads in genome-wide bins for all three BAMs.",
   "why": "Input for plotCorrelation."
  },
  {
   "n": 4,
   "tool": "plotCorrelation",
   "purpose": "Replicate agreement as a Spearman heatmap.",
   "why": "--whatToPlot heatmap is required in deepTools 3.5.6."
  },
  {
   "n": 5,
   "tool": "bamCoverage",
   "purpose": "One RPGC-normalised browser track per replicate.",
   "why": "RPGC needs --effectiveGenomeSize; single-end reads need an explicit --extendReads length."
  },
  {
   "n": 6,
   "tool": "bamCompare",
   "purpose": "log2 ChIP over input per replicate.",
   "why": "The skill's ChIP-vs-input comparison uses readCount scaling; bamCompare has no RPGC."
  },
  {
   "n": 7,
   "tool": "computeMatrix",
   "purpose": "Signal around TSS (genes.bed) and around peak centres (H3K27ac_peaks.bed).",
   "why": "reference-point mode, 3 kb either side."
  },
  {
   "n": 8,
   "tool": "plotHeatmap",
   "purpose": "Heatmaps at promoters and at peaks.",
   "why": "What the description asks for."
  },
  {
   "n": 9,
   "tool": "plotProfile",
   "purpose": "Average profile at peaks.",
   "why": "A compact summary of the peak heatmap."
  }
 ],
 "normalization": {
  "method": "mixed",
  "why": "RPGC for the per-replicate bamCoverage tracks (GRCm39 non-N size, since no MAPQ filter is applied); log2 ratio with readCount scaling for bamCompare against the input. The heatmaps use the log2 tracks."
 },
 "script": "#!/bin/bash\nset -euo pipefail\n# H3K27ac ChIP-seq, mouse liver, single-end 75 bp, GRCm39, duplicates not removed\nCHIP1=\"H3K27ac_rep1.bam\"\nCHIP2=\"H3K27ac_rep2.bam\"\nINPUT=\"input.bam\"\nGENES=\"genes.bed\"\nPEAKS=\"H3K27ac_peaks.bed\"\nGENOME_SIZE=2654621783  # GRCm39 non-N value (multimapping reads kept)\nEXTEND=200  # the skill value for single-end reads; set your fragment length if known\nTHREADS=16\nOUT=\"deeptools_out\"\nmkdir -p \"$OUT\"\n\nsamtools index \"$CHIP1\"\nsamtools index \"$CHIP2\"\nsamtools index \"$INPUT\"\n\nplotFingerprint \\\n    --bamfiles \"$CHIP1\" \"$CHIP2\" \"$INPUT\" \\\n    --labels rep1 rep2 input \\\n    --extendReads \"$EXTEND\" \\\n    --ignoreDuplicates \\\n    --plotFile \"$OUT/fingerprint.png\" \\\n    --outQualityMetrics \"$OUT/fingerprint_metrics.txt\" \\\n    --numberOfProcessors \"$THREADS\"\n\nbamCoverage \\\n    --bam \"$CHIP1\" \\\n    --outFileName \"$OUT/H3K27ac_rep1.RPGC.bw\" \\\n    --normalizeUsing RPGC \\\n    --effectiveGenomeSize \"$GENOME_SIZE\" \\\n    --extendReads \"$EXTEND\" \\\n    --ignoreDuplicates \\\n    --binSize 10 \\\n    --numberOfProcessors \"$THREADS\"\n\nbamCompare \\\n    --bamfile1 \"$CHIP1\" \\\n    --bamfile2 \"$INPUT\" \\\n    --outFileName \"$OUT/H3K27ac_rep1.log2input.bw\" \\\n    --scaleFactorsMethod readCount \\\n    --operation log2 \\\n    --extendReads \"$EXTEND\" \\\n    --ignoreDuplicates \\\n    --binSize 10 \\\n    --numberOfProcessors \"$THREADS\"\n\n# ... the same bamCoverage and bamCompare for rep2, then multiBamSummary bins,\n# plotCorrelation --whatToPlot heatmap, computeMatrix reference-point at TSS and at\n# peak centres, plotHeatmap and plotProfile - trimmed in this example\n",
 "qc_checkpoints": [
  {
   "after": "plotFingerprint",
   "look_for": "How far the H3K27ac curves separate from the input curve, before going on."
  },
  {
   "after": "plotCorrelation",
   "look_for": "Whether rep1 and rep2 are more alike than either is to the input."
  }
 ],
 "to_confirm": [
  "The fragment length, if estimated (EXTEND uses the skill's 200 bp for single-end reads).",
  "That genes.bed holds one line per gene with strand, so reference-point TSS uses the right end."
 ]
}

Values the description does not give become shell variables set to TO_FILL and listed in to_confirm; the page's checks treat TO_FILL as unknown rather than wrong.

Input fields

Every field is a string; facts is JSON text.

fieldtyperequiredmeaning
taskstringyes"review" or "plan".
factsstringyesWhat the page established, as JSON text (below).
scriptstringreviewThe bash script as checked. The page sends at most its first lines and characters and records the cut in facts.script_sent.
experimentstringplanThe experiment in prose: assay, organism and build, samples and file names, read layout and length, what you want to see.
contextstringnoNotes: the library prep, the aligner, what the tracks are for, the machine - never a token.
questionstringnoReview lane: a question to answer in the reply.
retry_notestringnoOnly on a reformat retry.

The facts string

Both lanes: deeptools ("3.5.6", the version the checks model and the skill pins); experiment (assay = chip, atac, rnaseq, other or ""; layout = paired, single or ""; assembly, e.g. GRCh38, or ""); genome_sizes (for the chosen assembly: read_lengths 50 to 250 and, from the deepTools 3.5.6 table and from the current deepTools docs, nonN and by_read_length; null without an assembly).

Review lane:

{"deeptools": "3.5.6",
 "experiment": {"assay": "chip|atac|rnaseq|other|", "layout": "paired|single|", "assembly": "GRCh38|..."},
 "genome_sizes": {...} | null,
 "status": "blocked|fix_first|ready",
 "commands": [{"n": 1, "line": 21, "tool": "multiBamSummary bins",
               "parse": "ok or the argparse error", "exits": []}],
 "flags": [{"id": "F1", "source": "tool|page", "severity": "high|medium|low", "code": "parse",
            "command": 2, "text": "...", "quote": "..."}],
 "flags_total": 1,
 "generator": {"workflow": "chipseq_qc", "command_line": "..."} | null,
 "script_sent": {"lines": 64, "lines_cut": 0, "chars_cut": 0}}

commands lists the deepTools commands in order (line is where each starts in the script; exits are the tool's own argument checks that would stop it). A tool flag is what deepTools 3.5.6 does with that command line; a page flag is the page's check, and quote gives its source in the skill or the deepTools docs. generator is set when the script is unedited output of the skill's workflow_generator.py.

Plan lane:

{"deeptools": "3.5.6",
 "experiment": {"assay": "chip", "layout": "single", "assembly": "GRCm39"},
 "genome_sizes": {...} | null,
 "tools": ["bamCoverage", "bamCompare", "..."],
 "workflows": [{"id": "chipseq_qc", "name": "...", "description": "..."}]}

You can build facts yourself, but the page's browser checks are what make a review accountable: the model is told not to re-decide what the parser decided, so a flag list you write by hand is only as good as your own check. Paste the script into the page and use "Download .json" to get the exact facts it sent. For a script you have checked elsewhere, a minimal facts is accepted, and the reply then rests on the script text alone:

{"deeptools":"3.5.6","experiment":{"assay":"chip","layout":"paired","assembly":"GRCh38"},"commands":[],"flags":[]}

The output

One JSON object, serialised as a string at data.output.output. Parse it and check that task is the lane you sent.

review:
{"task": "review", "status": "blocked|fix_first|ready", "headline": "", "experiment_read": "",
 "flag_responses": [{"ref": "F1", "stance": "confirmed|context|disputed", "note": ""}],
 "findings": [{"severity": "high|medium|low", "command": 1, "issue": "", "why": "", "fix": ""}],
 "qc_checkpoints": [{"after": "", "look_for": ""}],
 "corrected_script": "#!/bin/bash ...", "to_confirm": [""]}

plan:
{"task": "plan", "status": "draft", "headline": "", "assumptions": [""],
 "steps": [{"n": 1, "tool": "bamCoverage", "purpose": "", "why": ""}],
 "normalization": {"method": "RPGC|CPM|BPM|RPKM|None|log2 ratio (readCount)|mixed", "why": ""},
 "script": "#!/bin/bash ...", "qc_checkpoints": [], "to_confirm": []}

stance: confirmed (a real problem here), context (true, but acceptable for this experiment, with the reason), disputed (the page's check misread the script). findings[].command is a command number from facts.commands, or null for the whole script. Scripts in a reply start with #!/bin/bash and set -euo pipefail and use only deepTools 3.5.6 commands and options (plus samtools index, mkdir and echo).

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":"deeptools-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. In every language the steps below continue the same script: Go and Java put steps 2 to 7 inside main, in order.

# 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="deeptools-desk"
TOKEN="${SKILLSAFE_TOKEN:-YOUR_TOKEN}"   # from https://deeptools-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":"deeptools-desk"}, can call /me and /estimate; both lanes are 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://deeptools-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 "$BASE/guest" \
  -H "Content-Type: application/json" -d '{"slug":"deeptools-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. hold_credits is a reservation, not the price. min_credits is the least balance that can start a run. What you pay is charged_credits, reported on the finished job, usually far lower. The body is the input object itself, with no {"input": ...} wrapper. /estimate does no input validation, so check the shape yourself: an object whose every value is a string, task equal to review or plan, a facts that is a JSON string parsing to an object, and a non-empty script (review) or experiment (plan). Save one of the worked examples above as body.json to try it.

# body.json is the input object itself - no {"input": ...} wrapper. 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 ("review", "plan"), "task must be review or plan"
assert all(isinstance(v, str) for v in b.values()), "every field is a string"
assert isinstance(json.loads(b["facts"]), dict), "facts is a JSON string of an object"
need = "script" if b["task"] == "review" else "experiment"
assert b.get(need, "").strip(), b["task"] + " needs " + need
'
INPUT=$(cat body.json)

call estimate "$INPUT"
# {"ok":true,"data":{"model":"...","model_alias":"gpt-terra",
#   "markup_bps":...,"hold_credits":...,"min_credits":...,"sponsor_enabled":false}}
# hold_credits is RESERVED, not the price; charged_credits after the run is the cost.

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: parse it (step 7). Send an Idempotency-Key built from the lane, a hash of the input and the attempt number, deeptools-desk:<lane>:<hash>:a<attempt>, so a retried request returns the same job instead of billing a second run. Use one key per distinct input: an edited script, facts, experiment, context or question is a new hash, and replaying an old key with a different body is a 409. Any stable digest of the body works. 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"])')   # review or plan
KEY="deeptools-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":"{\"task\":\"review\",\"status\":\"blocked\",\"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 as /run (use it instead of step 5, not after it with the same key; build the key as step 5 does) 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":"{\"task\":\"review\",\"status\":\"blocked\",\"headline\":\"The"}
# event: done   {"status":"succeeded","charged_credits":...,"truncated":false}

7. Parse the reply, and re-check the script

The reply is one JSON object serialised as a string (text from step 5, or streamed from step 6). Parse it and check that task is the lane you asked for. Then check the script it returns (corrected_script for a review, script for a plan) before you run it: paste it into the page, which parses it with the same deepTools 3.5.6 checks, and resolve every TO_FILL it holds.

# The reply is a JSON string inside data.output.output (saved as reply.json in step 5):
python3 -c '
import json
r = json.load(open("reply.json"))
print(r["task"], r["status"], r["headline"])
if r["task"] == "review":
    for f in r["flag_responses"]: print(f["ref"], f["stance"], f["note"])
    for f in r["findings"]: print("-", f["severity"], f["command"], f["issue"], "->", f["fix"])
    if r["corrected_script"]: open("corrected.sh", "w").write(r["corrected_script"])
else:
    for s in r["steps"]: print(s["n"], s["tool"], s["purpose"])
    open("plan.sh", "w").write(r["script"])
'
# Shell syntax only; the deepTools options are checked by pasting the script into the page.
[ -f corrected.sh ] && bash -n corrected.sh
[ -f plan.sh ] && bash -n plan.sh && grep -n TO_FILL plan.sh

Costs

Invariants worth asserting