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

# Data Model

> PostgreSQL schema — tables, relationships, and migration strategy

# Data Model Reference

> Source files: `src/qtg/infrastructure/db/models.py`, `session.py`

Defines the full data model of the qtg v3 runtime. The current schema has roughly 54 tables across two logical schemas; the ERD below shows a simplified view of the core movement + registry structure documented in detail in this page.

* **Plan Template layer** — design-time definition of the movement plan
* **Request layer** — runtime instance
* **Audit layer** — events / artifacts / callback records
* **Registry layer** — persistence for executor/signer registrations

***

## Schema layout (PostgreSQL, PG-only since v0.1.0)

QTG runs on PostgreSQL only and splits its tables into two logical schemas defined in `models.py` via `__table_args__`:

* **`public`** — deployment-global, network-independent identity tables. Exactly three:
  * `api_clients`, `api_client_keys`, `nonce_registry`
  * Alembic head is tracked in `public.alembic_version_global`.
* **`qtg_network`** — the logical schema used by every other model (\~51 tables). At runtime SQLAlchemy rewrites `qtg_network` to the concrete network bucket via `schema_translate_map={"qtg_network": <network>}`, and each session runs `SET LOCAL search_path` for its network class.

The logical network schema can be materialized in either of two physical buckets:

* **`mainnet`** and **`testnet`** — one full copy of the `qtg_network` table set per network class. Each carries its own `alembic_version` head.

A normal QTG process and its database are single-network: `qtg init --network <network>` creates `public` plus exactly the selected `mainnet` or `testnet` bucket. At startup, `boot_validate_schema_invariants()` checks those two schemas and their Alembic heads. The unselected bucket is not required or queried, even though the runtime engine registry can represent both `NetworkClass` values. See [Bootstrap & Configuration](/reference/infrastructure/bootstrap) for the bootstrap chain.

### Table families

Beyond the core movement/registry tables detailed below, the schema includes these families (all in `qtg_network` unless noted):

* **Identity (public)**: `api_clients`, `api_client_keys`, `nonce_registry`
* **Operator / policy**: `allowed_addresses`, `auto_approve_policies`, `approval_policy_targets`, `budget_ledger`, `execution_stats`, `template_proposals`
* **Audit**: `audit_events`, `audit_outbox`
* **Agent wallet top-up**: `agent_authorities` (+ `agent_authority_signers`, `agent_authority_allowed_addresses`, `agent_authority_budget_ledger`), `agent_wallet_funding_envelopes`, `agent_wallet_topup_ledger`
* **Capital transfers**: `capital_transfer_requests`
* **Balance reservations**: `balance_reservations`, `balance_reservation_pools`, `balance_snapshots`
* **Hyperliquid** (6): `hyperliquid_master_accounts`, `hyperliquid_agent_wallets`, `hyperliquid_strategy_bindings`, `hyperliquid_nonce_leases`, `hyperliquid_emergency_states`, plus related state
* **CCIP** (6): `ccip_router_registry`, `ccip_lane_registry`, `ccip_chain_token_lane`, `ccip_chain_source_token`, `ccip_router_preapproval`, `ccip_send_intent`
* **Stargate (Pro)**: `stargate_pools` (+ `stargate_pool_audit`), `stargate_chain_registry` (+ `stargate_chain_registry_audit`), `stargate_path_baseline` (+ `stargate_path_baseline_audit`), `stargate_send_intent`
* **Bridge send intents**: `usdt0_send_intent`, `stargate_send_intent`, `ccip_send_intent`
* **Runtime / signer ops**: `evm_nonce_reservation`, `worker_heartbeats`, `signer_rotation_events`

***

## Entity Relationship Diagram

```mermaid theme={null}
flowchart TB
    Template["MovementPlanTemplate<br/>(movement_plan_templates)"]
    Version["MovementPlanVersion<br/>(movement_plan_versions)<br/>UQ(template_id, version)"]
    PlanNode["PlanNode<br/>(plan_nodes)<br/>UQ(ver, key)"]
    PlanEdge["PlanEdge<br/>(plan_edges)<br/>FK→PlanNode x2"]
    Request["MovementRequest<br/>(movement_requests)<br/>FK→Template, FK→PlanVersion"]
    RequestNode["RequestNode<br/>(request_nodes)<br/>UQ(req, key)<br/>FK→PlanNode"]
    Event["MovementEvent<br/>(events)<br/>FK→Request<br/>FK→RequestNode"]
    Artifact["Artifact<br/>(artifacts)<br/>FK→Request<br/>FK→RequestNode"]
    Outbox["CallbackOutbox<br/>(callback_outbox)<br/>FK→Request"]
    ExecReg["ExecutorRegistryEntry<br/>(executor_registry)<br/>UQ(executor_key)"]
    SignReg["SignerRegistryEntry<br/>(signer_registry)<br/>UQ(signer_key)"]

    Template -->|1:N| Version
    Version -->|1:N| PlanNode
    Version -->|1:N| PlanEdge
    PlanNode -.plan_node_id FK.-> RequestNode
    Request -->|1:N| RequestNode
    Request -->|1:N| Event
    Request -->|1:N| Artifact
    Request -->|1:N| Outbox
```

***

## 1. MovementPlanTemplate

**Table name**: `movement_plan_templates`

