Skip to main content

Verify your alkane on SUBFROST

You deployed an alkane. Anyone can read its bytecode on chain, and nobody can read its intent. Verification closes that gap. You hand the explorer your source and the recipe you built it with, the explorer rebuilds it, and if the rebuilt wasm matches the bytes on chain, your alkane page shows the badge and serves your source next to it.

This page is the practical guide. Verifying alkanes explains the verdicts in more depth, and Reproducible builds goes through the reproduction formula axis by axis.

How it works​

A verification has five steps. Each one exists for a reason, and knowing the reason makes failures easy to read.

  1. You submit. A POST to the verify endpoint carries the alkane id and your source, either a public git URL or the whole tree packed into the request as an inline bundle. The recipe can be a handful of flat fields or a full BuildInfo document. The API checks everything before it spends a build: every problem comes back at once, each one naming the field that caused it.

  2. The recipe fills itself in. The explorer reads your alkane's on-chain bytecode and reverses the build environment out of it. A Rust wasm carries more than code. Panic messages embed file paths, and those paths spell out your build HOME, your toolchain directory, the git revisions of your git dependencies and the registry layout. The producers section records the rustc and clang versions. Whatever you leave out of the request is taken from there, and whatever you send overrides it.

  3. The build runs in isolation. Your build scripts are code from the internet as far as we are concerned, so they run where they can do no harm. Each job gets a fresh virtual machine on a separate host, built from a sealed image and thrown away afterwards. The VM accepts no inbound connections and can reach the public internet only on the ports needed to fetch sources and crates. It has no route to any SUBFROST system. Inside the VM the work splits in two containers: a fetch step that downloads your dependencies, then a build step with no network at all. Both run read-only, unprivileged, with every capability dropped.

  4. The build is reproducible. The VM rebuilds with the exact toolchain your bytecode names, from a pinned image, at fixed paths. The toolchain matters because every rustc release generates different code. The paths matter because they are embedded in the wasm. The rust-src component matters because its presence changes how standard library paths are written. When your repo commits a Cargo.lock, that lockfile is used as is. When it does not, the crates.io index is frozen to your deploy date so dependency resolution gives the versions you had then.

  5. The bytes are compared and the result is recorded. The rebuilt wasm is compared with the decompressed on-chain bytecode, section by section. A match is promoted on its own, with no human in the loop, and stored under the sha256 of the bytecode, so every clone of the same template shows as verified too. The alkane page then shows the verdict, a browsable copy of the exact source tree that was built, and the reproduction recipe.

Nothing in this flow is trusted because of who sent it. A wrong recipe simply fails to match, so any active API key may submit.

Verified and reproducible are different claims​

The explorer gives one of four verdicts. Two of them earn the badge.

VerdictWhat the explorer showsWhat it proves
reproducibleReproducibleThe rebuilt wasm has the same sha256 as the on-chain bytecode. Byte for byte, your published source produces what is deployed.
verified≈ Verified (not byte-exact)The code section is byte-identical, or at least 97% of the non-custom bytes match. The logic is the same; a few bytes elsewhere differ, usually embedded paths or a C object compiled on a different host.
partialPartialAt least 90% matches. No badge. Some part of the recipe is still wrong.
mismatchMismatchBelow 90%. No badge. This source does not produce this bytecode.

reproducible is the strong claim and the one to aim for. verified tells a reader the deployed logic is your source, while admitting the artifact is not a bit-exact rebuild. The status response gives you both hashes, so you can always see which one you got and why.

Quick start​

You need an active SUBFROST API key (API Keys). The key goes in the path.

export SUBFROST_API_KEY=...            # your key
export API=https://explorer.subfrost.io/api/v1/$SUBFROST_API_KEY

1. Public git repo​

curl -sS -X POST "$API/verify" \
-H 'content-type: application/json' \
-d '{
"alkane": "4:797",
"repo_url": "https://github.com/kungfuflex/alkanes-rs",
"commit": "d787cdd0",
"package": "free-mint"
}'

