Drive Proto Verif from your own code
Proto Verif reads the material behind one cryptographic protocol and returns one of two structured results over that same text: an annotated Mermaid sequence diagram of the message flow, or a ProVerif model with the security queries that matter. Everything the web app does is reachable over HTTP with a token, and this page documents the exact contract — taken from the app's own parsing code, not from intent.
Derived from @trailofbits/crypto-protocol-diagram and
@trailofbits/mermaid-to-proverif. The service runs no verifier and executes no pasted
code.
Base URL and the envelope
Every call goes to https://api.skillsafe.ai/v1/app-api and every reply is one of:
{"ok": true, "data": { ... }}
{"ok": false, "error": {"code": "...", "message": "...", "details": { ... }}}
| code | HTTP | what it means |
|---|---|---|
unauthorized | 401 | Missing, malformed or expired token. Mint a new one. |
forbidden | 403 | A guest token tried to /run. Runs need a personal token. |
payment_required | 402 | Balance below min_credits. Call /estimate first and compare against /me. |
validation_error | 400 | The run body was not an object, or a field was the wrong type. |
rate_limited | 429 | Back off and retry. Never tight-loop. |
internal | 500 | The run failed server-side. Failed runs are not billed. |
The input contract — read this before anything else
task — the router field, and it comes first
The whole app is one system prompt with an explicit task router. Every run must carry a
task field whose value is exactly "diagram" or
"model". Omit it and the model picks the closest lane and names the one it chose
— which is a fallback, not a feature.
The run body is the input object itself. There is no input wrapper
and no X-App-Slug header. Wrapping the object returns 200 while hiding every field
from the model.
task: "diagram"
Extract the message flow from a protocol description into an annotated Mermaid sequence diagram.
| field | type | required | notes |
|---|---|---|---|
task | string | yes | the literal "diagram" |
protocol | string | yes | the protocol material: an RFC excerpt, a paper's pseudocode, prose, source code, a Mermaid diagram, a ProVerif .pv model or a Tamarin .spthy theory. The web app clips this at 88,000 characters, keeping the participant block or both ends and announcing the cut in-band. |
context | string | no | where the protocol runs, who is trusted, what is out of scope. It changes the answer. |
read_as | string | no | one of unsure rfc paper prose code proverif tamarin mermaid. Picks the extraction workflow. A formal model is read as a specification, not as source code. |
prescan_facts | object | no | the browser reader's output. Every flags[].id in it must appear exactly once in the reply's coverage_check. See below. |
task: "model"
Turn a Mermaid sequence diagram into a ProVerif model with security queries.
| field | type | required | notes |
|---|---|---|---|
task | string | yes | the literal "model" |
protocol | string | yes | ideally a Mermaid sequenceDiagram — that is what this lane is designed around. Prose works: the model derives the flow itself first and says so in assumptions. |
context | string | no | same as above. |
properties | string | no | free text naming the properties you care about. Reachability, session-key secrecy and injective authentication are always written; this only adds queries. |
prescan_facts | object | no | as above. |
prescan_facts — optional, and it is what makes the reply accountable
The web app runs a real reader in the browser before it spends anything, and passes the result in. You do not have to send it. If you do, the shape that matters is:
{
"input_kind": "mermaid",
"input_kind_label": "a Mermaid sequenceDiagram",
"named_protocols": ["Noise"],
"crypto_ops": [{"op": "dh", "label": "Diffie-Hellman", "party": "C",
"proverif_category": "dh/dhpk with a commutativity equation",
"evidence": "z = DH(ek_C, epk_S)", "line": 14}],
"diagram": {"participants": [...], "messages": [...], "blocks": [...]},
"counts": {"blocker": 0, "high": 1, "medium": 2, "low": 0, "info": 1},
"flags": [{"id": "MMD-VERIFY-NO-ABORT", "severity": "high", "area": "flow",
"title": "Verification steps with no abort path", "line": 18}]
}
The contract on flags is exact. Every id you send must come back
exactly once in coverage_check, with handling either
"addressed" or "set-aside". Ids that were not sent must not appear. The
web app renders the difference as a reconciliation table, and so should you.
1. A tiny client, and where the token comes from
A guest token is enough for /me and /estimate. Running a lane is metered and needs a personal token: open the token page, sign in, and press “Copy shell export”.
# 1. Mint a guest token (free calls only: /me and /estimate)
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/guest" \
-H "Content-Type: application/json" \
-d '{"slug":"proto-verif"}'
# The reply carries {"data":{"token":"aut_..."}}.
# Running a lane is metered and needs a PERSONAL token: open
# https://proto-verif.skillsafe.ai/tokens.html
# sign in there, and press "Copy shell export".
export SKILLSAFE_TOKEN="YOUR_TOKEN"
import json, urllib.request
API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # from https://proto-verif.skillsafe.ai/tokens.html
def call(path, body=None, token=TOKEN, extra_headers=None):
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = "Bearer " + token
headers.update(extra_headers or {})
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(API + path, data=data, headers=headers,
method="POST" if data is not None else "GET")
with urllib.request.urlopen(req) as r:
payload = json.loads(r.read().decode())
if not payload.get("ok"):
raise RuntimeError(payload.get("error"))
return payload["data"]
# A guest token is enough for /me and /estimate:
guest = call("/guest", {"slug": "proto-verif"}, token=None)["token"]
const API = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "YOUR_TOKEN"; // from https://proto-verif.skillsafe.ai/tokens.html
async function call(path, body, opts = {}) {
const headers = { "Content-Type": "application/json", ...(opts.headers || {}) };
if (opts.token !== null) headers.Authorization = `Bearer ${opts.token ?? TOKEN}`;
const res = await fetch(API + path, {
method: body === undefined ? "GET" : "POST",
headers,
body: body === undefined ? undefined : JSON.stringify(body),
});
const payload = await res.json();
if (!payload.ok) throw Object.assign(new Error(payload.error?.message), payload.error);
return payload.data;
}
// A guest token is enough for /me and /estimate:
const { token: guest } = await call("/guest", { slug: "proto-verif" }, { token: null });
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
const api = "https://api.skillsafe.ai/v1/app-api"
func token() string {
if t := os.Getenv("SKILLSAFE_TOKEN"); t != "" {
return t
}
return "YOUR_TOKEN" // from https://proto-verif.skillsafe.ai/tokens.html
}
type envelope struct {
OK bool `json:"ok"`
Data json.RawMessage `json:"data"`
Error json.RawMessage `json:"error"`
}
func call(path string, body any, hdr map[string]string) (json.RawMessage, error) {
var rdr io.Reader
method := http.MethodGet
if body != nil {
b, _ := json.Marshal(body)
rdr = bytes.NewReader(b)
method = http.MethodPost
}
req, _ := http.NewRequest(method, api+path, rdr)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token())
for k, v := range hdr {
req.Header.Set(k, v)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var e envelope
if err := json.NewDecoder(res.Body).Decode(&e); err != nil {
return nil, err
}
if !e.OK {
return nil, fmt.Errorf("%s", e.Error)
}
return e.Data, nil
}
import java.net.URI;
import java.net.http.*;
import java.util.Map;
public class ProtoVerif {
static final String API = "https://api.skillsafe.ai/v1/app-api";
// from https://proto-verif.skillsafe.ai/tokens.html
static final String TOKEN = System.getenv().getOrDefault("SKILLSAFE_TOKEN", "YOUR_TOKEN");
static final HttpClient HTTP = HttpClient.newHttpClient();
static String call(String path, String jsonBody, Map<String, String> extra) throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(API + path))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + TOKEN);
extra.forEach(b::header);
HttpRequest req = jsonBody == null
? b.GET().build()
: b.POST(HttpRequest.BodyPublishers.ofString(jsonBody)).build();
HttpResponse<String> res = HTTP.send(req, HttpResponse.BodyHandlers.ofString());
return res.body(); // {"ok":true,"data":{...}} or {"ok":false,"error":{...}}
}
}
require "json"
require "net/http"
require "uri"
API = "https://api.skillsafe.ai/v1/app-api"
# from https://proto-verif.skillsafe.ai/tokens.html
TOKEN = ENV.fetch("SKILLSAFE_TOKEN", "YOUR_TOKEN")
def call(path, body = nil, extra = {})
uri = URI(API + path)
req = body.nil? ? Net::HTTP::Get.new(uri) : Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer #{TOKEN}"
extra.each { |k, v| req[k] = v }
req.body = JSON.dump(body) unless body.nil?
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
payload = JSON.parse(res.body)
raise payload["error"].to_s unless payload["ok"]
payload["data"]
end
<?php
const API = "https://api.skillsafe.ai/v1/app-api";
// from https://proto-verif.skillsafe.ai/tokens.html
$TOKEN = getenv("SKILLSAFE_TOKEN") ?: "YOUR_TOKEN";
function call(string $path, $body = null, array $extra = []) {
global $TOKEN;
$headers = ["Content-Type: application/json", "Authorization: Bearer " . $TOKEN];
foreach ($extra as $k => $v) { $headers[] = "$k: $v"; }
$ch = curl_init(API . $path);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => $headers,
]);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
$payload = json_decode(curl_exec($ch), true);
curl_close($ch);
if (empty($payload["ok"])) { throw new RuntimeException(json_encode($payload["error"])); }
return $payload["data"];
}
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
static class ProtoVerif {
const string Api = "https://api.skillsafe.ai/v1/app-api";
// from https://proto-verif.skillsafe.ai/tokens.html
static readonly string Token =
Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN") ?? "YOUR_TOKEN";
static readonly HttpClient Http = new();
public static async Task<JsonElement> CallAsync(
string path, object? body = null, IDictionary<string, string>? extra = null) {
var req = new HttpRequestMessage(body is null ? HttpMethod.Get : HttpMethod.Post, Api + path);
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token);
if (extra is not null) foreach (var kv in extra) req.Headers.Add(kv.Key, kv.Value);
if (body is not null)
req.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
var res = await Http.SendAsync(req);
var doc = JsonDocument.Parse(await res.Content.ReadAsStringAsync());
if (!doc.RootElement.GetProperty("ok").GetBoolean())
throw new Exception(doc.RootElement.GetProperty("error").ToString());
return doc.RootElement.GetProperty("data");
}
}
2. Session state with /me
Returns subject_type (user or guest), the username and the credit balance. Compare the balance against hold_credits before you submit, so a 402 never happens after the fact.
curl -s "https://api.skillsafe.ai/v1/app-api/me" -H "Authorization: Bearer $SKILLSAFE_TOKEN"
# {"ok":true,"data":{"subject_type":"user","username":"...","credits":128400}}
# subject_type "guest" means /estimate works but /run will 403.
me = call("/me")
print(me["subject_type"], me.get("credits"))
if me["subject_type"] != "user":
raise SystemExit("running a lane needs a personal token")
const me = await call("/me");
console.log(me.subject_type, me.credits);
if (me.subject_type !== "user") throw new Error("running a lane needs a personal token");
raw, err := call("/me", nil, nil)
if err != nil {
panic(err)
}
var me struct {
SubjectType string `json:"subject_type"`
Credits int `json:"credits"`
}
json.Unmarshal(raw, &me)
fmt.Println(me.SubjectType, me.Credits)
String me = call("/me", null, Map.of());
System.out.println(me); // subject_type must be "user" before /run
me = call("/me")
puts "#{me["subject_type"]} #{me["credits"]}"
abort "running a lane needs a personal token" unless me["subject_type"] == "user"
$me = call("/me");
printf("%s %d\n", $me["subject_type"], $me["credits"] ?? 0);
var me = await ProtoVerif.CallAsync("/me");
Console.WriteLine($"{me.GetProperty("subject_type")} {me.GetProperty("credits")}");
3. Price it with POST /estimate — free
Creates no job and costs nothing. It returns model, model_alias, markup_bps, hold_credits and min_credits. The hold is what is reserved, not the price — you are charged only for what the run uses, which is usually far less. Re-estimate on every lane switch: the two lanes have different prompt sections and different output caps, so their holds differ.
A clean estimate is not evidence the input is right. /estimate performs no body validation whatsoever: a bare string, a number and null all return a well-formed estimate with a correct model binding. It proves the app is wired to the right model at the right markup, and nothing about your input shape. Always send an object.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/estimate" \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"task":"diagram","protocol":"sequenceDiagram\n participant C as Client\n C->>S: hello","read_as":"mermaid"}'
# {"ok":true,"data":{"model":"gpt-5.6-terra","model_alias":"gpt-terra",
# "markup_bps":1000,"hold_credits":4120,"min_credits":900}}
run_input = {
"task": "diagram", # or "model"
"protocol": open("handshake.mmd").read(),
"context": "Long-term keys are registered out of band.",
"read_as": "mermaid", # diagram lane only
}
est = call("/estimate", run_input)
print(est["hold_credits"], "credits reserved;", est["model"], est["model_alias"])
# /estimate is FREE and creates no job. It also performs no body validation at
# all, so it cannot tell you the input shape is wrong - only that the model
# binding is right. Send an object, never a bare string.
const runInput = {
task: "diagram", // or "model"
protocol: mermaidSource,
context: "Long-term keys are registered out of band.",
read_as: "mermaid", // diagram lane only
};
const est = await call("/estimate", runInput);
console.log(est.hold_credits, "reserved;", est.model, est.model_alias);
runInput := map[string]any{
"task": "diagram",
"protocol": mermaidSource,
"context": "Long-term keys are registered out of band.",
"read_as": "mermaid",
}
raw, _ := call("/estimate", runInput, nil)
var est struct {
Model string `json:"model"`
ModelAlias string `json:"model_alias"`
Hold int `json:"hold_credits"`
MarkupBps int `json:"markup_bps"`
}
json.Unmarshal(raw, &est)
fmt.Println(est.Hold, est.Model, est.ModelAlias, est.MarkupBps)
String body = """
{"task":"diagram","protocol":"sequenceDiagram\\n participant C as Client",
"read_as":"mermaid"}
""";
System.out.println(call("/estimate", body, Map.of()));
run_input = {
"task" => "diagram",
"protocol" => File.read("handshake.mmd"),
"context" => "Long-term keys are registered out of band.",
"read_as" => "mermaid",
}
est = call("/estimate", run_input)
puts "#{est["hold_credits"]} reserved; #{est["model"]} (#{est["model_alias"]})"
$runInput = [
"task" => "diagram",
"protocol" => file_get_contents("handshake.mmd"),
"context" => "Long-term keys are registered out of band.",
"read_as" => "mermaid",
];
$est = call("/estimate", $runInput);
printf("%d reserved; %s\n", $est["hold_credits"], $est["model"]);
var runInput = new Dictionary<string, object> {
["task"] = "diagram",
["protocol"] = File.ReadAllText("handshake.mmd"),
["context"] = "Long-term keys are registered out of band.",
["read_as"] = "mermaid",
};
var est = await ProtoVerif.CallAsync("/estimate", runInput);
Console.WriteLine($"{est.GetProperty("hold_credits")} reserved; {est.GetProperty("model")}");
4. Run it with POST /run, then poll GET /jobs/{job_id}
Returns {"job_id": "..."} immediately. Poll until status leaves running; the reply text is at data.output.output and is the single JSON object documented below.
# The Idempotency-Key must carry the LANE. Two lanes over the same protocol
# are two distinct runs and must not collide on one key.
KEY="proto-verif:diagram:$(shasum -a 256 handshake.mmd | cut -c1-32):a1"
JOB=$(curl -s -X POST "https://api.skillsafe.ai/v1/app-api/run" \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-d @run-input.json | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["job_id"])')
until [ "$(curl -s "https://api.skillsafe.ai/v1/app-api/jobs/$JOB" -H "Authorization: Bearer $SKILLSAFE_TOKEN" \
| python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["status"])')" != "running" ]; do
sleep 2
done
curl -s "https://api.skillsafe.ai/v1/app-api/jobs/$JOB" -H "Authorization: Bearer $SKILLSAFE_TOKEN" \
| python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["output"]["output"])'
import hashlib, time
basis = "|~|".join([run_input["task"], run_input["protocol"],
run_input.get("context", ""), run_input.get("read_as", ""),
run_input.get("properties", "")])
key = "proto-verif:{}:{}:a1".format(run_input["task"], hashlib.sha256(basis.encode()).hexdigest()[:32])
job = call("/run", run_input, extra_headers={"Idempotency-Key": key})
job_id = job["job_id"]
while True:
j = call("/jobs/" + job_id)
if j["status"] != "running":
break
time.sleep(2)
result = json.loads(j["output"]["output"]) # the single JSON object
print(result["lane"], result["verdict"], len(result["message_flow"]), "steps")
const enc = new TextEncoder().encode([
runInput.task, runInput.protocol, runInput.context ?? "",
runInput.read_as ?? "", runInput.properties ?? "",
].join("|~|"));
const digest = [...new Uint8Array(await crypto.subtle.digest("SHA-256", enc))]
.map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, 32);
const key = `proto-verif:${runInput.task}:${digest}:a1`;
const { job_id } = await call("/run", runInput, { headers: { "Idempotency-Key": key } });
let job;
do {
await new Promise((r) => setTimeout(r, 2000));
job = await call(`/jobs/${job_id}`);
} while (job.status === "running");
const result = JSON.parse(job.output.output);
console.log(result.lane, result.verdict, result.message_flow.length, "steps");
sum := sha256.Sum256([]byte(strings.Join([]string{
"diagram", mermaidSource, contextText, "mermaid", "",
}, "|~|")))
key := fmt.Sprintf("proto-verif:diagram:%x:a1", sum[:16])
raw, _ := call("/run", runInput, map[string]string{"Idempotency-Key": key})
var started struct {
JobID string `json:"job_id"`
}
json.Unmarshal(raw, &started)
for {
raw, _ = call("/jobs/"+started.JobID, nil, nil)
var job struct {
Status string `json:"status"`
Output struct {
Output string `json:"output"`
} `json:"output"`
}
json.Unmarshal(raw, &job)
if job.Status != "running" {
fmt.Println(job.Output.Output)
break
}
time.Sleep(2 * time.Second)
}
String key = "proto-verif:diagram:" + Integer.toHexString(bodyJson.hashCode()) + ":a1";
String started = call("/run", bodyJson, Map.of("Idempotency-Key", key));
// parse job_id out of `started`, then poll GET /jobs/{job_id} until
// status != "running", and read data.output.output as the single JSON object.
require "digest"
basis = [run_input["task"], run_input["protocol"], run_input["context"].to_s,
run_input["read_as"].to_s, run_input["properties"].to_s].join("|~|")
key = "proto-verif:#{run_input["task"]}:#{Digest::SHA256.hexdigest(basis)[0, 32]}:a1"
job_id = call("/run", run_input, { "Idempotency-Key" => key })["job_id"]
loop do
job = call("/jobs/#{job_id}")
break puts(job["output"]["output"]) if job["status"] != "running"
sleep 2
end
$basis = implode("|~|", [
$runInput["task"], $runInput["protocol"], $runInput["context"] ?? "",
$runInput["read_as"] ?? "", $runInput["properties"] ?? "",
]);
$key = sprintf("proto-verif:%s:%s:a1", $runInput["task"], substr(hash("sha256", $basis), 0, 32));
$jobId = call("/run", $runInput, ["Idempotency-Key" => $key])["job_id"];
do {
sleep(2);
$job = call("/jobs/" . $jobId);
} while ($job["status"] === "running");
echo $job["output"]["output"];
var basis = string.Join("|~|", new[] {
"diagram", mermaidSource, contextText, "mermaid", "" });
var digest = Convert.ToHexString(
System.Security.Cryptography.SHA256.HashData(Encoding.UTF8.GetBytes(basis)))[..32].ToLower();
var key = $"proto-verif:diagram:{digest}:a1";
var started = await ProtoVerif.CallAsync("/run", runInput,
new Dictionary<string, string> { ["Idempotency-Key"] = key });
var jobId = started.GetProperty("job_id").GetString();
JsonElement job;
do {
await Task.Delay(2000);
job = await ProtoVerif.CallAsync($"/jobs/{jobId}");
} while (job.GetProperty("status").GetString() == "running");
Console.WriteLine(job.GetProperty("output").GetProperty("output"));
5. Or stream it with POST /run-stream
Server-Sent Events. Three event types: job once at the start, delta repeatedly with a text chunk, and done at the end with status, charged_credits and truncated. Concatenate every delta and parse the concatenation. If the stream dies mid-flight, keep what parsed rather than discarding it — the run was billed either way.
curl -N -X POST "https://api.skillsafe.ai/v1/app-api/run-stream" \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-H "Idempotency-Key: $KEY" \
-d @run-input.json
# event: job {"job_id":"..."}
# event: delta {"text":"{\"lane\":\"diagram\","}
# event: done {"status":"succeeded","charged_credits":1180}
req = urllib.request.Request(
API + "/run-stream",
data=json.dumps(run_input).encode(),
headers={"Content-Type": "application/json", "Accept": "text/event-stream",
"Authorization": "Bearer " + TOKEN, "Idempotency-Key": key},
)
buf, event = "", None
with urllib.request.urlopen(req) as stream:
for raw in stream:
line = raw.decode().rstrip("\n")
if line.startswith("event: "):
event = line[7:]
elif line.startswith("data: ") and event == "delta":
buf += json.loads(line[6:]).get("text", "")
result = json.loads(buf[buf.index("{"):buf.rindex("}") + 1])
print(result["verdict"], result["headline"])
const res = await fetch(`${API}/run-stream`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "text/event-stream",
Authorization: `Bearer ${TOKEN}`,
"Idempotency-Key": key,
},
body: JSON.stringify(runInput),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buf = "", pending = "", event = null;
for (;;) {
const { value, done } = await reader.read();
if (done) break;
pending += decoder.decode(value, { stream: true });
const lines = pending.split("\n");
pending = lines.pop();
for (const line of lines) {
if (line.startsWith("event: ")) event = line.slice(7);
else if (line.startsWith("data: ") && event === "delta") {
buf += JSON.parse(line.slice(6)).text ?? "";
}
}
}
const result = JSON.parse(buf.slice(buf.indexOf("{"), buf.lastIndexOf("}") + 1));
console.log(result.verdict, result.headline);
req, _ := http.NewRequest(http.MethodPost, api+"/run-stream", bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "text/event-stream")
req.Header.Set("Authorization", "Bearer "+token())
req.Header.Set("Idempotency-Key", key)
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
var buf strings.Builder
var event string
sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 1<<20), 1<<20)
for sc.Scan() {
line := sc.Text()
switch {
case strings.HasPrefix(line, "event: "):
event = strings.TrimPrefix(line, "event: ")
case strings.HasPrefix(line, "data: ") && event == "delta":
var d struct {
Text string `json:"text"`
}
json.Unmarshal([]byte(strings.TrimPrefix(line, "data: ")), &d)
buf.WriteString(d.Text)
}
}
fmt.Println(buf.String())
HttpRequest req = HttpRequest.newBuilder(URI.create(API + "/run-stream"))
.header("Content-Type", "application/json")
.header("Accept", "text/event-stream")
.header("Authorization", "Bearer " + TOKEN)
.header("Idempotency-Key", key)
.POST(HttpRequest.BodyPublishers.ofString(bodyJson))
.build();
StringBuilder buf = new StringBuilder();
final String[] event = { null };
HTTP.send(req, HttpResponse.BodyHandlers.ofLines()).body().forEach(line -> {
if (line.startsWith("event: ")) event[0] = line.substring(7);
else if (line.startsWith("data: ") && "delta".equals(event[0])) {
buf.append(extractText(line.substring(6))); // your JSON reader
}
});
System.out.println(buf);
uri = URI(API + "/run-stream")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req["Accept"] = "text/event-stream"
req["Authorization"] = "Bearer #{TOKEN}"
req["Idempotency-Key"] = key
req.body = JSON.dump(run_input)
buf = +""
event = nil
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(req) do |res|
res.read_body do |chunk|
chunk.each_line do |line|
line = line.chomp
if line.start_with?("event: ") then event = line[7..]
elsif line.start_with?("data: ") && event == "delta"
buf << (JSON.parse(line[6..])["text"] || "")
end
end
end
end
end
puts JSON.parse(buf[buf.index("{")..buf.rindex("}")])["verdict"]
$buf = "";
$event = null;
$ch = curl_init(API . "/run-stream");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($runInput),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"Accept: text/event-stream",
"Authorization: Bearer " . $TOKEN,
"Idempotency-Key: " . $key,
],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$buf, &$event) {
foreach (explode("\n", $chunk) as $line) {
if (str_starts_with($line, "event: ")) { $event = substr($line, 7); }
elseif (str_starts_with($line, "data: ") && $event === "delta") {
$buf .= json_decode(substr($line, 6), true)["text"] ?? "";
}
}
return strlen($chunk);
},
]);
curl_exec($ch);
curl_close($ch);
echo $buf;
var req = new HttpRequestMessage(HttpMethod.Post, Api + "/run-stream");
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token);
req.Headers.Add("Accept", "text/event-stream");
req.Headers.Add("Idempotency-Key", key);
req.Content = new StringContent(JsonSerializer.Serialize(runInput), Encoding.UTF8, "application/json");
using var res = await Http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());
var buf = new StringBuilder();
string? evt = null, line;
while ((line = await reader.ReadLineAsync()) is not null) {
if (line.StartsWith("event: ")) evt = line[7..];
else if (line.StartsWith("data: ") && evt == "delta")
buf.Append(JsonDocument.Parse(line[6..]).RootElement.GetProperty("text").GetString());
}
Console.WriteLine(buf.ToString());
The output contract
The reply is exactly one JSON object — no prose, no code fences. These
fields are taken from the app's own normalize(), which is the render contract: a field
with the wrong type degrades to an empty state in the UI rather than throwing, so a wrong type is a
silent defect, not a loud one.
The envelope, in both lanes
| field | type | notes |
|---|---|---|
lane | string | equals the requested task. A reply that disagrees is rendered as the requested lane and the disagreement is surfaced. |
title | string | short name for the protocol. |
protocol | string | the named protocol, or the literal "unnamed". |
headline | string | one sentence a reviewer could paste into a ticket. |
verdict | string | diagram: complete / partial / not-a-protocol. model: ready-to-verify / needs-diagram-detail / not-modelable. |
verdict_reason | string | two sentences at most, citing the input. |
input_kind | string | mermaid proverif tamarin code rfc prose. The model's own judgement, which may disagree with prescan_facts. |
parties[] | array | {id, name, role, inferred, note}. inferred: true means the input never named that party. |
assumptions[] | string[] | present even when empty. |
open_questions[] | string[] | what a human must answer before this is trustworthy. |
coverage_check[] | array | {flag_id, handling, note}; handling is addressed or set-aside. |
summary | string | three to five sentences. |
diagram lane body
| field | type | notes |
|---|---|---|
message_flow[] | array | {step, from, to, label, kind, phase, note}. kind is network logical broadcast abort async reply. One row is exactly one arrow in mermaid, same order. |
crypto_ops[] | array | {party, operation, expression, after_step, purpose}. party must be an id from parties. |
phases[] | array | {name, from_step, to_step, purpose}. |
abort_paths[] | array | {trigger, party, behaviour, in_diagram, from_input}. from_input: false means the protocol needs it but the text never described it. |
mermaid | string | the sequenceDiagram source. Every participant declared explicitly before its first arrow. |
ascii | string | a monospace rendering, for places Mermaid will not render. |
protocol_summary | object | keys parties, round_complexity, key_primitives, authentication, forward_secrecy, notable. |
ambiguities[] | array | {id, what, where, inference, marker}. |
model lane body
| field | type | notes |
|---|---|---|
pv | string | the complete .pv file in canonical section order. |
model_sections[] | array | {order, section, code, why}. The eleven sections: channels, noselect, types, constants, functions, equations, tables, events, queries, let-processes, main process. |
channels[] | array | {name, visibility, carries}; visibility is public or private. |
events[] | array | {name, params, fired_by, when}. |
queries[] | array | {id, property, proverif, proves, expected, strength}; strength is sanity / weak / strong. A query whose expected is false exists to demonstrate a weakness. |
delivery_checklist[] | array | {check, status, evidence}; status is pass / fail / n/a. |
model_assumptions[] | array | {assumption, why, risk_if_wrong} — what the symbolic abstraction hides. |
run_plan[] | array | {order, action, command, expect}. |
What the web app checks afterwards, and you should too
- Every
message_flowrow is one arrow inmermaid, in order. The page parses the generated Mermaid and compares the counts. - Every
crypto_ops[].partyis an id present inparties. - No implicit participants in the generated Mermaid.
- If any
abort_paths[].in_diagramis true, the Mermaid contains analt/optblock or a crossed arrow. - Every event named in a
queries[].proverifstring appears inevents. - At least one reachability query exists. Without it, no other query result means anything.
- The generated
pvis re-linted by the same in-browser ProVerif reader that checked the input: a channel is declared; destructor-shaped functions (verify,adec,sdec,aead_dec,open) carry an inlinereduc; every declared event is fired; every channel is both written and read; the main process replicates its roles with!; parentheses and(* *)comments balance.
Idempotency — and why the key must carry the lane
Pass Idempotency-Key on every /run and /run-stream. The web
app uses proto-verif:{lane}:{hash of the input}:a{attempt}, where the hash covers
task, protocol, context, read_as and
properties.
The lane belongs in the key. Diagramming and then modelling the same protocol are
two distinct runs; a key without the lane would make the second one return the first one's answer.
Conversely, a retry after a network blip or a malformed reply must reuse a key derived from the
same input — otherwise a transport error double-bills. The web app's reformat retry increments
only the a{attempt} suffix and tells the user it is spending one extra run.
Metering, in one place
/guest,/meand/estimateare free./runand/run-streamare metered and need a personal token. A guest gets 403.hold_creditsis reserved, priced against the full output cap. The settled figure incharged_creditsis usually far lower. Never quote the hold as a price.- If the balance sits between
min_creditsandhold_credits, the run still executes with a reduced output cap and the terminal job carriestruncated: true. Render what arrived and offer a top-up; do not present a clipped model as complete. - Failed runs are not billed.
- The publisher's markup is
markup_bps: 1000— ten per cent of the run's base cost.
Not a verifier
This service reads text and writes text. It does not run ProVerif, Tamarin or any other verifier, and it does not execute pasted code. A model it returns is a starting point for verification by a person who understands the protocol. A reply never claims a protocol is secure — it says what each query would establish if it came back true, which is a different and more useful statement.