Top-level entity of the movement plan. A single template\_key may have multiple versions.

| Column                 | Type                     | Constraint                | Description                                               |
| ---------------------- | ------------------------ | ------------------------- | --------------------------------------------------------- |
| `id`                   | UUID                     | PK, default uuid7         | Unique template ID                                        |
| `template_key`         | String(256)              | NOT NULL, UNIQUE, INDEX   | Human-readable unique key (e.g. `upbit-to-binance-xrp`)   |
| `name`                 | Text                     | NOT NULL                  | Template display name                                     |
| `status`               | Enum(PlanTemplateStatus) | NOT NULL, default `draft` | `draft` / `active` / `archived`                           |
| `owner`                | String(128)              | NULLABLE                  | Template owner identifier                                 |
| `namespace`            | String(128)              | NULLABLE, INDEX           | Logical grouping namespace (e.g. `agent_wallet_topup`)    |
| `auto_approve_enabled` | Boolean                  | NOT NULL, default `False` | Whether this template participates in auto-approve        |
| `daily_cap_amount`     | String(64)               | NULLABLE                  | Per-day cap amount (required when `auto_approve_enabled`) |
| `daily_cap_asset`      | String(32)               | NULLABLE                  | Per-day cap asset (required when `auto_approve_enabled`)  |
| `created_at`           | DateTime(tz)             | NOT NULL                  | Creation time                                             |
| `updated_at`           | DateTime(tz)             | NOT NULL, onupdate        | Last updated timestamp                                    |

**PlanTemplateStatus enum values**: `draft`, `active`, `archived`

**Check constraint**: a `CHECK` enforces `NOT auto_approve_enabled OR (daily_cap_amount IS NOT NULL AND daily_cap_asset IS NOT NULL)` — auto-approve-enabled templates must define a daily cap.

### Internal UUID policy

Internal persisted UUID PKs are created via the common helper `new_internal_uuid()`,
and the current implementation uses stdlib `uuid.uuid7()`.

* DB/API types remain plain `UUID`
* Purpose: improved insert locality and near-time-ordered UUID generation
* Current query ordering semantics still use `created_at`
* Using `id(uuid7)` ordering is a follow-up optimization candidate

***

## 2. MovementPlanVersion

**Table name**: `movement_plan_versions`

Version management unit for a template. Stores node/edge construction, approval/completion policy, and a risk-controls snapshot.

| Column                   | Type             | Constraint                              | Description                                                                  |
| ------------------------ | ---------------- | --------------------------------------- | ---------------------------------------------------------------------------- |
| `id`                     | UUID             | PK                                      | Unique version ID                                                            |
| `template_id`            | UUID             | FK->movement\_plan\_templates.id, INDEX | Owning template                                                              |
| `version`                | Integer          | NOT NULL                                | Version number (starting at 1)                                               |
| `graph_hash`             | String(128)      | NULLABLE                                | Graph construction hash (template\_key + version + nodes + edges + policies) |
| `schema_version`         | String(32)       | NOT NULL, default `v3.0`                | Schema format version                                                        |
| `approval_policy`        | JSON             | NOT NULL, default `{}`                  | Approval policy settings                                                     |
| `completion_policy`      | JSON             | NOT NULL, default `{}`                  | Completion criteria                                                          |
| `risk_controls`          | JSON             | NOT NULL, default `{}`                  | Risk-controls rules                                                          |
| `binding_rules`          | JSON             | NOT NULL, default `{}`                  | Executor/signer binding rules                                                |
| `graph_shape`            | Enum(GraphShape) | NOT NULL, default `LINEAR`              | Graph shape classification                                                   |
| `requires_graph_runtime` | Boolean          | NOT NULL, default `False`               | Whether branch/merge runtime is required                                     |
| `executable_in_v3_0`     | Boolean          | NOT NULL, default `True`                | Whether the current v3.0 engine can execute                                  |
| `non_executable_reasons` | JSON (list)      | NOT NULL, default `[]`                  | List of reasons for non-executability                                        |
| `transport_family`       | String(64)       | NULLABLE                                | Denormalized transport family for this version (e.g. `cex`, `cctp`)          |
| `source_venue`           | String(128)      | NULLABLE                                | Denormalized source venue                                                    |
| `destination_venue`      | String(128)      | NULLABLE                                | Denormalized destination venue                                               |
| `asset`                  | String(32)       | NULLABLE                                | Denormalized asset symbol                                                    |
| `created_at`             | DateTime(tz)     | NOT NULL                                | Creation time                                                                |

**Unique constraint**: `(template_id, version)` — the same version number cannot be reused within a template.

> The `transport_family` / `source_venue` / `destination_venue` / `asset` columns are denormalized routing metadata stored at version-compile time for fast filtering and reporting; the authoritative source remains the node graph.

### GraphShape enum values

| Value       | Meaning                                                       |
| ----------- | ------------------------------------------------------------- |
| `linear`    | Simple sequential graph with nodes connected in a single line |
| `branching` | One node branches into multiple successor nodes               |
| `merging`   | Multiple nodes merge into a single successor node             |
| `split`     | Parallel branches (fan-out)                                   |
| `hybrid`    | Mix of the above patterns                                     |

### completion\_policy JSON structure

