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

# Going Live

> Production setup with real credentials, KMS signing, and live verification

# 04 - Going Live

> Settings required to move from dry-run to real operation.
> Enable auth, wire callbacks, start workers, deploy with Docker, and configure the address allowlist.

***

## 1. Enable Workers

The key setting for moving from dry-run to live:

```bash theme={null}
MG_WORKERS_ENABLED=true
```

Enabled workers:

| Worker               | Role                                   | Related settings                                          |
| -------------------- | -------------------------------------- | --------------------------------------------------------- |
| dispatcher           | Hand off READY nodes to executors      | -                                                         |
| observer             | Poll state for running nodes           | `MG_NODE_OBSERVE_INTERVAL_SECONDS`                        |
| recovery             | Try recovery for FAILED/UNKNOWN nodes  | `MG_NODE_RECOVERY_INTERVAL_SECONDS`                       |
| callback\_dispatcher | Deliver outbox -> callback             | `MG_CALLBACK_TIMEOUT_SECONDS`, `MG_CALLBACK_MAX_ATTEMPTS` |
| expiration\_checker  | Expire nodes that exceeded the timeout | `MG_APPROVAL_TTL_SECONDS`                                 |
| executor\_health     | Periodically check executor status     | `MG_EXECUTOR_HEALTH_INTERVAL_SECONDS`                     |

***

## 2. Configure Exchange Credentials

To use the CEX executor, you need exchange API keys:

```bash theme={null}
# Upbit
MG_UPBIT_ACCESS_KEY=your-upbit-access-key
MG_UPBIT_SECRET_KEY=your-upbit-secret-key

# Binance
MG_BINANCE_ACCESS_KEY=your-binance-access-key
MG_BINANCE_SECRET_KEY=your-binance-secret-key
MG_BINANCE_BASE_URL=https://api.binance.com
```

The distribution ships Upbit, Binance, Bybit, Coinbase, and OKX adapters.

