Starkscan

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 gets 404 regardless 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 lacks prove scope.

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 allowance

Read 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.

SituationResponse
New key202 with a new jobId
Same key, same body200 with the original jobId. No second proof, no second budget debit
Same key, different body409 idempotency_key_reused
Missing or malformed key400 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.

statusterminalMeaning
queuedfalseAccepted, waiting for the prover
dispatchedfalseProof running
succeededtrueresult carries the proof
failedtrueYour request was rejected; error.code is the prover's own code
unavailabletrueRetry the same key safely; before opening a new job, see the caveat below
unknown_deliverytrueThe 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":

CodeMeaning
24Block not found
55Account validation failed
61Unsupported transaction version
1000Invalid transaction input
-32603Transaction reverted only when error.data begins Reverted transactions are not supported; revert reason:. The remainder includes the Cairo diagnostic, for example ('NEGATIVE_INTERMEDIATE_BALANCE').

Absorbedstatus: "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 uncertainstatus: "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.

ResponseCodeMeaning
409idempotency_key_reusedThe key was already used for a different request
429prover_daily_budget_exhaustedOut of proofs for this UTC day. Retry-After points at UTC midnight
429prover_key_concurrencyYou already hold your maximum in-flight proofs
503prover_queue_fullShared queue saturated; self-clearing
503prover_unavailableProver 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')"
done

Limits worth knowing

  • Mainnet only. There is no Sepolia prover.
  • Invoke transactions only.
  • Request bodies are capped at 1 MiB.
  • starknet_proveTransaction is the only proving operation exposed. It is not part of the public Starknet JSON-RPC specification.

On this page