```json theme={null}
{
  "completion_assurance": "destination_finalized"
}
```

`CompletionAssurance` values:

* `provider_completed` — provider (exchange/protocol) reports completion
* `destination_observed` — on-chain receive at destination detected
* `destination_credited` — destination deposit confirmed
* `destination_finalized` — destination chain finality confirmed
* `protocol_finalized` — protocol-level finality confirmed

### approval\_policy JSON structure

Stored and covered by the plan-graph hash, but **not read by the runtime**. Every movement
enters `PENDING_APPROVAL` regardless of its contents.

```json theme={null}
{"mode": "manual_first"}
```

### risk\_controls JSON structure

Free-form and carried whole into the compiled-plan hash. Only `max_amount` is enforced, as a
per-node amount cap at dispatch; `min_amount` is read for route filtering. Any other key is inert.

```json theme={null}
{"max_amount": "1000"}
```

***

## 3. MovementPlanNode

**Table name**: `movement_plan_nodes`

Definition of a DAG graph node. Belongs to one plan version and is instantiated as MovementRequestNode at execution time.

| Column              | Type        | Constraint                             | Description                                                   |
| ------------------- | ----------- | -------------------------------------- | ------------------------------------------------------------- |
| `id`                | UUID        | PK                                     | Node definition ID                                            |
| `plan_version_id`   | UUID        | FK->movement\_plan\_versions.id, INDEX | Owning version                                                |
| `node_key`          | String(128) | NOT NULL                               | Node identifier key (e.g. `withdraw_xrp`, `observe_deposit`)  |
| `node_kind`         | String(32)  | NOT NULL                               | Node kind                                                     |
| `action_type`       | String(128) | NOT NULL                               | Action type to execute                                        |
| `executor_selector` | JSON        | NOT NULL, default `{}`                 | Executor selection criteria                                   |
| `signer_selector`   | JSON        | NULLABLE                               | Signer selection criteria (null when signing is not required) |
| `input_schema`      | JSON        | NULLABLE                               | Node input schema definition                                  |
| `config`            | JSON        | NOT NULL, default `{}`                 | Per-node settings                                             |
| `has_side_effect`   | Boolean     | NOT NULL, default `False`              | Whether there are side effects (actual withdrawal, etc.)      |
| `timeout_policy`    | JSON        | NOT NULL, default `{}`                 | Timeout policy                                                |
| `retry_policy`      | JSON        | NOT NULL, default `{}`                 | Retry policy                                                  |
| `risk_patch`        | JSON        | NOT NULL, default `{}`                 | Per-node risk override                                        |

**Unique constraint**: `(plan_version_id, node_key)` — node\_key cannot be duplicated within a version.

### node\_kind values

Defined by the `NodeKind` enum:

| Value          | Description                                                      |
| -------------- | ---------------------------------------------------------------- |
| `action`       | Execution request to an external system (withdrawal, burn, etc.) |
| `observe`      | State observation/polling (deposit check, chain finality, etc.)  |
| `manual_gate`  | Wait for operator manual confirmation                            |
| `compensation` | Compensating transaction on failure                              |
| `post_action`  | Post-processing after completion                                 |

### action\_type examples

| action\_type                         | Description                            |
| ------------------------------------ | -------------------------------------- |
| `cex_withdrawal`                     | CEX withdrawal execution               |
| `cex_withdrawal_status`              | CEX withdrawal state observation       |
| `cex_deposit_status`                 | CEX deposit state observation          |
| `cctp_burn`                          | CCTP burn transaction execution        |
| `cctp_attestation`                   | CCTP attestation observation           |
| `cctp_mint_status`                   | CCTP mint state observation            |
| `destination_chain_receive_observe`  | Destination chain receive observation  |
| `destination_chain_finality_observe` | Destination chain finality observation |
| `protocol_observe`                   | Protocol-level observation             |

### executor\_selector JSON structure

The matching criteria used to choose an executor. The `resolve_bindings()` function binds the actual executor based on this value.

```json theme={null}
{
  "executor_key": "exec.cex.withdrawal_action",
  "exchange": "upbit"
}
```

### signer\_selector JSON structure

Criteria for choosing the signer on nodes that require signing.

```json theme={null}
{
  "signer_key": "signer.evm.local",
  "chain_family": "evm"
}
```

### config (node\_config) JSON structure

When the node executes, this is passed to the executor in the `node_config` field of ExecutionContext. The contents differ by node kind.

**CEX withdrawal node config example**:

```json theme={null}
{
  "exchange": "upbit",
  "asset": "XRP",
  "network": "XRP",
  "destination_exchange": "binance"
}
```

**CCTP burn node config example**:

```json theme={null}
{
  "source_chain_id": 1,
  "destination_chain_id": 43114,
  "token_address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
  "destination_domain": 1
}
```

**Observe node config example**:

```json theme={null}
{
  "chain_family": "evm",
  "chain_id": 43114,
  "match_mode": "tx_hash",
  "confirmations_required": 35
}
```

### timeout\_policy JSON structure

```json theme={null}
{
  "timeout_seconds": 600,
  "on_timeout": "fail"
}
```

### retry\_policy JSON structure

```json theme={null}
{
  "max_attempts": 3,
  "backoff_seconds": 10
}
```

***

## 4. MovementPlanEdge

