Run the C# best-practices reviewer from your own tools
Send C# source — a controller, a service, a worker, a library type, a test class
— and get back one JSON object: which shape the code belongs to and why, a
ship-it / tidy-first / rework verdict, the findings
with a severity, a rule group, a location, a verbatim evidence line from your own
source, the consequence in this code, the fix and a before/after excerpt, an
adjudication of every deterministic finding your own scanner sent in, the namespace and
project layout read, and the tests worth having. Everything this app does goes through the
SkillSafe App API — plain JSON over HTTPS — so you can run it across a
solution's projects in bulk, or post its findings onto a pull request. Be careful about
making it a blocking gate: every call bills credits and takes tens of seconds, and
the verdict is a model's judgement rather than a deterministic check, so the same code can
come back tidy-first one day and rework the next. For something
that must pass or fail reproducibly on every commit, use Roslyn analyzers, StyleCop, the
.NET analyzers built into the SDK or SonarAnalyzer.CSharp; use this where a human reviewer
would otherwise have to read the diff.
Every code step below is shown in cURL, Python, JavaScript, Go, Java, Ruby, PHP and C#;
pick a language once and the whole page follows.
Basics
Base URL: https://api.skillsafe.ai/v1/app-api, app slug
dotnet-clinic. Every request sends
Authorization: Bearer <token> and JSON bodies with
Content-Type: application/json. Responses are wrapped in an envelope:
{"data": …} on success, {"error": {"code", "message"}} on
failure. The review is produced by the gpt-terra model at a
10% publisher markup. Estimates are free; runs are metered against your
credit balance. There is a single run task — source in, one review out — plus
a reviews collection holding your saved runs.
| Status | Meaning |
|---|---|
400 | Malformed body — usually a where filter written as a bare value instead of an operator object. |
401 | Missing or expired token — create a new session. |
402 | Not enough credits — top up at skillsafe.ai/account/billing. |
403 | The token isn't allowed to do this (e.g. a guest submitting a very large paste). |
404 | Unknown job or record id. |
429 | Rate limited — back off and retry. |
5xx | Transient platform error — retry with backoff, reusing the same idempotency key. |
Browsers enforce CORS for this API, so run these examples from a server, script or terminal — not from another website's frontend.
Step 0 — A tiny client
Every task below is a single HTTP call, so start with a short helper that adds the auth
header, sends JSON and unwraps the data envelope. The later steps reuse it.
export API="https://api.skillsafe.ai/v1/app-api"
export TOKEN="YOUR_TOKEN" # see step 1
# every call looks like:
# curl -s "$API/..." -H "Authorization: Bearer $TOKEN" [-d '{json}']
# jq is used below to pull fields out of the {"data": ...} envelope
import json, requests
API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # see step 1 — read it from your shell environment in real code
def api(method, path, body=None, **headers):
res = requests.request(method, API + path, json=body,
headers={"Authorization": f"Bearer {TOKEN}", **headers})
payload = res.json()
if not res.ok:
raise RuntimeError(payload.get("error", {}).get("message", res.reason))
return payload["data"]
// Node 18+ (built-in fetch)
const API = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN"; // see step 1 — read it from your shell environment in real code
async function api(method, path, body, extraHeaders = {}) {
const res = await fetch(API + path, {
method,
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json", ...extraHeaders },
body: body === undefined ? undefined : JSON.stringify(body),
});
const json = await res.json();
if (!res.ok) throw new Error(json.error?.message ?? res.statusText);
return json.data;
}
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
const API = "https://api.skillsafe.ai/v1/app-api"
var token = os.Getenv("SKILLSAFE_TOKEN") // see step 1
func call(method, path string, body, out any) error {
var buf bytes.Buffer
if body != nil {
json.NewEncoder(&buf).Encode(body)
}
req, _ := http.NewRequest(method, API+path, &buf)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
var env struct {
Data json.RawMessage `json:"data"`
Error *struct{ Message string `json:"message"` } `json:"error"`
}
json.NewDecoder(res.Body).Decode(&env)
if res.StatusCode >= 400 {
return fmt.Errorf("api %s %s: %s", method, path, env.Error.Message)
}
if out == nil {
return nil
}
return json.Unmarshal(env.Data, out)
}
// Java 17+, no dependencies. Pair with your JSON library (Jackson, Gson…)
// to read fields out of the returned envelope.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
class Api {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final String TOKEN = System.getenv("SKILLSAFE_TOKEN"); // see step 1
static final HttpClient HTTP = HttpClient.newHttpClient();
static String call(String method, String path, String jsonBody) throws Exception {
var body = jsonBody == null
? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(jsonBody);
var req = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.method(method, body)
.build();
var res = HTTP.send(req, HttpResponse.BodyHandlers.ofString());
if (res.statusCode() >= 400) throw new RuntimeException(res.body());
return res.body(); // {"data": ...}
}
}
require "json"
require "net/http"
require "uri"
API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = ENV.fetch("SKILLSAFE_TOKEN") # see step 1
def api(method, path, body = nil, extra = {})
uri = URI(API + path)
klass = { "GET" => Net::HTTP::Get, "POST" => Net::HTTP::Post,
"DELETE" => Net::HTTP::Delete }.fetch(method)
req = klass.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
extra.each { |k, v| req[k] = v }
req.body = JSON.dump(body) if body
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
payload = JSON.parse(res.body)
raise payload.dig("error", "message").to_s unless res.is_a?(Net::HTTPSuccess)
payload["data"]
end
<?php
const API = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = getenv("SKILLSAFE_TOKEN"); // see step 1
function api(string $method, string $path, ?array $body = null, array $extra = []): array {
global $TOKEN;
$headers = ["Authorization: Bearer $TOKEN", "Content-Type: application/json"];
foreach ($extra as $k => $v) { $headers[] = "$k: $v"; }
$ch = curl_init(API . $path);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
]);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$payload = json_decode($raw, true);
if ($status >= 400) { throw new RuntimeException($payload["error"]["message"] ?? "request failed"); }
return $payload["data"];
}
// .NET 6+
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
static class Api {
const string Base = "https://api.skillsafe.ai/v1/app-api";
static readonly string Token = Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN")!; // step 1
static readonly HttpClient Http = new();
public static async Task<JsonElement> Call(HttpMethod method, string path, object? body = null,
(string, string)? extraHeader = null) {
var req = new HttpRequestMessage(method, Base + path);
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token);
if (extraHeader is var (hk, hv) && hk is not null) req.Headers.Add(hk, hv);
if (body is not null)
req.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
var res = await Http.SendAsync(req);
var json = JsonDocument.Parse(await res.Content.ReadAsStringAsync()).RootElement;
if (!res.IsSuccessStatusCode)
throw new Exception(json.GetProperty("error").GetProperty("message").GetString());
return json.GetProperty("data");
}
}
Step 1 — Get a token
Two kinds. A guest token needs no account and is enough for
/me and the free /estimate. A personal token
bills metered runs to your own balance — get one from
the token page, which shows the token this browser already
holds, lets you sign in for a personal one, and copies a ready-made
export SKILLSAFE_TOKEN="…" line. You never need the DevTools console.
# A scripted guest token — no browser, no account. Good for /me and /estimate.
# The slug goes in the JSON body, not in a header.
curl -s -X POST "$API/guest" \
-H "Content-Type: application/json" \
-d '{"slug":"dotnet-clinic"}' | jq -r .data.token
# For a personal token (metered runs bill your account), open
# https://dotnet-clinic.skillsafe.ai/tokens.html
# sign in, and press "Copy shell export".
# A scripted guest token — no browser, no account. Good for /me and /estimate.
# The slug goes in the JSON body, not in a header.
guest = requests.post(API + "/guest", json={"slug": "dotnet-clinic"}).json()["data"]
TOKEN = guest["token"] # aut_...
guest_id = guest["guest_id"] # gst_... — keep it if you later migrate the wallet on sign-in
# For a personal token (metered runs bill your account), open
# https://dotnet-clinic.skillsafe.ai/tokens.html
# sign in, and press "Copy shell export".
// A scripted guest token — no browser, no account. Good for /me and /estimate.
// The slug goes in the JSON body, not in a header.
const guest = await (await fetch(API + "/guest", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ slug: "dotnet-clinic" }),
})).json();
const token = guest.data.token;
// For a personal token (metered runs bill your account), open
// https://dotnet-clinic.skillsafe.ai/tokens.html
// sign in, and press "Copy shell export".
// A scripted guest token — no browser, no account. Good for /me and /estimate.
// The slug goes in the JSON body, not in a header.
guestBody, _ := json.Marshal(map[string]string{"slug": "dotnet-clinic"})
req, _ := http.NewRequest("POST", API+"/guest", bytes.NewReader(guestBody))
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
var env struct {
Data struct {
Token string `json:"token"`
} `json:"data"`
}
json.NewDecoder(res.Body).Decode(&env)
token = env.Data.Token
// For a personal token, open https://dotnet-clinic.skillsafe.ai/tokens.html
// A scripted guest token — no browser, no account. Good for /me and /estimate.
// The slug goes in the JSON body, not in a header.
var req = HttpRequest.newBuilder(URI.create(Api.BASE + "/guest"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{\"slug\":\"dotnet-clinic\"}"))
.build();
var res = Api.HTTP.send(req, HttpResponse.BodyHandlers.ofString());
// res.body() is {"data":{"token":"aut_...","guest_id":"gst_...","expires_at":"..."}}
// — read data.token with your JSON library.
// For a personal token, open https://dotnet-clinic.skillsafe.ai/tokens.html
# A scripted guest token — no browser, no account. Good for /me and /estimate.
# The slug goes in the JSON body, not in a header.
uri = URI(API + "/guest")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req.body = JSON.dump({ "slug" => "dotnet-clinic" })
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
token = JSON.parse(res.body).dig("data", "token")
# For a personal token, open https://dotnet-clinic.skillsafe.ai/tokens.html
<?php
// A scripted guest token — no browser, no account. Good for /me and /estimate.
// The slug goes in the JSON body, not in a header.
$ch = curl_init(API . "/guest");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode(["slug" => "dotnet-clinic"]),
]);
$guest = json_decode(curl_exec($ch), true);
curl_close($ch);
$TOKEN = $guest["data"]["token"];
// For a personal token, open https://dotnet-clinic.skillsafe.ai/tokens.html
// A scripted guest token — no browser, no account. Good for /me and /estimate.
// The slug goes in the JSON body, not in a header.
var guestReq = new HttpRequestMessage(HttpMethod.Post, "https://api.skillsafe.ai/v1/app-api/guest") {
Content = new StringContent("{\"slug\":\"dotnet-clinic\"}", Encoding.UTF8, "application/json"),
};
var guestRes = await new HttpClient().SendAsync(guestReq);
var guest = JsonDocument.Parse(await guestRes.Content.ReadAsStringAsync()).RootElement;
var token = guest.GetProperty("data").GetProperty("token").GetString();
// For a personal token, open https://dotnet-clinic.skillsafe.ai/tokens.html
Treat the token like a password: anyone holding it can spend its credits through this app. Keep it in your shell environment rather than in source control.
Step 2 — Check the session and the balance
GET /me is free and tells you whether the token is a guest or a real user, and
how many credits it can spend. The app calls this before enabling its run button, and so
should you — a 402 after submitting is avoidable.
curl -s "$API/me" -H "Authorization: Bearer $TOKEN" | jq .data
# { "subject_type": "user", "subject_id": "...", "credits": 184220 }
me = api("GET", "/me")
print(me["subject_type"], me["credits"])
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
var me struct {
SubjectType string `json:"subject_type"`
Credits int64 `json:"credits"`
}
if err := call("GET", "/me", nil, &me); err != nil {
panic(err)
}
fmt.Println(me.SubjectType, me.Credits)
String me = Api.call("GET", "/me", null);
System.out.println(me); // {"data":{"subject_type":"user","credits":184220}}
me = api("GET", "/me")
puts me["subject_type"], me["credits"]
<?php
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"], "\n";
var me = await Api.Call(HttpMethod.Get, "/me");
Console.WriteLine($"{me.GetProperty("subject_type")} {me.GetProperty("credits")}");
Step 3 — Estimate (free, no job created)
POST /estimate takes the exact body you would send to /run and
returns the model binding and the price envelope without creating a job or charging
anything. hold_credits is a worst-case reservation, priced against the
full output cap; the settled charged_credits is usually much lower. If the
balance sits between min_credits and hold_credits, the run still
executes with a reduced cap and comes back "truncated": true.
Input fields
| Field | Type | Meaning |
|---|---|---|
source | string | Required. The C# source. Several files are allowed, separated by a line reading // File: Name.cs. The app clips anything over 28,000 characters on whole-line boundaries, dropping the middle and keeping the head and the tail, with a // ---- N lines omitted ---- marker where the cut happened. |
shape | string | aspnet, console, library or tests — the shape to review against. Several conventions invert between them — Console.WriteLine is the output of a console app and a defect in a service — so this is not cosmetic. |
shape_evidence | string | Why that shape: the weighted markers the client scan matched, or that the user chose it explicitly. |
focus | string | all, or one rule group: async, exceptions, di, disposal, logging, security, naming, testing. |
context | string | What the code does, what the caller already knows, what they are worried about. This is what turns a rule violation into a judgement — a documented boundary handler gets set aside rather than repeated. |
prescan | object | Structural facts the client measured: files, types, methods, code_lines, aspnet_score, console_score, tests_score, source_clipped. |
prescan_findings | array | {"id": "A2-1", "rule": "A2", "severity": "high", "line": 31, "title": "…", "evidence": "…"} entries from your own deterministic scan. rule is one of the 32 rule codes — N1 N2 N3 N4 N5 D1 A1 A2 A3 A4 E1 E2 E3 NU1 DI1 DI2 DS1 DS2 L1 L2 L3 SEC1 SEC2 CF1 SM1 SM2 SM3 SM4 SM5 SM6 T1 T2. The review must return exactly one prescan_check entry per id. Send [] if you have no scanner of your own. |
retry_note | string | Optional. Sent only on the app's one reformat retry. |
curl -s -X POST "$API/estimate" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"source":"public class orderService { public Order Get(int id) { return _repo.FindAsync(id).Result; } }","shape":"aspnet","focus":"all","prescan_findings":[]}' | jq .data
# { "model": "gpt-5.6-terra", "model_alias": "gpt-terra", "markup_bps": 1000,
# "hold_credits": 3140, "min_credits": 420, "sponsor_enabled": false }
SOURCE = open("OrderService.cs").read()
payload = {
"source": SOURCE,
"shape": "aspnet",
"shape_evidence": "[ApiController], app.MapPost and Microsoft.AspNetCore usings.",
"focus": "all",
"context": "Orders endpoint, ~400 req/s. Worried about the error handling.",
"prescan": {"files": 1, "types": 1, "methods": 4, "code_lines": 67,
"aspnet_score": 9, "console_score": 0, "tests_score": 0,
"source_clipped": ""},
"prescan_findings": [
{"id": "A2-1", "rule": "A2", "severity": "high", "line": 31,
"title": "Blocking on a task (.Result / .Wait / GetAwaiter().GetResult)",
"evidence": "var order = _repo.FindAsync(id).Result;"},
],
}
est = api("POST", "/estimate", payload)
print(est["model"], est["model_alias"], est["hold_credits"], est["min_credits"])
const source = await fs.promises.readFile("OrderService.cs", "utf8");
const payload = {
source,
shape: "aspnet",
shape_evidence: "[ApiController], app.MapPost and Microsoft.AspNetCore usings.",
focus: "all",
context: "Orders endpoint, ~400 req/s. Worried about the error handling.",
prescan: { files: 1, types: 1, methods: 4, code_lines: 67,
aspnet_score: 9, console_score: 0, tests_score: 0, source_clipped: "" },
prescan_findings: [
{ id: "A2-1", rule: "A2", severity: "high", line: 31,
title: "Blocking on a task (.Result / .Wait / GetAwaiter().GetResult)",
evidence: "var order = _repo.FindAsync(id).Result;" },
],
};
const est = await api("POST", "/estimate", payload);
console.log(est.model, est.hold_credits, est.min_credits);
source, err := os.ReadFile("OrderService.cs")
if err != nil {
panic(err)
}
payload := map[string]any{
"source": string(source),
"shape": "aspnet",
"shape_evidence": "[ApiController], app.MapPost and Microsoft.AspNetCore usings.",
"focus": "all",
"context": "Orders endpoint, ~400 req/s.",
"prescan_findings": []any{},
}
var est struct {
Model string `json:"model"`
HoldCredits int64 `json:"hold_credits"`
MinCredits int64 `json:"min_credits"`
}
if err := call("POST", "/estimate", payload, &est); err != nil {
panic(err)
}
fmt.Println(est.Model, est.HoldCredits, est.MinCredits)
String source = Files.readString(Path.of("OrderService.cs"));
String payload = Json.object(
"source", source,
"shape", "aspnet",
"shape_evidence", "[ApiController], app.MapPost and Microsoft.AspNetCore usings.",
"focus", "all",
"context", "Orders endpoint, ~400 req/s.",
"prescan_findings", List.of());
String est = Api.call("POST", "/estimate", payload);
System.out.println(est); // model, model_alias, markup_bps, hold_credits, min_credits
source = File.read("OrderService.cs")
payload = {
"source" => source,
"shape" => "aspnet",
"shape_evidence" => "[ApiController], app.MapPost and Microsoft.AspNetCore usings.",
"focus" => "all",
"context" => "Orders endpoint, ~400 req/s.",
"prescan_findings" => [],
}
est = api("POST", "/estimate", payload)
puts est["model"], est["hold_credits"], est["min_credits"]
<?php
$source = file_get_contents("OrderService.cs");
$payload = [
"source" => $source,
"shape" => "aspnet",
"shape_evidence" => "[ApiController], app.MapPost and Microsoft.AspNetCore usings.",
"focus" => "all",
"context" => "Orders endpoint, ~400 req/s.",
"prescan_findings" => [],
];
$est = api("POST", "/estimate", $payload);
echo $est["model"], " ", $est["hold_credits"], " ", $est["min_credits"], "\n";
var source = await File.ReadAllTextAsync("OrderService.cs");
var payload = new {
source,
shape = "aspnet",
shape_evidence = "[ApiController], app.MapPost and Microsoft.AspNetCore usings.",
focus = "all",
context = "Orders endpoint, ~400 req/s.",
prescan_findings = Array.Empty<object>(),
};
var est = await Api.Call(HttpMethod.Post, "/estimate", payload);
Console.WriteLine($"{est.GetProperty("model")} {est.GetProperty("hold_credits")}");
The three assertions worth making in a test: model is
gpt-5.6-terra, model_alias is gpt-terra, and
markup_bps is 1000. Together they prove the app is bound to the
right model at the right markup, and they cost nothing to check.
Step 4 — Run and poll
POST /run creates a job; GET /jobs/{id} polls it to a terminal
state. Always send an Idempotency-Key, and build it from three parts: a hash of
the input, a value that is fresh for each run you intend to make, and an attempt counter.
Re-POSTing with the same key returns the original job instead of billing a second one —
that is what makes a network retry safe. It is also why the fresh-per-run part matters: a key
built from the input alone means a deliberate second review of unchanged code silently
replays the first answer instead of running, and there is no way to ask for a new one. Reuse
the key to retry; change it to re-run. The completed job's output.output is the
review as a JSON string.
# The Idempotency-Key makes a retried POST return the original job instead of
# billing a second one. Content hash + a per-run nonce + an attempt counter:
# reuse the whole key to RETRY, mint a new nonce to RE-RUN unchanged input.
HASH=$(printf %s "$SOURCE" | shasum -a 256 | cut -c1-16)
NONCE=$(head -c 8 /dev/urandom | od -An -tx1 | tr -d ' \n')
KEY="dotnet-clinic:$HASH:$NONCE:a1"
JOB=$(curl -s -X POST "$API/run" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-d @payload.json | jq -r .data.job_id)
# Poll until terminal.
while :; do
J=$(curl -s "$API/jobs/$JOB" -H "Authorization: Bearer $TOKEN")
S=$(echo "$J" | jq -r .data.status)
[ "$S" = "succeeded" ] || [ "$S" = "failed" ] && break
sleep 2
done
echo "$J" | jq -r .data.output.output | jq . # the review, as JSON
import hashlib, secrets, time
# content hash + per-run nonce + attempt: reuse to retry, re-mint to re-run.
digest = hashlib.sha256(payload["source"].encode()).hexdigest()[:16]
nonce = secrets.token_hex(8)
key = f"dotnet-clinic:{digest}:{nonce}:a1"
job = api("POST", "/run", payload, **{"Idempotency-Key": key})
while True:
j = api("GET", f"/jobs/{job['job_id']}")
if j["status"] in ("succeeded", "failed"):
break
time.sleep(2)
review = json.loads(j["output"]["output"])
print(review["verdict"], len(review["findings"]), "findings")
import { createHash, randomBytes } from "node:crypto";
// content hash + per-run nonce + attempt: reuse to retry, re-mint to re-run.
const digest = createHash("sha256").update(payload.source).digest("hex").slice(0, 16);
const nonce = randomBytes(8).toString("hex");
const key = `dotnet-clinic:${digest}:${nonce}:a1`;
const job = await api("POST", "/run", payload, { "Idempotency-Key": key });
let j;
do {
await new Promise((r) => setTimeout(r, 2000));
j = await api("GET", `/jobs/${job.job_id}`);
} while (j.status !== "succeeded" && j.status !== "failed");
const review = JSON.parse(j.output.output);
console.log(review.verdict, review.findings.length, "findings");
import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"time"
)
// content hash + per-run nonce + attempt: reuse to retry, re-mint to re-run.
sum := sha256.Sum256([]byte(source))
nonceBytes := make([]byte, 8)
rand.Read(nonceBytes)
key := "dotnet-clinic:" + hex.EncodeToString(sum[:])[:16] + ":" + hex.EncodeToString(nonceBytes) + ":a1"
// call() with an extra header — add req.Header.Set("Idempotency-Key", key) there.
var job struct {
JobID string `json:"job_id"`
}
if err := call("POST", "/run", payload, &job); err != nil {
panic(err)
}
var j struct {
Status string `json:"status"`
Output struct {
Output string `json:"output"`
} `json:"output"`
}
for {
if err := call("GET", "/jobs/"+job.JobID, nil, &j); err != nil {
panic(err)
}
if j.Status == "succeeded" || j.Status == "failed" {
break
}
time.Sleep(2 * time.Second)
}
fmt.Println(j.Output.Output) // the review, as a JSON string
// Add the header inside Api.call(), or build the request inline:
var runReq = HttpRequest.newBuilder(URI.create(Api.BASE + "/run"))
.header("Authorization", "Bearer " + Api.TOKEN)
.header("Content-Type", "application/json")
// content hash + per-run nonce + attempt: reuse to retry, re-mint to re-run.
.header("Idempotency-Key", "dotnet-clinic:" + Integer.toHexString(source.hashCode())
+ ":" + java.util.UUID.randomUUID().toString().substring(0, 16) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
var runRes = Api.HTTP.send(runReq, HttpResponse.BodyHandlers.ofString());
// read data.job_id, then poll GET /jobs/{job_id} until status is succeeded or failed.
String job = Api.call("GET", "/jobs/" + jobId, null);
System.out.println(job);
require "digest"
require "securerandom"
# content hash + per-run nonce + attempt: reuse to retry, re-mint to re-run.
key = "dotnet-clinic:#{Digest::SHA256.hexdigest(payload["source"])[0, 16]}:#{SecureRandom.hex(8)}:a1"
job = api("POST", "/run", payload, { "Idempotency-Key" => key })
loop do
@j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed].include?(@j["status"])
sleep 2
end
review = JSON.parse(@j["output"]["output"])
puts review["verdict"], review["findings"].length
<?php
// content hash + per-run nonce + attempt: reuse to retry, re-mint to re-run.
$key = "dotnet-clinic:" . substr(hash("sha256", $payload["source"]), 0, 16)
. ":" . bin2hex(random_bytes(8)) . ":a1";
$job = api("POST", "/run", $payload, ["Idempotency-Key" => $key]);
do {
sleep(2);
$j = api("GET", "/jobs/" . $job["job_id"]);
} while (!in_array($j["status"], ["succeeded", "failed"], true));
$review = json_decode($j["output"]["output"], true);
echo $review["verdict"], " ", count($review["findings"]), " findings\n";
using System.Security.Cryptography;
// content hash + per-run nonce + attempt: reuse to retry, re-mint to re-run.
var hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(source)))[..16].ToLower();
var nonce = Convert.ToHexString(RandomNumberGenerator.GetBytes(8)).ToLower();
var job = await Api.Call(HttpMethod.Post, "/run", payload, ("Idempotency-Key", $"dotnet-clinic:{hash}:{nonce}:a1"));
var jobId = job.GetProperty("job_id").GetString();
JsonElement j;
do {
await Task.Delay(2000);
j = await Api.Call(HttpMethod.Get, $"/jobs/{jobId}");
} while (j.GetProperty("status").GetString() is not ("succeeded" or "failed"));
var review = JsonDocument.Parse(j.GetProperty("output").GetProperty("output").GetString()!).RootElement;
Console.WriteLine(review.GetProperty("verdict"));
Step 5 — Stream instead (SSE)
POST /run-stream is the same call with
Accept: text/event-stream. It emits an event: job frame, then a
sequence of event: delta frames whose data.text carries fragments
of the JSON in order, then event: done with the settled charge (or
event: error). The frame name is on the event: line — the
data: payload carries no type field of its own, so track the current event
name as you read. This is what the app itself uses, and it is what lets a progress
UI advance on real signals — the arrival of "verdict" or
"findings" in the stream — rather than on a timer. Concatenate every
delta and parse the whole thing once at the end.
curl -N -s -X POST "$API/run-stream" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-H "Accept: text/event-stream" -H "Idempotency-Key: $KEY" \
-d @payload.json
# Standard SSE frames, separated by a blank line. The frame NAME is on the
# `event:` line; the payload is on the `data:` line and carries no type field of
# its own, so you must track the current event name as you read:
#
# event: job
# data: {"job_id":"job_..."}
#
# event: delta
# data: {"text":"{\"review_title\":\"..."}
#
# event: done
# data: {"status":"succeeded","charged_credits":812,"output":{"output":"..."}}
#
# Concatenate every delta's `text` in order and parse the result as one JSON
# object. An `event: error` frame carries a failure instead.
with requests.post(API + "/run-stream", json=payload, stream=True,
headers={"Authorization": f"Bearer {TOKEN}",
"Accept": "text/event-stream",
"Idempotency-Key": key}) as res:
raw, event, done = "", "message", None
for line in res.iter_lines(decode_unicode=True):
if line is None:
continue
if line == "": # blank line ends a frame
event = "message"
continue
if line.startswith("event:"):
event = line[6:].strip()
elif line.startswith("data:"):
data = json.loads(line[5:].strip())
if event == "delta":
raw += data.get("text", "")
elif event == "done":
done = data
elif event == "error":
raise RuntimeError(data.get("message", "stream failed"))
review = json.loads(raw[raw.index("{"): raw.rindex("}") + 1])
print(review["review_title"], review["verdict"], done["charged_credits"])
const res = await fetch(API + "/run-stream", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
Accept: "text/event-stream",
"Idempotency-Key": key,
},
body: JSON.stringify(payload),
});
let raw = "", buf = "", done = null;
const reader = res.body.getReader();
const dec = new TextDecoder();
for (;;) {
const chunk = await reader.read();
if (chunk.done) break;
buf += dec.decode(chunk.value, { stream: true });
let idx;
while ((idx = buf.indexOf("\n\n")) >= 0) { // frames are blank-line separated
const frame = buf.slice(0, idx);
buf = buf.slice(idx + 2);
let event = "message", dataStr = "";
for (const line of frame.split("\n")) {
if (line.startsWith("event:")) event = line.slice(6).trim();
else if (line.startsWith("data:")) dataStr += line.slice(5).trim();
}
if (!dataStr) continue;
const data = JSON.parse(dataStr);
if (event === "delta") raw += data.text ?? "";
else if (event === "done") done = data;
else if (event === "error") throw new Error(data.message ?? "stream failed");
}
}
const review = JSON.parse(raw.slice(raw.indexOf("{"), raw.lastIndexOf("}") + 1));
console.log(review.review_title, review.verdict, done?.charged_credits);
import (
"bufio"
"strings"
)
body, _ := json.Marshal(payload)
req, _ = http.NewRequest("POST", API+"/run-stream", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "text/event-stream")
req.Header.Set("Idempotency-Key", key)
stream, _ := http.DefaultClient.Do(req)
defer stream.Body.Close()
var raw bytes.Buffer
event := "message"
sc := bufio.NewScanner(stream.Body)
sc.Buffer(make([]byte, 0, 64*1024), 1024*1024)
for sc.Scan() {
line := sc.Text()
switch {
case line == "":
event = "message" // blank line ends the frame
case strings.HasPrefix(line, "event:"):
event = strings.TrimSpace(line[6:])
case strings.HasPrefix(line, "data:"):
var d struct {
Text string `json:"text"`
}
json.Unmarshal([]byte(line[5:]), &d)
if event == "delta" {
raw.WriteString(d.Text)
}
}
}
fmt.Println(raw.String()) // the review, as one JSON document
var streamReq = HttpRequest.newBuilder(URI.create(Api.BASE + "/run-stream"))
.header("Authorization", "Bearer " + Api.TOKEN)
.header("Content-Type", "application/json")
.header("Accept", "text/event-stream")
.header("Idempotency-Key", key)
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
var raw = new StringBuilder();
var event = new String[]{"message"};
Api.HTTP.send(streamReq, HttpResponse.BodyHandlers.ofLines()).body().forEach(line -> {
if (line.isEmpty()) {
event[0] = "message"; // blank line ends the frame
} else if (line.startsWith("event:")) {
event[0] = line.substring(6).trim();
} else if (line.startsWith("data:") && event[0].equals("delta")) {
// parse {"text":"..."} with your JSON library, then append the text
raw.append(line.substring(5).trim());
}
});
System.out.println(raw);
uri = URI(API + "/run-stream")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Accept"] = "text/event-stream"
req["Idempotency-Key"] = key
req.body = JSON.dump(payload)
raw = +""
buf = +""
event = "message"
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(req) do |res|
res.read_body do |chunk|
buf << chunk
while (i = buf.index("\n\n"))
frame = buf.slice!(0, i + 2)
frame.each_line do |line|
line = line.chomp
if line.start_with?("event:")
event = line[6..].strip
elsif line.start_with?("data:") && event == "delta"
raw << (JSON.parse(line[5..].strip)["text"] || "")
end
end
event = "message" # the frame ended
end
end
end
end
review = JSON.parse(raw[raw.index("{")..raw.rindex("}")])
puts review["review_title"]
<?php
$raw = "";
$buf = "";
$event = "message";
$ch = curl_init(API . "/run-stream");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer $TOKEN",
"Content-Type: application/json",
"Accept: text/event-stream",
"Idempotency-Key: $key",
],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$raw, &$buf, &$event) {
$buf .= $chunk;
while (($i = strpos($buf, "\n\n")) !== false) {
$frame = substr($buf, 0, $i);
$buf = substr($buf, $i + 2);
foreach (explode("\n", $frame) as $line) {
if (str_starts_with($line, "event:")) {
$event = trim(substr($line, 6));
} elseif (str_starts_with($line, "data:") && $event === "delta") {
$d = json_decode(substr($line, 5), true);
$raw .= $d["text"] ?? "";
}
}
$event = "message"; // the frame ended
}
return strlen($chunk);
},
]);
curl_exec($ch);
curl_close($ch);
$start = strpos($raw, "{");
$review = json_decode(substr($raw, $start, strrpos($raw, "}") - $start + 1), true);
echo $review["review_title"], "\n";
var streamReq = new HttpRequestMessage(HttpMethod.Post, "https://api.skillsafe.ai/v1/app-api/run-stream");
streamReq.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
streamReq.Headers.Add("Accept", "text/event-stream");
streamReq.Headers.Add("Idempotency-Key", key);
streamReq.Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
var streamRes = await new HttpClient().SendAsync(streamReq, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await streamRes.Content.ReadAsStreamAsync());
var raw = new StringBuilder();
var evt = "message";
while (await reader.ReadLineAsync() is { } line) {
if (line.Length == 0) { evt = "message"; continue; } // blank line ends the frame
if (line.StartsWith("event:")) { evt = line[6..].Trim(); continue; }
if (!line.StartsWith("data:")) continue;
var data = JsonDocument.Parse(line[5..]).RootElement;
if (evt == "delta") raw.Append(data.GetProperty("text").GetString());
}
Console.WriteLine(raw.ToString());
If the stream dies mid-review you still hold every delta received so far. Slicing from the
first { and appending the closing brackets recovers a partial review often enough
to be worth trying before you show an error.
Step 6 — Your saved reviews
Every run the app completes is written to the reviews collection
(acl_read: owner, acl_write: user), so a review follows the user
across devices. Six fields are declared and therefore filterable and orderable:
title, shape, verdict,
finding_count, high_count and ran_at. The rest of the
document — the whole review and the (trimmed) source it was run against —
round-trips intact but is not indexed.
| Operator | Use |
|---|---|
eq, ne | Exact match, e.g. {"verdict": {"eq": "rework"}}. |
lt, lte, gt, gte | Ranges over numbers and timestamps. |
in | Up to 20 values. |
contains | Substring match on a string field. |
# List your saved reviews, newest first.
curl -s -X POST "$API/collections/reviews/query" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"order_by":[{"field":"ran_at","dir":"desc"}],"limit":10}' | jq '.data.records[].doc.title'
# Only the ones that came back rework with something genuinely serious in them.
curl -s -X POST "$API/collections/reviews/query" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"where":{"verdict":{"eq":"rework"},"high_count":{"gte":2}},
"order_by":[{"field":"ran_at","dir":"desc"}],"limit":20}' | jq .data
# Every `where` entry must be an operator object — the bare-value shorthand
# ({"verdict": "rework"}) is rejected.
recent = api("POST", "/collections/reviews/query", {
"where": {"verdict": {"eq": "rework"}, "high_count": {"gte": 2}},
"order_by": [{"field": "ran_at", "dir": "desc"}],
"limit": 20,
})
for rec in recent["records"]:
d = rec["doc"]
print(d["ran_at"], d["title"], d["finding_count"], "findings", d["high_count"], "high")
const recent = await api("POST", "/collections/reviews/query", {
where: { verdict: { eq: "rework" }, high_count: { gte: 2 } },
order_by: [{ field: "ran_at", dir: "desc" }],
limit: 20,
});
for (const rec of recent.records) {
const d = rec.doc;
console.log(d.ran_at, d.title, d.finding_count, d.high_count);
}
query := map[string]any{
"where": map[string]any{"verdict": map[string]any{"eq": "rework"}},
"order_by": []map[string]string{{"field": "ran_at", "dir": "desc"}},
"limit": 20,
}
var out struct {
Records []struct {
RecordID string `json:"record_id"`
Doc map[string]any `json:"doc"`
} `json:"records"`
}
if err := call("POST", "/collections/reviews/query", query, &out); err != nil {
panic(err)
}
for _, r := range out.Records {
fmt.Println(r.Doc["ran_at"], r.Doc["title"])
}
String query = """
{"where":{"verdict":{"eq":"rework"}},
"order_by":[{"field":"ran_at","dir":"desc"}],"limit":20}
""";
String reviews = Api.call("POST", "/collections/reviews/query", query);
System.out.println(reviews); // {"data":{"records":[{"record_id":"...","doc":{...}}]}}
recent = api("POST", "/collections/reviews/query", {
"where" => { "verdict" => { "eq" => "rework" } },
"order_by" => [{ "field" => "ran_at", "dir" => "desc" }],
"limit" => 20,
})
recent["records"].each { |r| puts "#{r["doc"]["ran_at"]} #{r["doc"]["title"]}" }
<?php
$recent = api("POST", "/collections/reviews/query", [
"where" => ["verdict" => ["eq" => "rework"]],
"order_by" => [["field" => "ran_at", "dir" => "desc"]],
"limit" => 20,
]);
foreach ($recent["records"] as $rec) {
echo $rec["doc"]["ran_at"], " ", $rec["doc"]["title"], "\n";
}
var query = new {
where = new { verdict = new { eq = "rework" } },
order_by = new[] { new { field = "ran_at", dir = "desc" } },
limit = 20,
};
var recent = await Api.Call(HttpMethod.Post, "/collections/reviews/query", query);
foreach (var rec in recent.GetProperty("records").EnumerateArray())
Console.WriteLine(rec.GetProperty("doc").GetProperty("title"));
Every where entry must be an operator object. The bare-value shorthand
{"verdict": "rework"} is rejected with
where.verdict must be an object of operators. Note also that each
POST /guest mints a new guest subject, and
acl_read: owner scopes rows to the calling subject — so a script must
reuse one token across create and query or it will see an empty collection.
The output contract
One JSON object. These are the fields the app's own render path parses; anything missing makes the app fall back to showing the raw reply, so treat them as required.
| Field | Type | Meaning |
|---|---|---|
review_title | string | Names what was reviewed. |
shape | string | aspnet, console, library or tests. Anything else is rejected. |
shape_reason | string | One sentence echoing or correcting the request's shape. |
verdict | string | ship-it, tidy-first or rework. Anything else is rejected. |
headline | string | One sentence justifying the verdict. |
summary | string | Two to four sentences a reviewer could paste into the pull request. |
findings | array | May be [] — that is what a clean review looks like — but never omitted. Entries are {id, severity, rule, title, location, evidence, why, fix, patch_before, patch_after}. |
findings[].severity | string | high, medium or low. The app sorts high first. |
findings[].rule | string | One of naming, docs, async, exceptions, di, disposal, logging, nulls, config, security, smells, testing, layout. |
findings[].evidence | string | A verbatim line from the request's source. The app checks each one, whitespace-normalised, and labels anything it cannot locate as unverified rather than showing it as fact. Assert on this if you post-process. |
prescan_check | array | {id, status, note}, exactly one per id in the request's prescan_findings. status is confirmed or set-aside; anything the review never mentions is shown to the user as unadjudicated. |
strengths, next_steps | array<string> | May be empty arrays, but never omitted. A rework verdict with empty next_steps is a contract violation. |
package_layout | object | {assessment, suggested_tree[]} — the namespace and project layout read, one path per tree entry, e.g. Services/OrderService.cs. |
test_plan | array | {target, kind, annotation, why}. kind is unit, integration or endpoint; annotation is something like [Fact] or WebApplicationFactory<Program>, and may be "". |
Two invariants worth asserting on in a pipeline. First, every findings[].evidence
should be locatable in the source you sent — if it is not, the finding is describing
code that is not yours. Second, the set of prescan_check ids should equal the
set of prescan_findings ids you sent; a mismatch means the review either skipped
one of your scanner's results or invented an id. The app renders both conditions rather than
hiding them, and so should you.
Rendering it yourself
The two CSV exports the app ships are pure functions of the payloads, so they are easy to
reproduce server-side. The findings worksheet flattens
findings[] into
id, severity, rule, location, title, why, fix, evidence, status — one row
per finding, with a status column pre-filled as open for whoever
works the queue. The browser-scan export flattens your own
prescan_findings[] into
id, severity, rule, file, line, title, detail, evidence, which is the artifact
to diff across runs: it is deterministic, so a second run on edited source shows exactly
which mechanical findings cleared. Fields containing a comma, a quote or a newline are
quoted and internal quotes doubled, per RFC 4180.