moxzi
Docs / On-chain builds / Your first on-chain build

Your first on-chain buildalpha

How do I submit a build, watch it, and get the artifact?

A remote build is one command: moxzi build <entry.mo> --remote <url> --canister <id>. The constraint to know before you run it is that the command blocks for the whole build — it drives the job itself, one step update call at a time — so a large project holds the terminal for as long as the compile takes.

Before you start#

The CLI builds with its own identity, never the anonymous principal, because the canister keys the filesystem, the storage quota and job ownership by caller. On first use it generates a 32-byte key and announces it:

moxzi: new build identity at /Users/you/Library/Application Support/moxzi/identity.key

Set MOXZI_IDENTITY to put it elsewhere. That file is the account that pays; fund the principal the CLI prints on connect. If the deployment has no ledger configured, it compiles for free and says so.

Signing with a key you already have#

There is no shared identity store on the Internet Computer: dfx and icp-cli keep separate ones, down to different keyring service names, and neither reads the other's. What every tool does agree on is a PEM file, so that is the interop surface:

FlagWhat it does
--pem <FILE>Sign with this PEM. ed25519, secp256k1 and prime256v1, PKCS#8 or SEC1 — what dfx identity export and icp identity export emit.
--identity-dfx <NAME>Shortcut for a dfx identity: ~/.config/dfx/identity/<NAME>/identity.pem (or under $DFX_CONFIG_ROOT).
--identity-icp <NAME>Shortcut for an icp-cli identity: <data-dir>/identity/keys/<NAME>.pem (or under $ICP_HOME).

The shortcuts are conveniences over --pem, not a separate mechanism, so a key either tool holds in its keyring cannot be read by either of them — icp-cli uses the keyring by default, and dfx does when you ask for it. In that case export once and pass the file:

icp identity export alice > /tmp/alice.pem   # or: dfx identity export alice > /tmp/alice.pem
moxzi build src/main.mo --remote … --canister … --pem /tmp/alice.pem
rm /tmp/alice.pem

Think before reaching for these. A dfx or icp-cli identity is usually the controller of the canisters it created; moxzi's own key is a hot wallet holding what a few builds cost. If you point --remote at a compiler canister you do not control, the difference is the whole story. The default exists for that reason.

The command#

moxzi build src/main.mo --remote https://icp-api.io --canister xcn6l-iqaaa-aaaai-raq6q-cai

Useful alongside it:

flagdefaultwhat it does
--progress N0 (quiet)print step i every N steps
--max-steps N5000000give up after N steps
--yesoffskip the payment confirmation (for scripts)
--package NAME PATHfrom mopsmake a package available to mo:<name>
--root DIRnearest mops.toml/dfx.json ancestorwhat /src maps to
--out WASM<entry>.wasmwhere the artifact lands
--no-linkoffkeep the canister's raw unlinked module
--no-idloffdo not write the .did

--status-secs is accepted but has no effect on the remote path; the remote progress reporter only knows --progress. --stack-budget defaults to 2000 for remote builds (the on-chain value every proven build ran with) rather than the canister's own default of 200 — an explicit --stack-budget still wins.

What the CLI actually does#

#callkindwhy
1getPricequeryis payment configured at all? ledger = null means a free deployment
2quoteEscrowupdatewhat will be pulled, at the CMC's live ICP/XDR rate
3icrc1_balance_of on the ledgerqueryso "you cannot afford this" is said before any money moves
4icrc2_approve on the ledgerupdateapproves escrowE8s + ledgerFeeE8s to the compiler canister
5write / appendChunkupdateone call per source file; files over 1,900,000 B are chunked
6startEscrowed (or start if free)updatepulls the escrow and returns the job id
7stepupdate, in a loopone call per compile step until #done
8jobWasmSize + jobWasmChunkquerypages the artifact out when it exceeded the reply cap
9settleStep then getSettlementupdate, querydrives the refund machine and reports what it cost
10getCandidquerythe job's interface text

Then it links the module locally and writes <entry>.wasm plus <entry>.did.

What you see#

A real free build — the first wasm this compiler ever produced on mainnet, 2026-08-20:

$ moxzi build examples/hello.mo --remote https://icp-api.io --canister xcn6l-iqaaa-aaaai-raq6q-cai --yes
remote: payment not configured on this deployment (free build)
remote: uploaded 1 file(s), 217 B
examples/hello.mo -> hello-onchain.wasm (remote, 220356 bytes, 1 file(s), 52 steps, 71.08s)

The same source compiled locally seconds later gave the same sha256, 6ba1d78373e6d949e2432fdc475b5019eff92b0d91a1009f06f1d510b7d583a8.

On a deployment that charges, four more lines appear before the upload and one after the build:

remote: this build escrows 0.3501 ICP (rate 35000 XDR-permyriad/ICP); settlement charges 3x actual use and refunds the rest
remote: your balance is 2.0000 ICP
proceed? [y/N] y
remote: approved 0.3502 ICP
remote: escrow pulled, job 0
remote: settled (done): charged 0.0009 ICP, refunded 0.3489 ICP

The settlement line is best-effort. If it lags, the CLI prints settlement pending and still keeps your wasm — settleStep is idempotent and an operator can recover a stranded settlement, so a slow refund never turns a good artifact into an error.

Watching a build from outside#

The CLI's own loop does not query status. If you want to watch a running job from another terminal or a dashboard, these are the read methods — all owner-or-operator, and they refuse silently to anyone else:

methodreturns
status(id)phase, yields, instructionsUsed, messages — the phase string carries heap and memory figures
jobDiags(id)the job's diagnostics as rendered file:line.col: [code] text
jobWasmSize(id)artifact size, 0 while unfinished
jobStats()jobs, finished, artifactBytes, errored (open to anyone)
sdStatus()(active, steps, last) for the canister's self-driven stepper

instructionsUsed and messages together are the billing basis, which is why they are readable: a customer can check the charge arithmetic.

Things that go wrong#

symptomcausefix
--remote requires --canister <ID>no canister idpass --canister
insufficient balance before any transferunfunded identitysend ICP to the principal printed on connect — or sign with one that has it: --identity-dfx <name>, --identity-icp <name>, --pem <file>
has no PEM on disk — icp-cli stores keys in the system keyringthe key is in the OS keyring, where no other tool can read iticp identity export <name> > /tmp/x.pem, then --pem /tmp/x.pem
is stored in the system keyring, not as a file (dfx)same, for dfxdfx identity export <name> > /tmp/x.pem, then --pem /tmp/x.pem
this deployment cannot fund a build of that size right nowthe canister's own cycle balance cannot finish the jobask the operator to top up; nothing was charged
step budget exhausted after N steps--max-steps too low for the projectraise it
the entry imports the actor class library <p>, which a remote build cannot yet pre-buildan actor class import the CLI could not orderbuild the library first, or compile locally
the artifact fails wasm-validateyou passed --no-linksee Linking and installing
the second build sits doing nothingone canister is one queuewait, or use another canister

Next#

On this pageBefore you startSigning with a key you already haveThe commandWhat the CLI actually doesWhat you seeWatching a build from outsideThings that go wrongNext