Add "subdir": "path/to/crate" when the crate is not at the root of the repo. The clone happens inside the build VM, so only public repos on github.com, gitlab.com, codeberg.org or bitbucket.org work this way. Anything else goes in an inline bundle.

2. Inline bundle (private, self-hosted or deleted repos)​

An inline bundle is your source tree as a gzipped tarball, base64 encoded, sent in the request. No git access is needed and nothing is fetched from your servers. The tree you send is the tree that is built and the tree that is published on your alkane's source page, so bundle exactly the workspace the alkane builds from.

Pack it from the workspace root, the directory that holds the Cargo.toml with [workspace] (or the crate's own Cargo.toml for a single crate) and your Cargo.lock:

cd path/to/workspace
tar czf /tmp/src.tgz --exclude=./target --exclude=./.git --exclude=./node_modules .
base64 -w0 /tmp/src.tgz > /tmp/src.b64 # macOS: base64 -i /tmp/src.tgz -o /tmp/src.b64

Build the request body with jq. --rawfile reads the bundle from a file, which matters because a large bundle does not fit in a single shell argument:

jq -n --rawfile b64 /tmp/src.b64 '{
alkane: "2:96782",
repo_url: "inline://subminer/alkanes/subminer-treasury@2a5739e1",
package: "subminer-treasury",
inline_src_b64: ($b64 | rtrimstr("\n"))
}' > /tmp/body.json

The inline:// label is provenance only. It is shown on the alkane page and never fetched, so use something that tells a reader where the tree came from, such as a repo path and a commit. Leave repo_url out and the explorer labels the bundle inline:///<block>/<tx>.

3. Dry run first​

A dry run does every check a real submission does and builds nothing. It unpacks your bundle the way the build VM will, hashes it, confirms the alkane has bytecode on chain, and shows the recipe the explorer reversed from that bytecode. Dry runs have their own, larger rate limit, so iterate here.

curl -sS -X POST "$API/verify?dry_run=1" \
-H 'content-type: application/json' --data-binary @/tmp/body.json | jq
{
"ok": true,
"dry_run": true,
"alkane": "2:96782",
"request_hash": "5e0c…",
"bundle": {
"b64_bytes": 22192, "gz_bytes": 16643,
"sha256": "b8ab8162…", "unpacked_bytes": 71680,
"files": 7, "dirs": 2, "root_dir": "",
"has_cargo_toml": true, "has_cargo_lock": true
},
"onchain": { "sha256": "…", "size": 223578 },
"recipe_from_bytecode": {
"rustc": "1.91.0 (…)", "clang": "18.1.3",
"home_dir": "/home/ubuntu", "host_triple": "x86_64-unknown-linux-gnu",
"git_deps": [ … ]
},
"warnings": []
}

Read warnings carefully. A missing Cargo.lock, a target/ directory in the bundle or a rust-toolchain.toml that will be ignored all show up here.

4. Submit and poll​

Send the same body without dry_run:

curl -sS -X POST "$API/verify" -H 'content-type: application/json' \
--data-binary @/tmp/body.json > /tmp/run.json
RUN=$(jq -r .run_id /tmp/run.json)

The response is the new run, queued. Poll its status until done is true. A build takes a few minutes; the worked example below took about eight and a half.

while :; do
curl -sS "$API/verify/$RUN" > /tmp/status.json
jq -r '"\(.status) \(.stage // "")"' /tmp/status.json
[ "$(jq -r .done /tmp/status.json)" = true ] && break
sleep "$(jq -r '.poll_after_seconds // 15' /tmp/status.json)"
done
jq '{verdict_class, match_pct, hashes, note}' /tmp/status.json

Sending the identical request again while it is still building returns the same run with "deduplicated": true instead of queuing a second build.

5. Read the result​

{
"verdict_class": "reproducible",
"match_pct": 100,
"hashes": {
"expected": { "sha256": "035c0653…", "size": 223578 },
"built": { "sha256": "035c0653…", "size": 223578 },
"byte_exact": true
}
}

expected is the on-chain bytecode and built is what the build VM produced. Equal hashes mean reproducible. When they differ, match_pct and sections tell you where, and logs_tail holds the end of the build log for a failed build.