> **These credentials are plaintext on disk.** That is a deliberate tradeoff, and it is survivable — but only if your exchange-side IP restriction and withdrawal address whitelist are configured. See [Security Tradeoffs § Exchange credentials](/security-tradeoffs#2-exchange-credentials) before you deposit real capital.

***

## 3. Configure EVM / Signer (CCTP, Gateway)

To use on-chain executors:

```bash theme={null}
# EVM RPC endpoints (chain_id -> URL)
MG_EVM_RPC_ENDPOINTS_JSON='{"84532":"https://sepolia.base.org","421614":"https://sepolia-rollup.arbitrum.io/rpc"}'

# Local AWS KMS signer
MG_LOCAL_SIGNER_BACKEND=aws_kms_evm
MG_LOCAL_SIGNER_KEY=my-evm-signer
MG_LOCAL_SIGNER_KMS_KEY_ID=arn:aws:kms:ap-northeast-2:123456:key/xxx
MG_LOCAL_SIGNER_AWS_REGION=ap-northeast-2

# Gateway (additional)
MG_GATEWAY_API_BASE_URL=https://gateway-api.circle.com
```

After configuring the signer, check it with preflight:

```bash theme={null}
uv run python -m qtg.interfaces.tools.cctp_live_preflight
```

> Details: [Signer protocol](/reference/signers/signer-protocol), [CCTP bridge lane](/reference/executors/bridges/cctp-lane)

***

## 4. Enable HMAC Auth

### 4-1. Create API Keys

```bash theme={null}
MG_AUTH_ENABLED=true
```

Generate API keys with the seed CLI:

```bash theme={null}
# Operator key (template/movement creation, approval)
uv run python -m qtg.interfaces.tools.seed_auth_client \
  --client-name dashboard \
  --key-id dashboard-prod \
  --role operator

# Admin key (executor/signer/policy management)
uv run python -m qtg.interfaces.tools.seed_auth_client \
  --client-name admin-cli \
  --key-id admin-prod \
  --role admin

# Agent key (read + create/execute proposals)
uv run python -m qtg.interfaces.tools.seed_auth_client \
  --client-name strategy-engine \
  --key-id agent-prod \
  --role agent
```

Store the output `key_id` and `secret` safely.

### 4-2. HMAC Signing on the Client

Canonical string layout:

```
METHOD\n
PATH\n
QUERY\n
SHA256(raw_body_bytes)\n
TIMESTAMP\n
NONCE
```

* `PATH` is the routed, percent-decoded path the server dispatches on — no origin, no query string.
* `QUERY` is the query **exactly as sent**: no leading `?`, **no sorting**, empty string if absent.
  Sorting before signing transmits bytes you did not sign, and every such request 401s.

Required headers:

```
X-QTG-Key-Id: <key_id>
X-QTG-Timestamp: <unix_epoch>
X-QTG-Nonce: <uuid>
X-QTG-Signature: <hmac_sha256_hex>
```

### 4-3. curl Example (With Signature)

```bash theme={null}
KEY_ID="dashboard"
SECRET="your-secret-here"
TIMESTAMP=$(date +%s)
NONCE=$(uuidgen | tr '[:upper:]' '[:lower:]')
BODY='{"template_key":"cex.upbit_to_binance.xrp"}'
BODY_HASH=$(echo -n "$BODY" | shasum -a 256 | cut -d' ' -f1)

CANONICAL="GET\n/v3/movements\n\n${BODY_HASH}\n${TIMESTAMP}\n${NONCE}"
SIGNATURE=$(echo -n "$CANONICAL" | openssl dgst -sha256 -hmac "$SECRET" | cut -d' ' -f2)

curl -s http://localhost:8100/v3/movements \
  -H "X-QTG-Key-Id: ${KEY_ID}" \
  -H "X-QTG-Timestamp: ${TIMESTAMP}" \
  -H "X-QTG-Nonce: ${NONCE}" \
  -H "X-QTG-Signature: ${SIGNATURE}"
```

### 4-4. Access Scope by Role

Access is governed by a **3-role axis** (`admin` / `operator` / `agent`), set
per key with `--role` on `seed_auth_client`. Each route maps to the set of
roles allowed to call it; `admin` has the widest reach.

| Role       | Representative endpoints                                                                                                                                     |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `agent`    | GET reads, `POST /v3/movements`, agent-authority template/bridge/topup proposals                                                                             |
| `operator` | All `agent` access plus `POST /v3/movements/{id}/approve`, reject, actions/resume, retry, cancel; the full capital-transfer surface; GET executors/signers   |
| `admin`    | All `operator` access plus `POST /v3/plan-templates`, PATCH /v3/executors/*, PATCH /v3/signers/*, admin signer rotation, auto-approve policies, verify-drift |

**Capital-transfer surface** (operator + admin):

| Method | Path                                                           |
| ------ | -------------------------------------------------------------- |
| `POST` | `/v3/capital-transfers`                                        |
| `GET`  | `/v3/capital-transfers`, `/v3/capital-transfers/{transfer_id}` |
| `POST` | `/v3/capital-transfers/{transfer_id}/actions/resolve`          |

> Details: [Auth rollout](/reference/infrastructure/auth-rollout), [v3 API endpoints](/reference/api/v3-endpoints)

***

## 5. Wire Callbacks

### 5-1. Callback Configuration

```bash theme={null}
MG_CALLBACK_HMAC_SECRET=strong-random-secret-here
MG_CALLBACK_ALLOWED_HOSTS_CSV=your-callback-host.example.com,localhost
MG_CALLBACK_TIMEOUT_SECONDS=10
MG_CALLBACK_MAX_ATTEMPTS=5
```

If you set `callback: {"url": "..."}` when creating a movement, a callback is delivered on every state change.

### 5-2. Implement the Callback Receiver

The receiving server should validate:

1. **Signature verification** - `X-QTG-Callback-Signature` header
2. **Timestamp freshness** - within 5 minutes
3. **Reject duplicate nonces** - replay defense
4. **200 OK response** - 4xx is permanent failure, 5xx is retried

```python theme={null}
import hashlib
import hmac

def verify_callback(body: bytes, timestamp: str, nonce: str, signature: str, secret: str) -> bool:
    body_hash = hashlib.sha256(body).hexdigest()
    canonical = f"{timestamp}\n{nonce}\n{body_hash}"
    expected = hmac.new(secret.encode(), canonical.encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(signature, expected)
```

> Full contract: [Callback verification contract](/callback-verification-contract)
> Example: [Callback receiver example](/callback-receiver-example)

***

## 6. Address Allowlist

The address guard is applied at dispatch time to on-chain executors (CCTP,
Gateway, CCIP, Stargate, USDT0, ERC-20 transfer) **and to CEX withdrawals**.
Register the wallet addresses funds move between — not token contracts.

### Register Addresses

```bash theme={null}
export MG_ADMIN_CLI_KEY_ID=...       # secret goes in MG_ADMIN_CLI_KEY_SECRET (env-only)
export MG_ADMIN_CLI_KEY_SECRET=...

python -m qtg.interfaces.tools.seed_allowed_address \
  --chain-family evm \
  --chain-id 8453 \
  --address 0x0077777d7eba4688bdef3e311b846f25870a19b9 \
  --label "Gateway Wallet" \
  --reason "base gateway lane"
```

The dashboard exposes the same mutations behind the writer HMAC boundary
(`POST /dashboard/allowed-addresses`, operator UI `/governance/allowlist`).
Avoid raw SQL `INSERT`s — they bypass the audit log, actor attribution, and the
cooling-period stamp.

* EVM addresses are normalized to **lowercase** automatically
* Revoke with `python -m qtg.interfaces.tools.revoke_allowed_address --allowed-address-id <id> --reason ...` (soft-delete, preferred over deletion)
* A row is `(chain_family, address, chain_id?)` — **no token scope**. An
  allowlisted address accepts any token, and omitting `--chain-id` matches every
  chain.
* A CEX withdrawal also needs the destination registered in the **exchange's own
  whitelist**; QTG checks both and fails with `ADDRESS_NOT_WHITELISTED` when the
  exchange address book does not match.

> Details: [Runbook — Address Allowlist Management](/reference/runbook-v3)
> · [CEX Stuck Node Recovery](/runbooks/cex-stuck-node-recovery)

***

## 7. Docker Deployment

```bash theme={null}
# Prepare the .env file (project root)
cp .env.example .env
# ... edit .env ...

# Start Docker Compose
docker compose -f infra/docker-compose.yml up -d postgres qtg-mainnet qtg-dashboard
```

Name the services. The file also defines `qtg-testnet` and two throwaway test
databases; starting everything runs a mainnet **and** a testnet app against the same
database, and the one whose bucket was not migrated fails its boot schema check.

Service layout:

| Service         | Port | Description                                |
| --------------- | ---- | ------------------------------------------ |
| `qtg-mainnet`   | 8100 | FastAPI backend, `MG_NETWORK_MODE=mainnet` |
| `qtg-testnet`   | 8101 | FastAPI backend, `MG_NETWORK_MODE=testnet` |
| `qtg-dashboard` | 3000 | React SPA dashboard                        |

One database holds one network bucket, so run one of the two app services — not both.

> See [operator-network-access.md](/runbooks/operator-network-access) for production exposure topology.

```bash theme={null}
# Check status
docker compose -f infra/docker-compose.yml ps
curl -s http://localhost:8100/healthz | python -m json.tool

# Check logs
docker compose -f infra/docker-compose.yml logs -f qtg-mainnet
```

> Docker uses the project-root `.env` through `env_file: ../.env`.
> `MG_DATABASE_URL` is overridden in docker-compose to use the PostgreSQL hostname.

***

## 8. Live Cutover Checklist

Things to confirm before production cutover:

* [ ] `MG_WORKERS_ENABLED=true` configured
* [ ] Exchange API keys configured and balances checked
* [ ] EVM RPC endpoints configured (if using on-chain paths)
* [ ] Signer configured and preflight passing (if using on-chain paths)
* [ ] `MG_AUTH_ENABLED=true` plus API keys issued
* [ ] Callback secret configured plus receiving server ready
* [ ] Address allowlist registered (on-chain paths and CEX withdrawal destinations)
* [ ] CEX withdrawal destinations also registered in the exchange's own whitelist
* [ ] **Small-value live test** - verify end-to-end with a real small transfer, not dry-run
* [ ] Dashboard access confirmed
* [ ] Callback receiver verification confirmed (signature, nonce, timestamp)

> Operational procedures: [Secret Rotation](/runbooks/secret-rotation-checklist) · [CEX Stuck Node](/runbooks/cex-stuck-node-recovery)

***

## Replace default RPC endpoints

The quickstart ships PublicNode-based defaults, which are rate-limited and not
suitable for live transfers. For production:

1. Pick a provider per chain (Alchemy, Infura, QuickNode, or self-hosted).

2. Override per-chain in `.env` — keep defaults for any chain you do not yet
   intend to enable:

   ```bash theme={null}
   MG_EVM_RPC_ENDPOINTS_JSON='{
     "1":  "https://eth-mainnet.g.alchemy.com/v2/<key>",
     "8453": "https://base-mainnet.g.alchemy.com/v2/<key>",
     "42161": "https://arb-mainnet.g.alchemy.com/v2/<key>"
   }'
   ```

3. In air-gapped or regulated environments, also set
   `MG_EVM_RPC_USE_DEFAULTS=false` to block any silent fallback to PublicNode.

Live preflight / drill / e2e tools already hard-code defaults off — they
require explicit RPC URLs even with `MG_EVM_RPC_USE_DEFAULTS=true`. Set those
explicit URLs before running them.

See [rpc-defaults.md](/reference/rpc-defaults) for the full shipped chain
list and the CCIP path caveat.

***

## Related Docs

| Topic                          | Document                                                                    |
| ------------------------------ | --------------------------------------------------------------------------- |
| Full environment variable list | [Bootstrap reference](/reference/infrastructure/bootstrap)                  |
| API endpoint details           | [v3 API endpoints](/reference/api/v3-endpoints)                             |
| Auth rollout runbook           | [Auth rollout](/reference/infrastructure/auth-rollout)                      |
| Callback contract              | [Callback verification contract](/callback-verification-contract)           |
| Signer protocol                | [Signer protocol](/reference/signers/signer-protocol)                       |
| Bridge lane references         | [CCTP bridge lane](/reference/executors/bridges/cctp-lane)                  |
| Operator runbooks              | [Secret Rotation](/runbooks/secret-rotation-checklist) and related runbooks |
