V3 API Endpoints Reference
Source files:This document covers the QTG v3 HTTP API. All v3 endpoints are mounted undersrc/qtg/interfaces/api/routes/,src/qtg/interfaces/api/schemas/
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
WhenMG_AUTH_ENABLED=true, all /v3/** requests except /healthz require inbound HMAC authentication.
Required headers:
X-QTG-Key-IdX-QTG-TimestampX-QTG-NonceX-QTG-Signature
Role axis (admin / operator / agent)
Authorization is a 3-role axis, not the legacy 6-purpose model. Eachapi_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_OPSset).agent— agent only.- All roles — admin + operator + agent (the
_ALLset).
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 aSee the document below for canonical string construction, signing examples, and the key seed procedure:Purpose:scope per endpoint (e.g.read,capital_transfer). Those are legacy labels — the live authorization control is the role axis above. Treat a legacyreadas “all roles”,approval/operate/capital_transferas “operator”,admin/writeas documented byROLE_ALLOWED_ROUTES.
docs/reference/infrastructure/auth-rollout.md— HMAC authentication enablement procedure and validation record
401— missing header / invalid key / signature mismatch / replay / timestamp out of range403— key is valid but role not allowed for the route (or agent namespace mismatch)503— route role mapping missing (fail-closed)
Rate Limiting
WhenMG_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 (
agentrole): 300 RPM (defaultMG_RATE_LIMIT_READ_RPM) - write-class (
admin/operatorroles): 60 RPM (defaultMG_RATE_LIMIT_WRITE_RPM) - When the limit is exceeded:
429 {"detail": "rate limit exceeded"}+Retry-Afterheader /healthzand/dashboard/*are not subject to the/v3HMAC rate limiter; dashboard writes have their own mutation limiter (MG_DASHBOARD_RATE_LIMIT_WRITE_RPM, default 30,429+Retry-Afteron 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 returns200 OKwith the existing resource - same
idempotency_key+ materially different request returns409 Conflict - pending provider outcomes remain in
PENDING_PROVIDER_CONFIRMATIONwith 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 OK2. POST /v3/plan-templates
Registers a transfer plan template. Multiple versions can be registered for the sametemplate_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. Passinclude_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 thePENDING_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
- Look up the plan version by
template_key+template_version - If
executable_in_v3_0 == false, return 409 Conflict - Load nodes/edges, then finalize executor/signer bindings via
resolve_bindings() - Determine DAG root nodes via
current_frontier_node_keys() - Generate the hash via
build_compiled_snapshot()+compute_compiled_plan_hash() - Create MovementRequest (state:
PENDING_APPROVAL, expires afterapproval_ttl_seconds) - 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; supportsstate, 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 carryCache-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 toREADY state and execution begins.
Path Parameters
Request Body — MovementApproveRequest
Response — MovementActionResponse
200 OK
Internal processing flow
- Lock the movement and check that request state is
PENDING_APPROVAL - Verify
compiled_plan_hashmatch - 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
- Apply maker-checker and brief guard decisions
- Verify availability of executor/signer bindings for frontier nodes
approval_status->approved,reservation_status->held- Transition frontier nodes
BLOCKED->READY - Transition request state to
APPROVED - Enqueue
request_approvedcallback
Error Responses
10. POST /v3/movements//reject
Rejects a movement request.Request Body — MovementRejectRequest
Response — MovementActionResponse
200 OK (request_state: REJECTED)
Internal processing
- Check that request state is
PENDING_APPROVAL approval_status->rejected- Transition request state to
REJECTED - Enqueue
request_rejectedcallback
Error Responses
11. POST /v3/movements//actions/resume
Resumes a request inWAITING_MANUAL_ACTION or MANUAL_INTERVENTION state.
Request Body
None (empty POST).Response — MovementActionResponse
200 OK (request_state: EXECUTING)
Internal processing
- Check that request state is
WAITING_MANUAL_ACTIONorMANUAL_INTERVENTION - Transition request state to
EXECUTING - Enqueue
request_resumedcallback
Error Responses
12. POST /v3/movements//actions/retry
Finds and retries nodes inFAILED or UNKNOWN state.
Request Body
None (empty POST).Response — MovementActionResponse
200 OK (request_state: EXECUTING)
Internal processing
- Search
FAILEDorUNKNOWNnodes ordered byupdated_atascending - Transition the node to
READY - Set
current_frontierto that node’snode_key - Transition request state to
EXECUTING - Enqueue
request_retriedcallback
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
- Check that the plan version has an
on_canceledge - Transition request state to
CANCELLED - If there are completed nodes with side effects, keep
reservation_statusasheld - If no side effects, set
reservation_statustoreleased - Enqueue
request_cancelledcallback
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 OKError 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 OKError 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 suppliedprevious_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. Theclient_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
WhenMG_AUTO_APPROVE_ENABLED=true and a POST /v3/movements request contains strategy_id:
- Extract
client_idfrom HMAC auth - Look up the matching policy set by
client_id + strategy_idinauto_approve_policies - Five-stage evaluation:
is_active->asset->template->amount->binding_available - Agent-originated movements (
actor_role == "agent") never auto-approve regardless of a matching policy — evaluation is skipped and the result is stamped withagent_auto_approve_skipped - If all checks pass:
PENDING_APPROVAL->APPROVEDauto transition, frontier nodesREADY - On failure: stay in
PENDING_APPROVAL, record the reason inauto_approve_result
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 returnsconfiguration_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_executableexecutor_unavailableamount_below_minamount_above_max
Error Responses
Auth
Purpose:read
MovementActionResponse common response
approve, reject, resume, retry, and cancel all use the same response schema.Authentication model
WhenMG_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 OKAuth
- 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=trueand a fetcher exists for thevenue_key, on-demand fetch is attempted first, then the latest snapshot is retrieved. - When the
account_typequery param is provided, it is trim + uppercased. Example:?account_type=usdt_futureretrieves theUSDT_FUTUREsnapshot. - 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 OKwith an emptybalancesarray. SPOT/USDT_FUTUREare the primary account types linked to Binance capital-transfer reservations. Balance-only account types such asFUNDING(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-signeduniversalTransfer; 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
SPOTandUSDT_FUTUREunder supported topologies:- master
SPOT-> subSPOTorUSDT_FUTURE - sub
SPOTorUSDT_FUTURE-> masterSPOT - same sub
SPOT↔USDT_FUTURE
- master
- Cross-sub transfers are intentionally rejected in v1.
- Same-account no-op transfers are rejected: for the same sub-account,
source_account_typeanddestination_account_typemust 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 type2. Master→master and every sub→sub form are rejected before HTTP. FUNDINGmaps to OKX account code6;TRADINGmaps to18. 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
clientIdfrom the QTG idempotency key. QTG stores it with the durableSUBMITTINGrow 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
typeandsubAcct, so itstransIdis deliberately not bound. The follow-up worker starts by storedclientIdand bindstransIdonly fromGET /api/v5/asset/transfer-stateafter 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 theSUBMITTINGrow 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 UPDATEand applies the submit result only if state is stillSUBMITTING; 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 toCapitalTransferView.
Path Parameters
Auth
- Purpose:
capital_transfer
30. POST /v3/capital-transfers//actions/resolve
Manually resolves a capital transfer inPENDING_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 insrc/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).
Related documents
- Data Model Reference — table details
- Bootstrap & Configuration — settings and initialization
- Runtime Workers — per-worker behavior details