Your first actoralpha
How do I run an actor, call it, upgrade it, and watch state survive?
moxzid is a server that hosts the wasm moxzi build produced and speaks IC message semantics to it: one message at a time per actor, await as a commit point, a trapping message rolled back, upgrades that keep state. The constraint that shapes every example below is that request bodies are raw candid bytes, not JSON — the empty argument is the six bytes DIDL\x00\x00, never an empty body.
Build two versions#
cat > counter.mo <<'EOF'
persistent actor {
var count : Nat = 0;
public func inc() : async Nat { count += 1; count };
public query func peek() : async Nat { count };
};
EOF
moxzi build counter.mo -o counter.wasm
Then a v2 that adds a method, so the upgrade has something to show:
cat > counter2.mo <<'EOF'
persistent actor {
var count : Nat = 0;
public func inc() : async Nat { count += 1; count };
public query func peek() : async Nat { count };
public query func doubled() : async Nat { count * 2 };
};
EOF
moxzi build counter2.mo -o counter2.wasm
Start the server#
A manifest names the actors to host:
cat > actors.json <<'EOF'
{ "actors": [ { "name": "counter", "wasm": "counter.wasm" } ] }
EOF
moxzid -m actors.json -s ./state
-s ./state is what makes the state durable. Without it the server is in-memory and everything dies with the process — the right default for a scratch run and the wrong one for anything else, so it is explicit either way. The default bind is 127.0.0.1:7000; a non-loopback bind without --auth-token is refused at startup, because POST /install runs arbitrary wasm.
Wait for readiness before sending traffic. /health only says the process is up; the listener binds before journal recovery finishes:
curl -s http://127.0.0.1:7000/health/ready # -> ready
Call it#
printf 'DIDL\x00\x00' > unit
curl -s --data-binary @unit http://127.0.0.1:7000/call/counter/inc | xxd -p
curl -s --data-binary @unit http://127.0.0.1:7000/call/counter/inc | xxd -p
curl -s --data-binary @unit http://127.0.0.1:7000/query/counter/peek | xxd -p
4449444c00017d01
4449444c00017d02
4449444c00017d02
Those replies are candid: 4449444c is DIDL, 00 an empty type table, 01 one return value, 7d the type nat, and the last byte the value — 1, then 2, then 2. Any candid encoder (didc, @dfinity/candid, whatever dfx generate writes) produces and decodes these bytes.
The split between the two endpoints is the IC's: /call/… is replicated and its writes stick, /query/… is non-replicated and its writes roll back when it returns.
Upgrade it#
curl -s -X POST --data-binary @counter2.wasm http://127.0.0.1:7000/upgrade/counter
upgraded
curl -s --data-binary @unit http://127.0.0.1:7000/query/counter/peek | xxd -p # 4449444c00017d02
curl -s --data-binary @unit http://127.0.0.1:7000/query/counter/doubled | xxd -p # 4449444c00017d04
The counter still says 2, and doubled — a method that did not exist a moment ago — answers 4. The upgrade runs the IC's own sequence: pre_upgrade, memory persists, the new module's start function runs over it, post_upgrade.
upgrade and install are different verbs on purpose. POST /install/<name> over an existing actor is refused, because installing would discard state.
Watch it survive a crash#
moxzid -m actors.json -s ./state & PID=$!
# … calls as above …
kill -9 $PID
moxzid -m actors.json -s ./state &
curl -s http://127.0.0.1:7000/health/ready
curl -s --data-binary @unit http://127.0.0.1:7000/query/counter/peek | xxd -p
ready
4449444c00017d02
Not a graceful stop — kill -9, no pre_upgrade, no checkpoint on the way out. The state comes back from a snapshot plus write-ahead-log replay. Two recovery paths exist and both are exercised by scripts/moxzid_crash.sh: snapshot (the default for actors at or below --snapshot-below, 50 MB) and pure log replay (--snapshot-below 0).
For an orderly backup, stop with SIGTERM instead: it finishes the in-flight message, checkpoints every actor and exits 0. Do not copy the state directory while the server is running — you will get torn writes.
The endpoints you will use first#
| Endpoint | Auth | What |
|---|---|---|
GET /health/ready | open | ready once the manifest is installed and journals replayed |
POST /call/<actor>/<method> | token | candid in, candid out; 402 means out of cycles, 400 a trap or reject |
POST /query/<actor>/<method> | token | same, non-replicated |
POST /upgrade/<name> | token | body = wasm; keeps state |
POST /install/<name> | token | body = wasm; refused if the name exists; replies with the new actor's hex id |
GET /logs/<actor> | token | Debug.print output, taken and cleared |
GET / | open | the inspector dashboard, in a browser |
On a loopback bind with no --auth-token, the token checks are off — that is why the curls above carry no Authorization header. Set --auth-token (or MOXZID_TOKEN) and every endpoint except /health, /health/ready and /version needs Authorization: Bearer <token>.
Things you cannot do#
- You cannot use
@dfinity/agent'sActorclass. It polls for a certificate, which is a subnet BLS signature, and there is no subnet here.ic0.data_certificate_presentreports 0 by design. The supported surface is the generatedidlFactory. - You cannot ignore a
stable-typeswarning on upgrade. It means the new module changes the actor's stable signature incompatibly. Both the IC's replica and moxzid refuse such an upgrade — moxzid by pre-flighting the new signature throughmoxzi stable-compatibleand reporting the reasons; the old code keeps serving - You cannot rely on
--heartbeat-ms 0to disable timers. Heartbeats and global timers are separate mechanisms;Timer.setTimerruns off--timer-ms(default 100 ms).
Next#
- Choosing a runtime — when the server is the right host, and when the browser or the IC is.
- Reading diagnostics — when the actor will not compile in the first place.
- Projects and packages — grow this one file into a real multi-file project.