> ## Documentation Index
> Fetch the complete documentation index at: https://jephalabs.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Security Model

> How QTG protects your keys, approvals, and audit trail

# 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

```mermaid theme={null}
flowchart TD
    subgraph agents["Agents & Bots"]
        A1["Trading Bot"]
        A2["LLM Agent\n(qtg-mcp)"]
    end

    subgraph qtg["QTG Control Plane"]
        PROPOSE["Propose\nMovement"]
        APPROVE["Approval\nGate"]
        DISPATCH["Dispatch\n& Sign"]
    end

    subgraph external["External Infrastructure"]
        KMS["AWS KMS\n(your account)"]
        CEX["Exchange\nAPIs"]
        CHAIN["EVM\nChains"]
    end

    A1 -->|"HMAC-signed request"| PROPOSE
    A2 -->|"HMAC-signed request"| PROPOSE
    PROPOSE --> APPROVE
    APPROVE -->|"operator approves"| DISPATCH
    DISPATCH --> KMS
    DISPATCH --> CEX
    DISPATCH --> CHAIN

    style APPROVE fill:#f59e0b,stroke:#d97706,color:#000
    style KMS fill:#059669,stroke:#047857,color:#fff
```

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.

For local development or evaluation, QTG supports a **1Password-backed local signer** that fetches keys on demand from your vault. A plain environment variable signer exists for development only -- it holds the key in process memory and should never be used with real funds.

[Security Tradeoffs § Signing keys](/security-tradeoffs#1-signing-keys) compares the three
side by side against what an attacker can actually do — read files on disk, dump process
memory, run code inside the QTG process, or steal your AWS credentials. The rows are what
separates them; the summary above is not.

<Warning>
  The plain environment variable signer (`MG_LOCAL_SIGNER_KEY` with backend `plain`) is for development only. It provides no protection if the server process is compromised. Migrate to AWS KMS before moving real funds.
</Warning>

## 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.

Auto-approve does not remove the approval stage. It evaluates the policy automatically instead of waiting for a human. If no policy matches, the movement waits.

## 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 stays `PENDING_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](/guide/outflow-velocity-cap) 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:

1. Operator registers address `0xABC...` on the allowlist.
2. A movement is created targeting `0xABC...` and approved.
3. Before dispatch, the operator removes `0xABC...` from the allowlist.
4. Dispatch fails with an address guard violation. No funds move.

This means revoking an address actually stops the next transfer, even if movements targeting that address are already approved and queued.

The allowlist also distinguishes two action classes, because not every on-chain executor controls both ends of a transfer:

* **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.

<Tip>
  The address allowlist is enforced at the database level for on-chain executors. It is not a client-side check that can be bypassed.
</Tip>

## 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 terminalizes `FAILED`.

When the check fails, one of these error codes is raised:

| Error code                  | Raised when                                                                                                             |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `SIGNED_RECIPIENT_MISMATCH` | The recipient re-derived from the signed artifact does not equal the allowlist-validated destination.                   |
| `SIGNED_SOURCE_MISMATCH`    | The source re-derived from the signed artifact does not equal the validated source (strict source+destination actions). |
| `SIGNED_TARGET_MISMATCH`    | The contract/target re-derived from the signed artifact does not equal the trusted config value.                        |
| `SIGNED_TX_DECODE_FAILED`   | The signed legacy transaction could not be decoded to extract recipient/target.                                         |
| `EIP712_DIGEST_MISMATCH`    | The re-hashed Gateway burn-intent EIP-712 digest does not match the signed digest.                                      |
| `MISSING_PREPARED_PAYLOAD`  | The persisted prepared payload required for re-derivation is absent at submit time.                                     |

<Warning>
  This invariant defends against a malicious or swapped signer, injected signature output, and prepared-action tampering. It does NOT defend against a full DB-row tamper that rewrites the trusted anchor and signed artifact together. QTG treats that as the DB-write threat boundary rather than an in-band approved-row-hash problem.
</Warning>

## RBAC: Three Roles

Every API key is bound to exactly one role. The role determines what operations that key can perform:

| Role         | Can Do                                                                                         | Cannot Do                                                                  |
| ------------ | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| **Admin**    | Register templates, manage signers (rotate, retire), manage the registry, configure allowlists | --                                                                         |
| **Operator** | Approve/reject movements, retry failed movements, read balances, trigger capital transfers     | Register templates, manage signers                                         |
| **Agent**    | Propose movements within its authority binding, read its own movement status                   | Approve, reject, manage templates, manage signers, read other agents' data |

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.

Even if an agent key is leaked, the attacker can only propose movements within that authority binding -- and those proposals still require operator approval (unless an auto-approve policy is in scope).

```mermaid theme={null}
flowchart LR
    subgraph roles["Role Hierarchy"]
        ADMIN["Admin\n(full control)"]
        OPERATOR["Operator\n(approve + operate)"]
        AGENT["Agent\n(propose only)"]
    end

    ADMIN --> OPERATOR
    OPERATOR --> AGENT

    AGENT -->|"propose"| MOVEMENT["Movement\n(PENDING_APPROVAL)"]
    OPERATOR -->|"approve"| MOVEMENT
    MOVEMENT -->|"dispatch"| KMS["KMS Sign"]

    style AGENT fill:#6366f1,stroke:#4f46e5,color:#fff
    style OPERATOR fill:#f59e0b,stroke:#d97706,color:#000
    style ADMIN fill:#dc2626,stroke:#b91c1c,color:#fff
```

## Audit Trail

Every state transition in QTG is recorded in an append-only `audit_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.

Audit events are queryable via the API. The primary endpoint is `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.

<Note>
  For the receiver-side implementation contract (what your callback endpoint must verify), see [Callback Verification Contract](/callback-verification-contract). For a working example, see [Callback Receiver Example](/callback-receiver-example).
</Note>

## 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.

This means every API call is attributable to a specific key, and therefore a specific role. Combined with the audit trail, you have a complete chain: who requested what, who approved it, and what happened.

## Summary: Defense in Depth

No single layer is the entire security story. The layers work together:

| Layer                          | What it prevents                                                                                                                                              |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **KMS signing**                | Key theft -- private keys never exist in QTG's memory.                                                                                                        |
| **Approval gate**              | Unauthorized transfers -- nothing moves without explicit approval.                                                                                            |
| **Outflow velocity cap**       | Runaway aggregate volume -- a per-token rolling-window ceiling brakes a flood of individually-valid movements (runaway bot, stolen strategy key, fat-finger). |
| **Address allowlist**          | Wrong-destination transfers -- checked at dispatch time, not just creation.                                                                                   |
| **Signed-recipient invariant** | Signer substitution / prepared-action tamper -- the broadcast tx is decoded and its recipient must equal the allowlist-validated destination.                 |
| **RBAC**                       | Privilege escalation -- agents cannot approve, operators cannot manage signers.                                                                               |
| **HMAC auth**                  | Request forgery -- every API call is signed and attributable.                                                                                                 |
| **Audit trail**                | Undetected changes -- every state transition is recorded.                                                                                                     |
| **Callback signing**           | Notification spoofing -- receivers can verify authenticity and reject replays.                                                                                |

For the full architecture overview, see [Architecture Overview](/reference/overview).
