Skip to main content

V3 API Endpoints Reference

Source files: src/qtg/interfaces/api/routes/, src/qtg/interfaces/api/schemas/
This document covers the QTG v3 HTTP API. All v3 endpoints are mounted under settings.api_prefix (default /v3). Only healthz is mounted at the root, and the /dashboard/* BFF is mounted at the root outside the HMAC scope. The detailed sections below document the core movement / template / registry / route-catalog / capital-transfer lane in full. Additional endpoint families that are registered but documented in less detail here are listed under Additional endpoint families — refer to their route source files for request/response schemas.

Inbound auth overview

When MG_AUTH_ENABLED=true, all /v3/** requests except /healthz require inbound HMAC authentication. Required headers:
  • X-QTG-Key-Id
  • X-QTG-Timestamp
  • X-QTG-Nonce
  • X-QTG-Signature

Role axis (admin / operator / agent)

Authorization is a 3-role axis, not the legacy 6-purpose model. Each api_client_keys row carries a role constrained by a DB CHECK to one of admin, operator, agent. Keys are seeded with seed_auth_client --role <role> (there is no --purpose flag, and there is no read role). Per-route allowed roles live in ROLE_ALLOWED_ROUTES in src/qtg/interfaces/api/middleware/auth/hmac.py (Pro routes extend it at boot via register_route_role()). The three role sets used by the route map:
  • admin — admin only.
  • operator — admin + operator (the _OPS set).
  • agent — agent only.
  • All roles — admin + operator + agent (the _ALL set).
Representative route -> role mapping: The agent role is bound 1:1 to a network-scoped AgentAuthority. AgentNamespaceMiddleware enforces that the request path namespace matches the authority’s namespace, returning 403 agent_namespace_mismatch on a mismatch.
Note: Older sections of this doc still reference a Purpose: scope per endpoint (e.g. read, capital_transfer). Those are legacy labels — the live authorization control is the role axis above. Treat a legacy read as “all roles”, approval/operate/capital_transfer as “operator”, admin/write as documented by ROLE_ALLOWED_ROUTES.
See the document below for canonical string construction, signing examples, and the key seed procedure:
  • docs/reference/infrastructure/auth-rollout.md — HMAC authentication enablement procedure and validation record
Auth failures generally respond as follows.
  • 401 — missing header / invalid key / signature mismatch / replay / timestamp out of range
  • 403 — key is valid but role not allowed for the route (or agent namespace mismatch)
  • 503 — route role mapping missing (fail-closed)

Rate Limiting

When MG_RATE_LIMIT_ENABLED=true, a per-key in-memory fixed-window rate check is performed after HMAC authentication. Buckets are keyed by role class:
  • read-class (agent role): 300 RPM (default MG_RATE_LIMIT_READ_RPM)
  • write-class (admin / operator roles): 60 RPM (default MG_RATE_LIMIT_WRITE_RPM)
  • When the limit is exceeded: 429 {"detail": "rate limit exceeded"} + Retry-After header
  • /healthz and /dashboard/* are not subject to the /v3 HMAC rate limiter; dashboard writes have their own mutation limiter (MG_DASHBOARD_RATE_LIMIT_WRITE_RPM, default 30, 429 + Retry-After on breach)
  • Configuration: MG_RATE_LIMIT_ENABLED, MG_RATE_LIMIT_READ_RPM, MG_RATE_LIMIT_WRITE_RPM

Endpoint Summary


Capital transfer API family

capital-transfers is a treasury/capital-ops subsystem separated from the existing movement/template/route-catalog lane. v1 contract:
  • provider-neutral public surface
  • admin/operator-only
  • first create returns 201 Created
  • same idempotency_key + semantically same request returns 200 OK with the existing resource
  • same idempotency_key + materially different request returns 409 Conflict
  • pending provider outcomes remain in PENDING_PROVIDER_CONFIRMATION with source-side hold retained and destination capacity unchanged

1. GET /healthz

Checks service state. It is mounted at the root without a prefix.

Response

200 OK

2. POST /v3/plan-templates

Registers a transfer plan template. Multiple versions can be registered for the same template_key.

Request Body — PlanTemplateCreateRequest

What risk_controls actually enforces: risk_controls is a free-form object. The API accepts any keys, and every key is carried into the compiled plan snapshot and into compiled_plan_hash, so whatever you put here is approval-pinned and auditable. But only two keys change runtime behaviour: max_amount is denominated in the intent asset, not USD — it is compared against the node’s resolved amount. On an XRP template, "max_amount": "1000" is 1,000 XRP. Every other key is descriptive metadata. asset_allowlist (seeded templates, and the quickstart examples) and network_allowlist (seeded templates only) are read by nothing — a template carrying "asset_allowlist": ["XRP"] does not reject a movement whose intent.asset is USDT. Amount-shaped keys other than max_amount and min_amount (max_amount_usd, hardcap_usd) are likewise inert; use max_amount if you want a cap that binds. Fund-safety controls that do bind live elsewhere, and none of them is configured through a plan-template field: the address allowlist (a DB plane, checked at on-chain dispatch), the approval-policy plane (the approval_policy_targets table), and the outflow / balance-reservation caps. In particular the template’s own approval_policy object — listed just above risk_controls in the table — is not that plane. Nothing reads its keys; approval is required by default and is waived only by the policy plane. It is covered by the plan-graph hash, so it is auditable as a declaration of intent, but setting {"required_approvals": 2} there does not require two approvals. PlanNodeCreate: PlanEdgeCreate: CompletionPolicyView: completion_assurance allowed values: provider_completed, destination_observed, destination_credited, destination_finalized, protocol_finalized

Request Example

executor_selector is an abstract selector. action_type is required and provider / chain_family are optional. On-chain nodes use signer_selector in the form {"chain_family": "evm", "profile": ...} when needed.

Response — PlanTemplateCreateResponse

201 Created

Error Responses


3. GET /v3/plan-templates/

Retrieves detailed information about a registered template. Pass include_readiness=true to add the same DB-only canonical readiness projection used by /v3/route-catalog; the response is Cache-Control: no-store. The projection is catalog evidence, not movement approval, dispatch authority, a fee quote, or a venue-whitelist result.

Path Parameters

Response — PlanTemplateDetailResponse

200 OK All PlanTemplateCreateResponse fields plus the additional fields below: PlanNodeView: all PlanNodeCreate fields plus id (UUID) PlanEdgeView:

Error Responses


4. POST /v3/movements

Creates a movement request. Immediately after creation it enters the PENDING_APPROVAL state.

Request Body — MovementCreateRequest

If input_params.node_config is an object, it supplements each node’s template config at execution time. For the same key, the template config takes precedence. Example: Hyperliquid hl.topup passes the Arbitrum RPC endpoint via input_params.node_config.arbitrum_rpc_endpoint.

Request Example

Response — MovementCreateResponse

201 Created

Internal processing flow

  1. Look up the plan version by template_key + template_version
  2. If executable_in_v3_0 == false, return 409 Conflict
  3. Load nodes/edges, then finalize executor/signer bindings via resolve_bindings()
  4. Determine DAG root nodes via current_frontier_node_keys()
  5. Generate the hash via build_compiled_snapshot() + compute_compiled_plan_hash()
  6. Create MovementRequest (state: PENDING_APPROVAL, expires after approval_ttl_seconds)
  7. Create a MovementRequestNode for each plan node (state: BLOCKED)

Error Responses


5. GET /v3/movements

Retrieves the list of movement requests. Sorted by creation time descending; supports state, strategy_id, and limit filters.

Query Parameters

Response — MovementListResponse

200 OK MovementSummary:

Example


6. GET /v3/movements/

Retrieves detailed information about a movement request.

Path Parameters

Response — MovementDetailResponse

200 OK TemplateRef: MovementNodeView: MovementArtifactView:

Error Responses


7. GET /v3/movements//timeline

Retrieves the movement request event timeline (audit trail). Ordered chronologically.

Path Parameters

Response — list[MovementTimelineEventView]

200 OK

8. POST /v3/movements//approval-brief

Builds the DB-only review object that an operator must inspect before manual approval. This endpoint does not call a provider, quote a fee, reserve funds, approve the movement, or dispatch a node. Responses carry Cache-Control: no-store.

Request Body — ApprovalBriefRequest

The response includes the movement state, binding status, compiled_plan_hash, canonical authorization-facts version/digest, approval_brief_digest, generation time, and the structured brief. The brief shows source, destination, asset/token, amount semantics, network, debit/fee posture, completion assurance, policy posture, and phase-labelled guard verdicts. unknown and pending are explicit states; they are not green readiness claims. Clients must treat the options and returned digest as one immutable review snapshot. If any digest-bound option changes, fetch and review a new brief. A late response for a closed dialog, another movement, or older options must be discarded.

9. POST /v3/movements//approve

Approves a movement request. On approval, frontier nodes transition to READY state and execution begins.

Path Parameters

Request Body — MovementApproveRequest

Response — MovementActionResponse

200 OK

Internal processing flow

  1. Lock the movement and check that request state is PENDING_APPROVAL
  2. Verify compiled_plan_hash match
  3. For a supplied digest, rebuild the DB-only brief under the lock and require an exact digest/options match; in enforce mode, reject a missing digest or legacy-unbound facts
  4. Apply maker-checker and brief guard decisions
  5. Verify availability of executor/signer bindings for frontier nodes
  6. approval_status -> approved, reservation_status -> held
  7. Transition frontier nodes BLOCKED -> READY
  8. Transition request state to APPROVED
  9. Enqueue request_approved callback

Error Responses


10. POST /v3/movements//reject

Rejects a movement request.

Request Body — MovementRejectRequest

Response — MovementActionResponse

200 OK (request_state: REJECTED)

Internal processing

  1. Check that request state is PENDING_APPROVAL
  2. approval_status -> rejected
  3. Transition request state to REJECTED
  4. Enqueue request_rejected callback

Error Responses


11. POST /v3/movements//actions/resume

Resumes a request in WAITING_MANUAL_ACTION or MANUAL_INTERVENTION state.

Request Body

None (empty POST).

Response — MovementActionResponse

200 OK (request_state: EXECUTING)

Internal processing

  1. Check that request state is WAITING_MANUAL_ACTION or MANUAL_INTERVENTION
  2. Transition request state to EXECUTING
  3. Enqueue request_resumed callback

Error Responses


12. POST /v3/movements//actions/retry

Finds and retries nodes in FAILED or UNKNOWN state.

Request Body

None (empty POST).

Response — MovementActionResponse

200 OK (request_state: EXECUTING)

Internal processing

  1. Search FAILED or UNKNOWN nodes ordered by updated_at ascending
  2. Transition the node to READY
  3. Set current_frontier to that node’s node_key
  4. Transition request state to EXECUTING
  5. Enqueue request_retried callback

Error Responses


13. POST /v3/movements//actions/cancel

Cancels a movement request. Cancellation is possible only for templates that define a cancel path (on_cancel edge).

Request Body

None (empty POST).

Response — MovementActionResponse

200 OK (request_state: CANCELLED)

Internal processing

  1. Check that the plan version has an on_cancel edge
  2. Transition request state to CANCELLED
  3. If there are completed nodes with side effects, keep reservation_status as held
  4. If no side effects, set reservation_status to released
  5. Enqueue request_cancelled callback

Error Responses


14. GET /v3/executors

Retrieves the registered executor registry. Merges DB-persisted entries with in-memory registrations.

Response — list[ExecutorRegistryView]

200 OK

15. PATCH /v3/executors/

Changes the executor registry state. This is an operational control that blocks/allows new movement approvals; it does not perform in-process hot reload. Even if no DB row exists, when an in-memory binding exists for the same key, the row is auto-materialized before the state is reflected.

Request Body — UpdateRegistryStatusRequest

Response

200 OK

Error Responses


16. GET /v3/signers

Retrieves the registered signer registry. Returns a merge of the persistent registry and the in-memory registry.

Response — list[SignerRegistryView]

200 OK
status=active is an administrative registry state, not proof that the current process can sign. Consumers that present current operability must use ready_to_sign. A host .env that is not loaded into the process is not runtime evidence and does not affect this value.

17. PATCH /v3/signers/

Changes the signer registry state. This is an operational control that blocks/allows new movement approvals; it does not perform in-process hot reload. Even if no DB row exists, when an in-memory binding exists for the same key, the row is auto-materialized before the state is reflected. PATCH allows only active and disabled as status.

Request Body — UpdateRegistryStatusRequest

Response

200 OK

Error Responses


18. PUT /v3/signers//expected-identity

Pins the expected identity for an enrolled EVM signer. This is an admin-only Free registry operation; it neither rotates a signer nor changes movement bindings.

Request Body — SetExpectedSignerIdentityRequest

Request Example

CAS and runtime semantics

The route validates the target is a persisted EVM signer, resolves the live identity through the optional signer identity protocol, and locks the registry row. A change succeeds only when the supplied previous_expected_address equals the stored value and the submitted expected address equals the freshly resolved runtime identity. No-op precedence is explicit: when stored expected identity, submitted identity, and live identity are all A, the response is noop even if previous_expected_address is stale C; the stale compare-and-set value matters only when a change is requested.

Response — SetExpectedSignerIdentityResponse

200 OK

Stable error codes

This surface establishes only the expected-versus-observed registry contract. It does not enforce movement-time or rotation-time address drift; that enforcement is a separate follow-up.

19. POST /v3/auto-approve-policies

Creates an auto-approval policy. The client_id + strategy_id combination must be unique.

Request Body — CreatePolicyRequest

AllowedTemplateEntry:

Response — PolicyResponse

201 Created

Error Responses


20. GET /v3/auto-approve-policies

Retrieves the list of registered auto-approval policies.

Response — list[PolicyResponse]

200 OK

21. GET /v3/auto-approve-policies/

Retrieves auto-approval policy details.

Path Parameters

Response — PolicyResponse

200 OK

Error Responses


22. PATCH /v3/auto-approve-policies/

Updates an auto-approval policy. Updates only the submitted fields.

Path Parameters

Request Body — UpdatePolicyRequest

Response — PolicyResponse

200 OK

Error Responses


Auto-approve behavior flow

When MG_AUTO_APPROVE_ENABLED=true and a POST /v3/movements request contains strategy_id:
  1. Extract client_id from HMAC auth
  2. Look up the matching policy set by client_id + strategy_id in auto_approve_policies
  3. Five-stage evaluation: is_active -> asset -> template -> amount -> binding_available
  4. Agent-originated movements (actor_role == "agent") never auto-approve regardless of a matching policy — evaluation is skipped and the result is stamped with agent_auto_approve_skipped
  5. If all checks pass: PENDING_APPROVAL -> APPROVED auto transition, frontier nodes READY
  6. On failure: stay in PENDING_APPROVAL, record the reason in auto_approve_result
Important: policies are bound to client_id. Client A’s policy does not apply to client B’s request.

23. GET /v3/route-catalog

Retrieves the catalog of available routes. Returns a combination of template metadata, execution statistics, and executor health.

Query Parameters

Response — RouteCatalogResponse

200 OK

Response fields

With include_readiness=true, generated_at and readiness.evaluated_at use one server timestamp and the response has Cache-Control: no-store. The read path batch-loads persisted template, registry-health, and literal-destination allowlist facts only: it does not construct an executor/adapter, read credentials, call a provider, or create authority. A configuration pass is only a static template/registry result. Fee, approval, movement input, destination, health freshness, and venue evidence can therefore leave the separate execution aggregate unknown or fail.

Error Responses

Auth

Purpose: read

24. POST /v3/routes/recommend

Retrieves an optimal route recommendation (advisory). Based on route catalog candidates, it computes feasibility and priority scores. Each recommendation also returns configuration_readiness_state, execution_readiness_state, execution_readiness_status, and execution_readiness_reasons. Compatibility feasibility="feasible" is now possible only when canonical execution readiness is pass as well as the existing amount/balance checks. Missing or unknown readiness is non-feasible; the response retains readiness_unknown and the canonical reasons instead of silently treating a degraded route as feasible.

Request Body

Response — RouteRecommendResponse

200 OK

Feasibility rules

  • not_executable
  • executor_unavailable
  • amount_below_min
  • amount_above_max
Feasible routes always rank ahead of infeasible ones.

Error Responses

Auth

Purpose: read

MovementActionResponse common response

approve, reject, resume, retry, and cancel all use the same response schema.

Authentication model

When MG_AUTH_ENABLED=true, the HMAC authentication middleware applies to all /v3/** requests except /healthz. The middleware validates the X-QTG-Key-Id / X-QTG-Timestamp / X-QTG-Nonce / X-QTG-Signature headers and controls route access based on the 3-role axis (admin / operator / agent) described in Inbound auth overview. When MG_RATE_LIMIT_ENABLED=true, a per-key fixed-window rate limit check is performed additionally after authentication passes. In the approval API (approve), validating compiled_plan_hash equality serves as a plan-integrity check. The Approval Brief separately binds the reviewed, DB-derived authority facts, guard posture, and digest-bound options. MG_AUTHORIZATION_PROJECTION_MODE=shadow is the rollout default; switching to enforce makes a captured matching brief digest mandatory for bound manual approvals and rejects legacy-unbound rows. The approver_id is the operator label carried in the request body and is stored as such, while maker-checker uses the authenticated HMAC key identity. For the authentication enablement procedure and key seed CLI usage, refer to docs/reference/infrastructure/auth-rollout.md.

Callback event kinds

Callback events enqueued to the outbox on state changes:

25. GET /v3/budgets/

Retrieves the per-strategy remaining budget. Returns active policies with a window cap configured.

Path Parameters

Query Parameters

Response

200 OK

Auth

  • Purpose: read

26. GET /v3/balances

Retrieves the latest per-venue balance snapshots. The lookup unit is the latest row by (venue_key, asset, account_type), and only the latest row per combination is returned.

Query Parameters

Response — BalanceResponse

200 OK

Response meaning

Behavior notes

  • When fresh=true and a fetcher exists for the venue_key, on-demand fetch is attempted first, then the latest snapshot is retrieved.
  • When the account_type query param is provided, it is trim + uppercased. Example: ?account_type=usdt_future retrieves the USDT_FUTURE snapshot.
  • Latest-row identity is (venue_key, asset, account_type). Spot treasury and USDT-M Futures inventory are distinct snapshot/reservation pools even for the same asset.
  • If on-demand fetch fails, falls back to the last stored snapshot.
  • If no snapshot exists, returns 200 OK with an empty balances array.
  • SPOT / USDT_FUTURE are the primary account types linked to Binance capital-transfer reservations. Balance-only account types such as FUNDING (e.g. OKX) / FUND (e.g. Bybit) may also be returned as snapshot/filter values.

Auth

  • Purpose: read

25A. GET /v3/withdrawal-capacities

Returns a point-in-time, always-live CEX capacity observation for one configured venue, asset, and provider-native network. It performs provider GETs and reads QTG reservations; it does not store a balance/capacity snapshot or create a movement.

Query Parameters

The response separates network capability, account withdrawable balance, provider quotas, QTG pending outflow, and derived asset capacity. Every quota retains its native unit/scope/window. A fiat, quote, or unknown-unit quota is never compared with an asset amount; effective_max_sendable_asset is null with quota_unit_unconvertible while asset_constraints_max remains visible. Provider auth, timeout, rate-limit, and invalid-response failures return 200 with withdrawal_state=unknown, a redacted reason code, and retry hints. These responses carry Cache-Control: no-store, expire using the existing balance staleness threshold, and are not persisted, so the next call re-observes the provider rather than replaying unknown state. unavailable is different from unknown: a provider-confirmed disabled or unsupported network returns an exact zero capacity; missing/unjudgeable evidence returns a null effective capacity. Unknown minimum, maximum-per-request, amount-step, or fee evidence is never treated as zero or unbounded. QTG may still return an asset-only bound for the constraints it can prove, but effective_max_sendable_asset remains null. OKX fee evidence uses the current fee/feeCcy and burningFeeRate fields; deprecated minFee/maxFee fields are ignored.

Auth

  • Purpose: read (admin, operator, and agent)

27. POST /v3/capital-transfers

Creates a provider-internal treasury/capital transfer. Binance uses master-signed universalTransfer; OKX uses master-signed asset/transfer. Source/destination account type is a first-class QTG contract field and reservation-pool key. QTG Pro only. Creation is disabled by default and returns 503 until Pro is installed and MG_CAPITAL_TRANSFER_MUTATIONS_ENABLED=true is explicitly set. Existing pending rows may still be reconciled independently through the follow-up setting. Submission retains the existing immediate-submit behavior; it is not an approval workflow.

Request Body — CapitalTransferCreateRequest

Request Example — master Spot -> sub USDT-M Futures

Request Example — sub USDT-M Futures -> master Spot

Response — CapitalTransferView

201 Created for a new semantic request. 200 OK when the same idempotency_key is replayed with a semantically identical request.

Response meaning

Binance v1 topology notes

  • Binance master binding is Spot treasury only: if either side is master, that side’s account type must be SPOT.
  • Sub-account bindings can move between SPOT and USDT_FUTURE under supported topologies:
    • master SPOT -> sub SPOT or USDT_FUTURE
    • sub SPOT or USDT_FUTURE -> master SPOT
    • same sub SPOT ↔ USDT_FUTURE
  • Cross-sub transfers are intentionally rejected in v1.
  • Same-account no-op transfers are rejected: for the same sub-account, source_account_type and destination_account_type must differ.
  • Master-to-master topology is not a supported capital-transfer operation.

OKX topology and settlement notes

  • Only master↔sub transfers are supported. Master→sub uses OKX transfer type 1; sub→master uses type 2. Master→master and every sub→sub form are rejected before HTTP.
  • FUNDING maps to OKX account code 6; TRADING maps to 18. Both fields are mandatory for OKX—there is no implicit account default.
  • The master binding signs every direction. The sub binding has no credentials and stores the exact OKX sub-account name in sub_account_uid.
  • A side-effect-free executor preparation step derives the provider clientId from the QTG idempotency key. QTG stores it with the durable SUBMITTING row before HTTP; submit and follow-up require that stored identity and never recompute an authoritative replacement.
  • No submit response is terminal. The documented OKX submit row omits type and subAcct, so its transId is deliberately not bound. The follow-up worker starts by stored clientId and binds transId only from GET /api/v5/asset/transfer-state after one exact tuple match across client/transfer id, type, currency, Decimal-equivalent amount, account codes, and sub-account name. Empty, multi-row, mismatched, malformed, unavailable, or unknown responses retain the hold in pending/manual evidence.
  • Automatic OKX reconciliation runs only when MG_CAPITAL_TRANSFER_FOLLOWUP_ENABLED=true. Creation durably commits the SUBMITTING row and source hold before HTTP, so a crash or result-commit failure leaves a no-resubmit recovery anchor even when the sidecar is disabled.
  • After provider POST returns, creation reacquires the request row under FOR UPDATE and applies the submit result only if state is still SUBMITTING; a follow-up/manual terminal or holding decision cannot be demoted by a late response.
  • Dashboard attention and detail polling include SUBMITTING. Operator detail exposes the non-secret provider client id, lookup mode, normalized provider state, mismatch fields, and follow-up error type.

Error Responses

Auth

  • Purpose: capital_transfer

28. GET /v3/capital-transfers

Retrieves capital transfers, ordered newest first.

Query Parameters

Response — CapitalTransferListResponse

Auth

  • Purpose: capital_transfer

29. GET /v3/capital-transfers/

Retrieves a single capital transfer. The response schema is identical to CapitalTransferView.

Path Parameters

Auth

  • Purpose: capital_transfer

30. POST /v3/capital-transfers//actions/resolve

Manually resolves a capital transfer in PENDING_PROVIDER_CONFIRMATION or MANUAL_INTERVENTION state to a terminal/holding state.

Request Body — CapitalTransferResolveRequest

Auth

  • Purpose: capital_transfer

Additional endpoint families

Beyond the movement / template / registry / route-catalog / capital-transfer lane documented in detail above, the following families are registered in src/qtg/interfaces/api/routes/. They are summarized here; consult the cited source files for full request/response schemas.

Identity and audit

GET /v3/audit/events supports filters: time_range_start, time_range_end, actor_client_id, actor_key_id, entity_type, entity_key, action, request_id, outcome, namespace, cursor, limit (max 500).

Agent authorities and template proposals

Agent wallet top-ups / funding envelopes

The agent-wallet top-up lane is dedicated and must not be driven through generic movement creation — see the Agent Wallet Top-Up rules in CLAUDE.md.

CCIP registry, verify-drift, and Hyperliquid

Wallet signing, credential administration, and human access (JEP-740/877)

Wallet signing (SIWE) and browser-wallet opt-in stay disabled by default (wallet_siwe_signing_enabled=False).

Dashboard BFF (/dashboard/*)

The dashboard backend-for-frontend is mounted at the root outside the /v3 HMAC scope (build_dashboard_router() in routes/dashboard/, prefix /dashboard). Dashboard writes use per-operator HMAC v3 (nonce + timestamp): the browser holds a non-extractable CryptoKey and signs each write. The old bearer-token-in-bundle model is gone — MG_DASHBOARD_WRITE_TOKEN / VITE_DASHBOARD_WRITE_TOKEN have been removed, and main.py hard-fails startup if MG_DASHBOARD_WRITE_TOKEN is set (even to empty). Localhost binding is now defense-in-depth, not the primary control. The expected writer-key count defaults to dashboard_writer_expected_key_count = 8. See runbooks/dashboard-auth-v1-to-hmac.md. Trusted-target governance is separate from the destination allowlist: The read response exposes eligible, pending, or revoked plus usable_after, and omits free-form policy metadata. Mutation payloads accept only the exact EVM chain/address/token tuple, optional policy profile, and a required reason. An operator key that can edit destination allowlist rows receives 403 here; trust grants require an admin key. New and reactivated rows cannot match target_trust.registry before the configured delay expires (MG_TRUSTED_TARGET_COOLING_PERIOD_HOURS, minimum/default 0; increase it to opt in).