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:
| Flag | What 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:
| flag | default | what it does |
|---|---|---|
--progress N | 0 (quiet) | print step i every N steps |
--max-steps N | 5000000 | give up after N steps |
--yes | off | skip the payment confirmation (for scripts) |
--package NAME PATH | from mops | make a package available to mo:<name> |
--root DIR | nearest mops.toml/dfx.json ancestor | what /src maps to |
--out WASM | <entry>.wasm | where the artifact lands |
--no-link | off | keep the canister's raw unlinked module |
--no-idl | off | do 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#
| # | call | kind | why |
|---|---|---|---|
| 1 | getPrice | query | is payment configured at all? ledger = null means a free deployment |
| 2 | quoteEscrow | update | what will be pulled, at the CMC's live ICP/XDR rate |
| 3 | icrc1_balance_of on the ledger | query | so "you cannot afford this" is said before any money moves |
| 4 | icrc2_approve on the ledger | update | approves escrowE8s + ledgerFeeE8s to the compiler canister |
| 5 | write / appendChunk | update | one call per source file; files over 1,900,000 B are chunked |
| 6 | startEscrowed (or start if free) | update | pulls the escrow and returns the job id |
| 7 | step | update, in a loop | one call per compile step until #done |
| 8 | jobWasmSize + jobWasmChunk | query | pages the artifact out when it exceeded the reply cap |
| 9 | settleStep then getSettlement | update, query | drives the refund machine and reports what it cost |
| 10 | getCandid | query | the 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:
| method | returns |
|---|---|
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#
| symptom | cause | fix |
|---|---|---|
--remote requires --canister <ID> | no canister id | pass --canister |
insufficient balance before any transfer | unfunded identity | send 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 keyring | the key is in the OS keyring, where no other tool can read it | icp identity export <name> > /tmp/x.pem, then --pem /tmp/x.pem |
is stored in the system keyring, not as a file (dfx) | same, for dfx | dfx identity export <name> > /tmp/x.pem, then --pem /tmp/x.pem |
this deployment cannot fund a build of that size right now | the canister's own cycle balance cannot finish the job | ask the operator to top up; nothing was charged |
step budget exhausted after N steps | --max-steps too low for the project | raise it |
the entry imports the actor class library <p>, which a remote build cannot yet pre-build | an actor class import the CLI could not order | build the library first, or compile locally |
the artifact fails wasm-validate | you passed --no-link | see Linking and installing |
| the second build sits doing nothing | one canister is one queue | wait, or use another canister |
Next#
- How a build runs — what those thousands of steps are doing.
- What a build costs — the escrow quote, the charge, the refund.
- Linking and installing — the artifact is unlinked; here is why.