The alkane page at https://explorer.subfrost.io/alkane/<id> shows the verdict and the recipe, and https://explorer.subfrost.io/alkane/<id>/source serves the verified tree. For tooling, the verification record is one call:

curl -sS "$API/verification/2/96782" | jq

It returns verified, verdict_class, the on-chain bytecode sha256 (hashed by the explorer itself), built_sha256, byte_exact, the source provenance and the recipe that was used.

Worked example: SUBMINER treasury 2:96782​

The SUBMINER treasury lives in a private repo, so it was verified from an inline bundle of seven files, about 22 KB of base64. The request carried these recipe fields alongside the bundle:

{
"alkane": "2:96782",
"repo_url": "inline://subminer/alkanes/subminer-treasury@2a5739e1",
"package": "subminer-treasury",
"rustc": "1.91.0",
"clang_version": "18",
"home_dir": "/home/ubuntu",
"host_triple": "x86_64-unknown-linux-gnu",
"build_env": "CLANG_SOURCE=ubuntu\nREMAP_PREFIX=/root/.rustup=/home/ubuntu/.rustup;/home/ubuntu/src=/home/ubuntu/subminer/alkanes"
}

The build ran in about eight and a half minutes and came back verified at 99.986%, shown as ≈ Verified (not byte-exact) on the alkane page with its source. The code section is identical. Sixteen bytes in the data section are not, and the reason is worth knowing because it catches many builds.

The original was built with its workspace at /home/ubuntu/subminer/alkanes and its toolchain under /home/ubuntu/.rustup. The build VM lays the source out at $HOME/src and the toolchain under /root/.rustup, so the recipe used REMAP_PREFIX to rewrite those paths with --remap-path-prefix. The rewrite worked for the paths themselves. But any non-empty RUSTFLAGS changes the hash cargo gives each compilation unit, and that hash is part of the directory a build script writes its output to: target/…/build/<crate>-<hash>/out/generated.rs. The crate includes generated code, its panic locations carry that path, and the hash inside it no longer matched. Rebuilt in the original layout with empty RUSTFLAGS, the same source is byte-exact.

The lesson is in the tips below: build what you deploy in a layout the verifier can repeat without remapping.

Request reference​

POST /api/v1/{key}/verify, JSON body. The schema is published at /schemas/verify-request-v1.json.

Only alkane and a source are required. Every other field is reversed from the bytecode when you leave it out, and overrides that reversal when you send it.

FieldTypeDefaultNotes
alkanestringrequired"block:tx", both decimal. Can come from build_info.identity.alkane_id instead.
repo_urlstringinline:///<block>/<tx> with a bundleAn https clone URL on github.com, gitlab.com, codeberg.org or bitbucket.org, or an inline:// label for a bundle.
inline_src_b64stringnonebase64 of a .tar.gz of the tree. Line breaks are tolerated.
commitstringdefault branchGit ref to check out. Provenance only for a bundle.
packagestringdefault memberscargo -p.
subdirstringtree rootThe crate's directory inside the tree. Relative, no ...
rustcstringfrom the bytecodeAn exact release such as 1.91.0, or a dated channel such as nightly-2025-03-20. stable is refused because it cannot reproduce a past build.
clang_versionstringfrom the bytecode18 or 18.1.3. Only matters when the crate compiles C, for example secp256k1.
home_dirstringfrom the bytecodeThe build HOME, for example /home/alice. CARGO_HOME is $HOME/.cargo unless CARGO_HOME_OVERRIDE says otherwise.
host_triplestringfrom the bytecodex86_64-unknown-linux-gnu, x86_64-apple-darwin, x86_64-pc-windows-msvc. Apple Silicon and aarch64 Linux have no build runner and are refused.
git_pinsstringfrom the bytecodeOne https-url rev [bare|rev] per line.
git_url, alkanes_rev, git_source_formstringalkanes-rs, from the lock, bareA single git dependency pin, the older form of git_pins.
git_date, freeze_commitstringthe deploy dateFor builds without a Cargo.lock: freeze the crates.io index and unpinned git deps to a date or an index-archive commit.
build_envstring or arraynoneKEY=VALUE lines from an allow-list: CLANG_SOURCE, USE_HOMEBREW_CLANG, NO_RUST_SRC, REMAP_PREFIX, CARGO_HOME_OVERRIDE, PUBLISH_PATHS, COMMITTED_LOCK, FREE_MINT_BUILD_IN_PROGRESS, GIT_SOURCE_FORM, GIT_DATE, TYPO_FIX, the Windows and macOS build directory knobs, and CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUSTFLAGS restricted to --remap-path-prefix rules.
typo_fixbooleanfalseRewrites a https:/ typo in Cargo.toml before resolving.
build_infoobjectnoneA BuildInfo document, below.
dry_runbooleanfalseSame as ?dry_run=1.

