Starkscan

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 pinned starkscan-exact-v25 toolchain 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/cli package;
  • an API key from Starkscan API keys;
  • a source directory containing root Scarb.toml and Scarb.lock;
  • every local/path dependency needed by the build inside that directory;
  • the exact finalized class hash and unambiguous Scarb target selectors.

The initial hosted service accepts only SN_MAIN, release builds, Starknet contract targets, and starkscan-exact-v25. 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 --help

For 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

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

Preparation is local and makes no network request. Both output files are create-only mode-0600; an existing path is never overwritten. The archive is one deterministic gzip member containing a sorted POSIX ustar tree. User/group IDs and modification times are normalized. Root .git, .starkscan, and target directories are excluded. Symlinks, devices, FIFOs, sockets, nested archives, absolute/traversing paths, ambiguous names, trailing gzip data, and archives over 10 MiB are rejected.

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

This reopens both regular files safely, validates the archive envelope, and checks its exact digest and byte length. It still does not compile or submit.

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 private

For 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.json

Cancellation 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 \
  --offline

The 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

StateMeaningRetry yourself?
awaiting_uploadMetadata accepted; source body not committedUpload before uploadExpiresAt
validatingArchive metadata is being committedNo
queuedReady for the isolated workerPoll with backoff
buildingNetworkless gVisor build in progressNo
comparingIndependent controller comparisonNo
succeededSigned receipt is availableDownload it
mismatchRebuilt hash differedFix source/target/toolchain; use a new job
failedTyped failureRetry only when retryable=true
cancelledOwner cancellation wonUse a new job if needed
expiredUpload window elapsedCreate 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.

CommandRequired inputsOptional inputsNetwork behavior
verify prepare--source, --output, --package, --target-name, --artifact-id, --module-path, --profile release, --class-hash, --toolchain-policy-id starkscan-exact-v25, --retention-policy-id, --source-visibility--archive chooses the new archive path; otherwise the deterministic sidecar name is usednone
verify package-check--package--archive; otherwise the sidecar is usednone
verify submitpackage positional path, --visibility, workspace API key--archive, explicit --idempotency-key; the public-intake retention class also requires --accept-public-verification-retentionone metadata POST followed by one source PUT; mutations are never retried automatically
verify statusjob IDglobal safe-read retry/timeout settingssafe GET; bounded transient retries
verify canceljob IDnoneone POST; never retried automatically
verify receiptjob ID, --outputglobal safe-read retry/timeout settingssafe GET; output is create-only
verify receipt-checkreceipt path, --trust-root, --offlinenonenone; 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. 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.

ExitClassMeaning
0successThe operation completed. For status, inspect the returned job state; exit 0 does not turn a pending job into verified.
1runtimeLocal I/O, malformed server response, or another non-HTTP runtime failure.
2usageInvalid flags, package, archive, receipt, trust root, or unsafe local path.
3authMissing, invalid, revoked, expired, or insufficient-scope API key.
4rate limitedWorkspace request/job quota. Respect Retry-After; do not create a second logical job.
5timeout/unavailableNetwork timeout or temporary service capacity. Read the structured retry signal and recover by idempotency/status rules.
6not foundThe 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 launch matrix is deliberately one immutable lane:

DimensionSupportedRejected
hosted chainSN_MAINSN_SEPOLIA and every other route value
policystarkscan-exact-v25earlier, future, or unrecognized policies
build profilereleasedev, custom profiles, implicit defaults
target kindstarknet-contractlibraries, tests, executables, plugins
selectionexplicit package, target name, artifact ID, and module pathnull, omitted, inferred, or ambiguous selectors
manifestroot Scarb.tomlnested-only or missing manifest
lockroot immutable Scarb.lockmissing, mutable, or unresolved dependency graph
dependenciescomplete locked material inside the submitted tree and accepted v25 policynetwork fetches, branch/tag resolution, registry mutation, scripts, missing path material
archivedeterministic gzip plus POSIX ustar within every published boundzip, zstd, multiple gzip members, unsafe or oversized tar content

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

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

  1. POST /v1/{chain}/verification-jobs with Package V1 and Idempotency-Key.
  2. PUT /v1/{chain}/verification-jobs/{jobId}/source with Content-Type: application/gzip, no Content-Encoding, and exact Content-Length.
  3. GET /v1/{chain}/verification-jobs/{jobId} until terminal.
  4. Optional POST .../{jobId}/cancel.
  5. GET .../{jobId}/receipt only after succeeded.

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 use v25/release.
  • verification_upload_mismatch: do not recompress the archive after prepare.
  • finalized_declaration_unavailable: wait for the declaration to finalize.
  • lockfile_missing: include the root Scarb.lock and all offline material.
  • verifier_dispatch_failed: temporary isolated-worker failure; inspect retryable before 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.

On this page