Skip to main content

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_APPROVAL for 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_approve proposal) or Pro auto_approve_policy proposals scoped to a strategy/client, allowed template versions, and amount caps. Agent-originated movements are never auto-approved.

Registration Ladder

Remote configuration writes are proposal-only (maker/checker): 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 no POST /v3/plan-templates step? Template and allowlist writes are configuration changes, not transfers, and they are governed: the direct remote routes always return 409 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

The tool is idempotent and seeds only your local database:
  • placeholder CEX allowlist rows for upbit and binance
  • 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

Check these fields on the entry:
  • version: 1
  • graph_shape: "linear" and executable: true
  • risk_controls.max_amount - the only binding risk_controls key (enforced fail-closed at dispatch)
What the template body does and does not decide. risk_controls is free-form and only max_amount binds; asset_allowlist is descriptive (hash-pinned, not enforced). The approval_policy block (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:
Check these fields in the response:
  • movement_id: UUID - used in later API calls
  • request_state: "PENDING_APPROVAL" - waiting for approval
  • compiled_plan_hash: "sha256:..." - required for approval
You can create a movement without callback. It still works, but no state-change notifications are sent. The callback URL is a nested field — callback: {"url": "..."}, not a top-level callback_url (a top-level callback_url is silently ignored).

Step 3: Approve the Movement

Send the approval request with compiled_plan_hash:
Check these fields in the response:
  • request_state: "APPROVED" -> if workers are enabled, it transitions to "EXECUTING"
  • approval_status: "approved"
Why compiled_plan_hash is required: it guarantees the plan was not altered between creation and approval. Sending the wrong hash returns 409 Conflict.

Step 4: Check Status

In dry-run mode (MG_WORKERS_ENABLED=false):
  • request_state stays at "APPROVED" (because the dispatcher is off)
  • Node state stays "BLOCKED" (the first frontier node) or remains inactive
If workers are enabled and exchange credentials are configured:
  • 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 skips AWAITING_SIGNATURE; a sync-terminal submit (e.g. cex_withdrawal) goes SUBMITTING -> COMPLETED directly; an observe-only node (e.g. cex_deposit_status) goes READY -> OBSERVING -> COMPLETED.

Step 5: List Movements


Python Client Example


Movement State Flow Summary

  • PENDING_APPROVAL: Waiting for approval. POST .../approve or POST .../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:
  1. Set MG_CONFIG_REMOTE_MUTATION_MODE=proposal_only (the shipped default disabled returns 503 from the proposal routes).
  2. An admin or operator creates the proposal in the dashboard: Governance -> Routes (/governance/routes) -> preset -> Register Template, or Destination addresses (/governance/allowlist) -> add. This submits POST /dashboard/configuration-proposals with kind route_template.register or allowlist.add.
  3. 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).
For host-level maintenance there is an audited maker/checker bypass CLI (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