An unknown field is ignored with a warning, so a typo such as reop_url shows up in warnings instead of silently doing nothing.

BuildInfo reference​

BuildInfo is the self-contained build description alkanes-cli build-info produces. Send it as build_info and the explorer maps each axis onto the recipe. A partial document is fine; only source is required. Flat fields in the same request win over BuildInfo values, and each override is reported in warnings. The schema is at /schemas/build-info-1.0.0.json.

BuildInfo fieldRequiredEffect
schema_versionnoMust be 1.0.0 when present.
identity.alkane_idnoUsed as alkane when the body has none; must equal it when both are sent.
identity.target_sha256noMust equal the on-chain bytecode sha256, or the request is refused with target_sha256_mismatch. A cheap guard against verifying the wrong build.
identity.target_sourcenoonchain. A local-file BuildInfo describes a wasm that is not deployed yet and is refused.
source.kindyesgit or inline.
source.repo, commit, subdir, packagerepo for gitAs the flat fields.
source.inline_filesfor inline[{path, content}]. Text as is, binary as base64:<data>. The explorer packs them into a bundle, sorted, with fixed timestamps, so the same files always give the same bundle hash.
source.featuresnoMust be empty. The build VM builds default features only.
source.build_commandnoRecorded. The build is always cargo build --release --target wasm32-unknown-unknown [-p package].
toolchain.rustc_versionnorustc.
toolchain.host_triplenohost_triple. A toolchain directory name such as 1.86.0-aarch64-apple-darwin is read as its triple.
toolchain.targetnoMust be wasm32-unknown-unknown.
toolchain.os_image, rustc_commit, linkernoRecorded, not used.
c_toolchain.versionnoclang_version.
c_toolchain.source.methodnoapt sets CLANG_SOURCE=ubuntu, homebrew-bottle sets CLANG_SOURCE=homebrew, llvm-release uses the upstream LLVM release of that version.
environment.homenohome_dir.
environment.cargo_homenoCARGO_HOME_OVERRIDE when it is not $HOME/.cargo.
environment.build_envnoAppended to build_env.
environment.remap_path_prefixnoFROM=TO rules, sent as REMAP_PREFIX. Read the tips before you use it.
registry.modenocommitted-lock needs nothing; the builder detects your Cargo.lock. time-machine uses archive_commit as freeze_commit and timestamp as git_date.
git_depsnoEach {url, rev, source_form} becomes a git_pins line.
cargo_lock_b64noWith inline files, written to the tree root as Cargo.lock when the files have none.
resolved_deps, result, notesnoAccepted and ignored.

Source types​

https:// on a public forge is cloned inside the build VM. Submodules come along. Credentials in the URL are refused, and so is every other host.

inline://<label> with inline_src_b64 builds the tree you sent. This is the way to verify a private, self-hosted or deleted repo, and the one that gives readers the exact tree that was built. The label can be anything printable up to 256 characters; inline:/// with three slashes and a path is a common style.

build_info.source.inline_files is the same thing in BuildInfo form, packed into a bundle for you. Send one or the other, not both.

Reproducibility tips​

Most failed verifications come from a handful of causes. The dry run response and the status hashes tell you which one you hit.

Paths and RUSTFLAGS​

The wasm embeds absolute paths from three places: your registry and git checkouts under CARGO_HOME, the standard library under RUSTUP_HOME when rust-src is installed, and any build script output under target/. Your own crate's source files are embedded relative to the workspace, so the workspace location usually does not matter, except when a build script generates code.