**Table name**: `movement_plan_edges`

DAG graph edge definition. Manages transition conditions and priority between nodes.

| Column            | Type        | Constraint                             | Description                              |
| ----------------- | ----------- | -------------------------------------- | ---------------------------------------- |
| `id`              | UUID        | PK                                     | Unique edge ID                           |
| `plan_version_id` | UUID        | FK->movement\_plan\_versions.id, INDEX | Owning version                           |
| `from_node_id`    | UUID        | FK->movement\_plan\_nodes.id           | Source node                              |
| `to_node_id`      | UUID        | FK->movement\_plan\_nodes.id           | Destination node                         |
| `edge_type`       | String(32)  | NOT NULL                               | Edge kind                                |
| `condition_expr`  | Text        | NULLABLE                               | Condition expression (future use)        |
| `priority`        | Integer     | NOT NULL, default 0                    | Edge priority (lower is higher priority) |
| `join_key`        | String(128) | NULLABLE                               | Join key (for merging graphs)            |

### EdgeType enum values

| Value              | Description                                |
| ------------------ | ------------------------------------------ |
| `on_success`       | Proceed when the predecessor node succeeds |
| `on_failure`       | Branch when the predecessor node fails     |
| `on_timeout`       | Branch when the predecessor node times out |
| `on_cancel`        | Compensation path on cancel request        |
| `on_manual_resume` | Proceed on manual resume                   |

***

## 5. MovementRequest

**Table name**: `movement_requests`

Runtime instance of a movement execution request. References template/version and tracks the full lifecycle.

| Column                       | Type                    | Constraint                              | Description                                                       |
| ---------------------------- | ----------------------- | --------------------------------------- | ----------------------------------------------------------------- |
| `id`                         | UUID                    | PK                                      | Unique request ID                                                 |
| `template_id`                | UUID                    | FK->movement\_plan\_templates.id, INDEX | Template used                                                     |
| `plan_version_id`            | UUID                    | FK->movement\_plan\_versions.id, INDEX  | Version used                                                      |
| `request_state`              | Enum(RequestState)      | NOT NULL, INDEX, default `RECEIVED`     | Current request state                                             |
| `compiled_plan_hash`         | String(128)             | NOT NULL, INDEX                         | Hash of the compiled execution plan                               |
| `intent`                     | JSON                    | NOT NULL, default `{}`                  | Movement intent                                                   |
| `input_params`               | JSON                    | NOT NULL, default `{}`                  | Execution parameters                                              |
| `completion_policy_snapshot` | JSON                    | NOT NULL, default `{}`                  | Completion policy snapshot at approval time                       |
| `callback_config`            | JSON                    | NOT NULL, default `{}`                  | Callback settings                                                 |
| `approval_status`            | Enum(ApprovalStatus)    | NOT NULL, default `pending`             | Approval state                                                    |
| `reservation_status`         | Enum(ReservationStatus) | NOT NULL, default `none`                | Resource reservation state                                        |
| `graph_runtime_required`     | Boolean                 | NOT NULL, default `False`               | Whether branch runtime is required                                |
| `executable_in_v3_0`         | Boolean                 | NOT NULL, default `True`                | Whether the v3.0 engine can execute                               |
| `non_executable_reasons`     | JSON (list)             | NOT NULL, default `[]`                  | Non-executability reasons                                         |
| `current_frontier`           | JSON (list)             | NOT NULL, default `[]`                  | Keys of nodes currently executable                                |
| `manual_reason`              | Text                    | NULLABLE                                | Reason for manual intervention                                    |
| `strategy_id`                | String(128)             | NULLABLE, INDEX                         | Originating strategy identifier                                   |
| `auto_approve_result`        | JSON                    | NULLABLE                                | Auto-approve **policy trace** (see below)                         |
| `stats_recorded_outcome`     | String(32)              | NULLABLE                                | Outcome already folded into `execution_stats` (idempotency guard) |
| `approved_at`                | DateTime(tz)            | NULLABLE                                | Approval timestamp stamp                                          |
| `approved_amount`            | String(64)              | NULLABLE                                | Amount captured at approval time                                  |
| `created_at`                 | DateTime(tz)            | NOT NULL                                | Creation time                                                     |
| `updated_at`                 | DateTime(tz)            | NOT NULL, onupdate                      | Last updated timestamp                                            |
| `expires_at`                 | DateTime(tz)            | NULLABLE                                | Approval expiration time                                          |

### auto\_approve\_result (policy trace)

`auto_approve_result` is always populated as the auto-approve **policy trace** — a machine-readable record of why the request was or was not auto-approved. Non-evaluated paths still seed a trace with a `machine_reason` (for example `manual_required`); the actual approval outcome is unchanged by the trace. This makes the auto-approve decision auditable even when the policy engine did not act.

### RequestState state transitions

```mermaid theme={null}
stateDiagram-v2
    [*] --> RECEIVED
    RECEIVED --> VALIDATED
    VALIDATED --> PENDING_APPROVAL
    PENDING_APPROVAL --> APPROVED
    PENDING_APPROVAL --> REJECTED
    PENDING_APPROVAL --> EXPIRED
    APPROVED --> EXECUTING
    EXECUTING --> COMPLETED
    EXECUTING --> FAILED
    EXECUTING --> MANUAL_INTERVENTION
    EXECUTING --> WAITING_MANUAL_ACTION
    EXECUTING --> CANCELLED
    MANUAL_INTERVENTION --> EXECUTING
    MANUAL_INTERVENTION --> FAILED
    MANUAL_INTERVENTION --> COMPLETED
    WAITING_MANUAL_ACTION --> EXECUTING
```

