Contract verification
Rebuild a Cairo contract in Starkscan's isolated verifier and obtain a portable signed exact-verification receipt.
Contract verification
Starkscan can rebuild a locked Cairo/Scarb source tree in a network-denied
gVisor sandbox and compare the produced Sierra class hash with the finalized
on-chain declaration. The workflow is public self-serve: every active
workspace API key has the independent verify scope. There is no invite list.
Use the starkscan CLI for the complete workflow. Direct HTTP is documented
below for integrations that need to own the asynchronous job lifecycle.
What exact verification means
A successful receipt proves that the uploaded source archive, the named build
target, and the selected pinned Scarb policy from starkscan-exact-v1 through
starkscan-exact-v25 reproduced the class hash
in a finalized Starknet declaration. It does not prove security, an audit,
official authorship, deployment configuration, or that a particular address
still uses the class. The receipt says this explicitly in notProofOf.
package-check, awaiting_upload, queued, and building are not verification
results. mismatch, failed, cancelled, and expired never issue receipts.
Only succeeded after independent controller validation and restricted receipt
admission represents verified_exact.
Prerequisites
- the current
@starkscan/clipackage; - an API key from Starkscan API keys;
- a source directory containing root
Scarb.tomlandScarb.lock; - every local/path dependency needed by the build inside that directory;
- the exact reviewed Scarb executable when
Scarb.lockcontains Git or registry dependencies; - the exact finalized class hash and unambiguous Scarb target selectors.
The hosted service accepts only SN_MAIN, release builds, Starknet contract
targets, and the 25 reviewed Scarb policies listed below. The SDK rejects verification lifecycle calls for
other chain IDs locally. You can prepare and check a chain-bound
SN_SEPOLIA package locally, but hosted submission rejects it until Starkscan
operates a separately reviewed controller attached to the canonical Sepolia
data owner. Unsupported chains and toolchains fail closed; Starkscan does not
silently substitute a compiler or declaration source.
Install the public CLI and confirm that this command family is present:
npm install --global @starkscan/cli
starkscan verify --helpFor a reproducible automation image, pin the exact stable version shown on the
@starkscan/cli registry page
instead of relying on the moving latest tag.
1. Prepare a deterministic package
First preview the exact files that would enter the archive by adding --list
to the complete prepare command below. Preview mode validates Package V1,
checks the declared manifest toolchain against the hosted policy, builds the
deterministic archive in memory, prints selectedPaths, and writes nothing.
All prepare arguments remain required so the preview covers the exact package
you intend to submit.
Choose the policy whose Scarb and Cairo versions match the locked source. For
example, use starkscan-exact-v1 for Argent sources pinned to Scarb/Cairo
2.6.3, starkscan-exact-v22 for Scarb/Cairo 2.13.1, or
starkscan-exact-v25 for 2.20.0. Starkscan never downloads or guesses a
toolchain from the submitted source.
starkscan verify prepare \
--source ./my-contract \
--output ./my-contract.package.json \
--archive ./my-contract.source.tar.gz \
--package my_contract \
--target-name my_contract \
--artifact-id my_contract_MyContract \
--module-path my_contract::MyContract \
--profile release \
--class-hash 0x1234 \
--toolchain-policy-id starkscan-exact-v25 \
--retention-policy-id private-24h-v1 \
--source-visibility private \
--scarb /absolute/path/to/scarb \
--listRemove --list and rerun the same command to create the package. Preparation
is local. For a dependency-free lockfile it makes no network request and
--scarb may be omitted. For a lockfile with Git or registry dependencies,
the CLI runs the exact policy-matching --scarb executable in a temporary,
empty cache, fetches the immutable lockfile closure, rejects any lockfile
change, removes active Git metadata, and embeds the credential-free offline
cache in the same archive. Git transport may include immutable history needed
to prove the locked commit, so two preparations at different times are not
promised to produce identical archive bytes; the upload digest binds the exact
archive and the verified source/material digests remain policy-canonical. That
preparation step uses your network; the hosted compiler remains network-denied.
Both output files are
create-only mode-0600; an existing path is never overwritten. The archive is
one canonical gzip member containing the selected source tree and, when
required, a reserved .starkscan dependency-material namespace. The source
tree contains Scarb.toml, Scarb.lock, .cairo compiler inputs, and only
manifest-declared README/license files. User/group IDs and modification times
are normalized. VCS, build, dependency, and local-state directories such as
.git, .starkscan, target, and node_modules are excluded at any depth;
unrelated files such as .env, keystores, snapshots, and undeclared Markdown
are not selected. Selected symlinks, devices, FIFOs, sockets, unapproved nested
archives, absolute/traversing paths, ambiguous names, and trailing gzip data are
rejected. The policy-bound source tree remains capped at 10 MiB compressed; a
single source-plus-dependency bundle is capped at 64 MiB compressed. Inspect
selectedPaths before removing --list:
the CLI cannot determine whether a selected Cairo or manifest file itself
contains material you should not upload.
Package V1 contains exactly these fields:
{
"selectedClassHash": "0x1234",
"uploadDigest": "sha256:<digest of the exact gzip bytes>",
"uploadByteLength": 12345,
"toolchainPolicyId": "starkscan-exact-v25",
"packageName": "my_contract",
"buildProfile": "release",
"targetName": "my_contract",
"targetKind": "starknet-contract",
"artifactId": "my_contract_MyContract",
"modulePath": "my_contract::MyContract",
"sourceVisibility": "private",
"retentionPolicyId": "private-24h-v1"
}For uploadByteLength, mathematically integral JSON spellings such as 12345,
12345.0, and 12345e0 normalize to one canonical unsigned integer before
validation and idempotency processing. Fractional, negative, and out-of-range
values are rejected.
chainId, local filenames, source bytes, and the idempotency key are not
Package V1 fields. The route selects the chain. The CLI sends idempotency only
in the case-sensitive Idempotency-Key HTTP header. Its value is 1–128
characters from A–Z, a–z, 0–9, ., _, :, and -; reuse the same
value only when retrying the exact same package submission.
2. Check locally
starkscan verify package-check \
--package ./my-contract.package.json \
--archive ./my-contract.source.tar.gzThis reopens both regular files safely, validates the archive envelope, and
checks its exact digest and byte length. It does not compile, reproduce the
class hash, upload, submit, or create a verification result. JSON output makes
those boundaries explicit with compilationPerformed=false,
classHashReproduced=false, uploaded=false, and verifiedExact=false.
It also revalidates every embedded dependency against the lockfile and rejects
missing, extra, or modified cache objects before a production job can be
created. The hosted worker additionally reconstructs every Git checkout from
the exact locked commit, rejects executable Scarb extensions in source and Git
dependency manifests, and the controller binds registry checksums to the
policy-allowlisted primary registry before dispatch.
3. Submit
export STARKSCAN_API_KEY='<your workspace API key>'
starkscan --chain SN_MAIN verify submit ./my-contract.package.json \
--archive ./my-contract.source.tar.gz \
--visibility privateFor the 30-day public-intake retention class, set Package V1 to public, use
--visibility public, and add --accept-public-verification-retention. This
is a local acknowledgement of the selected retention period. Package V1 does
not publish source or record a license or redistribution right; source
publication needs a separate, explicitly recorded agreement.
The CLI creates the job first, then uploads the exact gzip body. It does not
retry either mutation automatically. On an interrupted create it prints the
idempotency key required for an identical replay; after creation it prints both
the jobId and that key in any upload error. Replaying an already-committed
identical upload is safe, including two concurrent retries. Never invent a new
key until you have checked state.
4. Poll, cancel, and download
starkscan --chain SN_MAIN verify status vrf_<job-id>
starkscan --chain SN_MAIN verify cancel vrf_<job-id>
starkscan --chain SN_MAIN verify receipt vrf_<job-id> \
--output ./receipt.jsonCancellation is best effort. It wins before immutable receipt admission. If a valid receipt was admitted first, success wins the race and remains auditable.
5. Authenticate the receipt offline
Download and pin Starkscan's published receipt trust-root JSON once over your normal authenticated release channel. Then disconnect the network and run:
starkscan verify receipt-check ./receipt.json \
--trust-root ./starkscan-verification-receipt-trust-root-v1.json \
--offlineThe command checks the strict bundle shape, key ID, Ed25519 signature over
canonical JSON evidence, verified_exact outcome, and exact proof scope. It
does not contact Starkscan. A copied receipt without its separately trusted
public-key document is not authenticated.
Receipt receiptId and declaration.blockNumber accept mathematically
integral decimal or exponent spellings and normalize to canonical unsigned
integers before validation and signature verification. Receipt issuers sign
that canonical integer representation.
Job states
| State | Meaning | Retry yourself? |
|---|---|---|
awaiting_upload | Metadata accepted; source body not committed | Upload before uploadExpiresAt |
validating | Archive metadata is being committed | No |
queued | Ready for the isolated worker | Poll with backoff |
building | Networkless gVisor build in progress | No |
comparing | Independent controller comparison | No |
succeeded | Signed receipt is available | Download it |
mismatch | Rebuilt hash differed | Fix source/target/toolchain; use a new job |
failed | Typed failure | Retry only when retryable=true |
cancelled | Owner cancellation won | Use a new job if needed |
expired | Upload window elapsed | Create a new job |
The service allows one active job and five creates per UTC day per workspace,
with a bounded global queue. 429 is a workspace quota/concurrency response;
503 is temporary service or global-capacity unavailability. Neither is a
verification result.
Complete CLI reference
Global flags may appear before verify: --base-url (or
STARKSCAN_BASE_URL), --chain (or STARKSCAN_CHAIN), --timeout-ms (or
STARKSCAN_TIMEOUT_MS), --retries, --api-key (or STARKSCAN_API_KEY),
--output-format text|json, and --pretty. Keep API keys in the environment;
command arguments can be visible in shell history and process listings. The
hosted default base is https://api.starkscan.co and the default chain is
SN_MAIN.
| Command | Required inputs | Optional inputs | Network behavior |
|---|---|---|---|
verify prepare | --source, --output, --package, --target-name, --artifact-id, --module-path, --profile release, --class-hash, one --toolchain-policy-id from starkscan-exact-v1 through starkscan-exact-v25, --retention-policy-id, --source-visibility | --archive chooses the new archive path; --scarb is required for Git/registry locks; --list previews the exact selection and writes nothing | dependency-free locks: none; external locks: bounded Scarb fetch during preparation only |
verify package-check | --package | --archive; otherwise the sidecar is used | none |
verify submit | package positional path, --visibility, workspace API key | --archive, explicit --idempotency-key; the public-intake retention class also requires --accept-public-verification-retention | one metadata POST followed by one source PUT; mutations are never retried automatically |
verify status | job ID | global safe-read retry/timeout settings | safe GET; bounded transient retries |
verify cancel | job ID | none | one POST; never retried automatically |
verify receipt | job ID, --output | global safe-read retry/timeout settings | safe GET; output is create-only |
verify receipt-check | receipt path, --trust-root, --offline | none | none; any missing --offline is rejected |
JSON output is one object with a stable top-level command result. Prepare
reports both output paths, exact digest/length, and durability warnings. A
prepare --list preview reports selected paths/count/bytes, in-memory archive
length, policy compatibility, and explicit false compilation/verification
booleans. Submit
reports jobId, the effective idempotency key, initial state, and recommended
poll delay. Status returns the owner-visible job view. Receipt reports whether
the destination was durably committed. Receipt-check reports signature and
proof-scope validity without contacting Starkscan. Human text is for terminals;
automation should always select --output-format json and inspect the process
exit status before reading fields.
| Exit | Class | Meaning |
|---|---|---|
0 | success | The operation completed. For status, inspect the returned job state; exit 0 does not turn a pending job into verified. |
1 | runtime | Local I/O, malformed server response, or another non-HTTP runtime failure. |
2 | usage | Invalid flags, package, archive, receipt, trust root, or unsafe local path. |
3 | auth | Missing, invalid, revoked, expired, or insufficient-scope API key. |
4 | rate limited | Workspace request/job quota. Respect Retry-After; do not create a second logical job. |
5 | timeout/unavailable | Network timeout or temporary service capacity. Read the structured retry signal and recover by idempotency/status rules. |
6 | not found | The job/receipt is absent, not ready, or deliberately indistinguishable from another workspace's job. |
--retries applies only to safe reads. submit does not retry metadata creation
or source upload, and cancel does not retry its mutation. If submission is
interrupted after metadata creation, keep the job ID and run status. If the
metadata response itself was ambiguous, repeat only with the exact same
explicit idempotency key and identical Package V1. A different body with the
same key is a conflict. If the source response was ambiguous, retry the same
package and idempotency key: the matching quarantine object and ledger commit
are idempotent. Never create multiple keys to guess whether a previous mutation
committed.
Supported build matrix
The public matrix is deliberately bounded to the 25 already-reviewed Scarb policies. Every policy pins its executable and runtime digests; the worker has the tools preinstalled and runs with network disabled.
| Dimension | Supported | Rejected |
|---|---|---|
| hosted chain | SN_MAIN | SN_SEPOLIA and every other route value |
| policy | starkscan-exact-v1 through starkscan-exact-v25 | direct-Cairo, future, or unrecognized policies |
| build profile | release | dev, custom profiles, implicit defaults |
| target kind | starknet-contract | libraries, tests, executables, plugins |
| selection | explicit package, target name, artifact ID, and module path | null, omitted, inferred, or ambiguous selectors |
| manifest | root Scarb.toml | nested-only or missing manifest |
| lock | root immutable Scarb.lock | missing, mutable, or unresolved dependency graph |
| dependencies | complete local/path source plus CLI-resolved exact Git/registry lock material in the reserved archive namespace | worker network fetches, branch/tag resolution, registry mutation, scripts, procedural-macro workspaces, missing or extra cache material |
| archive | canonical gzip plus POSIX ustar within every published bound, content-addressed by the submitted digest | zip, zstd, multiple gzip members, unsafe or oversized tar content |
| Policy | Scarb | Cairo | Sierra |
|---|---|---|---|
starkscan-exact-v1 | 2.6.3 | 2.6.3 | 1.5.0 |
starkscan-exact-v2 | 2.10.1 | 2.10.1 | 1.7.0 |
starkscan-exact-v3 | 2.11.2 | 2.11.2 | 1.7.0 |
starkscan-exact-v4 | 2.16.1 | 2.16.1 | 1.7.0 |
starkscan-exact-v5 | 2.15.0 | 2.15.0 | 1.7.0 |
starkscan-exact-v6 | 2.17.0 | 2.17.0 | 1.8.0 |
starkscan-exact-v7 | 2.4.1 | 2.4.1 | 1.4.0 |
starkscan-exact-v8 | 2.9.4 | 2.9.4 | 1.6.0 |
starkscan-exact-v9 | 2.12.0 | 2.12.0 | 1.7.0 |
starkscan-exact-v10 | 2.6.4 | 2.6.4 | 1.5.0 |
starkscan-exact-v11 | 2.11.4 | 2.11.4 | 1.7.0 |
starkscan-exact-v12 | 2.12.2 | 2.12.2 | 1.7.0 |
starkscan-exact-v13 | 2.16.0 | 2.16.0 | 1.7.0 |
starkscan-exact-v14 | 2.5.1 | 2.5.1 | 1.4.0 |
starkscan-exact-v15 | 2.8.4 | 2.8.4 | 1.6.0 |
starkscan-exact-v16 | 2.15.1 | 2.15.0 | 1.7.0 |
starkscan-exact-v17 | 2.4.3 | 2.4.3 | 1.4.0 |
starkscan-exact-v18 | 2.14.0 | 2.14.0 | 1.7.0 |
starkscan-exact-v19 | 2.9.1 | 2.9.1 | 1.6.0 |
starkscan-exact-v20 | 2.9.2 | 2.9.2 | 1.6.0 |
starkscan-exact-v21 | 2.12.1 | 2.12.1 | 1.7.0 |
starkscan-exact-v22 | 2.13.1 | 2.13.1 | 1.7.0 |
starkscan-exact-v23 | 2.18.0 | 2.18.0 | 1.8.0 |
starkscan-exact-v24 | 2.19.0 | 2.19.0 | 1.9.0 |
starkscan-exact-v25 | 2.20.0 | 2.20.0 | 1.9.3 |
The direct-Cairo policies used by Starkscan's governed historical import are a different package/build contract and are not accepted by this public CLI lane.
unsupported_policy, unsupported_toolchain, and incomplete/offline-material
failures are terminal for that job and create no receipt. Update the locked
source tree or wait for a newer published policy; do not remove the lockfile,
change the declared hash, or use a nearby compiler to force a match.
CI and automation
This bounded polling example never prints the API key and uploads only the source-free signed receipt as a CI artifact:
set -euo pipefail
export STARKSCAN_API_KEY="${STARKSCAN_API_KEY:?CI secret is required}"
starkscan --chain SN_MAIN --output-format json verify package-check \
--package contract.package.json --archive contract.source.tar.gz
submit_json="$(starkscan --chain SN_MAIN --output-format json verify submit \
contract.package.json --archive contract.source.tar.gz --visibility private)"
job_id="$(printf '%s' "$submit_json" | jq -er '.data.jobId')"
state=queued
for attempt in $(seq 1 150); do
status_json="$(starkscan --chain SN_MAIN --output-format json verify status "$job_id")"
state="$(printf '%s' "$status_json" | jq -er '.data.state')"
case "$state" in
succeeded) break ;;
mismatch|failed|cancelled|expired) exit 1 ;;
esac
sleep 2
done
test "$state" = succeeded
starkscan --chain SN_MAIN --output-format json verify receipt "$job_id" \
--output receipt.json
starkscan --output-format json verify receipt-check receipt.json \
--trust-root starkscan-verification-receipt-trust-root-v1.json --offlineUse the CI provider's masked secret store and disable command tracing around credentialed calls. Do not upload private source, API keys, raw HTTP captures, compiler working directories, or controller evidence. Cancellation belongs in an explicit interrupted-job cleanup handler; it is best effort and must never be reported as successful verification.
Direct HTTP contract
All external requests use the preferred API-host shape
https://api.starkscan.co/v1/... with X-Starkscan-Api-Key. The compatibility
/api/v1/... shape remains available, but new integrations should use /v1.
POST /v1/{chain}/verification-jobswith Package V1 andIdempotency-Key.PUT /v1/{chain}/verification-jobs/{jobId}/sourcewithContent-Type: application/gzip, noContent-Encoding, and exactContent-Length.GET /v1/{chain}/verification-jobs/{jobId}until terminal.- Optional
POST .../{jobId}/cancel. GET .../{jobId}/receiptonly aftersucceeded.
Jobs are visible only to the owning workspace. Config-only internal keys,
browser sessions, and keys without verify cannot use the routes. Error bodies
never reveal whether another workspace owns an identifier.
Privacy, retention, and isolation
Source bytes stream directly to private encrypted object storage. PostgreSQL
stores only bounded metadata, digests, opaque object keys, lifecycle events,
resource counters, and the source-free receipt. The API process does not build
source. The controller fetches one exact object and sends it to a root-owned
forced command on the isolated class-verifier host. That command has no
database, object-store, API, signing, receipt-writer, deploy, or production
credentials. Each build runs non-root in gVisor with network denied, a read-only
root filesystem, dropped capabilities, no new privileges, bounded CPU/memory/
processes/time, and deterministic pinned tools. Working copies are deleted.
Private source is deleted after the selected 24-hour or seven-day policy. The
public Package V1 class selects the 30-day public-intake retention policy; it
does not make source bytes public. Source publication or redistribution remains
a separate, explicitly recorded agreement and is not implemented by Package V1.
Deleting source never deletes the minimum append-only audit record or falsifies
an older receipt.
Failure guide
invalid_verification_package: validate all 12 fields and select the matching supported Scarb policy withrelease.verification_upload_mismatch: do not recompress the archive after prepare.verification_upload_capacity_exhausted: wait for the response'sRetry-Afterdelay, then replay the same job and exact archive. This429happens before Starkscan creates a durable upload reservation. Each workspace may stream one upload at a time.verification_unavailable: retry the same job and exact archive after the response'sRetry-Afterdelay. Upload bodies that provide no nonempty chunk for 15 seconds fail closed with this503, so a stalled client cannot occupy shared intake capacity. If a later retry returns409because the old job no longer accepts uploads, create a new verification job instead of replaying it.finalized_declaration_unavailable: wait for the declaration to finalize.lockfile_missing: include the rootScarb.lockand all offline material.verifier_dispatch_failed: temporary isolated-worker failure; inspectretryablebefore resubmitting.class_hash_mismatch: the selected source, target, or toolchain does not reproduce the declared class. Starkscan intentionally does not turn this into a weaker verification tier.receipt_signing_failed: no success is published; retry only after operator recovery.
For machine-readable details, use the OpenAPI contract, capability discovery, its capabilities schema, the retention and isolation contract, and the Package V1 schema. Receipt consumers should also pin the portable receipt schema, trust-root schema, and current public trust root. The TypeScript SDK guide, Privacy Policy, and support apply to the same public service.