← Proto Verif / API
Tokens

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": { ... }}}
codeHTTPwhat it means
unauthorized401Missing, malformed or expired token. Mint a new one.
forbidden403A guest token tried to /run. Runs need a personal token.
payment_required402Balance below min_credits. Call /estimate first and compare against /me.
validation_error400The run body was not an object, or a field was the wrong type.
rate_limited429Back off and retry. Never tight-loop.
internal500The 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.

fieldtyperequirednotes
taskstringyesthe literal "diagram"
protocolstringyesthe 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.
contextstringnowhere the protocol runs, who is trusted, what is out of scope. It changes the answer.
read_asstringnoone 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_factsobjectnothe 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.

fieldtyperequirednotes
taskstringyesthe literal "model"
protocolstringyesideally 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.
contextstringnosame as above.
propertiesstringnofree text naming the properties you care about. Reachability, session-key secrecy and injective authentication are always written; this only adds queries.
prescan_factsobjectnoas 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"

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.

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}}

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"])'

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}

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

fieldtypenotes
lanestringequals the requested task. A reply that disagrees is rendered as the requested lane and the disagreement is surfaced.
titlestringshort name for the protocol.
protocolstringthe named protocol, or the literal "unnamed".
headlinestringone sentence a reviewer could paste into a ticket.
verdictstringdiagram: complete / partial / not-a-protocol. model: ready-to-verify / needs-diagram-detail / not-modelable.
verdict_reasonstringtwo sentences at most, citing the input.
input_kindstringmermaid 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.
summarystringthree to five sentences.

diagram lane body

fieldtypenotes
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.
mermaidstringthe sequenceDiagram source. Every participant declared explicitly before its first arrow.
asciistringa monospace rendering, for places Mermaid will not render.
protocol_summaryobjectkeys parties, round_complexity, key_primitives, authentication, forward_secrecy, notable.
ambiguities[]array{id, what, where, inference, marker}.

model lane body

fieldtypenotes
pvstringthe 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

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

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.