**Terminal states**: `COMPLETED`, `FAILED`, `REJECTED`, `EXPIRED`, `CANCELLED`

### intent JSON structure

Carries the movement intent passed by the caller. Structure varies by template.

```json theme={null}
{
  "asset": "XRP",
  "amount": "25.0",
  "source": {"venue": "upbit", "venue_type": "exchange"},
  "destination": {"venue": "binance", "venue_type": "exchange"},
  "network": "XRP"
}
```

### input\_params JSON structure

Concrete parameters required for execution.

```json theme={null}
{
  "amount": "25.0",
  "asset": "XRP",
  "network": "XRP",
  "chain_family": "ripple",
  "source_exchange": "upbit",
  "destination_exchange": "binance"
}
```

### callback\_config JSON structure

```json theme={null}
{
  "url": "https://dashboard.example.com/v1/callbacks/movement",
  "events": ["request_approved", "request_completed", "frontier_advanced"]
}
```

Notes:

* If `url` is present, the runtime requires `MG_CALLBACK_HMAC_SECRET` and a callback host allowlist.
* If the allowlist or secret is missing, request creation fails closed.

### current\_frontier meaning

Array of `node_key`s for nodes currently executable or executing. The frontier is updated as the DAG progresses.

* Initial creation: node\_key of the root node(s)
* After a node completes: advance to the successor node's node\_key
* Fully complete: empty array `[]`

### ApprovalStatus / ReservationStatus enum values

**ApprovalStatus**: `pending`, `approved`, `rejected`

**ReservationStatus**: `none`, `held`, `consumed`, `released`

* `none` — before approval
* `held` — resource held after approval
* `consumed` — used after normal completion
* `released` — released after failure/cancel

***

## 6. MovementRequestNode

**Table name**: `movement_request_nodes`

Runtime instance of MovementPlanNode. Tracks individual node execution state, provider references, and binding info.

| Column                 | Type            | Constraint                          | Description                                                |
| ---------------------- | --------------- | ----------------------------------- | ---------------------------------------------------------- |
| `id`                   | UUID            | PK                                  | Unique instance ID                                         |
| `request_id`           | UUID            | FK->movement\_requests.id, INDEX    | Owning request                                             |
| `plan_node_id`         | UUID            | FK->movement\_plan\_nodes.id, INDEX | Source plan node                                           |
| `node_key`             | String(128)     | NOT NULL                            | Node identifier key (copied from plan\_node node\_key)     |
| `node_state`           | Enum(NodeState) | NOT NULL, INDEX, default `BLOCKED`  | Current node state                                         |
| `attempt_no`           | Integer         | NOT NULL, default 0                 | Current attempt count                                      |
| `executor_binding`     | JSON            | NOT NULL, default `{}`              | Bound executor info                                        |
| `signer_binding`       | JSON            | NULLABLE                            | Bound signer info                                          |
| `prepared_action_hash` | String(128)     | NULLABLE                            | Hash of the prepared action                                |
| `provider_state`       | String(128)     | NULLABLE                            | Provider (exchange/chain) state string                     |
| `provider_ref_id`      | Text            | NULLABLE                            | Primary provider reference ID (txid, withdrawal\_id, etc.) |
| `provider_refs`        | JSON            | NOT NULL, default `{}`              | Full provider reference info                               |
| `artifacts_ref`        | JSON (list)     | NOT NULL, default `[]`              | Related artifact references                                |
| `error_code`           | String(64)      | NULLABLE                            | Error code                                                 |
| `error_detail`         | Text            | NULLABLE                            | Detailed error message                                     |
| `has_side_effect`      | Boolean         | NOT NULL, default `False`           | Whether side effects exist                                 |
| `started_at`           | DateTime(tz)    | NULLABLE                            | Execution start time                                       |
| `completed_at`         | DateTime(tz)    | NULLABLE                            | Completion time                                            |
| `last_observed_at`     | DateTime(tz)    | NULLABLE                            | Last observation time                                      |
| `next_observe_at`      | DateTime(tz)    | NULLABLE                            | Next scheduled observation time                            |
| `observe_count`        | Integer         | NOT NULL, default 0                 | Observation count                                          |
| `updated_at`           | DateTime(tz)    | NOT NULL, onupdate                  | Last updated timestamp                                     |

**Unique constraint**: `(request_id, node_key)` — node\_key cannot be duplicated within a request.

### NodeState state transitions

```mermaid theme={null}
stateDiagram-v2
    [*] --> BLOCKED
    BLOCKED --> READY
    BLOCKED --> SKIPPED
    READY --> PREPARING
    READY --> FAILED
    PREPARING --> AWAITING_SIGNATURE
    PREPARING --> SUBMITTING
    AWAITING_SIGNATURE --> SUBMITTING
    SUBMITTING --> SUBMITTED
    SUBMITTING --> FAILED
    SUBMITTING --> UNKNOWN
    SUBMITTED --> OBSERVING
    SUBMITTED --> COMPLETED
    SUBMITTED --> FAILED
    SUBMITTED --> UNKNOWN
    OBSERVING --> COMPLETED
    OBSERVING --> FAILED
    OBSERVING --> UNKNOWN
    UNKNOWN --> SUBMITTED
    UNKNOWN --> FAILED
```