See what your wasm carries before you submit:

strings target/wasm32-unknown-unknown/release/my_alkane.wasm | grep -E '^/|/\.cargo/|/\.rustup/|/build/.*/out/' | sort -u | head -40

--remap-path-prefix can rewrite paths, and the worked example shows its cost: any non-empty RUSTFLAGS changes cargo's unit hashes, and those hashes appear in build script output paths. If your crate has a build.rs that writes code, do not rely on remapping. Build the wasm you deploy in a layout the build VM repeats as is. The build VM's default layout is HOME=/root, CARGO_HOME=/root/.cargo, RUSTUP_HOME=/root/.rustup, with the workspace at /root/src. A build that ran there needs no remap at all:

docker run --rm -v "$PWD":/root/src -w /root/src ubuntu:24.04 bash -c '
apt-get update -qq && apt-get install -y -qq curl build-essential clang protobuf-compiler >/dev/null
curl -sSf https://sh.rustup.rs | sh -s -- -y -q --profile minimal --default-toolchain 1.91.0 -t wasm32-unknown-unknown
. /root/.cargo/env
cargo build --release --locked --target wasm32-unknown-unknown -p my-alkane'

Deploy the wasm that command produced, then verify with clang_version: "18", build_env: "CLANG_SOURCE=ubuntu\nNO_RUST_SRC=1" and your bundle. The minimal profile has no rust-src, which is why NO_RUST_SRC=1 is in the recipe.

rust-src​

With the rust-src component installed, rustc writes standard library panic paths as $RUSTUP_HOME/toolchains/<toolchain>/lib/rustlib/src/rust/library/…. Without it they read /rustc/<commit>/library/…, which is the same on every machine. The build VM installs rust-src unless the recipe says NO_RUST_SRC=1. If strings shows /rustc/ paths and no rustlib/src, send NO_RUST_SRC=1. If it shows rustlib/src under a RUSTUP_HOME other than /root/.rustup, expect the path problem above.

Lockfile​

Commit and bundle the Cargo.lock you deployed with. With it, the build uses exactly those versions with --locked. Without it, the build resolves against the crates.io index as of your deploy date, which usually works and sometimes does not, because a lockfile resolved weeks before the deploy picks older versions than the deploy date implies.

Features​

The build runs with default features. If you deployed with --features mainnet, make that feature set the default in the crate's Cargo.toml ([features] default = ["mainnet"]), commit it, and bundle that tree. The build output is identical; only how the features were selected changes.

Profile and opt-level​

[profile.release] comes from your workspace Cargo.toml, so opt-level, lto, codegen-units, strip and panic are whatever your tree says. Bundle the same Cargo.toml you built with, including the workspace root's when the crate is a member. A .cargo/config.toml in the tree is honoured as well, so keep it if you built with it.

wasm-opt and other post-processing​

The build VM runs nothing after cargo: no wasm-opt, no wasm-strip. It compares the raw cargo output. If you post-processed the wasm before deploying, the bytes cannot match. Deploy the cargo output directly when you want a verifiable alkane.

Toolchain and host​

rust-toolchain.toml files are removed before the build; the toolchain is the rustc in the recipe, or the one recorded in the bytecode. cargo hashes the build host into every crate, so a wasm built on macOS or Windows only reproduces on that host type. Intel macOS and Windows builds go to build VMs of that type. Apple Silicon builds cannot reproduce byte-exact on any other machine and are refused, so build on x86_64 Linux when you want a verifiable deploy.

clang​

Only crates that compile C, such as secp256k1, depend on clang. Its vendor and version string is written into the wasm. CLANG_SOURCE=ubuntu selects the distribution clang of the major you name, and CLANG_SOURCE=homebrew a Homebrew LLVM build.

Limits​

LimitValue
Inline bundle8 MiB of base64 (about 6 MiB of tar.gz)
Unpacked bundle256 MiB
Entries in a bundle20,000
Path length in a bundle255 bytes
Submissions6 per API key per hour
Dry runs60 per API key per hour
Build time90 minutes, then the run fails
Concurrent buildsa small shared queue; when it is full you get queue_full

