02 - First Movement (Dry-Run)
Experience the full flow in dry-run mode: seed a template -> create a movement -> approve it -> check status. Confirm behavior through the API layer only, without live exchange integration.Prerequisite: In 01-local-setup.md, the server is running on
localhost:8100 with MG_AUTH_ENABLED=false and MG_WORKERS_ENABLED=false.
Three Different Decisions
There is no single “approval template”. Moving funds takes three separate decisions, and each one is necessary but none is sufficient on its own:- Registering a template does not approve anything. By default every movement created from it
still waits in
PENDING_APPROVALfor a human. - An allowlisted destination only makes a movement eligible to dispatch. It is not an approval. CEX withdrawals also need the destination saved in the exchange’s own withdrawal whitelist.
- Automatic approval is a separate, bounded policy: a Free per-template cap
(
route_template.auto_approveproposal) or Proauto_approve_policyproposals scoped to a strategy/client, allowed template versions, and amount caps. Agent-originated movements are never auto-approved.
Registration Ladder
POST /v3/plan-templates and the
direct allowlist routes return 409 CONFIG_PROPOSAL_REQUIRED. Proposals are created and applied
from the dashboard (Governance -> Configuration proposals) when
MG_CONFIG_REMOTE_MUTATION_MODE=proposal_only; the applying admin must differ from the proposer.
The dry-run below seeds its template and allowlist rows with a dev-only tool instead. See
Movement Authorization Model for the full chain.
Full Flow
Why is there noPOST /v3/plan-templatesstep? Template and allowlist writes are configuration changes, not transfers, and they are governed: the direct remote routes always return409 CONFIG_PROPOSAL_REQUIRED. In a real deployment you register them as maker/checker configuration proposals (see Registering templates outside dev below). For this dry-run, a dev-only seed tool writes them straight into your local database.
Step 0: Seed the Dev Allowlist and Template
- placeholder CEX allowlist rows for
upbitandbinance - the dry-run template
dev.cex.upbit_to_binance.xrp(v1): withdraw -> withdrawal observe -> deposit observe - dev HMAC keys you do not need while
MG_AUTH_ENABLED=false
Step 1: Inspect the Route
version:1graph_shape:"linear"andexecutable:truerisk_controls.max_amount- the only bindingrisk_controlskey (enforced fail-closed at dispatch)
What the template body does and does not decide.risk_controlsis free-form and onlymax_amountbinds;asset_allowlistis descriptive (hash-pinned, not enforced). Theapproval_policyblock (for example{"mode": "manual_first"}) is stored and returned but no runtime code reads it — it does not select manual or automatic approval. Approval is decided per movement (Step 3) or by a separate auto-approve policy.
Step 2: Create a Movement
Create a movement from the seeded template:movement_id: UUID - used in later API callsrequest_state:"PENDING_APPROVAL"- waiting for approvalcompiled_plan_hash:"sha256:..."- required for approval
You can create a movement withoutcallback. It still works, but no state-change notifications are sent. The callback URL is a nested field —callback: {"url": "..."}, not a top-levelcallback_url(a top-levelcallback_urlis silently ignored).
Step 3: Approve the Movement
Send the approval request withcompiled_plan_hash:
request_state:"APPROVED"-> if workers are enabled, it transitions to"EXECUTING"approval_status:"approved"
Whycompiled_plan_hashis required: it guarantees the plan was not altered between creation and approval. Sending the wrong hash returns409 Conflict.
Step 4: Check Status
MG_WORKERS_ENABLED=false):
request_statestays at"APPROVED"(because the dispatcher is off)- Node state stays
"BLOCKED"(the first frontier node) or remains inactive
request_state:"EXECUTING"->"COMPLETED"- Node states advance per node kind. The representative path for a signing on-chain action node is
BLOCKED->READY->PREPARING->AWAITING_SIGNATURE->SUBMITTING->SUBMITTED->OBSERVING->COMPLETED. Common variants: a node needing no signature skipsAWAITING_SIGNATURE; a sync-terminal submit (e.g.cex_withdrawal) goesSUBMITTING->COMPLETEDdirectly; an observe-only node (e.g.cex_deposit_status) goesREADY->OBSERVING->COMPLETED.
Step 5: List Movements
Python Client Example
Movement State Flow Summary
- PENDING_APPROVAL: Waiting for approval.
POST .../approveorPOST .../reject - APPROVED: Approved. If workers are enabled, it automatically transitions to EXECUTING
- EXECUTING: Nodes are running
- COMPLETED: All nodes finished
- FAILED: An unrecoverable failure occurred
For the full state machine, see the States and Transitions reference.
Troubleshooting
Registering Templates Outside Dev
dev_quickstart writes directly to a local database and is not a deployment path. Real template and
allowlist changes are configuration proposals with maker/checker:
- Set
MG_CONFIG_REMOTE_MUTATION_MODE=proposal_only(the shipped defaultdisabledreturns503from the proposal routes). - An admin or operator creates the proposal in the dashboard: Governance -> Routes (
/governance/routes) -> preset -> Register Template, or Destination addresses (/governance/allowlist) -> add. This submitsPOST /dashboard/configuration-proposalswith kindroute_template.registerorallowlist.add. - A different admin reviews it under Governance -> Configuration proposals and applies it with the shown
proposal digest. The maker cannot apply their own proposal; proposals expire after
MG_CONFIGURATION_PROPOSAL_TTL_SECONDS(default 24h).
python -m qtg.interfaces.tools.direct_register_plan_template --template-json-file ... --reason ... --direct-maintenance --apply) that also requires MG_CONFIG_DIRECT_MAINTENANCE_ENABLED=true,
MG_CLI_AUTH_REQUIRED=true, and an admin CLI key. Without --apply it only previews.
Registering the template still approves nothing: every movement from it goes through Step 3.
Next: 03-template-cookbook.md - template examples by lane