**Terminal states**: `COMPLETED`, `FAILED`, `CANCELLED`, `SKIPPED`

### executor\_binding JSON structure

```json theme={null}
{
  "executor_key": "exec.cex.withdrawal_action",
  "exchange": "upbit"
}
```

### signer\_binding JSON structure

```json theme={null}
{
  "signer_key": "signer.evm.local",
  "chain_family": "evm"
}
```

### provider\_refs JSON structure

Accumulates provider-side reference info returned by the executor. Keys vary by provider/protocol.

**CEX withdrawal example**:

```json theme={null}
{
  "withdrawal_id": "abc123",
  "txid": "AABBCCDD...",
  "provider_state": "DONE"
}
```

**CCTP burn example**:

```json theme={null}
{
  "tx_hash": "0x1234...",
  "message_hash": "0xabcd...",
  "attestation": "0x5678..."
}
```

**EVM receive observe example**:

```json theme={null}
{
  "tx_hash": "0x9999...",
  "block_number": 12345678,
  "confirmations": 35
}
```

`provider_ref_id` is the representative ID chosen by the `select_primary_provider_ref()` function from `provider_refs` by priority.

***

## 7. MovementArtifact

**Table name**: `movement_artifacts`

Stores evidence (prepared action, proof, signed payload, etc.) produced during execution.

| Column                                   | Type         | Constraint                             | Description                 |
| ---------------------------------------- | ------------ | -------------------------------------- | --------------------------- |
| `id`                                     | UUID         | PK                                     | Unique artifact ID          |
| `request_id`                             | UUID         | FK->movement\_requests.id, INDEX       | Owning request              |
| `request_node_id`                        | UUID         | FK->movement\_request\_nodes.id, INDEX | Owning node instance        |
| `artifact_type`                          | String(64)   | NOT NULL                               | Artifact kind               |
| `artifact_digest`                        | String(128)  | NOT NULL                               | Content hash (SHA-256)      |
| `content_format`                         | String(32)   | NOT NULL                               | Content format              |
| `content_inline`                         | Text         | NULLABLE                               | Inline content (JSON, etc.) |
| `content_uri`                            | Text         | NULLABLE                               | External store URI          |
| `metadata` (mapped: `artifact_metadata`) | JSON         | NOT NULL, default `{}`                 | Additional metadata         |
| `created_at`                             | DateTime(tz) | NOT NULL                               | Creation time               |

> Note: in the Python model this is mapped as `artifact_metadata`, but the DB column name is `metadata`.

### artifact\_type values

| Value             | Description                                                                 |
| ----------------- | --------------------------------------------------------------------------- |
| `prepared_action` | Execution plan produced by executor.prepare()                               |
| `proof`           | Evidence collected by the observer (chain state, transaction receipt, etc.) |
| `signed_payload`  | Payload signed by the signer                                                |
| `attestation`     | CCTP attestation data                                                       |

### content\_format values

Typically one of `json`, `hex`, or `base64`.

### Storage strategy

* `content_inline`: small data is stored inline directly
* `content_uri`: large data references an external store URI (S3, etc.)
* Both fields are NULLABLE, but at least one must be present

***

## 8. MovementEvent

**Table name**: `movement_events`

Audit-trail table that records every state transition and major event in chronological order.

| Column            | Type         | Constraint                                       | Description                                           |
| ----------------- | ------------ | ------------------------------------------------ | ----------------------------------------------------- |
| `id`              | Integer      | PK, autoincrement                                | Event sequence number                                 |
| `request_id`      | UUID         | FK->movement\_requests.id, INDEX                 | Owning request                                        |
| `request_node_id` | UUID         | FK->movement\_request\_nodes.id, NULLABLE, INDEX | Related node (null for request-level events)          |
| `event_type`      | String(64)   | NOT NULL                                         | Event kind                                            |
| `actor_type`      | String(32)   | NOT NULL                                         | Actor kind                                            |
| `actor_id`        | String(128)  | NULLABLE                                         | Actor identifier                                      |
| `old_state`       | String(64)   | NULLABLE                                         | Previous state                                        |
| `new_state`       | String(64)   | NULLABLE                                         | New state                                             |
| `detail`          | JSON         | NOT NULL, default `{}`                           | Detailed information                                  |
| `prev_event_hash` | String(128)  | NULLABLE                                         | Hash of the previous event (for chaining, future use) |
| `created_at`      | DateTime(tz) | NOT NULL                                         | Event occurrence time                                 |

### event\_type values

| Value                      | Description              |
| -------------------------- | ------------------------ |
| `node_state_transition`    | Node state transition    |
| `request_state_transition` | Request state transition |

### actor\_type / actor\_id combinations

| actor\_type | actor\_id example                                          | Description     |
| ----------- | ---------------------------------------------------------- | --------------- |
| `system`    | `dispatcher`, `observer`, `recovery`, `expiration_checker` | System worker   |
| `operator`  | `approver-name`, `resume`, `retry`, `cancel`               | Operator action |

