Security Model
QTG is designed for a world where automated systems — trading bots, LLM agents, rebalancing scripts — initiate fund movements. The security model ensures that even if one of those systems is compromised or buggy, the blast radius is contained. No single component can unilaterally move funds.Trust Boundary Overview
The key insight: agents can propose but only operators can approve. The signing key lives in your KMS, not in QTG’s memory.Keys Like a Company
QTG’s default EVM signer uses AWS KMS in your own AWS account. This means:- QTG holds a key ID, never a private key. The key material never leaves the KMS hardware security module.
- Signing happens via an API call to KMS. Even if the QTG server is fully compromised, the attacker gets the ability to request signatures — but only through the approval gate, and only while they maintain access.
- Key rotation is a first-class operation with an audit trail. Rotating marks the old signer deprecated, which removes it from binding resolution for newly created movements. It does not reach into work already planned: for each signer-bound node the selected signer key is stored when the movement is created — approval later adds that signer’s address snapshot without re-resolving it — so a node created before the rotation still dispatches with the old signer afterwards. Marking a signer retired is a separate, gated step — among its checks it refuses while a committed nonterminal node still references that signer, matched by signer key and, where recoverable, by signer-address snapshot. Signer lifecycle administration (rotate, inventory, allowances, retire) is a QTG Pro surface.
- Multi-signer bootstrap is supported for redundancy or overlap during rotation.
Approval Gate
Every movement stops at PENDING_APPROVAL before any funds move. This is not optional — it is baked into the lifecycle. What this means in practice:- A trading bot can create 100 movements per second. None of them execute until approved.
- An LLM agent operating via the MCP server can propose transfers within its authority binding. It cannot approve its own proposals.
- A misconfigured script that accidentally passes the wrong amount still gets caught at the gate.
Auto-Approve Policies
For high-frequency operations where manual approval would be impractical, you can configure auto-approve policies. These policies are scoped:- By template — only movements using specific templates can be auto-approved.
- By strategy — only movements from specific strategy identifiers.
- With budget limits — per-interval caps on total approved volume.
Outflow Velocity Cap
Every guard above answers “is this one movement correct?” The outflow velocity cap answers a different question: “is the total volume leaving this instance normal?” It is the aggregate brake under the per-movement guards — a per-token, rolling-window ceiling enforced at the approval/reservation chokepoint. When approving a movement would push its token past the configured ceiling, the approval is rejected (the movement staysPENDING_APPROVAL) and the token self-throttles for the rest of the window, with no operator action and no global flag. Other tokens, each under their own ceiling, are unaffected. This bounds runaway strategy code, a stolen strategy/API key, and operator fat-fingers — all of which can emit individually-valid movements that each pass every per-movement guard.
The cap is a speed bump with an operator override, not a wall: an emergency withdraw that must exceed the cap is approved with cap_override: true on the authenticated approve action. The override is honored only for operator/admin roles and is always audited — an agent/strategy key can never set it, so the cap survives exactly the adversary it targets. Ceilings live in env, not the DB, so the API-surface adversary cannot raise them.
Coverage is CEX-balance lanes (the live Upbit ↔ Bithumb + Binance withdrawal path); on-chain bridge lanes are out of scope by construction. See the Outflow Velocity Cap guide for configuration, breach behaviour, and the override path.
Address Allowlist
Destination addresses are checked at dispatch time, not just at creation time. This distinction matters:- Operator registers address
0xABC...on the allowlist. - A movement is created targeting
0xABC...and approved. - Before dispatch, the operator removes
0xABC...from the allowlist. - Dispatch fails with an address guard violation. No funds move.
- Destination-only (
stargate_send,usdt0_send,lighter_secure_withdraw) — the source is bound to the signer, so only the destination is allowlist-checked. - Strict source + destination (
cctp_burn/cctp_mint,gateway_*,ccip_send,evm_erc20_transfer) — both the source and destination are validated against the allowlist.
Signed-Recipient Invariant (Validated == Signed)
The address allowlist answers “is this destination approved?” — but a separate question must also hold: does the transaction we are about to sign and broadcast actually pay that approved destination? A naive design validates one field (the intent’s destination) while signing a different, caller-controlled field — a “validated field != signed field” fund-outflow bypass. QTG closes this with a fail-closed, re-derive-from-the-artifact invariant on every on-chain send executor, at TWO chokepoints: (1) Prepare time — the recipient is resolved ONLY from the allowlist-validated intent destination (no caller-controlled override fallback) and asserted before signing; (2) Submit/broadcast time — the actual signed transaction is decoded (the signed legacy transaction is decoded from its raw bytes; for the Gateway EIP-712 lane the submitted burn-intent is re-hashed) and the recipient/target embedded in the bytes that go on-chain is re-derived and compared to the live approved destination (or, for source/contract-pinned actions, the trusted config value). A malicious or swapped signer, an injected signature result, or a tampered persisted prepared action therefore cannot broadcast a transaction that pays a different address than the approved destination — the executor fails closed before broadcast and the node terminalizesFAILED.
When the check fails, one of these error codes is raised:
RBAC: Three Roles
Every API key is bound to exactly one role. The role determines what operations that key can perform:
The Agent role is designed for LLM agents and automated systems. An agent key is bound to a specific authority that constrains:
- Which templates the agent can use.
- Which venues and assets the agent can target.
- What volume the agent can propose.
Audit Trail
Every state transition in QTG is recorded in an append-onlyaudit_events table. This includes:
- Movement creation, approval, rejection, and completion.
- Node state transitions (dispatch, observe, fail, recover).
- Signer rotation and retirement events.
- Template and registry changes.
GET /v3/audit/events (admin + operator — and the read is itself recorded as a meta-audit row); the registry-scoped GET /v3/registry/audit is retained as a legacy surface.
Callback Security
When QTG notifies external systems about state changes, those callbacks are secured with HMAC v3 signatures:- Every callback includes a timestamp, nonce, and signature in the headers.
- The signature covers the full request body, timestamp, and nonce — so tampering with any part invalidates the signature.
- Nonce replay defense: receivers must store seen nonces and reject duplicates. This prevents an attacker from capturing and replaying a legitimate callback.
- Callbacks are dispatched from a durable outbox — if delivery fails, QTG retries. The callback is not lost.
For the receiver-side implementation contract (what your callback endpoint must verify), see Callback Verification Contract. For a working example, see Callback Receiver Example.
Inbound Request Authentication
All API requests to QTG are authenticated using HMAC signatures:- Every request must include a key ID, timestamp, nonce, and signature.
- The signature covers the HTTP method, path, query parameters, body hash, timestamp, and nonce.
- Stale timestamps are rejected (clock skew tolerance is configurable).
- Nonce reuse is rejected.
Summary: Defense in Depth
No single layer is the entire security story. The layers work together:
For the full architecture overview, see Architecture Overview.