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

# Local Setup

> Get QTG running locally with Docker Compose and PostgreSQL

# 01 - Local Setup

> Start the QTG server locally and confirm the health check.

<Frame caption="QTG local setup in the CLI.">
  <img src="https://mintcdn.com/jephalabs/Gr6wyGT8kv-9bqrI/images/quickstart-cli.svg?fit=max&auto=format&n=Gr6wyGT8kv-9bqrI&q=85&s=72e7e62fb21c0952143f3620cefe4fe3" alt="QTG CLI and MCP quickstart" width="720" height="300" data-path="images/quickstart-cli.svg" />
</Frame>

***

## Prerequisites: a container engine

`qtg init` brings up PostgreSQL via Docker Compose, so you need a running
container engine first. Install **one** of:

* [Docker Desktop](https://www.docker.com/products/docker-desktop/) (macOS / Windows / Linux), or
* [OrbStack](https://orbstack.dev/) (macOS — lighter, faster Docker engine).

Confirm it works before running `qtg init`:

```bash theme={null}
docker compose version   # prints a version
docker info              # succeeds (daemon reachable) — start the app if it errors
```

If `docker` is missing or `docker info` fails, install/launch the engine above;
`qtg init` will still write your `.env` but will skip bring-up.

## Quick path (recommended)

`qtg init` is the single front door. It interactively writes a signing-capable
dev `.env` and optionally brings up PostgreSQL + runs migrations:

```bash theme={null}
git clone https://github.com/jephalabs/quant-transfer-guard
cd quant-transfer-guard

uv venv && source .venv/bin/activate && uv sync --extra dev

docker volume create quant-transfer-guard_qtg-postgres-data   # first install only
uv run qtg init
```

The Postgres data volume is declared `external` so a stray `docker compose down -v`
cannot delete it — which also means Compose will not create it for you. Create it once
on a first install. If Compose reports it missing on a machine that has run QTG before,
do **not** create it: the database volume is gone, and recovering it comes first.

`qtg init` prompts for:

* **Network mode** (`mainnet` / `testnet`) — and overlays the matching Circle
  Gateway / CCTP Iris endpoints automatically.
* **Signer private-key storage** — **1Password (recommended)** is the default.
  Select an existing secret, store an operator-generated development key in a new
  1Password item, or enter an `op://` reference. QTG never generates the key;
  it validates the resolved signer against the confirmed **public EVM address** and
  stores only the reference and non-secret `OP_ACCOUNT` account pin in `.env`.
  **Plaintext .env (development only)** remains an explicit, warned alternative
  for a low-value key; it writes that key unencrypted to `.env`.
* **Exchange API keys** — a multi-select for the Free venues (Upbit / Binance /
  OKX / Bybit / Coinbase); skip any you don't need yet.

DB URL, callback HMAC secret, allowed hosts, and RPC defaults are auto-filled.
`op` mode removes the EVM signer private key from the `.env`; it does not move
callback, exchange, database, RPC, or other application secrets. That broader
configuration/secret boundary is out of scope here.

Useful flags: `--network mainnet|testnet`, `--no-bringup` (write `.env` only),
`--force` (overwrite an existing `.env`, backed up first), `--yes` (non-interactive,
requires `--network` + a plaintext key when the signer source is omitted),
`--repo-root <path>`. Non-interactive 1Password setup is local-reference-only: it
does not create, edit, or delete an item. Supply all of its public inputs:

```bash theme={null}
uv run qtg init --yes --network testnet --signer-source op \
  --signer-ref 'op://<vault-id>/<item-id>/<field>' \
  --op-account <account-id> \
  --expected-signer-address 0x<40-hex-address>
```

`--op-account` may instead be supplied by ambient `OP_ACCOUNT` when it agrees with
an explicit value. The expected address is required so QTG can fail closed if the
reference resolves to another signer. In `--yes` mode, omitting `--signer-source`
preserves the compatible plaintext flow.

> For plaintext `--yes` mode, prefer supplying the key via the
> `QTG_DEV_SIGNER_KEY` environment variable rather than `--signer-key` — a CLI
> argument lands in shell history and process listings (`ps`). The interactive path
> always uses a hidden prompt and never touches argv.

Then start the dev server. For the recommended 1Password source, the local
1Password app/session and configured `op` executable must remain available at
bootstrap and whenever QTG signs. Use the source-aware command; it clears ambient
account and unsupported unattended selectors so the `OP_ACCOUNT` pin in `.env` is
authoritative:

```bash theme={null}
env -u OP_ACCOUNT -u OP_SERVICE_ACCOUNT_TOKEN -u OP_CONNECT_HOST -u OP_CONNECT_TOKEN \
  uv run --env-file .env uvicorn qtg.main:app --reload --port 8100
```

If you explicitly selected the plaintext development source, use the ordinary
`--env-file` command instead:

```bash theme={null}
uv run --env-file .env uvicorn qtg.main:app --reload --port 8100
```

> `docker compose up <app>` is **not** the dev signing path — the containerized
> app services intentionally blank the signer config. Use the matching local
> `uvicorn` command above for a signing-capable dev server; this onboarding does not
> configure Compose signing or unattended 1Password access.

Treat an edit to the selected 1Password item as signer rotation, not a transparent
configuration change: verify the signer address before use. If `qtg init --force`
replaces an existing `.env`, its mode-`0600` `.env.bak.*` backup can still contain a
plaintext key or other sensitive configuration and is not deleted automatically.

## Check the CLI surface

The current QTG control plane has three entry points:

| Command     | Use it for                                                                                                 |
| ----------- | ---------------------------------------------------------------------------------------------------------- |
| `qtg`       | Operator/admin shell checks such as `whoami`, audit query, signer inventory, venue probes, and `qtg init`. |
| `qtg-agent` | Agent-safe read and proposal commands. It cannot execute movements.                                        |
| `qtg-mcp`   | Stdio MCP server for agent clients such as Claude Desktop, Cursor, or Claude Agent SDK.                    |

After you seed an operator or agent HMAC key, point the CLI at the local API:

```bash theme={null}
export QTG_API_URL="http://localhost:8100"
export QTG_KEY_ID="<key-id>"
export QTG_HMAC_SECRET_FILE="/path/to/hmac-secret"
export QTG_AGENT_NAMESPACE="agent/<authority-key>"

qtg whoami
qtg-agent balances --json
```

<Note>
  `qtg-agent` and `qtg-mcp` are read + propose surfaces. They can inspect
  balances, movements, timelines, authority state, and template proposals, but
  they do not run an approved movement by themselves.
</Note>

To tear down docker: `./scripts/quickstart.sh --down`

### Troubleshooting bring-up

`qtg init` writes your `.env` **first**, then attempts bring-up — so a bring-up
failure never loses your config. If it prints `Bring-up skipped (…)`, your
`.env` is already in place; fix the engine and run the migrations manually
(below). Common causes:

| Symptom                                                                       | Cause                                                                | Fix                                                                                                                                                                                                                                                                                                                          |
| ----------------------------------------------------------------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Bring-up skipped (… docker: command not found)`                              | No container engine                                                  | Install Docker Desktop or OrbStack (see Prerequisites)                                                                                                                                                                                                                                                                       |
| `… healthy but unreachable from the host …`                                   | Published port forward not yet routing (engine warm-up)              | `qtg init` already retries \~30s; if it persists, the port is wedged/occupied — see next rows                                                                                                                                                                                                                                |
| `server closed the connection unexpectedly` on `localhost:5432`               | Another process owns `5432`, or the engine's `5432` forward is stuck | `docker port qtg-postgres` (confirm `127.0.0.1:5432`), then `lsof -iTCP:5432` / `sudo lsof -iTCP:5432` to find a squatter (e.g. a host Postgres). If only the engine is listed yet it still fails, fully restart the container engine. The container is also reachable directly at `qtg-postgres.orb.local:5432` (OrbStack). |
| `network Alembic bucket mismatch: … initialized for 'X', refusing to run 'Y'` | The DB already holds a different network bucket                      | One DB = one network bucket (plus `global`). Use a fresh database, or match `MG_NETWORK_MODE` to the bucket the DB was initialized with.                                                                                                                                                                                     |

Re-run migrations manually after fixing the engine (global bucket + **your**
network only — see Step 4):

```bash theme={null}
uv run alembic -c alembic_v3.ini -x network=global  upgrade global_chain@head
uv run alembic -c alembic_v3.ini -x network=mainnet upgrade network_chain@head   # OR testnet, not both
```

***

## Manual path

If you prefer to configure each step yourself:

### Step 1: Clone and install

```bash theme={null}
git clone https://github.com/jephalabs/quant-transfer-guard
cd quant-transfer-guard

uv venv
source .venv/bin/activate
uv sync --extra dev
```

### Step 2: Configure environment

```bash theme={null}
cp .env.example .env
```

Open `.env` and set at least these three values:

```bash theme={null}
# --- Required ---
MG_NETWORK_MODE=mainnet
MG_DATABASE_URL=postgresql+asyncpg://qtg:qtg@localhost:5432/qtg_v3
MG_CALLBACK_HMAC_SECRET=dev-callback-secret-change-me
MG_CALLBACK_ALLOWED_HOSTS_CSV=localhost,127.0.0.1
```

`MG_NETWORK_MODE` has **no default** — the server refuses to boot if it is
unset. Set it to `mainnet` or `testnet`. The shipped `.env.example` uses
`mainnet`.

### Key Environment Variables Summary

| Variable                        | Default                | Description                                                |
| ------------------------------- | ---------------------- | ---------------------------------------------------------- |
| `MG_NETWORK_MODE`               | (required, no default) | `mainnet` or `testnet` — server refuses to boot if unset   |
| `MG_DATABASE_URL`               | (required)             | DB connection URL                                          |
| `MG_CALLBACK_HMAC_SECRET`       | (required)             | Outbound callback signing key                              |
| `MG_CALLBACK_ALLOWED_HOSTS_CSV` | (required)             | Allowed hosts for callback URLs                            |
| `MG_WORKERS_ENABLED`            | `false`                | Enable workers (`false` = API only, `true` = full runtime) |
| `MG_AUTH_ENABLED`               | `false`                | Inbound API HMAC auth                                      |
| `MG_ENVIRONMENT`                | `local`                | Environment identifier                                     |
| `MG_CEX_BINDINGS_JSON`          | `{}`                   | Binance master/sub logical venue alias binding             |

### RPC endpoints — pre-populated

QTG ships a curated PublicNode-based default RPC table covering \~24 EVM
chains. You do not need to set `MG_EVM_RPC_ENDPOINTS_JSON` for the quickstart
to talk to lanes like Base Sepolia, Arbitrum One, Ethereum mainnet, etc.

When you go to production, override per-chain with a paid provider — see
[04-going-live.md](/quickstart/04-going-live) and
[rpc-defaults.md](/reference/rpc-defaults).

> For the full environment variable list, see the [Bootstrap reference](/reference/infrastructure/bootstrap).

### Binance Master / Sub Logical Venue Alias Example

To use a Binance main account and an experimental sub-account at the same time, set `.env` like this.
The sub binding must use a credential-less shape, and balance reads use the master key to call
`/sapi/v3/sub-account/assets`. Because of that, the master API key needs the
**Enable Sub-Account Management** permission.
Binance sub-account asset/transfer APIs take the sub-account **email** in the
`email` / `fromEmail` parameter, not a numeric `subUserId`. Because of that,
the current `sub_account_uid` field name stays the same, but the value must be an email.

```bash theme={null}
MG_CEX_BINDINGS_JSON='{
  "binance_master": {
    "provider": "binance",
    "account_role": "master",
    "access_key": "main-ak",
    "secret_key": "main-sk",
    "base_url": "https://api.binance.com",
    "recv_window_ms": 5000
  },
  "binance_lab": {
    "provider": "binance",
    "account_role": "sub",
    "sub_account_uid": "lab-sub@example.com",
    "master_alias": "binance_master"
  }
}'
```

After that, you can use an alias such as `source_exchange: "binance_lab"` directly in template/runtime bindings.
By contrast, the external-withdrawal proving adapter cannot be created directly from a sub binding and must use the master binding or the capital transfer subsystem.

### Step 3: Start PostgreSQL

The manual path does not run `qtg init`, so nothing has started a database yet.
Bring up the same Compose service the quick path uses — no external PostgreSQL is
required, and the credentials match the `MG_DATABASE_URL` default above:

```bash theme={null}
# First install only: the data volume is declared `external`, so Compose will not
# create it. That is deliberate — a missing volume otherwise means the database was
# removed, and Postgres must not come up on an empty data directory as if nothing had
# happened. Creating an existing volume is a no-op, so this is safe to re-run.
docker volume create quant-transfer-guard_qtg-postgres-data

docker compose -f infra/docker-compose.yml up -d postgres
docker compose -f infra/docker-compose.yml ps postgres   # wait for "healthy"
```

If Compose reports that the external volume is missing on a machine that has been
running QTG before, do **not** create it and start over — the database volume is gone
and recovering it comes first.

If you would rather point at a PostgreSQL you already run, set `MG_DATABASE_URL`
to it instead and skip this step; the migrations below then apply to that database.

## Step 4: Start the Server

> Database schema is managed by Alembic migrations. Run `uv run qtg init` (which runs migrations automatically) or apply manually. Migrate the **global** bucket plus the **one** network bucket matching `MG_NETWORK_MODE` — a single database holds exactly one network bucket (the schema guard rejects the second):
>
> ```bash theme={null}
> uv run alembic -c alembic_v3.ini -x network=global  upgrade global_chain@head
> # then EITHER mainnet OR testnet, matching MG_NETWORK_MODE — not both:
> uv run alembic -c alembic_v3.ini -x network=mainnet upgrade network_chain@head
> ```

If your `.env` uses the recommended 1Password signer source, use the source-aware
command from the quick path so the file's `OP_ACCOUNT` pin is not overridden. If you
explicitly configured a local plaintext development signer, pass `--env-file .env`
so the non-`MG_`-prefixed key variable lands in the process env:

```bash theme={null}
uv run --env-file .env uvicorn qtg.main:app --reload --port 8100
```

## Step 5: Health Check

```bash theme={null}
curl -s http://localhost:8100/healthz | python -m json.tool
```

Expected response:

```json theme={null}
{
    "ok": true,
    "service": "movement-guard",
    "api_prefix": "/v3"
}
```

***

## Understand the Run Modes

QTG runs in two modes:

| Mode             | `MG_WORKERS_ENABLED` | Purpose                                                                        |
| ---------------- | -------------------- | ------------------------------------------------------------------------------ |
| **API-only**     | `false` (default)    | Template registration, movement create/read, development/testing               |
| **Full runtime** | `true`               | Full behavior including executor dispatch, observer polling, callback delivery |

The quickstart starts in **API-only mode**. Real exchange/chain integrations are covered in [04-going-live.md](/quickstart/04-going-live).

***

## Dashboard (Optional)

```bash theme={null}
# Development mode (Vite dev server)
cd frontend && npm install && npm run dev    # port 5173, proxy -> localhost:8100

# Docker (backend + dashboard together)
docker compose -f infra/docker-compose.yml up -d postgres qtg-mainnet qtg-dashboard
```

The dashboard is available at `http://localhost:5173` (dev) or `http://localhost:3000` (Docker).

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

***

## Verification Commands

```bash theme={null}
make test                            # Tier 0 lane, no Docker required
uv run ruff check src tests                      # Lint
uv run mypy src                                  # Type check
```

***

**Next:** [02-first-movement.md](/quickstart/02-first-movement) - register your first template and create a movement
