STRK20 prover relay
Submit STRK20 transaction proofs through Starkscan as asynchronous jobs, with per-key budgets and an explicit delivery-safety contract.
STRK20 prover relay
Starkscan is the authenticated front door for the Starknet Foundation's STRK20 transaction prover. You submit a proof request with your API key, Starkscan queues it, forwards it, and hands back the proof.
Access is operator-issued and mainnet-only. Prove scope cannot be created from the API key page or redeemed through an access invite; an operator issues it only after a builder is approved.
Two different "not available" responses are worth distinguishing before you debug a client:
404— the relay is not enabled in that environment. The routes are not registered at all, so every caller gets404regardless of key or scope. This is the current state everywhere; the surface is dormant until the relay is turned on.403— the relay is enabled but your key lacksprovescope.
So a 404 is not a wrong URL and a 403 is not a missing route.
Why it is asynchronous
A proof occupies a dedicated prover slot for an unbounded workload-dependent
duration. Rather than hold an HTTP response open, proving is a job: you
submit, you get a jobId, you poll. Starkscan does not publish a latency or
throughput promise until representative workload evidence is certified.
Before you submit
The prover simulates your transaction at the block you pin and refuses to prove one that reverts. Three preconditions are easy to miss, because each surfaces as an error about something else.
Pin an explicit block accepted by the prover
Use an explicit finalized block rather than relying on a moving tag. The allowed distance from head is prover policy and may change. Do not hard-code an offset from one observed response: if the prover reports that the block is too recent, repin to an older explicit block.
State you depend on must exist at the pinned block
Because simulation happens at the pinned block, anything the transaction needs — an ERC20 approval, pool registration, channel setup — must already be on chain there, not merely at the head. Wait until the setup state exists at the explicit block you will prove; do not infer pinned-state readiness from elapsed time or from one error message.
Allowance must cover the amount plus the pool fee
The pool charges get_fee_amount() per apply_actions on top of whatever you
deposit or transfer, and pulls both over the same ERC20 allowance. Approving only
the transfer amount reverts with:
Insufficient ERC20 allowanceRead get_fee_amount() from the pool at the pinned block rather than assuming a
fee. Estimate gas for the exact transaction before signing; this page does not
publish an unverified fee or gas figure.
Submit a proof
curl -X POST \
-H "X-Starkscan-Api-Key: $STARKSCAN_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H 'content-type: application/json' \
-d '{
"block_id": {"block_number": 12446898},
"transaction": { "...": "an Invoke transaction" }
}' \
"https://api.starkscan.co/v1/SN_MAIN/prove"Idempotency-Key is required
Submission requires an Idempotency-Key header: 16–128 graphic ASCII
characters — no spaces, no control characters, no ". Surrounding whitespace is
rejected too. This is not boilerplate — it is what makes retrying safe. A UUID is
a good default and fits the range.
A proof holds the only prover slot for its whole duration and costs a unit of
your daily budget. If your 202 is lost to a timeout or a dropped connection you
have no job id to poll, and a naive retry would start a second proof. With the
key, the retry returns the original job instead.
| Situation | Response |
|---|---|
| New key | 202 with a new jobId |
| Same key, same body | 200 with the original jobId. No second proof, no second budget debit |
| Same key, different body | 409 idempotency_key_reused |
| Missing or malformed key | 400 idempotency_key_required / invalid_idempotency_key |
Notes:
- Use one fresh key per logical submission — a UUID is ideal — and reuse that same key for every retry of it.
- Keys are scoped to your workspace, so they cannot collide with another tenant's.
- Body comparison ignores JSON object key ordering, so re-serializing your request on retry is safe. Array order is significant, since calldata order is.
- Concurrent duplicates coalesce: fire the same key twice at once and exactly one job is created.
- Starkscan stores only one-way digests of the key and of your request, never the key itself and never the payload.
block_id and transaction are passed to the prover unchanged. The transaction
must be an Invoke transaction; the prover rejects other kinds.
HTTP/1.1 202 Accepted
X-Starkscan-Rpc-Class: rpc_prove{
"jobId": "prv_9f2c1ab34de56789012345ab",
"status": "queued",
"terminal": false,
"attemptCount": 0,
"queuePosition": 2,
"pollAfterSeconds": 10,
"createdAt": "2026-07-29T12:00:00+00:00"
}Poll the job
curl \
-H "X-Starkscan-Api-Key: $STARKSCAN_API_KEY" \
"https://api.starkscan.co/v1/SN_MAIN/prove/$JOB_ID"Poll until terminal is true. Honor pollAfterSeconds; do not spin.
status | terminal | Meaning |
|---|---|---|
queued | false | Accepted, waiting for the prover |
dispatched | false | Proof running |
succeeded | true | result carries the proof |
failed | true | Your request was rejected; error.code is the prover's own code |
unavailable | true | Retry the same key safely; before opening a new job, see the caveat below |
unknown_delivery | true | The prover may have received the request. Do not resubmit automatically |
A successful poll:
{
"jobId": "prv_9f2c1ab34de56789012345ab",
"status": "succeeded",
"terminal": true,
"attemptCount": 1,
"createdAt": "2026-07-29T12:00:00+00:00",
"completedAt": "2026-07-29T12:04:31+00:00",
"result": {
"proof": "...",
"proof_facts": "...",
"l2_to_l1_messages": []
}
}The result is delivered once
Read this before you build a client. Starkscan never writes proof payloads to disk. A completed proof is held in memory, delivered on the first successful poll, and then dropped. It also expires, and it does not survive a relay restart.
So: persist the proof the moment you receive it. If you poll again, or poll
too late, you get the job with no result and:
{
"status": "succeeded",
"terminal": true,
"resultUnavailableReason": "delivered_or_expired"
}The proof is not recoverable. You must resubmit, which costs another slot and another unit of your daily budget. This is the deliberate cost of Starkscan not retaining what you asked it to prove.
Errors
Three terminal outcomes tell you what to do next.
Your request — passed through with the prover's exact code, under
error.source: "prover":
| Code | Meaning |
|---|---|
24 | Block not found |
55 | Account validation failed |
61 | Unsupported transaction version |
1000 | Invalid transaction input |
-32603 | Transaction reverted only when error.data begins Reverted transactions are not supported; revert reason:. The remainder includes the Cairo diagnostic, for example ('NEGATIVE_INTERMEDIATE_BALANCE'). |
Absorbed — status: "unavailable" with
error.code: "prover_unavailable". This can be a relay failure or a complete
prover error outside the table above. The prover's message is not surfaced for
this outcome, and timing cannot distinguish the two. Retry the same idempotency
key to recover the existing terminal job without another debit. If it persists,
re-check the public preconditions above and contact support with the poll
response's jobId and attemptCount before submitting a new logical job. A new
key creates a new job and consumes another daily-budget unit; the same key does
not.
A bare or otherwise unconfirmed -32603 is also absorbed. -32603 is the
generic JSON-RPC internal-error code, so Starkscan only returns it as a caller
transaction revert when the prover supplies the documented revert-data prefix.
For every passed-through error, the full upstream error object is held only
in relay memory and delivered to the owning workspace on its first terminal
poll. Starkscan never logs or stores its message or data. Persist the error
object immediately: after a relay restart, expiry, or a later poll, the durable
numeric error.code remains but error.data is unavailable.
A sanctions-screening rejection is passed through rather than absorbed: the
deposit was refused, and resubmitting it unchanged will be refused again. For an
absorbed failure, do not rotate your Starkscan API key; retry the same logical
job key and contact support with jobId and attemptCount if it persists.
Delivery uncertain — status: "unknown_delivery" with
error.code: "prover_delivery_unknown". The relay cannot prove whether the
prover received the request, so it intentionally does not resend it. Keep the
jobId and attemptCount, then contact support before submitting a new proof.
Budgets
Prove requests are budgeted by concurrency and per-day volume, not per minute. One proof can hold the only slot for minutes, so a per-minute limit would be meaningless.
| Response | Code | Meaning |
|---|---|---|
409 | idempotency_key_reused | The key was already used for a different request |
429 | prover_daily_budget_exhausted | Out of proofs for this UTC day. Retry-After points at UTC midnight |
429 | prover_key_concurrency | You already hold your maximum in-flight proofs |
503 | prover_queue_full | Shared queue saturated; self-clearing |
503 | prover_unavailable | Prover is not currently available |
Exact numbers are per-key and set when your access is provisioned. Honor
Retry-After rather than hard-coding assumptions.
X-Starkscan-Rpc-Class is rpc_prove on these routes. See
Rate limits.
A minimal client loop
set -euo pipefail
# One key per logical submission; reuse it on every retry of that submission.
IDEMPOTENCY_KEY="$(uuidgen)"
JOB_ID="$(curl -sS -X POST \
-H "X-Starkscan-Api-Key: $STARKSCAN_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H 'content-type: application/json' \
--data-binary @request.json \
"https://api.starkscan.co/v1/SN_MAIN/prove" | jq -r '.jobId')"
while :; do
body="$(curl -sS -H "X-Starkscan-Api-Key: $STARKSCAN_API_KEY" \
"https://api.starkscan.co/v1/SN_MAIN/prove/$JOB_ID")"
if [ "$(printf '%s' "$body" | jq -r '.terminal')" = "true" ]; then
# Persist immediately: the result is delivered exactly once.
printf '%s' "$body" | jq '.result' > proof.json
printf '%s' "$body" | jq -r '.status'
break
fi
sleep "$(printf '%s' "$body" | jq -r '.pollAfterSeconds // 10')"
doneLimits worth knowing
- Mainnet only. There is no Sepolia prover.
- Invoke transactions only.
- Request bodies are capped at 1 MiB.
starknet_proveTransactionis the only proving operation exposed. It is not part of the public Starknet JSON-RPC specification.