***

## 9. MovementCallbackOutbox

**Table name**: `movement_callback_outbox`

Outbox-pattern table that manages callbacks to be delivered to external systems.

| Column               | Type                       | Constraint                         | Description                                                                       |
| -------------------- | -------------------------- | ---------------------------------- | --------------------------------------------------------------------------------- |
| `id`                 | UUID                       | PK                                 | Unique callback entry ID                                                          |
| `request_id`         | UUID                       | FK->movement\_requests.id, INDEX   | Owning request                                                                    |
| `callback_url`       | Text                       | NOT NULL                           | Target callback URL                                                               |
| `payload`            | JSON                       | NOT NULL, default `{}`             | Pure business callback payload (excluding auth metadata)                          |
| `callback_timestamp` | Integer                    | NULLABLE                           | Persisted callback timestamp (nullable for legacy row backfill)                   |
| `callback_nonce`     | String(128)                | NULLABLE, **UNIQUE**               | Persisted callback nonce (nullable for legacy row backfill; unique guards replay) |
| `event_sequence`     | Integer                    | NULLABLE                           | Monotonic per-request event ordinal so receivers can order/dedup deliveries       |
| `attempts`           | Integer                    | NOT NULL, default 0                | Current attempt count                                                             |
| `max_attempts`       | Integer                    | NOT NULL, default 5                | Maximum attempt count                                                             |
| `next_attempt_at`    | DateTime(tz)               | NOT NULL                           | Next scheduled attempt time                                                       |
| `status`             | Enum(CallbackOutboxStatus) | NOT NULL, INDEX, default `pending` | Delivery state                                                                    |
| `last_error`         | Text                       | NULLABLE                           | Last error message                                                                |
| `created_at`         | DateTime(tz)               | NOT NULL                           | Creation time                                                                     |

### CallbackOutboxStatus enum values

| Value     | Description                              |
| --------- | ---------------------------------------- |
| `pending` | Waiting to be delivered                  |
| `sent`    | Delivered                                |
| `dlq`     | Exceeded max attempts, Dead Letter Queue |

### Retry strategy

* On failure, `next_attempt_at` is set to `2 ** min(attempts, 6)` seconds of backoff
* When `max_attempts` (default 5) is reached, transition to `dlq` state
* The callback\_dispatcher worker periodically polls records in `pending` state

Additional implementation notes:

* `MovementCallbackOutbox.id` also follows the internal UUID policy and is thus uuid7-based.
* `enqueue_callback()` creates the persisted outbox row `id`, and mirrors that value as `callback_id` in the payload.

***

## 10. ExecutorRegistryEntry

**Table name**: `executor_registry`

Persists executor registration info. The executor\_health worker refreshes it periodically.

| Column                   | Type         | Constraint                  | Description                                |
| ------------------------ | ------------ | --------------------------- | ------------------------------------------ |
| `id`                     | UUID         | PK                          | Registry entry ID                          |
| `executor_key`           | String(128)  | NOT NULL, UNIQUE, INDEX     | Unique executor key                        |
| `mode`                   | String(16)   | NOT NULL                    | Execution mode (`local` / `remote`)        |
| `protocol_version`       | String(32)   | NOT NULL, default `1.0`     | Protocol version                           |
| `base_url`               | Text         | NULLABLE                    | Remote executor base URL                   |
| `auth_mode`              | String(16)   | NOT NULL, default `none`    | Authentication method (`none` / `bearer`)  |
| `capabilities`           | JSON         | NOT NULL, default `{}`      | Executor capability metadata               |
| `status`                 | String(16)   | NOT NULL, default `active`  | Registration state (`active` / `disabled`) |
| `health_state`           | String(16)   | NOT NULL, default `unknown` | Health state (`unknown` / `up` / `down`)   |
| `last_health_checked_at` | DateTime(tz) | NULLABLE                    | Last health check time                     |

***

## 11. SignerRegistryEntry

**Table name**: `signer_registry`

Persists signer registration info.

| Column                   | Type         | Constraint                  | Description                                                             |
| ------------------------ | ------------ | --------------------------- | ----------------------------------------------------------------------- |
| `id`                     | UUID         | PK                          | Registry entry ID                                                       |
| `signer_key`             | String(128)  | NOT NULL, UNIQUE, INDEX     | Unique signer key                                                       |
| `protocol_version`       | String(32)   | NOT NULL, default `1.0`     | Protocol version                                                        |
| `base_url`               | Text         | NULLABLE                    | Remote signer base URL                                                  |
| `auth_mode`              | String(16)   | NOT NULL, default `none`    | Authentication method                                                   |
| `chain_families`         | JSON (list)  | NOT NULL, default `[]`      | Supported chain families                                                |
| `capabilities`           | JSON         | NOT NULL, default `{}`      | Signer capability metadata                                              |
| `status`                 | String(32)   | NOT NULL, default `active`  | Registration state (`active` / `disabled` / `rotation_pending_restart`) |
| `health_state`           | String(16)   | NOT NULL, default `unknown` | Health state                                                            |
| `last_health_checked_at` | DateTime(tz) | NULLABLE                    | Last health check time                                                  |

***

## 12. BalanceSnapshot

**Table name**: `balance_snapshots`

