Review a frontend's distinctiveness from your own scripts
Send frontend source — HTML, CSS, JSX or Tailwind markup — with an optional brief,
and get back one JSON object: a distinctive / leaning-generic /
templated verdict, the AI-default cluster the page most resembles, six axis
readings, findings that each quote the verbatim line of your code they rest on, a keep list,
and a revision direction — a subject-driven palette, a display/body type pairing, a
layout concept and one signature element. Everything this app does goes through the
SkillSafe App API — plain JSON over HTTPS — so the review can sit wherever design
ships: a pre-release check on a marketing page, a design-system audit script, or a bot that
reviews the static export of every new template. Pick a language once and the whole page
follows.
Basics
Base URL: https://api.skillsafe.ai/v1/app-api, app slug
slop-lens. 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 itself is produced by the gpt-terra model. Estimates are free;
runs are metered against your credit balance. There is a single run task — one page's
source in, one review out, no follow-up calls and no session state to carry.
| Status | Meaning |
|---|---|
401 | Missing, malformed or expired token — mint a new one (step 1) and retry once. |
402 | Not enough credits to place the hold for this review — top up at skillsafe.ai/account/credits. The estimate's min_credits is the floor below which a run will not start. |
404 | Unknown job id (or a job belonging to another subject). Job ids are only readable by the token that created them. |
422 | The body did not validate — code missing or empty, or a bad enum in surface / ambition. The message names the field. The app itself also refuses source under 80 characters client-side; that is a UI rule, not a server one. |
429 | Too many requests in flight for this subject. Back off and retry — reuse the same Idempotency-Key so a retry cannot start a second, double-charged run. |
5xx | Transient platform error — retry with backoff, again with 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}/{error} envelope. Set
SKILLSAFE_TOKEN in your environment — the token
page can copy a ready-made export line.
export API="https://api.skillsafe.ai/v1/app-api"
export TOKEN="YOUR_TOKEN" # from /tokens.html, or step 1 below
# every call: curl -s "$API/<path>" -H "Authorization: Bearer $TOKEN" \
# -H "Content-Type: application/json" [-d "$BODY"] | jq '.data'
import json, os, requests
API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = os.environ["SKILLSAFE_TOKEN"]
def api(method, path, body=None, **kw):
r = requests.request(method, API + path, json=body,
headers={"Authorization": f"Bearer {TOKEN}"}, timeout=180, **kw)
envelope = r.json()
if "error" in envelope:
raise RuntimeError(f"{envelope['error']['code']}: {envelope['error']['message']}")
return envelope["data"]
// Node 18+ (built-in fetch)
const API = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN"; // read it from your env or secret store
async function api(method, path, body) {
const res = await fetch(API + path, {
method,
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
body: body ? JSON.stringify(body) : undefined,
});
const envelope = await res.json();
if (envelope.error) throw new Error(`${envelope.error.code}: ${envelope.error.message}`);
return envelope.data;
}
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
const API = "https://api.skillsafe.ai/v1/app-api"
func api(method, path string, body any, out any) error {
var buf bytes.Buffer
if body != nil {
if err := json.NewEncoder(&buf).Encode(body); err != nil {
return err
}
}
req, _ := http.NewRequest(method, API+path, &buf)
req.Header.Set("Authorization", "Bearer "+os.Getenv("SKILLSAFE_TOKEN"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
var envelope struct {
Data json.RawMessage `json:"data"`
Error *struct{ Code, Message string } `json:"error"`
}
if err := json.NewDecoder(res.Body).Decode(&envelope); err != nil {
return err
}
if envelope.Error != nil {
return fmt.Errorf("%s: %s", envelope.Error.Code, envelope.Error.Message)
}
if out != nil {
return json.Unmarshal(envelope.Data, out)
}
return nil
}
// Java 17+, no dependencies. Pair with your JSON library (Jackson, Gson…)
import java.net.URI;
import java.net.http.*;
public class SkillSafe {
static final String API = "https://api.skillsafe.ai/v1/app-api";
static final String TOKEN = System.getenv("SKILLSAFE_TOKEN");
static final HttpClient http = HttpClient.newHttpClient();
static String api(String method, String path, String jsonBody) throws Exception {
var b = HttpRequest.newBuilder(URI.create(API + path))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json");
b = jsonBody == null ? b.GET()
: b.method(method, HttpRequest.BodyPublishers.ofString(jsonBody));
var res = http.send(b.build(), HttpResponse.BodyHandlers.ofString());
if (res.body().contains("\"error\""))
throw new RuntimeException(res.body());
return res.body(); // {"data": …} — unwrap with your JSON library
}
}
require "net/http"
require "json"
API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = ENV.fetch("SKILLSAFE_TOKEN")
def api(method, path, body = nil)
uri = URI(API + path)
req = Net::HTTP.const_get(method.capitalize).new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req.body = body.to_json if body
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, read_timeout: 180) { |h| h.request(req) }
envelope = JSON.parse(res.body)
raise "#{envelope.dig('error', 'code')}: #{envelope.dig('error', 'message')}" if envelope["error"]
envelope["data"]
end
<?php
const API = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = getenv("SKILLSAFE_TOKEN");
function api(string $method, string $path, ?array $body = null) {
global $TOKEN;
$ch = curl_init(API . $path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 180,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer $TOKEN",
"Content-Type: application/json",
],
]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$envelope = json_decode(curl_exec($ch), true);
curl_close($ch);
if (isset($envelope["error"]))
throw new RuntimeException($envelope["error"]["code"] . ": " . $envelope["error"]["message"]);
return $envelope["data"];
}
// .NET 8+
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
static class SkillSafe {
const string Api = "https://api.skillsafe.ai/v1/app-api";
static readonly string Token = Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN")!;
static readonly HttpClient Http = new() { Timeout = TimeSpan.FromMinutes(3) };
public static async Task<JsonElement> ApiAsync(HttpMethod method, string path, object? body = null) {
var req = new HttpRequestMessage(method, Api + path);
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token);
if (body is not null) req.Content = JsonContent.Create(body);
var res = await Http.SendAsync(req);
var envelope = await res.Content.ReadFromJsonAsync<JsonElement>();
if (envelope.TryGetProperty("error", out var err))
throw new Exception(err.GetProperty("code").GetString() + ": " + err.GetProperty("message").GetString());
return envelope.GetProperty("data");
}
}
Step 1 — Get a token
POST /guest
Two kinds of token work here. A personal token is the one the app itself
uses once you sign in — grab it from the token page (never
from DevTools), and metered runs bill your account. A guest token can be
minted by any script with no browser at all: it can call /me and the free
/estimate, and it can run only if the app sponsors usage — Slop Lens does
not, so use a personal token for real reviews.
curl -s -X POST "$API/guest" \
-H "Content-Type: application/json" \
-d '{"slug": "slop-lens"}' | jq -r '.data.token'
token = api("POST", "/guest", {"slug": "slop-lens"})["token"]
const { token } = await api("POST", "/guest", { slug: "slop-lens" });
var guest struct{ Token string `json:"token"` }
err := api("POST", "/guest", map[string]string{"slug": "slop-lens"}, &guest)
String envelope = api("POST", "/guest", """
{"slug": "slop-lens"}
"""); // parse .data.token from the envelope
token = api("POST", "/guest", { slug: "slop-lens" })["token"]
$token = api("POST", "/guest", ["slug" => "slop-lens"])["token"];
var guest = await SkillSafe.ApiAsync(HttpMethod.Post, "/guest",
new { slug = "slop-lens" });
var token = guest.GetProperty("token").GetString();
Step 2 — Check who you are and your balance
GET /me
Returns subject_type (user or guest),
subject_id and credits. Compare credits against the
estimate's hold_credits before running — a script that submits a run it
cannot afford earns a 402 it could have predicted.
curl -s "$API/me" -H "Authorization: Bearer $TOKEN" | jq '.data'
# {"subject_type":"user","subject_id":"usr_…","credits":50000,…}
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"`
}
err := api("GET", "/me", nil, &me)
String envelope = api("GET", "/me", null);
// parse .data.subject_type and .data.credits
me = api("GET", "/me")
puts "#{me['subject_type']} #{me['credits']}"
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"];
var me = await SkillSafe.ApiAsync(HttpMethod.Get, "/me");
Console.WriteLine($"{me.GetProperty("subject_type")} {me.GetProperty("credits")}");
Step 3 — Estimate the cost, and prove the model binding
POST /estimate
Free, and worth calling every time: it returns hold_credits (the worst-case
amount reserved for the run — not the price), min_credits (the
floor below which the run will not start) and the exact model the app is bound
to. The input shape is the same as /run: code is required;
brief, surface, ambition and
prescan_facts are optional. When the run settles you are charged only what it
actually used — typically far less than the hold, which prices the full output cap.
cat > page.html <<'PAGE'
<body class="bg-zinc-950 text-zinc-100 font-sans">
<h1 class="text-6xl font-extrabold">Unlock your team's
<span class="bg-gradient-to-r from-purple-500 to-pink-500 bg-clip-text text-transparent">full potential</span></h1>
<a class="rounded-xl bg-lime-400 px-6 py-3 font-semibold text-zinc-950">Start free trial</a>
</body>
PAGE
BODY=$(jq -n --rawfile code page.html \
'{code: $code, brief: "SaaS landing page for a project tool", surface: "landing", ambition: "restyle"}')
curl -s -X POST "$API/estimate" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "$BODY" | jq '.data'
# {"model":"gpt-5.6-terra","model_alias":"gpt-terra","hold_credits":…,"min_credits":…,…}
CODE = open("page.html").read()
payload = {
"code": CODE,
"brief": "SaaS landing page for a project tool",
"surface": "landing", # landing | app | docs | portfolio | commerce | other
"ambition": "restyle", # restyle | rethink
}
est = api("POST", "/estimate", payload)
assert est["model_alias"] == "gpt-terra"
print(f"hold {est['hold_credits']}, floor {est['min_credits']}, model {est['model']}")
import assert from "node:assert";
import { readFileSync } from "node:fs";
const payload = {
code: readFileSync("page.html", "utf8"),
brief: "SaaS landing page for a project tool",
surface: "landing", // landing | app | docs | portfolio | commerce | other
ambition: "restyle", // restyle | rethink
};
const est = await api("POST", "/estimate", payload);
assert.equal(est.model_alias, "gpt-terra");
console.log(`hold ${est.hold_credits}, floor ${est.min_credits}, model ${est.model}`);
code, _ := os.ReadFile("page.html")
payload := map[string]any{
"code": string(code),
"brief": "SaaS landing page for a project tool",
"surface": "landing",
"ambition": "restyle",
}
var est struct {
Model string `json:"model"`
ModelAlias string `json:"model_alias"`
HoldCredits int64 `json:"hold_credits"`
MinCredits int64 `json:"min_credits"`
}
if err := api("POST", "/estimate", payload, &est); err != nil {
panic(err)
}
fmt.Printf("hold %d, floor %d, model %s\n", est.HoldCredits, est.MinCredits, est.Model)
String code = java.nio.file.Files.readString(java.nio.file.Path.of("page.html"));
// Build the JSON with your library; fields: code, brief, surface, ambition
String payload = toJson(java.util.Map.of(
"code", code,
"brief", "SaaS landing page for a project tool",
"surface", "landing",
"ambition", "restyle"));
String envelope = api("POST", "/estimate", payload);
// assert .data.model_alias == "gpt-terra"; read .data.hold_credits
payload = {
code: File.read("page.html"),
brief: "SaaS landing page for a project tool",
surface: "landing",
ambition: "restyle",
}
est = api("POST", "/estimate", payload)
raise "unexpected model" unless est["model_alias"] == "gpt-terra"
puts "hold #{est['hold_credits']}, floor #{est['min_credits']}, model #{est['model']}"
$payload = [
"code" => file_get_contents("page.html"),
"brief" => "SaaS landing page for a project tool",
"surface" => "landing",
"ambition" => "restyle",
];
$est = api("POST", "/estimate", $payload);
assert($est["model_alias"] === "gpt-terra");
echo "hold {$est['hold_credits']}, floor {$est['min_credits']}, model {$est['model']}";
var payload = new {
code = await File.ReadAllTextAsync("page.html"),
brief = "SaaS landing page for a project tool",
surface = "landing",
ambition = "restyle",
};
var est = await SkillSafe.ApiAsync(HttpMethod.Post, "/estimate", payload);
if (est.GetProperty("model_alias").GetString() != "gpt-terra") throw new Exception("unexpected model");
Console.WriteLine($"hold {est.GetProperty("hold_credits")}, floor {est.GetProperty("min_credits")}");
prescan_facts is optional on the API. In the app it carries what the free
client-side scanner found — {"tokens": {…}, "signals": [{"id","label"}]}
— and the review must reconcile every signal id in coverage_check. From a
script you can omit it (you get an empty reconciliation) or replicate it; the review judges
the full source either way.
There is one further optional field, retry_note, which the app sends only when a
previous attempt at the same review came back in the wrong format. It is a plain-text string
naming what was wrong with that reply; the review then re-runs in full and returns the
corrected payload without referring to the earlier failure. From a script you almost never
want it — send the same Idempotency-Key to replay a run, and reserve
retry_note for the case where you genuinely got an unparseable reply and are
deliberately paying for one more attempt.
Step 4 — Run the review and wait for the verdict
POST /run GET /jobs/{id}
POST /run returns a job_id immediately; poll
GET /jobs/{id} until status is terminal. The review object arrives
as a JSON string in output.output — parse it and you have the same object
the app renders. Always send an Idempotency-Key derived from a hash of the
input: a retry after a network blip then replays the original run instead of paying for a
second one.
KEY="slop-lens:$(shasum -a 256 page.html | cut -c1-40)"
JOB_ID=$(curl -s -X POST "$API/run" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-d "$BODY" | jq -r '.data.job_id')
while :; do
JOB=$(curl -s "$API/jobs/$JOB_ID" -H "Authorization: Bearer $TOKEN")
STATUS=$(echo "$JOB" | jq -r '.data.status')
[ "$STATUS" = "succeeded" ] || [ "$STATUS" = "failed" ] && break
sleep 2
done
echo "$JOB" | jq -r '.data.output.output' > review.json
jq '{verdict, cluster, one_line}' review.json
import hashlib, json, time
key = "slop-lens:" + hashlib.sha256(json.dumps(payload, sort_keys=True).encode()).hexdigest()[:40]
job = api("POST", "/run", payload, headers={"Idempotency-Key": key})
while True:
j = api("GET", f"/jobs/{job['job_id']}")
if j["status"] in ("succeeded", "failed"):
break
time.sleep(2)
assert j["status"] == "succeeded", j.get("error")
review = json.loads(j["output"]["output"])
print(review["verdict"], review["cluster"], "-", review["one_line"])
for f in review["findings"]:
print(f"[{f['severity']}] {f['id']} {f['what']}")
import { createHash } from "node:crypto";
const key = "slop-lens:" + createHash("sha256")
.update(JSON.stringify(payload)).digest("hex").slice(0, 40);
const { job_id } = await fetch(API + "/run", {
method: "POST",
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json",
"Idempotency-Key": key },
body: JSON.stringify(payload),
}).then(r => r.json()).then(e => e.data);
let job;
do {
await new Promise(r => setTimeout(r, 2000));
job = await api("GET", `/jobs/${job_id}`);
} while (job.status !== "succeeded" && job.status !== "failed");
const review = JSON.parse(job.output.output);
console.log(review.verdict, review.cluster, "-", review.one_line);
sum := sha256.Sum256(bodyBytes) // the JSON payload you send
key := "slop-lens:" + hex.EncodeToString(sum[:])[:40]
req, _ := http.NewRequest("POST", API+"/run", bytes.NewReader(bodyBytes))
req.Header.Set("Authorization", "Bearer "+os.Getenv("SKILLSAFE_TOKEN"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", key)
// … send, decode envelope, keep data.job_id …
var job struct {
Status string `json:"status"`
Output struct{ Output string `json:"output"` } `json:"output"`
}
for {
if err := api("GET", "/jobs/"+jobID, nil, &job); err != nil {
panic(err)
}
if job.Status == "succeeded" || job.Status == "failed" {
break
}
time.Sleep(2 * time.Second)
}
var review map[string]any
json.Unmarshal([]byte(job.Output.Output), &review)
fmt.Println(review["verdict"], review["cluster"])
String key = "slop-lens:" + sha256Hex(payload).substring(0, 40);
// send POST /run with header Idempotency-Key: key, keep data.job_id
while (true) {
String envelope = api("GET", "/jobs/" + jobId, null);
// parse .data.status; break on "succeeded" or "failed"
Thread.sleep(2000);
}
// parse .data.output.output as JSON — that string is the review object
require "digest"
key = "slop-lens:" + Digest::SHA256.hexdigest(payload.to_json)[0, 40]
job = api_with_headers("POST", "/run", payload, { "Idempotency-Key" => key })
loop do
j = api("GET", "/jobs/#{job['job_id']}")
break @job = j if %w[succeeded failed].include?(j["status"])
sleep 2
end
review = JSON.parse(@job["output"]["output"])
puts "#{review['verdict']} #{review['cluster']} - #{review['one_line']}"
$key = "slop-lens:" . substr(hash("sha256", json_encode($payload)), 0, 40);
$job = api_with_headers("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"], " ", $review["cluster"], " - ", $review["one_line"];
var key = "slop-lens:" + Convert.ToHexString(
System.Security.Cryptography.SHA256.HashData(
JsonSerializer.SerializeToUtf8Bytes(payload)))[..40].ToLower();
// send POST /run with header Idempotency-Key: key, keep data.job_id
JsonElement job;
do {
await Task.Delay(2000);
job = await SkillSafe.ApiAsync(HttpMethod.Get, $"/jobs/{jobId}");
} while (job.GetProperty("status").GetString() is not ("succeeded" or "failed"));
var review = JsonDocument.Parse(job.GetProperty("output").GetProperty("output").GetString()!).RootElement;
Console.WriteLine($"{review.GetProperty("verdict")} {review.GetProperty("cluster")}");
The review object — output schema
One JSON object. Top-level fields, in the order they stream:
| Field | Type | Meaning |
|---|---|---|
design_name | string | Short name for what was reviewed, 3–9 words. |
verdict | enum | distinctive · leaning-generic · templated. |
cluster | enum | The AI-default look the page most resembles: cream-serif · dark-acid · broadsheet · mixed · none. |
one_line | string | The verdict in one sentence, with the single highest-leverage change. |
exec_summary | string | 2–4 paragraphs, the crit itself. Paragraphs are separated by blank lines. |
readings | array | Exactly six: {axis, reading, note} for color, typography, layout, structure, motion, copy; reading is deliberate · leaning-default · default. |
findings | array | {id, axis, severity, what, evidence, why, fix} — evidence is a verbatim quote from the submitted code, or "" only when the finding is an absence (no focus styles, no reduced-motion guard). |
keep | array | What is already distinctive and must survive the revision. |
coverage_check | array | {id, addressed, note} — one entry per prescan signal id sent in prescan_facts.signals, confirmed or overruled with a reason. |
direction | object | {thesis, palette[], type[], layout, signature, motion, copy_notes[]}. palette is 4–6 {name, hex, role} entries including background, ink and accent roles; type is 2–3 {role, family, fallback, note} entries including display and body. |
summary | string | A closing paragraph ready to paste into a design-review thread. |
The two client-side verdict invariants
The app re-applies two rules after parsing, so the badge can never contradict the findings
under it — apply the same ones in your script if you surface the verdict anywhere that
matters: a review containing any high-severity finding is never
distinctive (it degrades to leaning-generic), and a review with
zero findings is never templated.
The fourteen prescan signal families
Signal ids are family:hash, where the hash is derived from the matched text so
a re-scan after a revision compares like with like. Families:
cluster-cream, cluster-dark, cluster-broadsheet (the
three AI-default looks), default-type, single-face,
numbered-markers, gradient-accent, purple-pink,
glass, uniform-radius, buzzword,
stat-hero, and the two quality-floor checks no-focus and
no-reduced-motion.
Step 5 — Stream the review as it is written
POST /run-stream
Same body as /run, but the response is a Server-Sent Events stream:
delta events carry output text as it is generated, and the final
job event carries the terminal job object (including
charged_credits). The app drives its progress panel off the top-level JSON keys
appearing in the accumulated text — "verdict", "readings",
"findings", "direction", "summary" — which you
can replicate for a live console display.
# -N disables buffering so events print as they arrive
curl -sN -X POST "$API/run-stream" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-d "$BODY"
# event: delta data: {"text":"{\n \"design_name\""}
# …
# event: job data: {"status":"succeeded","charged_credits":…}
import json, requests
with requests.post(API + "/run-stream", json=payload, stream=True, timeout=300,
headers={"Authorization": f"Bearer {TOKEN}", "Idempotency-Key": key}) as r:
event, raw = None, []
for line in r.iter_lines(decode_unicode=True):
if line.startswith("event:"):
event = line[6:].strip()
elif line.startswith("data:"):
data = json.loads(line[5:])
if event == "delta":
raw.append(data.get("text", ""))
elif event == "job" and data["status"] == "succeeded":
review = json.loads("".join(raw) or data["output"]["output"])
print(review["verdict"], "-", review["one_line"])
const res = await fetch(API + "/run-stream", {
method: "POST",
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json",
"Idempotency-Key": key },
body: JSON.stringify(payload),
});
const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = "", raw = "", event = null;
for (;;) {
const { done, value } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
for (const line of buf.split("\n")) {
if (line.startsWith("event:")) event = line.slice(6).trim();
else if (line.startsWith("data:")) {
const data = JSON.parse(line.slice(5));
if (event === "delta") raw += data.text ?? "";
else if (event === "job" && data.status === "succeeded")
console.log(JSON.parse(raw || data.output.output).verdict);
}
}
buf = buf.slice(buf.lastIndexOf("\n") + 1);
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", API+"/run-stream", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("SKILLSAFE_TOKEN"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", key)
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 1024*1024), 1024*1024)
var event string
var raw strings.Builder
for sc.Scan() {
line := sc.Text()
switch {
case strings.HasPrefix(line, "event:"):
event = strings.TrimSpace(line[6:])
case strings.HasPrefix(line, "data:"):
var data map[string]any
json.Unmarshal([]byte(line[5:]), &data)
if event == "delta" {
if t, ok := data["text"].(string); ok {
raw.WriteString(t)
}
}
}
}
// Java 17+ — read the stream line by line instead of buffering the body.
var req = HttpRequest.newBuilder(URI.create(API + "/run-stream"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.header("Idempotency-Key", key)
.POST(HttpRequest.BodyPublishers.ofString(payloadJson))
.build();
var res = http.send(req, HttpResponse.BodyHandlers.ofLines());
var raw = new StringBuilder();
String[] event = {null};
res.body().forEach(line -> {
if (line.startsWith("event:")) event[0] = line.substring(6).trim();
else if (line.startsWith("data:") && "delta".equals(event[0]))
raw.append(parseTextField(line.substring(5))); // your JSON library
});
require "net/http"
uri = URI(API + "/run-stream")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = key
req.body = payload.to_json
raw, event = +"", nil
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, read_timeout: 300) do |h|
h.request(req) do |res|
res.read_body do |chunk|
chunk.each_line do |line|
if line.start_with?("event:") then event = line[6..].strip
elsif line.start_with?("data:") && event == "delta"
raw << (JSON.parse(line[5..])["text"] || "")
end
end
end
end
end
review = JSON.parse(raw)
$event = null;
$raw = "";
$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",
"Idempotency-Key: $key",
],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$event, &$raw) {
foreach (explode("\n", $chunk) as $line) {
if (str_starts_with($line, "event:")) $event = trim(substr($line, 6));
elseif (str_starts_with($line, "data:") && $event === "delta") {
$data = json_decode(substr($line, 5), true);
$raw .= $data["text"] ?? "";
}
}
return strlen($chunk);
},
]);
curl_exec($ch);
curl_close($ch);
$review = json_decode($raw, true);
var req = new HttpRequestMessage(HttpMethod.Post, Api + "/run-stream") {
Content = JsonContent.Create(payload)
};
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token);
req.Headers.Add("Idempotency-Key", key);
using var res = await Http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var stream = new StreamReader(await res.Content.ReadAsStreamAsync());
string? line, ev = null;
var raw = new StringBuilder();
while ((line = await stream.ReadLineAsync()) is not null) {
if (line.StartsWith("event:")) ev = line[6..].Trim();
else if (line.StartsWith("data:") && ev == "delta") {
var data = JsonDocument.Parse(line[5..]).RootElement;
if (data.TryGetProperty("text", out var t)) raw.Append(t.GetString());
}
}
var review = JsonDocument.Parse(raw.ToString()).RootElement;
Step 6 — The recipe: a distinctiveness gate before the page ships
The job this app was built for. A landing page (or a template, or a themed export) is about
to ship, and nobody has asked whether it looks like every other AI-generated page. Instead:
feed the built HTML and CSS into Slop Lens, print the findings into the build output, and
flag the release when verdict comes back templated. The
Idempotency-Key is derived from a hash of the input, so a CI retry of the same
build replays the first review instead of paying for a second one.
#!/usr/bin/env bash
# design-gate.sh — run against the built static export.
set -euo pipefail
API="https://api.skillsafe.ai/v1/app-api"
TOKEN="${SKILLSAFE_TOKEN:?set SKILLSAFE_TOKEN in the CI secret store}"
cat dist/index.html dist/assets/*.css 2>/dev/null | head -c 60000 > page-src.txt
jq -n --rawfile code page-src.txt \
--arg brief "${DESIGN_BRIEF:-Marketing page for this repo's product}" \
'{code: $code, brief: $brief, surface: "landing", ambition: "restyle"}' > input.json
KEY="slop-lens:$(shasum -a 256 input.json | cut -c1-40)"
JOB_ID=$(curl -sf -X POST "$API/run" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-d @input.json | jq -r '.data.job_id')
for _ in $(seq 1 120); do
JOB=$(curl -sf "$API/jobs/$JOB_ID" -H "Authorization: Bearer $TOKEN")
STATUS=$(echo "$JOB" | jq -r '.data.status')
case "$STATUS" in succeeded|failed) break ;; esac
sleep 2
done
[ "$STATUS" = "succeeded" ] || { echo "review run $STATUS"; exit 2; }
echo "$JOB" | jq -r '.data.output.output' > review.json
VERDICT=$(jq -r '.verdict' review.json)
echo "design verdict: $VERDICT ($(jq -r '.cluster' review.json))"
jq -r '.findings[] | "[\(.severity)] \(.id) \(.what)"' review.json
jq -r '"direction: " + .direction.thesis' review.json
# warn on leaning-generic, fail only on templated — tune to taste
[ "$VERDICT" = "templated" ] && exit 1 || exit 0
import hashlib, json, pathlib, sys, time
src = pathlib.Path("dist/index.html").read_text()[:60000]
payload = {"code": src, "brief": "Marketing page for this repo's product",
"surface": "landing", "ambition": "restyle"}
key = "slop-lens:" + hashlib.sha256(json.dumps(payload, sort_keys=True).encode()).hexdigest()[:40]
job = api("POST", "/run", payload, headers={"Idempotency-Key": key})
while True:
j = api("GET", f"/jobs/{job['job_id']}")
if j["status"] in ("succeeded", "failed"):
break
time.sleep(2)
assert j["status"] == "succeeded"
review = json.loads(j["output"]["output"])
print(f"design verdict: {review['verdict']} ({review['cluster']})")
for f in review["findings"]:
print(f"[{f['severity']}] {f['id']} {f['what']}")
print("direction:", review["direction"]["thesis"])
pathlib.Path("review.json").write_text(json.dumps(review, indent=2))
sys.exit(1 if review["verdict"] == "templated" else 0)
import { readFileSync, writeFileSync } from "node:fs";
import { createHash } from "node:crypto";
const payload = {
code: readFileSync("dist/index.html", "utf8").slice(0, 60000),
brief: "Marketing page for this repo's product",
surface: "landing", ambition: "restyle",
};
const key = "slop-lens:" + createHash("sha256")
.update(JSON.stringify(payload)).digest("hex").slice(0, 40);
// POST /run with Idempotency-Key: key (step 4), poll /jobs/{id} …
const review = JSON.parse(job.output.output);
console.log(`design verdict: ${review.verdict} (${review.cluster})`);
for (const f of review.findings) console.log(`[${f.severity}] ${f.id} ${f.what}`);
console.log("direction:", review.direction.thesis);
writeFileSync("review.json", JSON.stringify(review, null, 2));
process.exit(review.verdict === "templated" ? 1 : 0);
src, _ := os.ReadFile("dist/index.html")
if len(src) > 60000 {
src = src[:60000]
}
payload := map[string]any{"code": string(src),
"brief": "Marketing page for this repo's product",
"surface": "landing", "ambition": "restyle"}
// derive key, POST /run, poll /jobs/{id} — step 4 —
var review struct {
Verdict string `json:"verdict"`
Cluster string `json:"cluster"`
Findings []struct{ ID, Severity, What string } `json:"findings"`
}
json.Unmarshal([]byte(job.Output.Output), &review)
fmt.Printf("design verdict: %s (%s)\n", review.Verdict, review.Cluster)
for _, f := range review.Findings {
fmt.Printf("[%s] %s %s\n", f.Severity, f.ID, f.What)
}
if review.Verdict == "templated" {
os.Exit(1)
}
String src = java.nio.file.Files.readString(java.nio.file.Path.of("dist/index.html"));
src = src.substring(0, Math.min(src.length(), 60000));
// build payload {code, brief, surface, ambition}; derive the key; run and poll (step 4)
// then parse .data.output.output as the review object:
String verdict = review.get("verdict").asText();
System.out.println("design verdict: " + verdict + " (" + review.get("cluster").asText() + ")");
for (var f : review.get("findings"))
System.out.println("[" + f.get("severity").asText() + "] " + f.get("id").asText()
+ " " + f.get("what").asText());
System.exit("templated".equals(verdict) ? 1 : 0);
src = File.read("dist/index.html")[0, 60000]
payload = { code: src, brief: "Marketing page for this repo's product",
surface: "landing", ambition: "restyle" }
key = "slop-lens:" + Digest::SHA256.hexdigest(payload.to_json)[0, 40]
# POST /run with Idempotency-Key: key, poll /jobs/{id} — step 4 —
review = JSON.parse(job["output"]["output"])
puts "design verdict: #{review['verdict']} (#{review['cluster']})"
review["findings"].each { |f| puts "[#{f['severity']}] #{f['id']} #{f['what']}" }
puts "direction: #{review.dig('direction', 'thesis')}"
exit(review["verdict"] == "templated" ? 1 : 0)
$src = substr(file_get_contents("dist/index.html"), 0, 60000);
$payload = ["code" => $src, "brief" => "Marketing page for this repo's product",
"surface" => "landing", "ambition" => "restyle"];
$key = "slop-lens:" . substr(hash("sha256", json_encode($payload)), 0, 40);
// POST /run with Idempotency-Key: $key, poll /jobs/{id} — step 4 —
$review = json_decode($job["output"]["output"], true);
echo "design verdict: {$review['verdict']} ({$review['cluster']})\n";
foreach ($review["findings"] as $f)
echo "[{$f['severity']}] {$f['id']} {$f['what']}\n";
echo "direction: " . $review["direction"]["thesis"] . "\n";
exit($review["verdict"] === "templated" ? 1 : 0);
var src = (await File.ReadAllTextAsync("dist/index.html"));
src = src[..Math.Min(src.Length, 60000)];
var payload = new { code = src, brief = "Marketing page for this repo's product",
surface = "landing", ambition = "restyle" };
// derive the key, POST /run, poll /jobs/{id} — step 4 —
var review = JsonDocument.Parse(job.GetProperty("output").GetProperty("output").GetString()!).RootElement;
var verdict = review.GetProperty("verdict").GetString();
Console.WriteLine($"design verdict: {verdict} ({review.GetProperty("cluster")})");
foreach (var f in review.GetProperty("findings").EnumerateArray())
Console.WriteLine($"[{f.GetProperty("severity")}] {f.GetProperty("id")} {f.GetProperty("what")}");
return verdict == "templated" ? 1 : 0;
Two things to decide before you turn this on for a team. What to do with
leaning-generic: failing on templated only, as above, is
the version people leave switched on; a warning comment carrying the finding list and
direction.thesis is the useful half either way. Whose taste this is:
the review is AI-generated judgement calibrated against the three known AI-default
looks — treat it as a crit from a colleague, not a court ruling, and let a human
overrule it with a brief that names the deliberate choice.