Errors​

Every error response has the same shape. error is a readable message, code is stable for programs, field is a JSON Pointer into your request body, and errors lists every problem found, not only the first.

{
"ok": false,
"error": "rustc must be an exact release (1.86.0) or a dated channel (nightly-2025-03-20), got \"stable\"",
"code": "invalid_field",
"field": "/rustc",
"errors": [
{ "code": "invalid_field", "field": "/rustc", "message": "…" },
{ "code": "invalid_field", "field": "/build_env", "message": "build_env key PATH is not allowed" }
]
}
StatusCodeMeaning
400invalid_json, invalid_bodyThe body is not a JSON object.
400invalid_field, invalid_type, missing_fieldA field is malformed, has the wrong type, or is required and absent. See field.
400unsupported_fieldA BuildInfo axis the build VMs cannot honour, such as non-default features.
400conflicting_fields, alkane_id_mismatchTwo parts of the request disagree.
400bundle_invalid_base64, bundle_not_gzip, bundle_corruptThe bundle cannot be read.
400bundle_unsafe_path, bundle_special_fileThe bundle has absolute or .. paths, or device files.
400bundle_no_cargo_toml, subdir_not_in_bundleThe builder would not find your crate.
401unauthorizedMissing or inactive API key.
404no_bytecodeThe alkane has no bytecode on chain.
404run_not_foundNo run with that id.
409target_sha256_mismatchYour BuildInfo describes different bytecode.
413bundle_too_large, bundle_too_large_unpackedOver a bundle limit.
422unsupported_hostNo build runner exists for that host triple.
429rate_limited, queue_fullSlow down; Retry-After says how long.
503runner_unavailableBuilds for that host are paused for maintenance. Retry later.
502, 504upstream_unreachable, upstream_timeoutThe verification service did not answer. Check the verification record before you resubmit.

Warnings never block a submission. The common ones are no_lockfile, bundle_has_junk, rust_toolchain_ignored, unknown_field, overridden and ignored_field.

Status reference​

GET /api/v1/{key}/verify/{run_id}

FieldMeaning
statusqueued, building, then one of verified, unverified, failed, error.
stageProgress inside building, such as resolving or building.
done, poll_after_secondsStop polling when done is true.
verdict, verdict_classThe raw verdict and the class the alkane page shows: reproducible, verified, code_match, partial, mismatch, pending, unverified.
provenTrue for the badge verdicts.
hashesexpected (on chain) and built, each {sha256, size}, and byte_exact.
match_pct, sectionsHow close a non-exact build came, per wasm section.
logs_tail, logs_truncatedThe last 16 KiB of the build log.
linksPaths to the status, the record and the alkane pages. {key} stands for your key.

FAQ​

Is my private source published?​

The tree you bundle is shown on the alkane's source page once it verifies, because that is what verification proves. Bundle only the workspace the alkane builds from. PUBLISH_PATHS in build_env limits the published files to the paths you list, for a monorepo where other members are private.

Can a submission break someone else's verified alkane?​

No. A result is only recorded when the rebuild matches, and a weaker verdict never replaces a stronger one for the same bytecode.

My build failed. Where do I look?​

logs_tail in the status response. Compile errors, a missing Cargo.lock entry with --locked, or a dependency that needs a system library are the usual causes. Run the same build yourself in the container from the tips first; if it fails there it fails in the build VM too.

I got verified, not reproducible. Should I resubmit?​

Only with a changed recipe. Identical requests produce identical results. Compare the two hashes, look at which sections differ, and work through the tips. For a contract you have not deployed yet, the reliable route to reproducible is to build the deploy wasm in the default layout shown above.

Do clones of my template need their own verification?​

No. Results are stored by bytecode hash, so every alkane that runs the same bytecode shows the same verdict.

Can I verify before I deploy?​

Not through this endpoint; it compares against bytecode on chain. Reproduce the build locally with alkanes-cli build-info, deploy that wasm, then verify.

Next steps​