Append-only store of the latest per-venue balance snapshots. Records Gateway unified USDC balance, CEX, and on-chain balances through the same schema.

| Column              | Type         | Constraint                                        | Description                                                                                                       |
| ------------------- | ------------ | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `id`                | UUID         | PK, default uuid7                                 | Snapshot row ID                                                                                                   |
| `venue_key`         | String(64)   | NOT NULL                                          | Venue identifier (`gateway`, `upbit`, `binance`, etc.)                                                            |
| `asset`             | String(64)   | NOT NULL                                          | Asset symbol                                                                                                      |
| `account_type`      | String(32)   | NOT NULL, default `SPOT` (server\_default `SPOT`) | Account scope (e.g. `SPOT`, `FUNDING`) — distinguishes balances held in different account types at the same venue |
| `available`         | String(64)   | NOT NULL                                          | Available amount reported by the venue                                                                            |
| `withdrawable`      | String(64)   | NULLABLE                                          | Withdrawable amount when the venue distinguishes it from `available`; null otherwise                              |
| `total`             | String(64)   | NULLABLE                                          | Stored when the venue provides a total; null otherwise                                                            |
| `snapshot_metadata` | JSON         | NULLABLE                                          | Normalized venue-specific details (`domains[]` for `gateway`)                                                     |
| `fetched_at`        | DateTime(tz) | NOT NULL, INDEX                                   | Time the fetch completed                                                                                          |

**Indexes**

* `ix_balance_snapshot_venue_asset_account_fetched` — `(venue_key, asset, account_type, fetched_at)`
* index on `fetched_at` — for retention cleanup

### Lookup semantics

* The latest lookup unit is `(venue_key, asset, account_type)`.
* Latest semantics including tie-breaks: `ORDER BY fetched_at DESC, id DESC`.
* Both `fresh=true` API calls and worker fetches write into the same append-only table.

***

## DB Session settings

Sessions are network-class aware. A single base engine owns the connection pool; per-network engines reuse that pool through `execution_options(schema_translate_map={"qtg_network": <network>})`, so `session_for(network_class)()` returns a sessionmaker that auto-tags `info["network_class"]`.

```python theme={null}
# src/qtg/infrastructure/db/session.py
base_engine = create_qtg_engine(url, pool_pre_ping=True, future=True)
network_engines = {
    nc: base_engine.execution_options(schema_translate_map={"qtg_network": nc.value})
    for nc in NetworkClass
}
network_sessionmakers = {
    nc: async_sessionmaker(bind=engine, class_=AsyncSession,
                           expire_on_commit=False, info={"network_class": nc})
    for nc, engine in network_engines.items()
}
```

* `pool_pre_ping=True` — liveness check when taking a connection from the pool
* `expire_on_commit=False` — keep loaded attribute values after commit
* `session_for(network_class)` — returns the sessionmaker for one network class; an `after_begin` listener issues `SET LOCAL search_path` so unqualified `qtg_network` tables resolve into the right bucket
* `iterate_per_network(...)` — runs a coroutine once per network class (used for registry-sync and heartbeat seeding at startup)
* `public`-schema models (`api_clients`, `api_client_keys`, `nonce_registry`) use a separate network-independent auth sessionmaker bound to the base engine

Configuration reference: `MG_DATABASE_URL` environment variable (default: `postgresql+asyncpg://qtg:qtg@localhost:5432/qtg_v3`). PostgreSQL is required since v0.1.0; the process network class is selected by the required `MG_NETWORK_MODE` setting.

***

## JSON field conventions summary

| Field                        | Location                                   | When stored                           | Use                                                         |
| ---------------------------- | ------------------------------------------ | ------------------------------------- | ----------------------------------------------------------- |
| `intent`                     | MovementRequest                            | At request creation                   | Movement intent (asset, amount, source, destination)        |
| `input_params`               | MovementRequest                            | At request creation                   | Execution parameters (exchange name, network, etc.)         |
| `callback_config`            | MovementRequest                            | At request creation                   | Callback URL and event filters                              |
| `completion_policy_snapshot` | MovementRequest                            | At request creation                   | Completion policy snapshot at approval time                 |
| `node_config` / `config`     | MovementPlanNode                           | At template registration              | Per-node settings (passed as ExecutionContext.node\_config) |
| `executor_selector`          | MovementPlanNode                           | At template registration              | Executor selection criteria                                 |
| `signer_selector`            | MovementPlanNode                           | At template registration              | Signer selection criteria                                   |
| `executor_binding`           | MovementRequestNode                        | When bound at request creation        | Finalized executor reference                                |
| `signer_binding`             | MovementRequestNode                        | When bound at request creation        | Finalized signer reference                                  |
| `provider_refs`              | MovementRequestNode                        | During executor execution/observation | Accumulated provider-side reference IDs                     |
| `detail`                     | MovementEvent                              | At event occurrence                   | Additional event info                                       |
| `payload`                    | MovementCallbackOutbox                     | At callback enqueue                   | Pure business payload (auth header separate)                |
| `capabilities`               | ExecutorRegistryEntry, SignerRegistryEntry | At health check                       | Capability metadata and recent health info                  |
| `snapshot_metadata`          | BalanceSnapshot                            | During worker / on-demand fetch       | Normalized venue-specific balance details                   |
